44 Commits

Author SHA1 Message Date
Zettat123
fed01c9807 Parse multiple default actions URLs (#200)
Follow https://gitea.com/gitea/act/pulls/58
Resolve https://github.com/go-gitea/gitea/issues/24789

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/200
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-06 04:03:02 +00:00
Bo-Yi Wu
a83f29d5a9 docs: improve examples README and organization (#230)
- Update the introduction and descriptions in the examples README
- Add a table with descriptions for each section (docker, docker-compose, kubernetes, vm)

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/230
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-06-06 02:42:58 +00:00
Bo-Yi Wu
69c55ee003 refactor: daemon, config, and logging for better clarity (#225)
- Import "path", "runtime", "strconv", and "strings" packages in daemon.go
- Move "Starting runner daemon" log message to a different location
- Refactor log formatter initialization and add debug level caller information
- Split Config struct into separate Log, Runner, Cache, and Container structs with comments in config.go

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/225
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-06-05 13:11:23 +00:00
a1012112796
01ef57c667 fix artifactServerPath and artifactServerAddr config for exec (#221)
fix exec logic to make `actions/download-artifact` and `actions/upload-artifact` can be used.

example result:

```YML
name: test-artifact
on:
  - push
  - pull_request

jobs:
  test-artifact-1:
    name: test 1
    runs-on: ubuntu-latest
    steps:
      - run: echo `date` | tee time.txt
      - name: cache build result
        uses: actions/upload-artifact@v3
        with:
          name: build-artifact
          path: time.txt
          retention-days: 1

  test-artifact-2:
    name: test 2
    needs: test-artifact-1
    runs-on: ubuntu-latest
    steps:
      - name: Retrieve saved build result
        uses: actions/download-artifact@v3
        with:
          name: build-artifact
          path: .
      - run: ls -lh
      - run: cat time.txt

```

![image](/attachments/5cad3b4a-930a-4d42-a1ae-45ac32e6bfc2)

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/221
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-06-05 08:51:44 +00:00
Chris Cureau
a384adbbc6 Documentation enhancements (#207)
This PR addresses the issue listed in issue #170 regarding how to set up rootless Docker. It also expands on the documentation to show how to create deployments for different environments.

Co-authored-by: ccureau <ccureau@noreply.gitea.io>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/207
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Chris Cureau <cmcureau@gmail.com>
Co-committed-by: Chris Cureau <cmcureau@gmail.com>
2023-06-05 08:46:15 +00:00
harryzcy
e3271d8469 Remove trailing slash from instance address (#197)
Related #136

Co-authored-by: harryzcy <harry@harryzheng.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/197
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: harryzcy <harryzcy@noreply.gitea.io>
Co-committed-by: harryzcy <harryzcy@noreply.gitea.io>
2023-05-22 23:50:29 +08:00
sillyguodong
84386c1b16 Add exec command flag of network (#192)
Related to #184
Add command flag of `network` for `exec`, the default value of `--network` is empty string. Valid values are: `host `, `bridge`, `<custom_network>` and empty string.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/192
Reviewed-by: a1012112796 <1012112796@qq.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-05-18 15:01:43 +08:00
ChristopherHX
fd7c8580af Prevent exposing GITEA_RUNNER_REGISTRATION_TOKEN to act (#188)
You can currently expose the token to jobs even while using docker in docker

`-e GITEA_RUNNER_REGISTRATION_TOKEN` tells the docker client of act to read GITEA_RUNNER_REGISTRATION_TOKEN from the process and now it can be stolen.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/188
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: ChristopherHX <christopherhx@noreply.gitea.io>
Co-committed-by: ChristopherHX <christopherhx@noreply.gitea.io>
2023-05-17 14:13:38 +08:00
sillyguodong
35596a182b Add configuration item of container.network (#184)
Close https://gitea.com/gitea/act_runner/issues/177
Related https://gitea.com/gitea/act/pulls/56

### ⚠️ Breaking
The `container.network_mode` is a deprecated configuration item. It may be removed after Gitea 1.20 released.
Previously, if the value of `container.network_mode` is `bridge`, it means that `act_runner` will create a new network for job.But `bridge` is easily confused with the bridge network created by Docker by default.
We recommand that using `container.network` to specify the network to which containers created by `act_runner` connect.

###  🆕 container.network
The configuration file of `act_runner` add a new item of `contianer.network`.
In `config.example.yaml`:
```yaml
container:
  # Specifies the network to which the container will connect.
  # Could be host, bridge or the name of a custom network.
  # If it's empty, act_runner will create a network automatically.
  network: ""
```

As the comment in the example above says, the purpose of the `container.network` is specifying the network to which containers created by `act_runner` will connect.

`container.network` accepts the following valid values:
- `host`: All of containers (including job containers and service contianers) created by `act_runner` will be connected to the network named `host` which is created automatically by Docker. Containers will share the host’s network stack and all interfaces from the host will be available to these containers.
- `bridge`: It is similar to `host`. All of containers created by `act_runner` will be connected to the network named `bridge` which is created automatically by Docker. All containers connected to the `bridge` (Perhaps there are containers that are not created by `act_runner`) are allowed to communicate with each other, while providing isolation from containers which are not connected to that `bridge` network.
- `<custom_network>`: Please make sure that the `<custom_network>` network already exists firstly (`act_runner` does not detect whether the specified network exists currently. If not exists yet, will return error in the stage of `docker create`). All of containers created by `act_runner` will be connected to `<custom_network>`. After the job is executed, containers are removed and automatically disconnected from the `<custom_network>`.
- empty: `act_runner` will create a new network for each job container and their service containers (if defined in workflow). So each job container and their service containers share a network environment, but are isolated from others container and the Docker host. Of course, these networks created by `act_runner` will be removed at last.

### Others
- If you do not have special needs, we highly recommend that setting `container.network` to empty string (and do not use `container.network_mode` any more). Because the containers created by `act_runner` will connect to the networks that are created by itself. This point will provide better isolation.
- If you set `contianer.network` to empty string or `<custom_network>`, we can be access to service containers by `<service-id>:<port>` in the steps of job. Because we added an alias to the service container when connecting to the network.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/184
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-05-16 14:46:59 +08:00
silverwind
c9d3f67264 Add .editorconfig and .gitattributes (#186)
Add some files that belong in every repo.

- `.editorconfig` is based on `gitea` repo.
- `.gitattributes` is useful for Windows users.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/186
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: delvh <dev.lh@web.de>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2023-05-13 23:51:22 +08:00
Alex Lau (AvengerMoJo)
94031fc198 Fix README.md typo on daemon (#183)
It is just a typo fix.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/183
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Alex Lau (AvengerMoJo) <avengermojo@gmail.com>
Co-committed-by: Alex Lau (AvengerMoJo) <avengermojo@gmail.com>
2023-05-11 14:25:39 +08:00
sillyguodong
d5e4baed54 Fix missing runner version when building images from a Dockerfile (#181)
regression of https://gitea.com/gitea/act_runner/pulls/172

https://gitea.com/gitea/act_runner/actions/runs/400/jobs/1
In the step of `Build and push`, log output that `git: not found`. This lead to runner's version not being injected when go compile.
![image](/attachments/031ecdf2-6baa-410c-bab7-c6945140c5ad)

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/181
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-05-09 16:44:31 +08:00
sando38
d4caa7e065 Dockerfile: Improve signal handling by adding a runtime init (#180)
This adds tini as a runtime init (https://github.com/krallin/tini). It improves signal handling for the container, see https://github.com/krallin/tini#why-tini.

An alternative could be to run the container with `docker run --init ...` which also places tini as a runtime init as PID 1.

Co-authored-by: sando38 <sandomir@tutanota.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/180
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sando38 <sando38@noreply.gitea.io>
Co-committed-by: sando38 <sando38@noreply.gitea.io>
2023-05-09 16:09:48 +08:00
Jason Song
de4160b023 Skip counting log length when parseLogRow return nil (#176)
Fix:
![image](/attachments/93e29bc0-3599-4f7e-8b90-512562a5d711)

Regression of #149, `LogLength` could be incorrect.

It may be related to https://github.com/go-gitea/gitea/issues/24458

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/176
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Reviewed-by: wxiaoguang <wxiaoguang@noreply.gitea.io>
2023-05-06 17:00:52 +08:00
a1012112796
609c0a0773 fix --event option logic for exec (#175)
- fix `--event` option logic
- by the way, apply a `TODO` logic

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/175
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-05-06 11:27:08 +08:00
Jason Song
0c029f7e79 Upgrade act and use new artifactcache (#174)
- Upgrade act to v0.245.1
- Replace `gitea.com/gitea/act_runner/internal/app/artifactcache` with `github.com/nektos/act/pkg/artifactcache`

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/174
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-05-04 18:45:01 +08:00
Bo-Yi Wu
eef3c32eb2 ci: improve release process and test robustness (#173)
- Add `.xz` and `.xz.sha256` files to the release extra files in `.goreleaser.yaml`

upload `.xz` and `.xz.sha256` to Gitea release page.

![image](/attachments/0218072d-c235-461a-8f52-969aeb6e5b07)

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/173
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-05-04 09:41:22 +08:00
Bo-Yi Wu
c40b651873 chore: improve Dockerfile, README, and testing settings (#172)
- Remove `git=2.38.5-r0` from the `apk add` command in Dockerfile
- Update the download link for act_runner in README.md to be a clickable link

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/172
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-05-04 09:36:31 +08:00
appleboy
b498341857 build: improve compression and update GitHub actions (#168)
- Add `dist` to .gitignore for gorelease binary folder
- Replace tar command with xz command in .goreleaser.yaml for better compression

ref: https://gitea.com/gitea/homebrew-gitea/pulls/164

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/168
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2023-05-01 21:59:43 +08:00
appleboy
0d727eb262 build: optimize Dockerfile and update dependencies (#162)
- Update base images to golang:1.20-alpine3.17 and alpine:3.17
- Replace `--update-cache` with `--no-cache` in apk add command
- Specify exact versions for make, git, and bash packages

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/162
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2023-04-29 12:07:15 +08:00
vbrandl
7c71c94366 Document persisting /data for docker container (#160)
`/data` must be kept between container restarts.

Co-authored-by: Valentin Brandl <mail@vbrandl.net>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/160
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: vbrandl <vbrandl@noreply.gitea.io>
Co-committed-by: vbrandl <vbrandl@noreply.gitea.io>
2023-04-29 03:05:00 +08:00
appleboy
49d2cb0cb5 ci: improve API usage and test robustness across platforms (#159)
- Add `dir: ./dist/` to `.goreleaser.yaml` builds configuration

fix https://gitea.com/gitea/act_runner/issues/158

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/159
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2023-04-28 23:46:46 +08:00
sillyguodong
85626b6bbd Support configuration variables (#157)
related to: https://gitea.com/gitea/act_runner/issues/127

`act_runner` only needs to pass `vars` from `Gitea` to `act`.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/157
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-04-28 22:06:08 +08:00
Zettat123
35400f76fa Add parent directory for working directory (#154)
Fixes #145

At present, the working directory of a work flow is a path like `/<owner>/<repo>`, so the directory may conflict with system directory like `/usr/bin`. We need to add a parent directory for the working directory.
In this PR, the parent directory is `/workspace` by default and users could configure it by the `workdir_parent` option.

This change doesn't affect the host mode because in host mode the working directory will always be in `$HOME/.cache/act/` directory.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/154
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-28 22:03:52 +08:00
Jason Song
0cf31b2d22 Update docker image tag (#153)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/153
2023-04-27 15:02:39 +08:00
Søren L. Hansen
c8cc7b2448 Workflow commands (#149)
Establishes a simple framework for supporting workflow commands.

Fully implements `::add-mask::`, `::debug::`, and `::stop-commands::`.

Addresses #148

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/149
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Søren L. Hansen <sorenisanerd@gmail.com>
Co-committed-by: Søren L. Hansen <sorenisanerd@gmail.com>
2023-04-27 12:32:48 +08:00
Jason Song
3be962cdb3 Rename the download folder from main -> nightly (#152)
Close https://gitea.com/gitea/act_runner/issues/117

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/152
2023-04-27 12:23:15 +08:00
Jason Song
a5edbc9ac4 Release docker images (#151)
Did some tests to make sure it worked.

See https://hub.docker.com/r/gitea/act_runner/tags

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/151
2023-04-27 12:08:41 +08:00
seepine
66bab3d805 ci(actions): add build docker image workflow (#118)
### Add secret
```
DOCKERHUB_TOKEN=xxx
```

### Tag
when tag like `v1.0.0`, it will build multi platform docker image `gitea/act_runner:1.0.0` and `gitea/act_runner:latest`, then push to docker hub

### Use
> volume `/data` save `.runner` config file
> volume `/root/.cache` save actcache and actions cache

```sh
docker run -e GITEA_INSTANCE_URL=***                    \
           -e GITEA_RUNNER_REGISTRATION_TOKEN=***       \
           -e GITEA_RUNNER_NAME=***                     \
           -v /var/run/docker.sock:/var/run/docker.sock \
           -v /root/act_runner/data:/data               \
           -v /root/act_runner/cache:/root/.cache       \
           gitea/act_runner
```
Test join runners success
![image](/attachments/f5287e93-e27c-420f-a3d5-8f9b54bfdbb6)
Test run action success
![image](/attachments/7235af17-f598-4fc8-88b4-d4771b1f07cd)

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/118
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: seepine <seepine@noreply.gitea.io>
Co-committed-by: seepine <seepine@noreply.gitea.io>
2023-04-27 11:32:28 +08:00
techknowlogick
293926f5d5 bump modernc.org/sqlite (#141)
fix #140

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/141
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Co-committed-by: techknowlogick <techknowlogick@gitea.io>
2023-04-25 03:16:01 +08:00
You-Sheng Yang
43c5ba923f make: skip --disable-content-trust at docker buildx (#139)
`docker build` may be aliased as `docker buildx build`, which doesn't support --disable-content-trust switch.

Signed-off-by: You-Sheng Yang <vicamo@gmail.com>

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/139
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: You-Sheng Yang <vicamo@gmail.com>
Co-committed-by: You-Sheng Yang <vicamo@gmail.com>
2023-04-25 03:15:48 +08:00
techknowlogick
acc5afc428 allow building act_runner with cgo (#137)
for platforms not supported by moderc.org/sqlite

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/137
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Co-committed-by: techknowlogick <techknowlogick@gitea.io>
2023-04-24 10:45:51 +08:00
Zettat123
27a1a90d25 Upgrade act (#135)
Related to:

- https://gitea.com/gitea/act/pulls/45
- https://gitea.com/gitea/act/pulls/47

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/135
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-21 10:48:26 +08:00
Jason Song
83ec0ba909 Support upload outputs and use needs context (#133)
See [Example usage of the needs context](https://docs.github.com/en/actions/learn-github-actions/contexts#example-usage-of-the-needs-context).

Related to:
- [actions-proto-def #5](https://gitea.com/gitea/actions-proto-def/pulls/5)
- [gitea #24230](https://github.com/go-gitea/gitea/pull/24230)

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/133
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-04-20 23:27:46 +08:00
Jason Song
ed86e2f15a Update workflow files (#131)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/131
2023-04-19 17:46:52 +08:00
Jason Song
d4bebccc12 Update dependencies (#130)
Related to:
- https://gitea.com/gitea/act/pulls/44
- https://gitea.com/gitea/actions-proto-def/pulls/5
- https://gitea.com/gitea/actions-proto-def/pulls/7

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/130
2023-04-19 16:50:07 +08:00
Zettat123
c75b67e892 Upgrade act (#128)
Follow https://gitea.com/gitea/act/pulls/42

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/128
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-19 14:53:23 +08:00
Zettat123
bc6031eff7 Fix reporting log in Reporter.Close (#126)
Previously, the `Close` func returns incorrectly so that the logs may not be reported.

This PR fixes the incorrect return and sets the `StoppedAt` to get the correct task duration.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/126
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-19 11:17:49 +08:00
Zettat123
c69c353d93 Upgrade act (#124)
Related to https://gitea.com/gitea/act/pulls/40

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/124
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-14 18:20:04 +08:00
Lunny Xiao
fcc016e9b3 Special the release tag signing sub key (#121)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/121
2023-04-13 18:56:15 +08:00
Jason Song
d5caee38f2 Add prefix to use ghaction-import-gpg (#120)
See https://gitea.com/gitea/act_runner/actions/runs/254

Related to #116

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/120
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-04-13 18:18:39 +08:00
Lunny Xiao
9e26208e13 Add signature for tag releases, upload to gitea releases itself (#116)
GPG signature keys are not set yet.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/116
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-04-13 16:49:50 +08:00
Lunny Xiao
a05c5ba3ad Add make docker (#115)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/115
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-04-13 04:17:08 +08:00
Jason Song
c248520a66 Set specific environments to distinguish between Gitea and GitHub (#113)
Close https://github.com/go-gitea/gitea/issues/24038

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/113
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-04-12 14:44:26 +08:00
36 changed files with 1451 additions and 1581 deletions

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{go}]
indent_style = tab
[Makefile]
indent_style = tab

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -1,20 +1,37 @@
name: goreleaser
name: release-nightly
on:
push:
branches: [ main ]
env:
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v3
with:
go-version: '>=1.20.1'
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |
go.mod
go.sum
- name: cache go
id: cache-go
uses: https://github.com/actions/cache@v3
with:
path: |
/go_path
/go_cache
key: go_path-${{ steps.hash-go.outputs.hash }}
- name: goreleaser
uses: https://github.com/goreleaser/goreleaser-action@v4
with:
@@ -28,3 +45,53 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
release-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # all history for all branches and tags
- name: dockerfile lint check
uses: https://github.com/hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

View File

@@ -1,21 +1,45 @@
name: goreleaser
name: release-tag
on:
on:
push:
tags:
- '*'
env:
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v3
with:
go-version: '>=1.20.1'
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |
go.mod
go.sum
- name: cache go
id: cache-go
uses: https://github.com/actions/cache@v3
with:
path: |
/go_path
/go_cache
key: go_path-${{ steps.hash-go.outputs.hash }}
- name: Import GPG key
id: import_gpg
uses: https://github.com/crazy-max/ghaction-import-gpg@v5
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
- name: goreleaser
uses: https://github.com/goreleaser/goreleaser-action@v4
with:
@@ -29,3 +53,56 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
GORELEASER_FORCE_TOKEN: 'gitea'
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
release-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # all history for all branches and tags
- name: dockerfile lint check
uses: https://github.com/hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

View File

@@ -1,10 +1,9 @@
name: checks
on:
on:
- push
- pull_request
env:
GOPROXY: https://goproxy.io,direct
GOPATH: /go_path
GOCACHE: /go_cache
@@ -13,31 +12,31 @@ jobs:
name: check and test
runs-on: ubuntu-latest
steps:
- name: cache go path
id: cache-go-path
uses: https://github.com/actions/cache@v3
with:
path: /go_path
key: go_path-${{ github.repository }}-${{ github.ref_name }}
restore-keys: |
go_path-${{ github.repository }}-
go_path-
- name: cache go cache
id: cache-go-cache
uses: https://github.com/actions/cache@v3
with:
path: /go_cache
key: go_cache-${{ github.repository }}-${{ github.ref_name }}
restore-keys: |
go_cache-${{ github.repository }}-
go_cache-
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.20
- uses: actions/checkout@v3
go-version: '>=1.20.1'
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |
go.mod
go.sum
- name: cache go
id: cache-go
uses: https://github.com/actions/cache@v3
with:
path: |
/go_path
/go_cache
key: go_path-${{ steps.hash-go.outputs.hash }}
- name: vet checks
run: make vet
- name: build
run: make build
- name: test
run: make test
run: make test
- name: dockerfile lint check
uses: https://github.com/hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ coverage.txt
# MS VSCode
.vscode
__debug_bin
# gorelease binary folder
dist

View File

@@ -71,9 +71,8 @@ builds:
no_unique_dist_dir: true
hooks:
post:
- cmd: tar -cJf {{ .Path }}.xz {{ .Path }}
env:
- XZ_OPT=-9
- cmd: xz -k -9 {{ .Path }}
dir: ./dist/
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
@@ -101,7 +100,16 @@ snapshot:
name_template: "{{ .Branch }}-devel"
nightly:
name_template: "{{ .Branch }}"
name_template: "nightly"
gitea_urls:
api: https://gitea.com/api/v1
download: https://gitea.com
release:
extra_files:
- glob: ./**.xz
- glob: ./**.xz.sha256
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

View File

@@ -1,17 +1,18 @@
FROM golang:alpine as builder
RUN apk add --update-cache make git
FROM golang:1.20-alpine3.17 as builder
# Do not remove `git` here, it is required for getting runner version when executing `make build`
RUN apk add --no-cache make=4.3-r1 git=2.38.5-r0
COPY . /opt/src/act_runner
WORKDIR /opt/src/act_runner
RUN make clean && make build
FROM alpine as runner
RUN apk add --update-cache \
git bash \
&& rm -rf /var/cache/apk/*
FROM alpine:3.17
RUN apk add --no-cache \
git=2.38.5-r0 bash=5.2.15-r0 tini=0.19.0-r1 \
&& rm -rf /var/cache/apk/*
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
COPY run.sh /opt/act/run.sh
ENTRYPOINT ["/opt/act/run.sh"]
ENTRYPOINT ["/sbin/tini","--","/opt/act/run.sh"]

View File

@@ -16,6 +16,10 @@ WINDOWS_ARCHS ?= windows/amd64
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
DOCKER_IMAGE ?= gitea/act_runner
DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
ifneq ($(shell uname), Darwin)
EXTLDFLAGS = -extldflags "-static" $(null)
else
@@ -61,6 +65,9 @@ else
endif
endif
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
TAGS ?=
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
@@ -101,7 +108,7 @@ test: fmt-check
vet:
@echo "Running go vet..."
@$(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet ./...
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
install: $(GOFILES)
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
@@ -156,6 +163,13 @@ release-check: | $(DIST_DIRS)
release-compress: | $(DIST_DIRS)
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
.PHONY: docker
docker:
if ! docker buildx version >/dev/null 2>&1; then \
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
fi; \
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
clean:
$(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST)

View File

@@ -6,11 +6,11 @@ Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/a
### Prerequisites
Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
Docker Engine Community version is required for docker mode. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
### Download pre-built binary
Visit https://dl.gitea.com/act_runner/ and download the right version for your platform.
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
### Build from source
@@ -18,6 +18,12 @@ Visit https://dl.gitea.com/act_runner/ and download the right version for your p
make build
```
### Build a docker image
```bash
make docker
```
## Quickstart
### Register
@@ -66,7 +72,6 @@ If the registry succeed, it will run immediately. Next time, you could run the r
./act_runner daemon
```
### Configuration
You can also configure the runner with a configuration file.
@@ -80,5 +85,9 @@ You can specify the configuration file path with `-c`/`--config` argument.
```bash
./act_runner -c config.yaml register # register with config file
./act_runner -c config.yaml deamon # run with config file
./act_runner -c config.yaml daemon # run with config file
```
### Example Deployments
Check out the [examples](examples) directory for sample deployment types.

12
examples/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Usage Examples for `act_runner`
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
| Section | Description |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`docker`](docker) | This section provides you with scripts and instructions tailored for running containers on a workstation or server where Docker is installed. It simplifies the process of setting up and managing your Gitea deployment using Docker. |
| [`docker-compose`](docker-compose) | In this section, you'll discover examples demonstrating how to utilize docker-compose to efficiently handle your Gitea deployments. It offers a straightforward approach to managing multiple containerized components of your Gitea setup. |
| [`kubernetes`](kubernetes) | If you're utilizing Kubernetes clusters for your infrastructure, this section is specifically designed for you. It presents examples and guidelines for configuring Gitea deployments within Kubernetes clusters, enabling you to leverage the scalability and flexibility of Kubernetes. |
| [`vm`](vm) | This section is dedicated to examples that assist you in setting up Gitea on virtual or physical servers. Whether you're working with virtual machines or physical hardware, you'll find helpful resources to guide you through the deployment process. |
We hope these resources provide you with valuable insights and solutions for your Gitea setup. Feel free to explore, contribute, and adapt these examples to suit your specific requirements.

View File

@@ -0,0 +1,20 @@
### Running `act_runner` using `docker-compose`
```yml
...
gitea:
image: gitea/gitea
...
runner:
image: gitea/act_runner
restart: always
depends_on:
- gitea
volumes:
- ./data/act_runner:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=<instance url>
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```

View File

@@ -0,0 +1,8 @@
### Run `act_runner` in a Docker Container
```sh
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/act_runner:nightly
```
The `/data` directory inside the docker container contains the runner API keys after registration.
It must be persisted, otherwise the runner would try to register again, using the same, now defunct registration token.

View File

@@ -0,0 +1,8 @@
## Kubernetes Docker in Docker Deployment with `act_runner`
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
Files in this directory:
- [`dind-docker.yaml`](dind-docker.yaml)
How to create a Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.

View File

@@ -0,0 +1,78 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: act-runner-vol
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
---
apiVersion: v1
data:
token: << base64 encoded registration token >>
kind: Secret
metadata:
name: runner-secret
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: act-runner
name: act-runner
spec:
replicas: 1
selector:
matchLabels:
app: act-runner
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: act-runner
spec:
restartPolicy: Always
volumes:
- name: docker-certs
emptyDir: {}
- name: runner-data
persistentVolumeClaim:
claimName: act-runner-vol
containers:
- name: runner
image: gitea/act_runner:nightly
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
env:
- name: DOCKER_HOST
value: tcp://localhost:2376
- name: DOCKER_CERT_PATH
value: /certs/client
- name: DOCKER_TLS_VERIFY
value: "1"
- name: GITEA_INSTANCE_URL
value: http://gitea-http.gitea.svc.cluster.local:3000
- name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom:
secretKeyRef:
name: runner-secret
key: token
volumeMounts:
- name: docker-certs
mountPath: /certs
- name: runner-data
mountPath: /data
- name: daemon
image: docker:23.0.6-dind
env:
- name: DOCKER_TLS_CERTDIR
value: /certs
securityContext:
privileged: true
volumeMounts:
- name: docker-certs
mountPath: /certs

6
examples/vm/README.md Normal file
View File

@@ -0,0 +1,6 @@
## `act_runner` on Virtual or Physical Servers
Files in this directory:
- [`rootless-docker.md`](rootless-docker.md)
How to set up a rootless docker implementation of the runner.

View File

@@ -0,0 +1,87 @@
## Using Rootless Docker with`act_runner`
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
As `root`:
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`.
```bash
useradd -m rootless
passwd rootless
```
- Install [`docker-ce`](https://docs.docker.com/engine/install/)
- (Recommended) Disable the system-wide Docker daemon
``systemctl disable --now docker.service docker.socket``
As the `rootless` user:
- Follow the instructions for [enabling rootless mode](https://docs.docker.com/engine/security/rootless/)
- Add the following lines to the `/home/rootless/.bashrc`:
```bash
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
export PATH=/home/rootless/bin:$PATH
export DOCKER_HOST=unix:///run/user/1001/docker.sock
```
- Reboot. Ensure that the Docker process is working.
- Create a directory for saving `act_runner` data between restarts
`mkdir /home/rootless/act_runner`
- Register the runner from the data directory
```bash
cd /home/rootless/act_runner
act_runner register
```
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system.
```bash
act_runner generate-config >/home/rootless/act_runner/config
```
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents:
```bash
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/act_runner
After=docker.service
[Service]
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/rootless/act_runner
TimeoutSec=0
RestartSec=2
Restart=always
StartLimitBurst=3
StartLimitInterval=60s
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
Type=notify
NotifyAccess=all
KillMode=mixed
[Install]
WantedBy=default.target
```
- Reboot
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea.
````bash
systemctl --user status act_runner
journalctl --user -xeu act_runner

89
go.mod
View File

@@ -3,109 +3,88 @@ module gitea.com/gitea/act_runner
go 1.20
require (
code.gitea.io/actions-proto-go v0.2.0
code.gitea.io/actions-proto-go v0.2.1
code.gitea.io/gitea-vet v0.2.3-0.20230113022436-2b1561217fa5
github.com/avast/retry-go/v4 v4.3.1
github.com/bufbuild/connect-go v1.3.1
github.com/docker/docker v23.0.1+incompatible
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/render v1.0.2
github.com/docker/docker v23.0.4+incompatible
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-isatty v0.0.18
github.com/nektos/act v0.0.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
golang.org/x/term v0.6.0
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.2
golang.org/x/term v0.7.0
golang.org/x/time v0.1.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.4.0
modernc.org/sqlite v1.14.2
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978
xorm.io/xorm v1.3.2
)
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/containerd/containerd v1.6.18 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/containerd/containerd v1.6.20 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/cli v23.0.4+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/goccy/go-json v0.8.1 // indirect
github.com/go-git/go-git/v5 v5.6.2-0.20230411180853-ce62f3e9ff86 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/buildkit v0.11.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/buildkit v0.11.6 // indirect
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.12.1 // indirect
github.com/onsi/gomega v1.10.3 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/opencontainers/runc v1.1.3 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rhysd/actionlint v1.6.23 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rhysd/actionlint v1.6.24 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/skeema/knownhosts v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.7.0 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.1.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/tools v0.8.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.35.18 // indirect
modernc.org/ccgo/v3 v3.12.82 // indirect
modernc.org/libc v1.11.87 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
)
replace github.com/nektos/act => gitea.com/gitea/act v0.243.2-0.20230323041428-929ea6df751b
replace github.com/nektos/act => gitea.com/gitea/act v0.245.2-0.20230606002131-6ce5c93cc815

794
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package artifactcache provides a cache handler for the runner.
//
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
//
// TODO: Authorization
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
package artifactcache

View File

@@ -1,416 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
_ "modernc.org/sqlite"
"xorm.io/builder"
"xorm.io/xorm"
)
const (
urlBase = "/_apis/artifactcache"
)
var logger = log.StandardLogger().WithField("module", "cache_request")
type Handler struct {
engine engine
storage *Storage
router *chi.Mux
listener net.Listener
gc atomic.Bool
gcAt time.Time
outboundIP string
}
func StartHandler(dir, outboundIP string, port uint16) (*Handler, error) {
h := &Handler{}
if dir == "" {
if home, err := os.UserHomeDir(); err != nil {
return nil, err
} else {
dir = filepath.Join(home, ".cache", "actcache")
}
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
e, err := xorm.NewEngine("sqlite", filepath.Join(dir, "sqlite.db"))
if err != nil {
return nil, err
}
if err := e.Sync(&Cache{}); err != nil {
return nil, err
}
h.engine = engine{e: e}
storage, err := NewStorage(filepath.Join(dir, "cache"))
if err != nil {
return nil, err
}
h.storage = storage
if outboundIP != "" {
h.outboundIP = outboundIP
} else if ip, err := getOutboundIP(); err != nil {
return nil, err
} else {
h.outboundIP = ip.String()
}
router := chi.NewRouter()
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}))
router.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r)
go h.gcCache()
})
})
router.Use(middleware.Logger)
router.Route(urlBase, func(r chi.Router) {
r.Get("/cache", h.find)
r.Route("/caches", func(r chi.Router) {
r.Post("/", h.reserve)
r.Route("/{id}", func(r chi.Router) {
r.Patch("/", h.upload)
r.Post("/", h.commit)
})
})
r.Get("/artifacts/{id}", h.get)
r.Post("/clean", h.clean)
})
h.router = router
h.gcCache()
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
if err != nil {
return nil, err
}
go func() {
if err := http.Serve(listener, h.router); err != nil {
logger.Errorf("http serve: %v", err)
}
}()
h.listener = listener
return h, nil
}
func (h *Handler) ExternalURL() string {
// TODO: make the external url configurable if necessary
return fmt.Sprintf("http://%s:%d",
h.outboundIP,
h.listener.Addr().(*net.TCPAddr).Port)
}
// GET /_apis/artifactcache/cache
func (h *Handler) find(w http.ResponseWriter, r *http.Request) {
keys := strings.Split(r.URL.Query().Get("keys"), ",")
version := r.URL.Query().Get("version")
cache, err := h.findCache(r.Context(), keys, version)
if err != nil {
responseJson(w, r, 500, err)
return
}
if cache == nil {
responseJson(w, r, 204)
return
}
if ok, err := h.storage.Exist(cache.ID); err != nil {
responseJson(w, r, 500, err)
return
} else if !ok {
_ = h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Delete(cache)
return err
})
responseJson(w, r, 204)
return
}
responseJson(w, r, 200, map[string]any{
"result": "hit",
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
"cacheKey": cache.Key,
})
}
// POST /_apis/artifactcache/caches
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request) {
cache := &Cache{}
if err := render.Bind(r, cache); err != nil {
responseJson(w, r, 400, err)
return
}
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
return sess.Where(builder.Eq{"key": cache.Key, "version": cache.Version}).Get(&Cache{})
}); err != nil {
responseJson(w, r, 500, err)
return
} else if ok {
responseJson(w, r, 400, fmt.Errorf("already exist"))
return
}
if err := h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Insert(cache)
return err
}); err != nil {
responseJson(w, r, 500, err)
return
}
responseJson(w, r, 200, map[string]any{
"cacheId": cache.ID,
})
return
}
// PATCH /_apis/artifactcache/caches/:id
func (h *Handler) upload(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responseJson(w, r, 400, err)
return
}
cache := &Cache{
ID: id,
}
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
return sess.Get(cache)
}); err != nil {
responseJson(w, r, 500, err)
return
} else if !ok {
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
return
}
if cache.Complete {
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
}
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
if err != nil {
responseJson(w, r, 400, err)
return
}
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
responseJson(w, r, 500, err)
}
h.useCache(r.Context(), id)
responseJson(w, r, 200)
}
// POST /_apis/artifactcache/caches/:id
func (h *Handler) commit(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responseJson(w, r, 400, err)
return
}
cache := &Cache{
ID: id,
}
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
return sess.Get(cache)
}); err != nil {
responseJson(w, r, 500, err)
return
} else if !ok {
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
return
}
if cache.Complete {
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
}
if err := h.storage.Commit(cache.ID, cache.Size); err != nil {
responseJson(w, r, 500, err)
return
}
cache.Complete = true
if err := h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.ID(cache.ID).Cols("complete").Update(cache)
return err
}); err != nil {
responseJson(w, r, 500, err)
return
}
responseJson(w, r, 200)
}
// GET /_apis/artifactcache/artifacts/:id
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responseJson(w, r, 400, err)
return
}
h.useCache(r.Context(), id)
h.storage.Serve(w, r, id)
}
// POST /_apis/artifactcache/clean
func (h *Handler) clean(w http.ResponseWriter, r *http.Request) {
// TODO: don't support force deleting cache entries
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
responseJson(w, r, 200)
}
// if not found, return (nil, nil) instead of an error.
func (h *Handler) findCache(ctx context.Context, keys []string, version string) (*Cache, error) {
if len(keys) == 0 {
return nil, nil
}
key := keys[0] // the first key is for exact match.
cache := &Cache{}
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
return sess.Where(builder.Eq{"key": key, "version": version, "complete": true}).Get(cache)
}); err != nil {
return nil, err
} else if ok {
return cache, nil
}
for _, prefix := range keys[1:] {
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
return sess.Where(builder.And(
builder.Like{"key", prefix + "%"},
builder.Eq{"version": version, "complete": true},
)).OrderBy("id DESC").Get(cache)
}); err != nil {
return nil, err
} else if ok {
return cache, nil
}
}
return nil, nil
}
func (h *Handler) useCache(ctx context.Context, id int64) {
// keep quiet
_ = h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Context(ctx).Cols("used_at").Update(&Cache{
ID: id,
UsedAt: time.Now().Unix(),
})
return err
})
}
func (h *Handler) gcCache() {
if h.gc.Load() {
return
}
if !h.gc.CompareAndSwap(false, true) {
return
}
defer h.gc.Store(false)
if time.Since(h.gcAt) < time.Hour {
logger.Infof("skip gc: %v", h.gcAt.String())
return
}
h.gcAt = time.Now()
logger.Infof("gc: %v", h.gcAt.String())
const (
keepUsed = 30 * 24 * time.Hour
keepUnused = 7 * 24 * time.Hour
keepTemp = 5 * time.Minute
)
var caches []*Cache
if err := h.engine.Exec(func(sess *xorm.Session) error {
return sess.Where(builder.And(builder.Lt{"used_at": time.Now().Add(-keepTemp).Unix()}, builder.Eq{"complete": false})).
Find(&caches)
}); err != nil {
logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
h.storage.Remove(cache.ID)
if err := h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Delete(cache)
return err
}); err != nil {
logger.Warnf("delete cache: %v", err)
continue
}
logger.Infof("deleted cache: %+v", cache)
}
}
caches = caches[:0]
if err := h.engine.Exec(func(sess *xorm.Session) error {
return sess.Where(builder.Lt{"used_at": time.Now().Add(-keepUnused).Unix()}).
Find(&caches)
}); err != nil {
logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
h.storage.Remove(cache.ID)
if err := h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Delete(cache)
return err
}); err != nil {
logger.Warnf("delete cache: %v", err)
continue
}
logger.Infof("deleted cache: %+v", cache)
}
}
caches = caches[:0]
if err := h.engine.Exec(func(sess *xorm.Session) error {
return sess.Where(builder.Lt{"created_at": time.Now().Add(-keepUsed).Unix()}).
Find(&caches)
}); err != nil {
logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
h.storage.Remove(cache.ID)
if err := h.engine.Exec(func(sess *xorm.Session) error {
_, err := sess.Delete(cache)
return err
}); err != nil {
logger.Warnf("delete cache: %v", err)
continue
}
logger.Infof("deleted cache: %+v", cache)
}
}
}

View File

@@ -1,30 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache
import (
"fmt"
"net/http"
)
type Cache struct {
ID int64 `xorm:"id pk autoincr" json:"-"`
Key string `xorm:"TEXT index unique(key_version)" json:"key"`
Version string `xorm:"TEXT unique(key_version)" json:"version"`
Size int64 `json:"cacheSize"`
Complete bool `xorm:"index(complete_used_at)" json:"-"`
UsedAt int64 `xorm:"index(complete_used_at) updated" json:"-"`
CreatedAt int64 `xorm:"index created" json:"-"`
}
// Bind implements render.Binder
func (c *Cache) Bind(_ *http.Request) error {
if c.Key == "" {
return fmt.Errorf("missing key")
}
if c.Version == "" {
return fmt.Errorf("missing version")
}
return nil
}

View File

@@ -1,129 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)
type Storage struct {
rootDir string
}
func NewStorage(rootDir string) (*Storage, error) {
if err := os.MkdirAll(rootDir, 0o755); err != nil {
return nil, err
}
return &Storage{
rootDir: rootDir,
}, nil
}
func (s *Storage) Exist(id int64) (bool, error) {
name := s.filename(id)
if _, err := os.Stat(name); os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *Storage) Write(id int64, offset int64, reader io.Reader) error {
name := s.tempName(id, offset)
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
return err
}
file, err := os.Create(name)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, reader)
return err
}
func (s *Storage) Commit(id int64, size int64) error {
defer func() {
_ = os.RemoveAll(s.tempDir(id))
}()
name := s.filename(id)
tempNames, err := s.tempNames(id)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
return err
}
file, err := os.Create(name)
if err != nil {
return err
}
defer file.Close()
var written int64
for _, v := range tempNames {
f, err := os.Open(v)
if err != nil {
return err
}
n, err := io.Copy(file, f)
_ = f.Close()
if err != nil {
return err
}
written += n
}
if written != size {
_ = file.Close()
_ = os.Remove(name)
return fmt.Errorf("broken file: %v != %v", written, size)
}
return nil
}
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id int64) {
name := s.filename(id)
http.ServeFile(w, r, name)
}
func (s *Storage) Remove(id int64) {
_ = os.Remove(s.filename(id))
_ = os.RemoveAll(s.tempDir(id))
}
func (s *Storage) filename(id int64) string {
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
}
func (s *Storage) tempDir(id int64) string {
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
}
func (s *Storage) tempName(id, offset int64) string {
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
}
func (s *Storage) tempNames(id int64) ([]string, error) {
dir := s.tempDir(id)
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var names []string
for _, v := range files {
if !v.IsDir() {
names = append(names, filepath.Join(dir, v.Name()))
}
}
return names, nil
}

View File

@@ -1,100 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache
import (
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"github.com/go-chi/render"
"xorm.io/xorm"
)
func responseJson(w http.ResponseWriter, r *http.Request, code int, v ...any) {
render.Status(r, code)
if len(v) == 0 || v[0] == nil {
render.JSON(w, r, struct{}{})
} else if err, ok := v[0].(error); ok {
logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
render.JSON(w, r, map[string]any{
"error": err.Error(),
})
} else {
render.JSON(w, r, v[0])
}
}
func parseContentRange(s string) (int64, int64, error) {
// support the format like "bytes 11-22/*" only
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
s1, s2, _ := strings.Cut(s, "-")
start, err := strconv.ParseInt(s1, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
}
stop, err := strconv.ParseInt(s2, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
}
return start, stop, nil
}
func getOutboundIP() (net.IP, error) {
// FIXME: It makes more sense to use the gateway IP address of container network
if conn, err := net.Dial("udp", "8.8.8.8:80"); err == nil {
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP, nil
}
if ifaces, err := net.Interfaces(); err == nil {
for _, i := range ifaces {
if addrs, err := i.Addrs(); err == nil {
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip.IsGlobalUnicast() {
return ip, nil
}
}
}
}
}
return nil, fmt.Errorf("no outbound IP address found")
}
// engine is a wrapper of *xorm.Engine, with a lock.
// To avoid racing of sqlite, we don't care performance here.
type engine struct {
e *xorm.Engine
m sync.Mutex
}
func (e *engine) Exec(f func(*xorm.Session) error) error {
e.m.Lock()
defer e.m.Unlock()
sess := e.e.NewSession()
defer sess.Close()
return f(sess)
}
func (e *engine) ExecBool(f func(*xorm.Session) (bool, error)) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
sess := e.e.NewSession()
defer sess.Close()
return f(sess)
}

View File

@@ -7,6 +7,10 @@ import (
"context"
"fmt"
"os"
"path"
"runtime"
"strconv"
"strings"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
@@ -23,14 +27,13 @@ import (
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
log.Infoln("Starting runner daemon")
cfg, err := config.LoadDefault(*configFile)
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
initLogging(cfg)
log.Infoln("Starting runner daemon")
reg, err := config.LoadRegistration(cfg.Runner.File)
if os.IsNotExist(err) {
@@ -79,10 +82,11 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
// initLogging setup the global logrus logger.
func initLogging(cfg *config.Config) {
isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{
format := &log.TextFormatter{
DisableColors: !isTerm,
FullTimestamp: true,
})
}
log.SetFormatter(format)
if l := cfg.Log.Level; l != "" {
level, err := log.ParseLevel(l)
@@ -90,6 +94,22 @@ func initLogging(cfg *config.Config) {
log.WithError(err).
Errorf("invalid log level: %q", l)
}
// debug level
if level == log.DebugLevel {
log.SetReportCaller(true)
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
// get function name
s := strings.Split(f.Function, ".")
funcname := "[" + s[len(s)-1] + "]"
// get file name and line number
_, filename := path.Split(f.File)
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
return funcname, filename
}
log.SetFormatter(format)
}
if log.GetLevel() != level {
log.Infof("log level changed to %v", level)
log.SetLevel(level)

View File

@@ -13,7 +13,9 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/joho/godotenv"
"github.com/nektos/act/pkg/artifactcache"
"github.com/nektos/act/pkg/artifacts"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
@@ -21,8 +23,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
"gitea.com/gitea/act_runner/internal/app/artifactcache"
)
type executeArgs struct {
@@ -57,6 +57,7 @@ type executeArgs struct {
dryrun bool
image string
cacheHandler *artifactcache.Handler
network string
}
// WorkflowsPath returns path to workflow file(s)
@@ -314,7 +315,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
if len(execArgs.event) > 0 {
log.Infof("Using chosed event for filtering: %s", execArgs.event)
eventName = args[0]
eventName = execArgs.event
} else if len(events) == 1 && len(events[0]) > 0 {
log.Infof("Using the only detected workflow event: %s", events[0])
eventName = events[0]
@@ -349,13 +350,31 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
}
// init a cache server
handler, err := artifactcache.StartHandler("", "", 0)
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
if err != nil {
return err
}
log.Infof("cache handler listens on: %v", handler.ExternalURL())
execArgs.cacheHandler = handler
if len(execArgs.artifactServerAddr) == 0 {
if ip := common.GetOutboundIP(); ip == nil {
return fmt.Errorf("unable to determine outbound IP address")
} else {
execArgs.artifactServerAddr = ip.String()
}
}
if len(execArgs.artifactServerPath) == 0 {
tempDir, err := os.MkdirTemp("", "gitea-act-")
if err != nil {
fmt.Println(err)
}
defer os.RemoveAll(tempDir)
execArgs.artifactServerPath = tempDir
}
// run the plan
config := &runner.Config{
Workdir: execArgs.Workdir(),
@@ -380,40 +399,29 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
AutoRemove: true,
ArtifactServerPath: execArgs.artifactServerPath,
ArtifactServerPort: execArgs.artifactServerPort,
ArtifactServerAddr: execArgs.artifactServerAddr,
NoSkipCheckout: execArgs.noSkipCheckout,
// PresetGitHubContext: preset,
// EventJSON: string(eventJSON),
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
ContainerMaxLifetime: maxLifetime,
ContainerNetworkMode: "bridge",
ContainerNetworkMode: container.NetworkMode(execArgs.network),
DefaultActionInstance: execArgs.defaultActionsUrl,
PlatformPicker: func(_ []string) string {
return execArgs.image
},
}
// TODO: handle log level config
// waiting https://gitea.com/gitea/act/pulls/19
// if !execArgs.debug {
// logLevel := log.Level(log.InfoLevel)
// config.JobLoggerLevel = &logLevel
// }
if !execArgs.debug {
logLevel := log.Level(log.InfoLevel)
config.JobLoggerLevel = &logLevel
}
r, err := runner.New(config)
if err != nil {
return err
}
if len(execArgs.artifactServerPath) == 0 {
tempDir, err := os.MkdirTemp("", "gitea-act-")
if err != nil {
fmt.Println(err)
}
defer os.RemoveAll(tempDir)
execArgs.artifactServerPath = tempDir
}
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
@@ -460,12 +468,14 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "Defines the address where the artifact server listens")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsUrl, "default-actions-url", "", "https://gitea.com", "Defines the default url of action instance.")
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
return execCmd
}

View File

@@ -4,7 +4,6 @@
package run
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -14,12 +13,13 @@ import (
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/docker/docker/api/types/container"
"github.com/nektos/act/pkg/artifactcache"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus"
"gitea.com/gitea/act_runner/internal/app/artifactcache"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels"
@@ -52,7 +52,12 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
envs[k] = v
}
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
cacheHandler, err := artifactcache.StartHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port)
cacheHandler, err := artifactcache.StartHandler(
cfg.Cache.Dir,
cfg.Cache.Host,
cfg.Cache.Port,
log.StandardLogger().WithField("module", "cache_request"),
)
if err != nil {
log.Errorf("cannot init cache server, it will be disabled: %v", err)
// go on
@@ -65,6 +70,10 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
// Set specific environments to distinguish between Gitea and GitHub
envs["GITEA_ACTIONS"] = "true"
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
return &Runner{
name: reg.Name,
cfg: cfg,
@@ -108,16 +117,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
reporter.Logf("%s(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
workflow, jobID, err := generateWorkflow(task)
if err != nil {
return err
}
jobIDs := workflow.GetJobIDs()
if len(jobIDs) != 1 {
return fmt.Errorf("multiple jobs found: %v", jobIDs)
}
jobID := jobIDs[0]
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
if err != nil {
return err
@@ -168,30 +172,31 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
runnerConfig := &runner.Config{
// On Linux, Workdir will be like "/<owner>/<repo>"
// On Windows, Workdir will be like "\<owner>\<repo>"
Workdir: filepath.FromSlash(string(filepath.Separator) + preset.Repository),
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", r.cfg.Container.WorkdirParent, preset.Repository)),
BindWorkdir: false,
ReuseContainers: false,
ForcePull: false,
ForceRebuild: false,
LogOutput: true,
JSONLogger: false,
Env: r.envs,
Secrets: task.Secrets,
GitHubInstance: r.client.Address(),
AutoRemove: true,
NoSkipCheckout: true,
PresetGitHubContext: preset,
EventJSON: string(eventJSON),
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
ContainerMaxLifetime: maxLifetime,
ContainerNetworkMode: r.cfg.Container.NetworkMode,
ContainerOptions: r.cfg.Container.Options,
Privileged: r.cfg.Container.Privileged,
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
PlatformPicker: r.labels.PickPlatform,
ReuseContainers: false,
ForcePull: false,
ForceRebuild: false,
LogOutput: true,
JSONLogger: false,
Env: r.envs,
Secrets: task.Secrets,
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
AutoRemove: true,
NoSkipCheckout: true,
PresetGitHubContext: preset,
EventJSON: string(eventJSON),
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
ContainerMaxLifetime: maxLifetime,
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
ContainerOptions: r.cfg.Container.Options,
Privileged: r.cfg.Container.Privileged,
DefaultActionsURLs: parseDefaultActionsURLs(taskContext["gitea_default_actions_url"].GetStringValue()),
PlatformPicker: r.labels.PickPlatform,
Vars: task.Vars,
}
rr, err := runner.New(runnerConfig)
@@ -205,5 +210,17 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
// add logger recorders
ctx = common.WithLoggerHook(ctx, reporter)
return executor(ctx)
execErr := executor(ctx)
reporter.SetOutputs(job.Outputs)
return execErr
}
func parseDefaultActionsURLs(s string) []string {
urls := strings.Split(s, ",")
trimmed := make([]string, 0, len(urls))
for _, u := range urls {
t := strings.TrimRight(strings.TrimSpace(u), "/")
trimmed = append(trimmed, t)
}
return trimmed
}

View File

@@ -0,0 +1,54 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"bytes"
"fmt"
"sort"
"strings"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3"
)
func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
if err != nil {
return nil, "", err
}
jobIDs := workflow.GetJobIDs()
if len(jobIDs) != 1 {
return nil, "", fmt.Errorf("multiple jobs found: %v", jobIDs)
}
jobID := jobIDs[0]
needJobIDs := make([]string, 0, len(task.Needs))
for id, need := range task.Needs {
needJobIDs = append(needJobIDs, id)
needJob := &model.Job{
Outputs: need.Outputs,
Result: strings.ToLower(strings.TrimPrefix(need.Result.String(), "RESULT_")),
}
workflow.Jobs[id] = needJob
}
sort.Strings(needJobIDs)
rawNeeds := yaml.Node{
Kind: yaml.SequenceNode,
Content: make([]*yaml.Node, 0, len(needJobIDs)),
}
for _, id := range needJobIDs {
rawNeeds.Content = append(rawNeeds.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: id,
})
}
workflow.Jobs[jobID].RawNeeds = rawNeeds
return workflow, jobID, nil
}

View File

@@ -0,0 +1,74 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"testing"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/nektos/act/pkg/model"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
)
func Test_generateWorkflow(t *testing.T) {
type args struct {
task *runnerv1.Task
}
tests := []struct {
name string
args args
assert func(t *testing.T, wf *model.Workflow)
want1 string
wantErr bool
}{
{
name: "has needs",
args: args{
task: &runnerv1.Task{
WorkflowPayload: []byte(`
name: Build and deploy
on: push
jobs:
job9:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: ./deploy --build ${{ needs.job1.outputs.output1 }}
- run: ./deploy --build ${{ needs.job2.outputs.output2 }}
`),
Needs: map[string]*runnerv1.TaskNeed{
"job1": {
Outputs: map[string]string{
"output1": "output1 value",
},
Result: runnerv1.Result_RESULT_SUCCESS,
},
"job2": {
Outputs: map[string]string{
"output2": "output2 value",
},
Result: runnerv1.Result_RESULT_SUCCESS,
},
},
},
},
assert: func(t *testing.T, wf *model.Workflow) {
assert.DeepEqual(t, wf.GetJob("job9").Needs(), []string{"job1", "job2"})
},
want1: "job9",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := generateWorkflow(tt.args.task)
require.NoError(t, err)
tt.assert(t, got)
assert.Equal(t, got1, tt.want1)
})
}
}

View File

@@ -9,6 +9,8 @@ import (
)
// A Client manages communication with the runner.
//
//go:generate mockery --name Client
type Client interface {
pingv1connect.PingServiceClient
runnerv1connect.RunnerServiceClient

View File

@@ -0,0 +1,193 @@
// Code generated by mockery v2.26.1. DO NOT EDIT.
package mocks
import (
context "context"
connect "github.com/bufbuild/connect-go"
mock "github.com/stretchr/testify/mock"
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
// Address provides a mock function with given fields:
func (_m *Client) Address() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// FetchTask provides a mock function with given fields: _a0, _a1
func (_m *Client) FetchTask(_a0 context.Context, _a1 *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
ret := _m.Called(_a0, _a1)
var r0 *connect.Response[runnerv1.FetchTaskResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) *connect.Response[runnerv1.FetchTaskResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.FetchTaskResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Insecure provides a mock function with given fields:
func (_m *Client) Insecure() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Ping provides a mock function with given fields: _a0, _a1
func (_m *Client) Ping(_a0 context.Context, _a1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
ret := _m.Called(_a0, _a1)
var r0 *connect.Response[pingv1.PingResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) *connect.Response[pingv1.PingResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[pingv1.PingResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[pingv1.PingRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Register provides a mock function with given fields: _a0, _a1
func (_m *Client) Register(_a0 context.Context, _a1 *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error) {
ret := _m.Called(_a0, _a1)
var r0 *connect.Response[runnerv1.RegisterResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) *connect.Response[runnerv1.RegisterResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.RegisterResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLog provides a mock function with given fields: _a0, _a1
func (_m *Client) UpdateLog(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
ret := _m.Called(_a0, _a1)
var r0 *connect.Response[runnerv1.UpdateLogResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) *connect.Response[runnerv1.UpdateLogResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateLogResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTask provides a mock function with given fields: _a0, _a1
func (_m *Client) UpdateTask(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
ret := _m.Called(_a0, _a1)
var r0 *connect.Response[runnerv1.UpdateTaskResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) *connect.Response[runnerv1.UpdateTaskResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateTaskResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewClient interface {
mock.TestingT
Cleanup(func())
}
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewClient(t mockConstructorTestingTNewClient) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -42,9 +42,14 @@ cache:
port: 0
container:
# Which network to use for the job containers. Could be bridge, host, none, or the name of a custom network.
network_mode: bridge
# Specifies the network to which the container will connect.
# Could be host, bridge or the name of a custom network.
# If it's empty, act_runner will create a network automatically.
network: ""
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
privileged: false
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
options:
# The parent directory of a job's working directory.
# If it's empty, /workspace will be used.
workdir_parent:

View File

@@ -10,34 +10,50 @@ import (
"time"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
// Log represents the configuration for logging.
type Log struct {
Level string `yaml:"level"` // Level indicates the logging level.
}
// Runner represents the configuration for the runner.
type Runner struct {
File string `yaml:"file"` // File specifies the file path for the runner.
Capacity int `yaml:"capacity"` // Capacity specifies the capacity of the runner.
Envs map[string]string `yaml:"envs"` // Envs stores environment variables for the runner.
EnvFile string `yaml:"env_file"` // EnvFile specifies the path to the file containing environment variables for the runner.
Timeout time.Duration `yaml:"timeout"` // Timeout specifies the duration for runner timeout.
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
}
// Cache represents the configuration for caching.
type Cache struct {
Enabled *bool `yaml:"enabled"` // Enabled indicates whether caching is enabled. It is a pointer to distinguish between false and not set. If not set, it will be true.
Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
Host string `yaml:"host"` // Host specifies the caching host.
Port uint16 `yaml:"port"` // Port specifies the caching port.
}
// Container represents the configuration for the container.
type Container struct {
Network string `yaml:"network"` // Network specifies the network for the container.
NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
Options string `yaml:"options"` // Options specifies additional options for the container.
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
}
// Config represents the overall configuration.
type Config struct {
Log struct {
Level string `yaml:"level"`
} `yaml:"log"`
Runner struct {
File string `yaml:"file"`
Capacity int `yaml:"capacity"`
Envs map[string]string `yaml:"envs"`
EnvFile string `yaml:"env_file"`
Timeout time.Duration `yaml:"timeout"`
Insecure bool `yaml:"insecure"`
FetchTimeout time.Duration `yaml:"fetch_timeout"`
FetchInterval time.Duration `yaml:"fetch_interval"`
} `yaml:"runner"`
Cache struct {
Enabled *bool `yaml:"enabled"` // pointer to distinguish between false and not set, and it will be true if not set
Dir string `yaml:"dir"`
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
} `yaml:"cache"`
Container struct {
NetworkMode string `yaml:"network_mode"`
Privileged bool `yaml:"privileged"`
Options string `yaml:"options"`
} `yaml:"container"`
Log Log `yaml:"log"` // Log represents the configuration for logging.
Runner Runner `yaml:"runner"` // Runner represents the configuration for the runner.
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
Container Container `yaml:"container"` // Container represents the configuration for the container.
}
// LoadDefault returns the default configuration.
@@ -91,8 +107,8 @@ func LoadDefault(file string) (*Config, error) {
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
}
}
if cfg.Container.NetworkMode == "" {
cfg.Container.NetworkMode = "bridge"
if cfg.Container.WorkdirParent == "" {
cfg.Container.WorkdirParent = "workspace"
}
if cfg.Runner.FetchTimeout <= 0 {
cfg.Runner.FetchTimeout = 5 * time.Second
@@ -101,5 +117,18 @@ func LoadDefault(file string) (*Config, error) {
cfg.Runner.FetchInterval = 2 * time.Second
}
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
log.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
if cfg.Container.NetworkMode == "bridge" {
// Previously, if the value of `container.network_mode` is `bridge`, we will create a new network for job.
// But “bridge” is easily confused with the bridge network created by Docker by default.
// So we set the value of `container.network` to empty string to make `act_runner` automatically create a new network for job.
cfg.Container.Network = ""
} else {
cfg.Container.Network = cfg.Container.NetworkMode
}
}
return cfg, nil
}

View File

@@ -6,6 +6,7 @@ package report
import (
"context"
"fmt"
"regexp"
"strings"
"sync"
"time"
@@ -31,8 +32,14 @@ type Reporter struct {
logOffset int
logRows []*runnerv1.LogRow
logReplacer *strings.Replacer
state *runnerv1.TaskState
stateM sync.RWMutex
oldnew []string
state *runnerv1.TaskState
stateMu sync.RWMutex
outputs sync.Map
debugOutputEnabled bool
stopCommandEndToken string
}
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
@@ -44,20 +51,27 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
oldnew = append(oldnew, v, "***")
}
return &Reporter{
rv := &Reporter{
ctx: ctx,
cancel: cancel,
client: client,
oldnew: oldnew,
logReplacer: strings.NewReplacer(oldnew...),
state: &runnerv1.TaskState{
Id: task.Id,
},
}
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
rv.debugOutputEnabled = true
}
return rv
}
func (r *Reporter) ResetSteps(l int) {
r.stateM.Lock()
defer r.stateM.Unlock()
r.stateMu.Lock()
defer r.stateMu.Unlock()
for i := 0; i < l; i++ {
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
Id: int64(i),
@@ -69,9 +83,16 @@ func (r *Reporter) Levels() []log.Level {
return log.AllLevels
}
func appendIfNotNil[T any](s []*T, v *T) []*T {
if v != nil {
return append(s, v)
}
return s
}
func (r *Reporter) Fire(entry *log.Entry) error {
r.stateM.Lock()
defer r.stateM.Unlock()
r.stateMu.Lock()
defer r.stateMu.Unlock()
log.WithFields(entry.Data).Trace(entry.Message)
@@ -95,7 +116,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
}
}
if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
return nil
}
@@ -108,7 +129,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
}
if step == nil {
if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
return nil
}
@@ -118,14 +139,16 @@ func (r *Reporter) Fire(entry *log.Entry) error {
}
if v, ok := entry.Data["raw_output"]; ok {
if rawOutput, ok := v.(bool); ok && rawOutput {
if step.LogLength == 0 {
step.LogIndex = int64(r.logOffset + len(r.logRows))
if row := r.parseLogRow(entry); row != nil {
if step.LogLength == 0 {
step.LogIndex = int64(r.logOffset + len(r.logRows))
}
step.LogLength++
r.logRows = append(r.logRows, row)
}
step.LogLength++
r.logRows = append(r.logRows, r.parseLogRow(entry))
}
} else if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
if v, ok := entry.Data["stepResult"]; ok {
if stepResult, ok := r.parseResult(v); ok {
@@ -155,9 +178,13 @@ func (r *Reporter) RunDaemon() {
}
func (r *Reporter) Logf(format string, a ...interface{}) {
r.stateM.Lock()
defer r.stateM.Unlock()
r.stateMu.Lock()
defer r.stateMu.Unlock()
r.logf(format, a...)
}
func (r *Reporter) logf(format string, a ...interface{}) {
if !r.duringSteps() {
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
@@ -166,10 +193,30 @@ func (r *Reporter) Logf(format string, a ...interface{}) {
}
}
func (r *Reporter) SetOutputs(outputs map[string]string) {
r.stateMu.Lock()
defer r.stateMu.Unlock()
for k, v := range outputs {
if len(k) > 255 {
r.logf("ignore output because the key is too long: %q", k)
continue
}
if l := len(v); l > 1024*1024 {
log.Println("ignore output because the value is too long:", k, l)
r.logf("ignore output because the value %q is too long: %d", k, l)
}
if _, ok := r.outputs.Load(k); ok {
continue
}
r.outputs.Store(k, v)
}
}
func (r *Reporter) Close(lastWords string) error {
r.closed = true
r.stateM.Lock()
r.stateMu.Lock()
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
if lastWords == "" {
lastWords = "Early termination"
@@ -184,14 +231,14 @@ func (r *Reporter) Close(lastWords string) error {
Time: timestamppb.Now(),
Content: lastWords,
})
return nil
r.state.StoppedAt = timestamppb.Now()
} else if lastWords != "" {
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: lastWords,
})
}
r.stateM.Unlock()
r.stateMu.Unlock()
return retry.Do(func() error {
if err := r.ReportLog(true); err != nil {
@@ -205,9 +252,9 @@ func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateM.RLock()
r.stateMu.RLock()
rows := r.logRows
r.stateM.RUnlock()
r.stateMu.RUnlock()
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
TaskId: r.state.Id,
@@ -224,10 +271,10 @@ func (r *Reporter) ReportLog(noMore bool) error {
return fmt.Errorf("submitted logs are lost")
}
r.stateM.Lock()
r.stateMu.Lock()
r.logRows = r.logRows[ack-r.logOffset:]
r.logOffset = ack
r.stateM.Unlock()
r.stateMu.Unlock()
if noMore && ack < r.logOffset+len(rows) {
return fmt.Errorf("not all logs are submitted")
@@ -240,21 +287,45 @@ func (r *Reporter) ReportState() error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateM.RLock()
r.stateMu.RLock()
state := proto.Clone(r.state).(*runnerv1.TaskState)
r.stateM.RUnlock()
r.stateMu.RUnlock()
outputs := make(map[string]string)
r.outputs.Range(func(k, v interface{}) bool {
if val, ok := v.(string); ok {
outputs[k.(string)] = val
}
return true
})
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
State: state,
State: state,
Outputs: outputs,
}))
if err != nil {
return err
}
for _, k := range resp.Msg.SentOutputs {
r.outputs.Store(k, struct{}{})
}
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
r.cancel()
}
var noSent []string
r.outputs.Range(func(k, v interface{}) bool {
if _, ok := v.(string); ok {
noSent = append(noSent, k.(string))
}
return true
})
if len(noSent) > 0 {
return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
}
return nil
}
@@ -288,11 +359,70 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
return ret, ok
}
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
return &originalContent
}
switch command {
case "add-mask":
r.addMask(value)
return nil
case "debug":
if r.debugOutputEnabled {
return &value
}
return nil
case "notice":
// Not implemented yet, so just return the original content.
return &originalContent
case "warning":
// Not implemented yet, so just return the original content.
return &originalContent
case "error":
// Not implemented yet, so just return the original content.
return &originalContent
case "group":
// Returning the original content, because I think the frontend
// will use it when rendering the output.
return &originalContent
case "endgroup":
// Ditto
return &originalContent
case "stop-commands":
r.stopCommandEndToken = value
return nil
case r.stopCommandEndToken:
r.stopCommandEndToken = ""
return nil
}
return &originalContent
}
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
matches := cmdRegex.FindStringSubmatch(content)
if matches != nil {
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
content = *output
} else {
return nil
}
}
content = r.logReplacer.Replace(content)
return &runnerv1.LogRow{
Time: timestamppb.New(entry.Time),
Content: content,
}
}
func (r *Reporter) addMask(msg string) {
r.oldnew = append(r.oldnew, msg, "***")
r.logReplacer = strings.NewReplacer(r.oldnew...)
}

View File

@@ -0,0 +1,197 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package report
import (
"context"
"strings"
"testing"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
connect_go "github.com/bufbuild/connect-go"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
)
func TestReporter_parseLogRow(t *testing.T) {
tests := []struct {
name string
debugOutputEnabled bool
args []string
want []string
}{
{
"No command", false,
[]string{"Hello, world!"},
[]string{"Hello, world!"},
},
{
"Add-mask", false,
[]string{
"foo mysecret bar",
"::add-mask::mysecret",
"foo mysecret bar",
},
[]string{
"foo mysecret bar",
"<nil>",
"foo *** bar",
},
},
{
"Debug enabled", true,
[]string{
"::debug::GitHub Actions runtime token access controls",
},
[]string{
"GitHub Actions runtime token access controls",
},
},
{
"Debug not enabled", false,
[]string{
"::debug::GitHub Actions runtime token access controls",
},
[]string{
"<nil>",
},
},
{
"notice", false,
[]string{
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
[]string{
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
},
{
"warning", false,
[]string{
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
[]string{
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
},
{
"error", false,
[]string{
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
[]string{
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
},
},
{
"group", false,
[]string{
"::group::",
"::endgroup::",
},
[]string{
"::group::",
"::endgroup::",
},
},
{
"stop-commands", false,
[]string{
"::add-mask::foo",
"::stop-commands::myverycoolstoptoken",
"::add-mask::bar",
"::debug::Stuff",
"myverycoolstoptoken",
"::add-mask::baz",
"::myverycoolstoptoken::",
"::add-mask::wibble",
"foo bar baz wibble",
},
[]string{
"<nil>",
"<nil>",
"::add-mask::bar",
"::debug::Stuff",
"myverycoolstoptoken",
"::add-mask::baz",
"<nil>",
"<nil>",
"*** bar baz ***",
},
},
{
"unknown command", false,
[]string{
"::set-mask::foo",
},
[]string{
"::set-mask::foo",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Reporter{
logReplacer: strings.NewReplacer(),
debugOutputEnabled: tt.debugOutputEnabled,
}
for idx, arg := range tt.args {
rv := r.parseLogRow(&log.Entry{Message: arg})
got := "<nil>"
if rv != nil {
got = rv.Content
}
assert.Equal(t, tt.want[idx], got)
}
})
}
}
func TestReporter_Fire(t *testing.T) {
t.Run("ignore command lines", func(t *testing.T) {
client := mocks.NewClient(t)
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
t.Logf("Received UpdateLog: %s", req.Msg.String())
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
}), nil
})
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
t.Logf("Received UpdateTask: %s", req.Msg.String())
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
})
ctx, cancel := context.WithCancel(context.Background())
taskCtx, err := structpb.NewStruct(map[string]interface{}{})
require.NoError(t, err)
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
Context: taskCtx,
})
defer func() {
assert.NoError(t, reporter.Close(""))
}()
reporter.ResetSteps(5)
dataStep0 := map[string]interface{}{
"stage": "Main",
"stepNumber": 0,
"raw_output": true,
}
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength)
})
}

2
run.sh
View File

@@ -41,5 +41,7 @@ if [[ ! -s .runner ]]; then
fi
done
fi
# Prevent reading the token from the act_runner process
unset GITEA_RUNNER_REGISTRATION_TOKEN
act_runner daemon ${CONFIG_ARG}