208 Commits
v0.0.1 ... main

Author SHA1 Message Date
f58a6daa6c [汉化] 汉化测试用例、修复部分龙架构测试用例失效的问题 2025-05-09 15:17:16 +08:00
54308690d0 [冲突修复] 修复和上游的冲突文件 2025-05-09 11:05:26 +08:00
4b90fa4325 [汉化] 更新main 2025-05-09 10:46:03 +08:00
5302c25feb Add environment variables for OIDC token service (#674)
Resurrecting [this PR](https://gitea.com/gitea/act_runner/pulls/272) (a dependency of [this one](https://github.com/go-gitea/gitea/pull/33945)) after the original author [lost motivation](https://github.com/go-gitea/gitea/pull/25664#issuecomment-2737099259) - though, to be clear, all credit belongs to them, and all blame for mistakes or misunderstandings to me.

Co-authored-by: Søren L. Hansen <sorenisanerd@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/674
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jack Jackson <scubbojj@gmail.com>
Co-committed-by: Jack Jackson <scubbojj@gmail.com>
2025-05-08 01:58:31 +00:00
a616ed1a10 feat: register interactive with values from cli (#682)
I used to be able to do something like `./act_runner register --instance https://gitea.com --token testdcff --name test` on GitHub Actions Runners, but act_runner always asked me to enter instance, token etc. again and requiring me to use `--no-interactive` including passing everything per cli.

My idea was to extract the preset input of some stages to skip the prompt for this value if it is a non empty string. Labels is the only question that has been asked more than once if validation failed, in this case the error path have to unset the values of the input structure to not end in a non-interactive loop.

_I have written this initially for my own gitea runner, might be useful to everyone using the official runner as well_

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/682
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-05-08 01:57:53 +00:00
f0b5aff3bb fix: invalid label NoInteractive exit code (#683)
* add test
* return validation error not nil from function

Closes #665

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/683
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-05-07 17:17:26 +00:00
44b4736703 feat: docker env vars for ephemeral and once (#685)
* GITEA_RUNNER_EPHEMERAL=1
* GITEA_RUNNER_ONCE=1

Related https://gitea.com/gitea/act_runner/issues/684

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/685
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-05-07 15:43:05 +00:00
e9c297600c [汉化] 添加一些运行时候调用函数的注释, 帮助理解运行流程 2025-05-07 18:15:42 +08:00
19bcf7e525 [汉化] 新增汉化两个文件修复poller中的汉化导致的一些异 2025-05-07 09:43:17 +08:00
e5259a8fb7 [汉化] 汉化项目的所有输出和各个注释方便阅读, 并且按照上游格式化进行格式化 2025-04-21 09:53:42 +08:00
9bed659875 [汉化] 翻译实例文档和修改名称 2025-04-20 15:50:01 +08:00
b1ae30dda8 ephemeral act runner (#649)
Works for both interactive and non-interactive registration mode.

A further enhancement would be jitconfig support of the daemon command, because after some changes in Gitea Actions the registration token became reusable.

removing runner and fail seems not possible at the current api level

Part of https://github.com/go-gitea/gitea/pull/33570

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/649
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-03-13 21:57:44 +00:00
0d687268c7 act_runner requires go 1.24 now 2025-03-02 05:36:24 +00:00
425a570261 use new docker image URLs (#661)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/661
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2025-03-01 20:21:52 +00:00
4c8179ee12 upgrade to go1.24, act to 0.261.4 and actions-proto-go to 0.4.1 (#662)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/662
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
2025-03-01 20:18:36 +00:00
5ae13f0bd7 Update xgo version to 1.24 (#651)
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/651
Co-authored-by: Pablo Carranza <pcarranza@gmail.com>
Co-committed-by: Pablo Carranza <pcarranza@gmail.com>
2025-02-15 16:07:18 +00:00
3510152e36 Fix Makefile make docker (#641)
Fix #640

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/641
2025-01-29 03:27:47 +00:00
8dfb805c62 Update examples/kubernetes/dind-docker.yaml to reflect recent changes to Dockerfile (#633)
With the changes made two months ago for the Dockerfile /opt/act/run.sh no longer exists in the docker container, this caused this example to fail, updating the example so that it correctly references run.sh now located in /usr/local/bin

I have used this to deploy on my own cluster and it is now working swimmingly

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/633
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: armistace <armistace@noreply.gitea.com>
Co-committed-by: armistace <armistace@noreply.gitea.com>
2025-01-26 02:10:17 +00:00
a7080f5457 Update examples for GITEA_RUNNER_REGISTRATION_TOKEN env (#630)
For https://github.com/go-gitea/gitea/pull/32946

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/630
Reviewed-by: Lunny Xiao <lunny@noreply.gitea.com>
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-01-05 22:25:32 +00:00
8b72d1c7ae add s390x and riscv64 as an arch for binaries 2024-12-09 18:49:38 +00:00
8bc0275e74 feat: add once flag to daemon command (#19) (#598)
Once flag polls and completes one job then exits.

I use this with Windows Sandbox (and creating users with local brew install on Mac) to create a fresh environment every time.

Co-authored-by: Garet Halliday <garet@pit.dev>
Co-authored-by: Jason Song <wolfogre@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/598
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-by: Jason Song <wolfogre@noreply.gitea.com>
Co-authored-by: garet90 <garet90@noreply.gitea.com>
Co-committed-by: garet90 <garet90@noreply.gitea.com>
2024-11-06 17:16:08 +00:00
0348aaac59 Wait for the Docker daemon to be ready before starting act runner (#620)
Follow #619.

Wait for the Docker daemon to be ready before starting act runner.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/620
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-11-06 07:38:31 +00:00
9712481bed Support basic, dind and dind-rootless as multiple kinds of images (#619)
- `basic`: Only the runner process in the container; users need to mount the Docker socket to it.
- `dind`: A Docker daemon will be started in the container with the root user.
- `dind-rootless`: A Docker daemon will be started in the container with a rootless user.

Use s6 instead of supervisord to start processes.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/619
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-11-06 03:15:51 +00:00
b5f901b2d9 Upgrade act from v0.261.2 -> v0.261.3 (#607)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/607
Reviewed-by: Jason Song <wolfogre@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-10-18 04:33:57 +00:00
0e2a3e00f5 examples/vm/rootless-docker.md aktualisiert (#487)
Depending on the VM's existing users the id can vary

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/487
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: frank-dspeed <frank-dspeed@noreply.gitea.com>
Co-committed-by: frank-dspeed <frank-dspeed@noreply.gitea.com>
2024-09-30 01:55:04 +00:00
b282356e9e update example for docker-compose to allow fix 502 errors in case Gitea not yet ready on runner startup (#605)
Minimalistic approach: Only adds what is needed to fix #600
Context: https://blog.schallbert.de/en/fix-gitea-runner/

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/605
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Co-authored-by: Schallbert <schallbert@mailbox.org>
Co-committed-by: Schallbert <schallbert@mailbox.org>
2024-09-26 05:54:54 +00:00
b075e3a1d5 Bump goreleaser-action to use v2 by default (#604)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/604
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-09-24 08:46:21 +00:00
e27189ea32 Fix releasing (#603)
```
[command]/opt/hostedtoolcache/goreleaser-action/2.3.2-pro/x64/goreleaser release --nightly
  • by using this software you agree with its EULA, available at https://goreleaser.com/eula
  • running goreleaser v2.3.2-pro
  • only configurations files on  version: 2  are supported, yours is  version: 0 , please update your configuration
  ⨯ release failed after 0s                  error=only configurations files on  version: 2  are supported, yours is  version: 0 , please update your configuration
::error::The process '/opt/hostedtoolcache/goreleaser-action/2.3.2-pro/x64/goreleaser' failed with exit code 1
```

```
#20 [linux/arm64 builder 2/5] RUN apk add --no-cache make git
#20 CANCELED
------
 > [linux/amd64 builder 5/5] RUN make clean && make build:
0.058 go clean -x -i ./...
0.061 go: go.mod requires go >= 1.22 (running go 1.21.10; GOTOOLCHAIN=local)
0.061 make: *** [Makefile:176: clean] Error 1
------
 1 warning found (use docker --debug to expand):
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
Dockerfile:8
--------------------
   6 |     WORKDIR /opt/src/act_runner
   7 |
   8 | >>> RUN make clean && make build
   9 |
  10 |     FROM alpine:3.18
--------------------
ERROR: failed to solve: process "/bin/sh -c make clean && make build" did not complete successfully: exit code: 2
::error::buildx failed with: ERROR: failed to solve: process "/bin/sh -c make clean && make build" did not complete successfully: exit code: 2
```

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/603
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-09-24 08:22:53 +00:00
59e478464e Bump act to v0.261.2 (#602)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/602
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-09-24 07:04:47 +00:00
e1c7b20898 Bump Go to 1.22 and upgrade dependencies (#580)
Co-authored-by: Chongyi Zheng <git@zcy.dev>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/580
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: harryzcy <harryzcy@noreply.gitea.com>
Co-committed-by: harryzcy <harryzcy@noreply.gitea.com>
2024-07-26 16:00:33 +00:00
2f78411c3d ci: standardize code style and update version extraction (#566)
- Change single quotes to double quotes in YAML files
- Update REPO_VERSION extraction method to use GITHUB_REF_NAME without the 'v' prefix

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/566
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2024-06-28 07:48:10 +00:00
d1d3cad4b0 feat: allow graceful shutdowns (#546)
Add a `Shutdown(context.Context) error` method to the Poller. Calling this method will first shutdown all active polling, preventing any new jobs from spawning. It will then wait for either all jobs to finish, or for the context to be cancelled. If the context is cancelled, it will then force all jobs to end, and then exit.

Fixes https://gitea.com/gitea/act_runner/issues/107

Co-authored-by: Rowan Bohde <rowan.bohde@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/546
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
Co-committed-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
2024-05-27 07:38:55 +00:00
1735b26e66 Don't log job output when debug logging is not enabled (#543)
We wanted the ability to disable outputting the logs from the individual job to the console. This changes the logging so that job logs are only output to the console whenever debug logging is enabled in `act_runner`, while still allowing the `Reporter` to receive these logs and forward them to Gitea when debug logging is not enabled.

Co-authored-by: Rowan Bohde <rowan.bohde@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/543
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
Co-committed-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
2024-05-07 05:58:33 +00:00
65ed62d2f5 Upgrade dependencies (#537)
Patially fix #513

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/537
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-11 04:36:00 +00:00
ec03f19650 Bump act to v0.261.1 (#535)
Related to https://gitea.com/gitea/act/compare/v0.261.0...v0.261.1

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/535
2024-04-10 07:07:56 +00:00
8567324a19 Release nightly versions for tags (#536)
Or the latest nightly version could be "v0.2.8-4-gbe2df36" even though there's "v0.2.9", although they are for the same commit.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/536
2024-04-10 07:06:08 +00:00
be2df361ef Ensure declare to use new labels (#530)
Ensure "declare" is supported, to use new labels, see https://gitea.com/gitea/act_runner/pulls/529

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/530
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-02 07:39:40 +00:00
a5085dde0c Use gitea/runner-images by default (#529)
See https://gitea.com/gitea/runner-images

Also give up ubuntu-18.04 since it's too old. And enable force_pull by default to check new versions of images.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/529
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-02 07:38:14 +00:00
cef86d1140 Bump act to v0.260.0 (#528)
Related to:

- https://gitea.com/gitea/act/pulls/104
- https://gitea.com/gitea/act/issues/102

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/528
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-02 06:25:33 +00:00
94c45acf6b Use node 20 by default (#526)
Because Octokit requires Node 18 or higher now.

See https://github.com/octokit/octokit.js/#fetch-missing .

However, for existing runners, users should manually modify `labels` in the `.runner` file or `runner.labels` in the `config.yaml` file to update. Don't forget to restart.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/526
Reviewed-by: sillyguodong <sillyguodong@noreply.gitea.com>
2024-03-29 03:03:26 +00:00
1760899d27 Set gitea token to release (#524)
Follow a7eca813ea/.gitea/workflows/release-tag.yml (L39)

It works before unexpectedly because of https://github.com/nektos/act/pull/2089

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/524
2024-03-27 10:51:45 +00:00
a7eca813ea Bump act to v0.260.2 (#523)
Related to

- https://gitea.com/gitea/act/pulls/86
- https://gitea.com/gitea/act/pulls/103

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/523
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-27 10:25:23 +00:00
23ec12b8cf Bump act to v0.260.0 (#522)
Related to https://gitea.com/gitea/act/issues/99.

Also update other main dependencies.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/522
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
2024-03-27 03:17:04 +00:00
a1fc2b3ca7 Improve the explanation of workdir_parent config (#519)
Fix #512

act_runner adds a '/' before the path (see 5977042b86/internal/app/run/runner.go (L186)) . So `workdir_parent` doesn't need to have the prefix '/'.

If `workdir_parent` has the prefix '/', errors will occur when reading files from the job container.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/519
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-22 02:30:31 +00:00
5977042b86 Bump act to v0.259.2 (#515)
Related to https://gitea.com/gitea/act/pulls/97

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/515
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-03-14 18:58:43 +00:00
6bc19cbc33 Improve workflows (#516)
Starting from setup-go v4, it will cache build dependencies by default, see https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs.

Also bump some versions.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/516
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2024-03-14 13:27:47 +00:00
75006a59cc Support cloning remote actions from insecure Gitea instances (#508)
Follow https://gitea.com/gitea/act/pulls/92

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/508
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-06 06:10:37 +00:00
4da97f53de Bump act to v0.259.0 (#491)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/491
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Co-authored-by: SupremeVoid <supremevoid@t-online.de>
Co-committed-by: SupremeVoid <supremevoid@t-online.de>
2024-03-03 16:34:37 +00:00
45270656df Set the status of steps to skipped if job is skipped (#500)
If a job is detected as skipped, its steps should also be `skipped`.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/500
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2024-03-01 08:33:32 +00:00
e14f42c40a Add ACTIONS_RESULTS_URL to env (#473)
actions/upload-artifact@v4 and actions/download-artifact@v4 depend on this variable

BaseUrl in a url are ignored by the nodejs code of the new actions, so this change doesn't append the path of the older `ACTIONS_RUNTIME_URL`.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/473
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2024-02-19 02:30:54 +00:00
e6630e2e36 Use artifacts v4 jwt if available (#471)
Needs https://github.com/go-gitea/gitea/pull/28885 to provide jwt if sent by server

Could fix #459, but that has not been verified.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/471
Reviewed-by: delvh <dev.lh@web.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2024-02-08 02:43:39 +00:00
f1f9142a3c fix: Exit docker container on any process exit (#462)
## Description
Issue described in #460

## Changes

- Edited `supervisord.conf` to exit if it detects any of the supervisored processes exiting.
- minor text fix

## Notes

Without this change (or something similar), if act_runner fails, then the container will stay up as a zombie container - it does nothing and does not restart. After this change, if act_runner fails (e.g. due to Gitea instance being down), then supervisord will exit and the container will be restarted.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/462
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: davidfrickert <david.frickert@protonmail.com>
Co-committed-by: davidfrickert <david.frickert@protonmail.com>
2024-01-30 13:47:42 +00:00
f17cad1bbe Update the docker image rebuild flag for config file and example gitea/act_runner#390 (#391)
this is a PR for the issue gitea/act_runner#390
It adding configuration for dockerfile container image to rebuild base on the configuration flag force_rebuild in config.yaml

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/391
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-12-20 07:13:33 +00:00
daf52d0e62 Sanitize UFT-8 content in logs (#453)
I accidently closed my previous PR #384

This PR replaces invalid UTF-8 character in a stream with `?` character. On Windows Server 2019 other characters are replaced by `?` as well so it's consistent.

fixes #452

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/453
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: hakito <hakito@noreply.gitea.com>
Co-committed-by: hakito <hakito@noreply.gitea.com>
2023-12-20 07:06:46 +00:00
8c8a8ce401 fix(deps): update module github.com/avast/retry-go/v4 to v4.5.1 (#411)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-28 05:46:05 +00:00
08c681be0c fix(deps): update module golang.org/x/time to v0.5.0 (#429)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-28 05:45:28 +00:00
91bfe4c186 fix(deps): update module golang.org/x/time to v0.4.0 (#424)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-26 00:01:51 +00:00
825c6f97b7 fix(deps): update module github.com/mattn/go-isatty to v0.0.20 (#414)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 08:33:37 +00:00
2f3e5c7125 fix(deps): update module github.com/docker/docker to v24.0.7+incompatible (#413)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 08:27:38 +00:00
4d9de6ca8c fix(deps): update module github.com/spf13/cobra to v1.8.0 (#416)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 08:27:07 +00:00
feb39666cc fix(deps): update module code.gitea.io/gitea-vet to v0.2.3 (#410)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 08:26:24 +00:00
61cd71c9f8 bump to actions/checkout v4 2023-11-24 03:06:21 +00:00
0adfc1c7cc chore(deps): update actions/setup-go action to v4 (#418)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 03:05:33 +00:00
e3c68668fa chore(deps): update docker/build-push-action action to v5 (#419)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 03:05:28 +00:00
f1b27d5274 chore(deps): update docker/login-action action to v3 (#420)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 03:05:22 +00:00
655a39fd61 chore(deps): update docker/setup-buildx-action action to v3 (#421)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 03:05:07 +00:00
cca7d54117 chore(deps): update docker/setup-qemu-action action to v3 (#422)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2023-11-24 03:05:00 +00:00
934471813a Fix #404: nil map error when reading env file (#405)
Co-authored-by: Mark Glines <mark@glines.org>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/405
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: infinoid <infinoid@noreply.gitea.com>
Co-committed-by: infinoid <infinoid@noreply.gitea.com>
2023-11-24 01:56:27 +00:00
1e940f028b Add renovate config (#408)
+ write access required for `renovate-bot` user

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/408
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: pat-s <patrick.schratz@gmail.com>
Co-committed-by: pat-s <patrick.schratz@gmail.com>
2023-11-23 20:41:10 +00:00
2020ce79bf Remove ACTIONS_RUNTIME_TOKEN workaround (#396)
The bug has been fixed for a long time in the GitHub version.
The fix commit is d8823bfaed and released in the 4.0.0 and after. The issue also mentions that https://gitea.com/gitea/act_runner/issues/119#issuecomment-738294

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/396
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: lng2020 <nanguanlin6@gmail.com>
Co-committed-by: lng2020 <nanguanlin6@gmail.com>
2023-11-07 05:15:14 +00:00
00e9b3d62b Bump act (#394)
Bump act to follow https://gitea.com/gitea/act/pulls/81

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/394
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-10-31 15:03:07 +00:00
130b9f1877 Added environment variable for run.sh to specify runner state file. (#377)
Added environment variable `RUNNER_STATE_FILE` to let users specify
where `run.sh` looks for the runner JSON file. Defaults to ``.runner``
to preserve the original behavior.

Addresses issue #368. It's not my preferred solution but it's the least invasive one I can think of.

I'm happy to make any changes you want.

I didn't see an appropriate place to reference the change in the documentation. I will add documentation wherever you think is appropriate.

To test this I did the following:
* Built an image with the `make docker` command and pushed it to my private registry.
* Added that private image as the image in my existing docker stack that was exhibiting the behavior described in #368.
* Added the RUNNER_STATE_FILE environment variable pointing to the runner JSON file set in my `config.yml` (``/data/runner.json`` in this case).
* Configured a new runner in gitea and added the token as an environment variable in the stack config.
* Deployed the stack and verified the new runner was recognized (in the idle state).
* Force updated the runner service to restart the container.
* Verified the same runner was still recognized in gitea (in the idle state) once it was back up and running.

Here is the relevant config. It's stored as a template so I've left the things that would normally be redacted as they are in the template.

```
    runner:
     image: hub.hax.in.net/haxwithaxe/act_runner:dev1
      networks:
        - swarm-net
      environment:
       RUNNER_STATE_FILE: /data/runner.json
        CONFIG_FILE: /data/config.yml
        GITEA_INSTANCE_URL: "https://git_gitea"
        GITEA_RUNNER_REGISTRATION_TOKEN: "{{ git_runner_reg_token }}"
        GITEA_RUNNER_NAME: "git_runner"
      volumes:
        - runner_data:/data
        - /var/run/docker.sock:/var/run/docker.sock
```

`runner_data` is a glusterfs subvolume

Thanks for creating this gitea specific fork. Apart from the issue this pull request addresses it works so well I don't have to think about it once it's set up.

Co-authored-by: haxwithaxe <spam@haxwithaxe.net>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/377
Co-authored-by: haxwithaxe <haxwithaxe@noreply.gitea.com>
Co-committed-by: haxwithaxe <haxwithaxe@noreply.gitea.com>
2023-10-15 23:21:53 +00:00
4c35288175 Add DIND docker-compose example (#365)
Co-authored-by: Nikita Vilunov <nikita@vilunov.me>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/365
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: vilunov <vilunov@noreply.gitea.com>
Co-committed-by: vilunov <vilunov@noreply.gitea.com>
2023-10-02 15:12:31 +00:00
990db1bfc0 Fix k8s rootless Docker filesystem permissions (#366)
Without this the deployment fails because the runner can't write to
/data/

Credit to @ccureau for identifying the fix

Relates to: gitea/act_runner#264

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/366
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Andrew Imeson <andrew@andrewimeson.com>
Co-committed-by: Andrew Imeson <andrew@andrewimeson.com>
2023-10-02 15:12:14 +00:00
d07fbfc8c3 bump act library to v0.2.51 (#360)
brings in node20 support

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/360
Reviewed-by: delvh <dev.lh@web.de>
2023-09-24 19:20:40 +00:00
m90
10dc6fb60d Allow reading registration token from file (#350)
When deploying the runner in a Docker Swarm setup, it can be useful to
read the potentially sensitive token from a secret instead of having to
pass it from an environment variable.

Co-authored-by: Frederik Ring <frederik.ring@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/350
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: m90 <m90@noreply.gitea.com>
Co-committed-by: m90 <m90@noreply.gitea.com>
2023-09-04 04:12:07 +00:00
ed35b09b8f change podman socket path (#341)
port of https://github.com/nektos/act/pull/1961
closes gitea/act_runner#274

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/341
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2023-08-21 04:01:12 +00:00
03f0829d09 Add ForcePull option (#339)
Close #271

What it does: instead of forcing the value of `ForcePull` to false, the user can now configure it on the runner yaml

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/339
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Gianni Angelozzi <gianni@sistema3.it>
Co-committed-by: Gianni Angelozzi <gianni@sistema3.it>
2023-08-17 06:51:57 +00:00
7fc1b91ba6 Update to go1.21 in Dockerfile.rootless (#332)
Follow #330

Fix https://gitea.com/gitea/act_runner/pulls/330#issuecomment-747099

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/332
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-08-12 00:37:08 +00:00
82c3c2df1a Upgrade Go to 1.21 and bump other dependencies (#330)
Co-authored-by: harryzcy <harry@harryzheng.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/330
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: harryzcy <harryzcy@noreply.gitea.com>
Co-committed-by: harryzcy <harryzcy@noreply.gitea.com>
2023-08-10 01:45:25 +00:00
9fc823e4b1 Fix "http: invalid Host header" issue (#319)
This issue  is caused by the addition of validation logic for `Host` in `http.Request` in golang 1.20.6 (see https://go-review.googlesource.com/c/go/+/507357/6/src/net/http/request.go)

In `act`, when execute `ContainerExecAttach()`(see 22d91e3ac3/pkg/container/docker_run.go (L594)), the actual value of `request.Host` is `"/var/run/docker.sock"`. This does not conform to the specification described in `validhostHeader`.
 <details> <summary>ValidHostHeader()</summary>

![image](/attachments/57fb13ba-1c74-47f6-ac48-231a72a1947e)
</details>
So this PR follow upstream: https://github.com/nektos/act/pull/1917 and revert https://gitea.com/gitea/act_runner/pulls/295

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/319
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-08-02 04:34:36 +00:00
12999b61dd Reduce unnecessary DB queries for Actions tasks (#219)
implement: https://github.com/go-gitea/gitea/issues/24544

Changes:
- Add a global variable `tasksVersion` to store the lastest version number which returned by Gitea.
- Pass `tasksVersion` to Gitea when invoking `FetchTask`.
- If there is no task in the `FetchTask` response, store the version from the `FetchTask` response into `tasksVersion` variable.
- If there is a task in the `FetchTask` response, set `tasksVersion` to zero to focre query db in next `FetchTask` request.

Related:
- Protocol: https://gitea.com/gitea/actions-proto-def/pulls/10
- Gitea side: https://github.com/go-gitea/gitea/pull/25199

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/219
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-07-25 03:25:50 +00:00
49a2fcc138 fix endless loop (#306)
fix endless loop in  poll
relate #305

Co-authored-by: CaiCandong <1290147055@qq.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/306
Co-authored-by: caicandong <caicandong@noreply.gitea.com>
Co-committed-by: caicandong <caicandong@noreply.gitea.com>
2023-07-24 07:07:53 +00:00
8f88e4f15a Update README.md (#302)
Correct Quick Start section.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/302
Co-authored-by: Konstantin Podsvirov <podsvirov@noreply.gitea.com>
Co-committed-by: Konstantin Podsvirov <podsvirov@noreply.gitea.com>
2023-07-24 05:11:42 +00:00
a1bb3b56fd Catch the panic and print the error (#305)
refactor # 215  Catch the panic and print the error
close #215

Co-authored-by: CaiCandong <1290147055@qq.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/305
Co-authored-by: caicandong <caicandong@noreply.gitea.com>
Co-committed-by: caicandong <caicandong@noreply.gitea.com>
2023-07-24 04:28:44 +00:00
1a7ec5f339 Upgrade gitea/act (#298)
Follow  https://gitea.com/gitea/act/pulls/75

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/298
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-07-20 05:12:59 +00:00
5d01cb8904 Add tips in config file (#297)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/297
2023-07-20 02:16:30 +00:00
dcf84d8a53 Avoid https://github.com/nektos/act/issues/1908 by specifying golang 1.20.5. (#295)
Without this, actions fail when attempting to clone the repo:

```
2023-07-18 17:11:02 [Smoke Test/Run basic test suite] failed to attach to exec: http: invalid Host header
```

This appears to be the same as https://github.com/nektos/act/issues/1908, and only shows up with golang 1.20.6.  Staying with golang 1.20.5 is a temporary solution to avoid the bug until it is fixed upstream.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/295
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-07-19 05:12:07 +00:00
73adae040d Upgrade act (#291)
Follow https://gitea.com/gitea/act/pulls/74
Fix #277, #290

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/291
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-07-17 03:44:42 +00:00
db662b3690 ci(lint): refactor code for clarity and linting compliance (#289)
- Removed `deadcode`, `structcheck`, and `varcheck` linters from `.golangci.yml`
- Fixed a typo in a comment in `daemon.go`
- Renamed `defaultActionsUrl` to `defaultActionsURL` in `exec.go`
- Removed unnecessary else clause in `exec.go` and `runner.go`
- Simplified variable initialization in `exec.go`
- Changed function name from `getHttpClient` to `getHTTPClient` in `http.go`
- Removed unnecessary else clause in `labels_test.go`

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/289
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-07-13 01:10:54 +00:00
cf92a979e2 refactor(register): refactor registration functions and improve logging (#288)
- Remove the else clause in the `registerInteractive` function, and unconditionally log "Runner registered successfully."
- Change the return value in the `doRegister` function from `nil` to `ctx.Err()`.

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/288
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-07-12 14:12:16 +00:00
87058716fb fix(register): refactor context usage in registration functions (#286)
- Add context parameter to `registerNoInteractive`, `registerInteractive`, and `doRegister` functions
- Remove the creation of a new context in `doRegister`, now using the passed context instead

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/286
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2023-07-12 13:11:55 +00:00
c701ba4787 Add a quick start runner method in README (#282)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/282
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-07-12 01:43:26 +00:00
57ff1df6e0 config: default container workspace set to host path (#279)
The container workspace path is overwritten by the default host workspace path ($HOME/.cache/act).

Workaround:
```yaml
host:
  workdir_parent: workspace
```

Ref: 34d15f21c2
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/279
Co-authored-by: Michael Santos <michael.santos@gmail.com>
Co-committed-by: Michael Santos <michael.santos@gmail.com>
2023-07-10 08:57:55 +00:00
3dcfd6ea3d Run as cache server (#275)
This PR
- adds the `cache-server` command so act_runner can run as a cache server. When running as a cache server, act_runner only processes the requests related to cache and does not run jobs.
- adds the `external_server` configuration for cache. If specified, act_runner will use this URL as the ACTIONS_CACHE_URL instead of starting a cache server itself.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/275
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-07-07 08:28:54 +00:00
c6006ee699 Upgrade act (#269)
Follow https://gitea.com/gitea/act/pulls/71.
Fix https://gitea.com/gitea/act_runner/issues/266

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/269
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-07-03 04:15:46 +00:00
f2629f2ea3 Add support for finding docker daemon from common socket paths (#263)
Caused by #260

act_runner will fail to start if user does not set `docker_host` configuration and `DOCKER_HOST` env. This PR adds the support for finding docker daemon from common socket paths so act_runner could detect the docker socket from these paths.

The `commonSocketPaths` is from [nektos/act](e60018a6d9/cmd/root.go (L124-L131))

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/263
Co-authored-by: Zettat123 <zettat123@noreply.gitea.com>
Co-committed-by: Zettat123 <zettat123@noreply.gitea.com>
2023-07-01 01:27:54 +00:00
cf48ed88ba Revert supporting multiple default actions URLs and use github for exec by default (#262)
## ⚠️ BREAKING ⚠️

Follow https://github.com/go-gitea/gitea/pull/25581 and gitea/act#70 .

- Revert "Parse multiple default actions URLs (#200)"
- Revert "fix defaultActionsUrls config for exec (#233)"
- Use `https://github.com` for exec by default.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/262
2023-06-30 07:53:18 +00:00
ccc27329dc Improve the usage of docker_host configuration (#260)
Follow #242, #244
Fixes #258

Users could use `docker_host` configuration to specify which docker daemon will be used by act_runner.
- If `docker_host` is **empty**, act_runner will find an available docker host automatically.
- If `docker_host` is **"-"**, act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
- If `docker_host` is **not empty or "-"**, the specified docker host will be used. An error will be returned if it doesn't work.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/260
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-30 04:00:04 +00:00
b0bd503b11 add token support for exec (#247)
allow to pass token from secrets

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/247
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tomasz Duda <tomaszduda23@gmail.com>
Co-committed-by: Tomasz Duda <tomaszduda23@gmail.com>
2023-06-20 08:41:22 +00:00
8c14933e70 Upgrade act (#248)
Follow https://gitea.com/gitea/act/pulls/68

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/248
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-20 08:33:42 +00:00
34d15f21c2 Add option to configure workspace on host (#238)
Adds a new section to the configuration which is used
to control options when running in host mode.

The first option added is to allow configuration
of the location workspaces get created in.

Depends on ~~gitea/act#65~~
Will resolve #235

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/238
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Marius Zwicker <marius.zwicker@mlba-team.de>
Co-committed-by: Marius Zwicker <marius.zwicker@mlba-team.de>
2023-06-20 08:29:05 +00:00
32d29f0813 add ACT_EXEC (#246)
Add env variable to distinguish build run locally from remote one.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/246
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Tomasz Duda <tomaszduda23@gmail.com>
Co-committed-by: Tomasz Duda <tomaszduda23@gmail.com>
2023-06-20 02:08:35 +00:00
2e2c0400c8 add --gitea-instance (#245)
add --gitea-instance to let user specify address of endpoint of exec
Related to https://gitea.com/gitea/act/pulls/68. Both can be merged independently though.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/245
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Tomasz Duda <tomaszduda23@gmail.com>
Co-committed-by: Tomasz Duda <tomaszduda23@gmail.com>
2023-06-20 01:57:21 +00:00
054c8d912f Move docker.host to container.docker_host (#244)
Follow #242.

Move `docker.host` to `container.docker_host`.

There are already some options for docker/container in `container`, so developers could get confused about where to add options.

It's breaking, but I think it's OK since `docker.host` was added just two days ago.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/244
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-06-19 09:01:16 +00:00
9e4a5f7363 feat: improve Docker configuration and detection handling (#242)
- Pass `cfg` to `envcheck.CheckIfDockerRunning` function
- Add `Docker` struct to `config.go` for Docker configuration
- Update `config.example.yaml` with `docker` configuration options
- Modify `CheckIfDockerRunning` in `docker.go` to use Docker host from config if provided

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/242
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: wxiaoguang <wxiaoguang@noreply.gitea.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2023-06-18 05:38:38 +00:00
ec38401097 Add ValidVolumes config (#226)
Follow https://gitea.com/gitea/act/pulls/60, https://gitea.com/gitea/act/pulls/64

This PR adds the `valid_volumes` configuration. `valid_volumes` is a sequence containing the volumes (including bind mounts) that can be mounted to the container. By default, `valid_volumes` is empty, which means that no volumes can be mounted. Users can specify multiple valid volumes and [glob](https://github.com/gobwas/glob) is supported.

All volumes will be allowed when using `exec` to run workflows locally.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/226
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-16 06:07:48 +00:00
45bfe0a9b2 Accept empty file as config (#241)
Close #240

`yaml.Decoder.Decode` will return EOF when the root node is nil , see https://github.com/go-yaml/yaml/blob/v3/yaml.go#L125

While `yaml.Unmarshal` will accept it, see https://github.com/go-yaml/yaml/blob/v3/yaml.go#L162

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/241
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
2023-06-15 05:40:37 +00:00
316534996a Build docker image gitea/act_runner/x.y.z-dind-rootless (#239)
Follow #208

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/239
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
2023-06-15 05:27:35 +00:00
67b1363d25 Support changing labels (#201)
Implement proposal: https://github.com/go-gitea/gitea/issues/24540

Related:
- Protocol: https://gitea.com/gitea/actions-proto-def/pulls/9
- Gitea side: https://github.com/go-gitea/gitea/pull/24806

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/201
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-06-15 03:59:15 +00:00
946c41cf4f Improve run.sh to handle empty labels and log to stdout (#237)
1. Print logs on standard output
2. Don't add labels if GITEA_RUNNER_LABELS not set

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/237
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Tomasz Duda <tomaszduda23@gmail.com>
Co-committed-by: Tomasz Duda <tomaszduda23@gmail.com>
2023-06-13 04:09:11 +00:00
341d49a24d implement act_runner rootless image (#208)
This PR creates a rootless Docker image that runs both `dockerd` and `act_runner` using `supervisord`.  It has been tested locally for a few days and seems stable.

Co-authored-by: ccureau <ccureau@noreply.gitea.io>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/208
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: ccureau <ccureau@noreply.gitea.com>
Co-committed-by: ccureau <ccureau@noreply.gitea.com>
2023-06-12 06:35:27 +00:00
b21d476aca Exit with Code 1 if registering a runner fails (#228)
### It's a "simple dirty fix" and I don't have any experiences with Go, so if this doesn't match your coding compliance, please adjust the code as needed

I'm using bash scripts to register a token

`./act_runner/act_runner register --no-interactive --name runner$number --instance http://localhost:3000 --token $token`

But when a token is invalid, the command still returns 0, which is not practical for automation.
A simple non-zero return would be more convenient for power users and developers.

Co-authored-by: Markus Löffler <markus.loeffler@netcare.de>
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/228
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: MarkusLoeffler01 <markusloeffler01@noreply.gitea.com>
Co-committed-by: MarkusLoeffler01 <markusloeffler01@noreply.gitea.com>
2023-06-09 17:34:23 +00:00
a29307a9d9 Remove hadolint and improve Dockerfile (#234)
Replace #190

See:

- https://gitea.com/gitea/act_runner/pulls/190#issuecomment-741196
- https://gitea.com/gitea/act_runner/pulls/208#issuecomment-741049

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/234
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-by: delvh <dev.lh@web.de>
2023-06-09 02:50:30 +00:00
4bfbfec477 fix defaultActionsUrls config for exec (#233)
follow #200

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/233
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-06-08 04:26:52 +00:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
0cf31b2d22 Update docker image tag (#153)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/153
2023-04-27 15:02:39 +08:00
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
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
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
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
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
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
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
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
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
ed86e2f15a Update workflow files (#131)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/131
2023-04-19 17:46:52 +08:00
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
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
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
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
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
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
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
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
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
10d639cc6b add release tag (#111)
Fix #108

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/111
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-11 14:04:08 +08:00
5a8134410d Run as a container (#8) including Docker-in-Docker. (#84)
This adds a very simple Dockerfile and run script for running `act_runner` as a container.

It also allows setting `Privileged` and `ContainerOptions` flags via the new config file when spawning task containers.  The combination makes it possible to use Docker-in-Docker (which requires `privileged` mode) as well as pass any other options child Docker containers may require.

For example, if Gitea is running in Docker on the same machine, for the `checkout` action to behave as expected from a task container launched by `act_runner`, it might be necessary to map the hostname via something like:

```
container:
  network_mode: bridge
  privileged: true
  options: --add-host=my.gitea.hostname:host-gateway
```

> NOTE: Description updated to reflect latest code.
> NOTE: Description updated to reflect latest code (again).

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/84
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-04-11 10:58:12 +08:00
b79c3aa1a3 feat: add artifact api from gitea server (#103)
add action api for artifacts upload and download.
It's related to https://github.com/go-gitea/gitea/pull/22738

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/103
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: fuxiaohei <fuxiaohei@vip.qq.com>
Co-committed-by: fuxiaohei <fuxiaohei@vip.qq.com>
2023-04-10 21:35:07 +08:00
9c6499ec08 Fix panic when response is nil (#105)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/105
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-06 21:51:46 +08:00
d139faa40c Supports configuring fetch_timeout and fetch_interval. (#100)
Fix #99.

Fix #94.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/100
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-04-06 10:57:36 +08:00
220efa69c0 Refactor to new framework (#98)
- Adjust directory structure
```text
├── internal
│   ├── app
│   │   ├── artifactcache
│   │   ├── cmd
│   │   ├── poll
│   │   └── run
│   └── pkg
│       ├── client
│       ├── config
│       ├── envcheck
│       ├── labels
│       ├── report
│       └── ver
└── main.go
```
- New pkg `labels` to parse label
- New pkg `report` to report logs to Gitea
- Remove pkg `engine`, use `envcheck` to check if docker running.
- Rewrite `runtime` to `run`
- Rewrite `poller` to `poll`
- Simplify some code and remove what's useless.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/98
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-04 21:32:04 +08:00
df3cb60978 Config for container network (#96)
Fix #66

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/96
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-04-04 14:32:01 +08:00
7e7096e60b Refactor environment variables to configuration and registration (#90)
Close #21.

Refactor environment variables to configuration file (config.yaml) and registration file (.runner).

The old environment variables are still supported, but warning logs will be printed.

Like:

```text
$ GITEA_DEBUG=true ./act_runner -c config.yaml daemon
INFO[0000] Starting runner daemon
WARN[0000] env GITEA_DEBUG has been ignored because config file is used

$ GITEA_DEBUG=true ./act_runner daemon
INFO[0000] Starting runner daemon
WARN[0000] env GITEA_DEBUG will be deprecated, please use config file instead
```

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/90
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-04-02 22:41:48 +08:00
8eea12dd78 Add CLI flag for specifying the Docker image to use. (#83)
Since the `exec` command does not use labels from `.runner`, there is no existing way to specify which Docker image to use for task execution.

This adds an `--image` flag for specifying it manually.  The default remains `node:16-bullseye`.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/83
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: telackey <telackey@noreply.gitea.io>
Co-committed-by: telackey <telackey@noreply.gitea.io>
2023-03-29 09:42:53 +08:00
c8fad20f49 handle possible panic (#88)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/88
Reviewed-by: Jason Song <i@wolfogre.com>
2023-03-28 23:51:38 +08:00
1596e4b1fd Fix potential log panic (#82)
If a job uses a [reusable workflow](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-of-jobsjob_iduses), the job's steps sequence will be empty.

But in log reporter, we don't check the length of `r.state.Steps`, which may cause panic.

``` go
if v, ok := entry.Data["stepNumber"]; ok {
	if v, ok := v.(int); ok {
		step = r.state.Steps[v]
	}
}
```

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/82
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-28 11:49:09 +08:00
c9e076db68 Get outbound IP in multiple ways or disable cache server if failed to init (#74)
Fix #64 (incompletely).

It's still not ideal. It makes more sense to use the gateway IP address of container network as outbound IP of cache server. However, this requires act to cooperate, some think like:

- act creates the network for new container, and returns the network to runner.
- runner extracts the gateway IP in the network.
- runner uses the gateway IP as outbound IP, and pass it to act as cache server endpoint.
- act It continues to create the container with the created network.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/74
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-24 17:55:13 +08:00
bc1842d649 Vet code (#73)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/73
2023-03-24 15:10:39 +08:00
90b8cc6a7a Clarify labels (#69)
The label will follow the format `label[:schema[:args]]`, and the schema will be `host` if it's omitted. So

- `ubuntu:docker://node:18`: Run jobs with label `ubuntu` via docker with image `node:18`
- `ubuntu:host`: Run jobs with label `ubuntu` on the host directly.
- `ubuntu`: Same as `ubuntu:host`.
- `ubuntu:vm:ubuntu-latest`: (Just a example, not Implemented) Run jobs with label `ubuntu` via virtual machine with iso `ubuntu-latest`.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/69
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Reviewed-by: wxiaoguang <wxiaoguang@noreply.gitea.io>
2023-03-23 20:48:33 +08:00
4d5a35ac65 Upgrade act (#68)
Related to https://gitea.com/gitea/act/pulls/27

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/68
2023-03-23 13:33:17 +08:00
8f81f40d62 Fix failed to create container if the runner works in root dir (#67)
Fix #56

This PR uses the `preset.Repository` as a part of the workdir and use `filepath.FromSlash` to convert the slash characters.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/67
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-23 09:41:22 +08:00
9f90cba993 Correct spaces in README.md and in Enter the runner name when running ./act_runner register (#65)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/65
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Benjamin Loison <benjamin.loison@orange.fr>
Co-committed-by: Benjamin Loison <benjamin.loison@orange.fr>
2023-03-22 14:48:35 +08:00
48b05a0ca8 Upgrade act to support go actions (#62)
See:
- https://gitea.com/gitea/act/pulls/20
- https://gitea.com/gitea/act/pulls/22
- https://gitea.com/gitea/act/pulls/26

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/62
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-21 16:33:08 +08:00
9eb8b08a69 checksum and compress 2023-03-18 01:58:21 -04:00
4d868b7f3c Update act to v0.243 (#54)
- Update act to v0.243.1
- Disable artifacts server when run daemon.
- Adjust cmd.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/54
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-17 09:45:46 +08:00
63a57edaa3 check go version when build (#53)
Fix #51

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/53
2023-03-16 11:37:08 +08:00
5180cd56e1 Support cache on ci (#47)
Fix #46

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/47
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-15 12:28:18 +08:00
370989b2d0 Print the kind of event that trigger the actions (#48)
![image](/attachments/28a866c6-3134-477d-a8c8-d624fa90db0b)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/48
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-03-15 09:44:13 +08:00
71f470d670 Fix make don't rebuild when go.mod changed (#49)
Fix #13

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/49
Reviewed-by: delvh <dev.lh@web.de>
2023-03-14 18:43:05 +08:00
c0c363bf59 Update readme to add pre-built binary download links (#45)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/45
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-14 13:39:12 +08:00
0d71463662 Inject version when building and report version to Gitea via log and header (#43)
close #42
1. Inject runner version when `make build`
After building, executing command line: `./act_runner -v` or `./act_runner --version`, the version of runner is printed.
![image](/attachments/e25efbd3-79b3-49a5-b93f-42646d42c707)

2. In `Actions` UI:
![image](/attachments/36c57470-2a1d-4796-9eb0-de3988ab88e1)

3. Set request header in http client interceptor.

Co-authored-by: sillyguodong <gedong_1994@163.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/43
Reviewed-by: delvh <dev.lh@web.de>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <sillyguodong@noreply.gitea.io>
Co-committed-by: sillyguodong <sillyguodong@noreply.gitea.io>
2023-03-13 18:57:35 +08:00
ebcf341de7 Fix wrong last step duration when job failed (#41)
This PR is to fix the wrong last step duration when job failed like shown in the screenshot.
The reason is because when job failed, `Fire` function did not pass in Time, and `r.state.StoppedAt` is by default set to `0001-01-01 08:05:43 +0805 LMT`, which is later on reported to gitea by `UpdateTask`, which calls `UpdateTaskByState` to update the `task.Stopped`, and `task.Stopped` is used in `FullSteps`, resulting in wrong calcaulation of last step duration.

Co-authored-by: nickname <test@123.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/41
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: HesterG <hesterg@noreply.gitea.io>
Co-committed-by: HesterG <hesterg@noreply.gitea.io>
2023-03-08 21:32:54 +08:00
14334f76ed Add exec subcommand for runner so that we can run the tasks locally(#39)
Most codes are copied from https://gitea.com/gitea/act/src/branch/main/cmd
and do some small changes to make it run again

examples:

```SHELL
./act_runner exec -l
./act_runner exec -j lint
./act_runner exec -j lint -n
```

some example result:

![屏幕截图 2023-03-06 135735](/attachments/547bd05c-ade2-41f7-ba60-c9937fa32d5f)

![屏幕截图 2023-03-06 140643](/attachments/e8f48dba-c7f3-4daa-a163-aa9b36b1dc32)

Signed-off-by: a1012112796 <1012112796@qq.com>

fix #32

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/39
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-03-08 10:55:31 +08:00
f24e0721dc Add runner name to log (#37)
User can get the name of the runner that executed the specified job.
![image](/attachments/61328f68-7223-4345-85c7-ac08781e81db)

Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/37
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@noreply.gitea.io>
Co-committed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-06 18:42:07 +08:00
e36300ce28 fix docker executor on windows and local actions (#34)
If the Workdir field doesn't ends with the filepath seperator,
bad things happen

Fixes #33

Sample for host mode on windows, needs be adjusted for linux e.g. replace pwsh with bash
Also fixes
```yaml
on: push
jobs:
  _:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v3
      with:
        path: subdir/action
    - uses: ./subdir/action
```

with an action.yml in the same repo
```yaml
runs:
  using: composite
  steps:
    - run: |
        echo "Hello World"
      shell: pwsh
```

Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/34
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: ChristopherHX <christopherhx@noreply.gitea.io>
Co-committed-by: ChristopherHX <christopherhx@noreply.gitea.io>
2023-03-06 13:24:32 +08:00
09ddbe166f disable more arch 2023-03-01 13:00:18 +08:00
da0713e629 disable arch that modernc does not support 2023-03-01 12:50:50 +08:00
bbd055ac3b run nightly on ubuntu runner 2023-03-01 12:32:00 +08:00
462b2660de disable arches that modernc/sqlite don't complie for 2023-03-01 12:31:27 +08:00
ebdbfeb54a fix lint error (#30)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/30
Reviewed-by: John Olheiser <john+gitea@jolheiser.com>
2023-03-01 06:40:20 +08:00
436b441cad Support cache (#25)
See [Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows).

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/25
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-02-28 23:39:30 +08:00
552dbcdda9 Add copyright header and gitea-vet (#29)
Add copyright header

Co-authored-by: sillyguodong <gedong_1994@163.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/29
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Co-authored-by: sillyguodong <sillyguodong@noreply.gitea.io>
Co-committed-by: sillyguodong <sillyguodong@noreply.gitea.io>
2023-02-28 18:44:46 +08:00
a50b094c1a update env var 2023-02-27 15:30:35 +08:00
6cc53f16d8 use aws runner 2023-02-27 14:42:57 +08:00
8fcd56dc7b skip dist/config.yaml 2023-02-26 15:40:22 +08:00
c9318f08e2 skip nightly publish 2023-02-26 15:39:22 +08:00
c7f8919470 mkdir 2023-02-26 13:35:30 +08:00
14dfa5cc15 s3 credentials 2023-02-26 13:34:26 +08:00
99a53a1f4c release to s3 2023-02-26 13:00:36 +08:00
df2219eeb8 use node16 version of aws cred config 2023-02-26 12:42:50 +08:00
216f3d1740 connect to aws s3 2023-02-26 12:39:41 +08:00
8aa186897f disable cache on golang setup 2023-02-26 12:24:49 +08:00
3fa7707bc1 goreleaser needs go to build binary 2023-02-26 12:21:50 +08:00
9038442191 pull tags for goreleaser 2023-02-26 12:20:47 +08:00
75 changed files with 4701 additions and 2874 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,22 +1,101 @@
name: goreleaser
name: release-nightly
on:
push:
branches: [ main ]
branches: [main]
tags:
- "*"
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: https://github.com/goreleaser/goreleaser-action@v4
- uses: actions/checkout@v4
with:
distribution: goreleaser-pro
version: latest
args: release --nightly --clean
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
args: release --nightly
env:
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
S3_BUCKET: ${{ secrets.S3_BUCKET }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }}
AWS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
GORELEASER_FORCE_TOKEN: "gitea"
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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@v4
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
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=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: basic
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
- name: Build and push dind
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
- name: Build and push dind-rootless
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind-rootless
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless

View File

@ -0,0 +1,111 @@
name: release-tag
on:
push:
tags:
- "*"
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
- name: goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
args: release
env:
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
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@v4
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
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=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: basic
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 }}
- name: Build and push dind
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
- name: Build and push dind-rootless
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind-rootless
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind-rootless
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless

View File

@ -1,23 +1,20 @@
name: checks
on:
on:
- push
- pull_request
env:
GOPROXY: https://goproxy.io,direct
jobs:
lint:
name: check and test
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.20
- uses: actions/checkout@v3
go-version-file: 'go.mod'
- name: vet checks
run: make vet
- name: build
run: make build
- name: test
run: make test
run: make test

17
.gitignore vendored
View File

@ -1,4 +1,17 @@
act_runner
/act_runner
/loong_runner
.env
.runner
coverage.txt
coverage.txt
/gitea-vet
/config.yaml
# Jetbrains
.idea
# MS VSCode
.vscode
__debug_bin
# gorelease binary folder
dist
# vim
*.sw*

View File

@ -1,14 +1,11 @@
linters:
enable:
- gosimple
- deadcode
- typecheck
- govet
- errcheck
- staticcheck
- unused
- structcheck
- varcheck
- dupl
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
- gofmt
@ -112,7 +109,6 @@ issues:
- gocritic
- linters:
- unused
- deadcode
text: "swagger"
- path: contrib/pr/checkout.go
linters:
@ -154,9 +150,6 @@ issues:
- path: cmd/dump.go
linters:
- dupl
- path: services/webhook/webhook.go
linters:
- structcheck
- text: "commentFormatting: put a space between `//` and comment text"
linters:
- gocritic

12
.goreleaser.checksum.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "usage: $0 <path>"
exit 1
fi
SUM=$(shasum -a 256 "$1" | cut -d' ' -f1)
BASENAME=$(basename "$1")
echo -n "${SUM} ${BASENAME}" > "$1".sha256

View File

@ -1,3 +1,5 @@
version: 2
before:
hooks:
- go mod tidy
@ -15,7 +17,7 @@ builds:
- arm
- arm64
- s390x
- ppc64le
- riscv64
goarm:
- "5"
- "6"
@ -40,6 +42,8 @@ builds:
- goos: windows
goarch: arm
goarm: "7"
- goos: windows
goarch: arm64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
@ -53,14 +57,15 @@ builds:
- goos: freebsd
goarch: arm
goarm: "7"
- goos: freebsd
goarch: arm64
flags:
- -trimpath
ldflags:
- -s -w
- -s -w -X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version={{ .Summary }}
binary: >-
{{ .ProjectName }}-
{{- if .IsSnapshot }}{{ .Branch }}-
{{- else }}{{- .Version }}-{{ end }}
{{- .Version }}-
{{- .Os }}-
{{- if eq .Arch "amd64" }}amd64
{{- else if eq .Arch "amd64_v1" }}amd64
@ -68,13 +73,22 @@ builds:
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}-{{ .Arm }}{{ end }}
no_unique_dist_dir: true
hooks:
post:
- cmd: xz -k -9 {{ .Path }}
dir: ./dist/
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
blobs:
-
provider: s3
bucket: "{{ .Env.S3_BUCKET }}"
region: "{{ .Env.S3_REGION }}"
folder: "act_runner/{{.Version}}"
directory: "act_runner/{{.Version}}"
extra_files:
- glob: ./**.xz
- glob: ./**.sha256
archives:
- format: binary
@ -83,10 +97,23 @@ archives:
checksum:
name_template: 'checksums.txt'
extra_files:
- glob: ./**.xz
snapshot:
name_template: "{{ incpatch .Version }}"
version_template: "{{ .Branch }}-devel"
nightly:
publish_release: false
name_template: "{{ .Branch }}"
version_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

54
Dockerfile Normal file
View File

@ -0,0 +1,54 @@
FROM golang:1.24-alpine AS builder
# Do not remove `git` here, it is required for getting runner version when executing `make build`
RUN apk add --no-cache make git
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-}
COPY . /opt/src/act_runner
WORKDIR /opt/src/act_runner
RUN make clean && make build
FROM docker:dind AS dind
RUN apk add --no-cache s6 bash git
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
COPY scripts/run.sh /usr/local/bin/run.sh
COPY scripts/s6 /etc/s6
VOLUME /data
ENTRYPOINT ["s6-svscan","/etc/s6"]
FROM docker:dind-rootless AS dind-rootless
USER root
RUN apk add --no-cache s6 bash git
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
COPY scripts/run.sh /usr/local/bin/run.sh
COPY scripts/s6 /etc/s6
VOLUME /data
RUN mkdir -p /data && chown -R rootless:rootless /etc/s6 /data
ENV DOCKER_HOST=unix:///run/user/1000/docker.sock
USER rootless
ENTRYPOINT ["s6-svscan","/etc/s6"]
FROM alpine AS basic
RUN apk add --no-cache tini bash git
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
COPY scripts/run.sh /usr/local/bin/run.sh
VOLUME /var/run/docker.sock
VOLUME /data
ENTRYPOINT ["/sbin/tini","--","run.sh"]

View File

@ -1,5 +1,5 @@
DIST := dist
EXECUTABLE := act_runner
EXECUTABLE := loong_runner
GOFMT ?= gofumpt -l
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
@ -7,13 +7,19 @@ GO ?= go
SHASUM ?= shasum -a 256
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
XGO_VERSION := go-1.18.x
XGO_VERSION := go-1.24.x
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
LINUX_ARCHS ?= linux/amd64,linux/arm64
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
WINDOWS_ARCHS ?= windows/amd64
GOFILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
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/loong_runner
DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
ifneq ($(shell uname), Darwin)
EXTLDFLAGS = -extldflags "-static" $(null)
@ -49,7 +55,7 @@ else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= master
VERSION ?= main
endif
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
@ -60,8 +66,11 @@ else
endif
endif
GO_PACKAGES_TO_VET ?= $(filter-out git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
TAGS ?=
LDFLAGS ?= -X 'main.Version=$(VERSION)'
LDFLAGS ?= -X "git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
all: build
@ -69,30 +78,43 @@ fmt:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt@latest; \
fi
$(GOFMT) -w $(GOFILES)
$(GOFMT) -w $(GO_FMT_FILES)
vet:
$(GO) vet ./...
.PHONY: go-check
go-check:
$(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
echo "Act Runner 需要 Go $(MIN_GO_VERSION_STR) 或更高版本才能构建。您可以从 https://go.dev/dl/ 获取。"; \
exit 1; \
fi
.PHONY: fmt-check
fmt-check:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt@latest; \
fi
@diff=$$($(GOFMT) -d $(GOFILES)); \
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "请运行'make fmt'并提交结果"; \
echo "$${diff}"; \
exit 1; \
fi;
test: fmt-check
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo -e "\n===> \e[32mOk\e[m\n" || exit 1
.PHONY: vet
vet:
@echo "运行 go vet..."
@$(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
install: $(GOFILES)
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
build: $(EXECUTABLE)
build: go-check $(EXECUTABLE)
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
@ -142,6 +164,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)

110
README.md
View File

@ -1,61 +1,111 @@
# act runner
# Loong Runner
Act runner is a runner for Gitea based on [act](https://gitea.com/gitea/act).
Loong Runner 是基于 [Gitea派生(fock)的act](https://gitea.com/gitea/act) 的 [二次派生](https://git.whlug.cn/LoongArchActions/loong_runner) 主要适用于当前龙架构的运行时。
## Prerequisites
> 上游的Gitea派生是可以直接在龙架构上编译使用的, 此派生仓库主要是解决上游的Docker是基于乌班图系统制作的, 目前龙架构暂无乌班图系统
> * 当前计划使用`Debian`来代替乌班图用于构建容器镜像
Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
## Quickstart
## 安装
### Build
### 前提条件
在 Docker 模式下需要 `Docker Engine Community` 版本。要安装 `Docker CE`,请遵循官方 [安装说明](https://docs.docker.com/engine/install/)。
### 下载预构建的二进制文件
访问 [这里](https://git.whlug.cn/LoongArchActions/loong_runner) 仅提供龙架构版本。
### 从源码构建
```bash
make build
```
### Register
### 构建 Docker 容器镜像
```bash
./act_runner register
make docker
```
And you will be asked to input:
## 快速入门
1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
and you should not use `localhost` or `127.0.0.1` as instance IP;
2. Runner token, you can get it from `http://192.168.8.8:3000/admin/runners`;
3. Runner name, you can just leave it blank;
4. Runner labels, you can just leave it blank.
默认情况下,操作是禁用的,因此您需要在 Gitea 实例的配置文件中添加以下内容以启用它:
The process looks like:
```ini
[actions]
ENABLED=true
```
### 注册
```bash
./loong_runner register
```
系统将提示您输入:
1. Gitea 实例 URL,例如 `http://192.168.8.8:3000/`。您应该使用您的 Gitea 实例的 ROOT_URL 作为实例参数,并且不应使用 `localhost``127.0.0.1` 作为实例 IP;
2. 运行器令牌,您可以从 `http://192.168.8.8:3000/admin/actions/runners` 获取;
3. 运行器名称,您可以留空;
4. 运行器标签,您可以留空。
过程如下:
```text
INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
WARN Runner in user-mode.
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
INFO 注册运行器,架构=loong64,操作系统=linux,版本=0.1.5
WARN 运行器处于用户模式。
INFO 输入 Gitea 实例 URL(例如,https://gitea.com/):
http://192.168.8.8:3000/
INFO Enter the runner token:
INFO 输入运行器令牌:
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
INFO Enter the runner name (if set empty, use hostname:Test.local ):
INFO 输入运行器名称(如果设置为空,则使用主机名:Test.local):
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, self-hosted,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster):
INFO 输入运行器标签,留空以使用默认标签(逗号分隔,例如,debian-latest:docker://docker.gitea.com/runner-images:latest)
INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://node:16-bullseye ubuntu-22.04:docker://node:16-bullseye ubuntu-20.04:docker://node:16-bullseye ubuntu-18.04:docker://node:16-buster].
DEBU Successfully pinged the Gitea instance server
INFO Runner registered successfully.
INFO 注册运行器,名称=Test.local,实例=http://192.168.8.8:3000/,标签=[debian-latest:docker://docker.gitea.com/runner-images:latest debian-sid:docker://docker.gitea.com/runner-images:debian-sid anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest]。
DEBU 成功 ping 到 Gitea 实例服务器
INFO 运行器注册成功。
```
You can also register with command line arguments.
您也可以使用命令行参数进行注册。
```bash
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
./loong_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
```
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
如果注册成功,它将立即运行。下次,您可以直接运行运行器。
### Run
### 运行
```bash
./act_runner daemon
```
./loong_runner daemon
```
### 使用 Docker 运行
```bash
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/loong_runner:nightly
```
### 配置
您还可以使用配置文件配置运行器。
配置文件是一个 YAML 文件,您可以使用 `./loong_runner generate-config` 生成一个示例配置文件。
```bash
./loong_runner generate-config > config.yaml
```
您可以使用 `-c`/`--config` 参数指定配置文件路径。
```bash
./loong_runner -c config.yaml register # 使用配置文件注册
./loong_runner -c config.yaml daemon # 使用配置文件运行
```
您可以在 [config.example.yaml](internal/pkg/config/config.example.yaml) 上在线查看配置文件的最新版本。
### 示例部署
查看 [examples](examples) 目录中的示例部署类型。

11
build.go Normal file
View File

@ -0,0 +1,11 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build vendor
package main
import (
// for vet
_ "code.gitea.io/gitea-vet"
)

View File

@ -1,63 +0,0 @@
package cmd
import (
"context"
"os"
"github.com/spf13/cobra"
)
const version = "0.1.5"
type globalArgs struct {
EnvFile string
}
func Execute(ctx context.Context) {
// task := runtime.NewTask("gitea", 0, nil, nil)
var gArgs globalArgs
// ./act_runner
rootCmd := &cobra.Command{
Use: "act [event name to run]\nIf no event name passed, will default to \"on: push\"",
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
Args: cobra.MaximumNArgs(1),
Version: version,
SilenceUsage: true,
}
rootCmd.PersistentFlags().StringVarP(&gArgs.EnvFile, "env-file", "", ".env", "Read in a file of environment variables.")
// ./act_runner register
var regArgs registerArgs
registerCmd := &cobra.Command{
Use: "register",
Short: "Register a runner to the server",
Args: cobra.MaximumNArgs(0),
RunE: runRegister(ctx, &regArgs, gArgs.EnvFile), // must use a pointer to regArgs
}
registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "Disable interactive mode")
registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Gitea instance address")
registerCmd.Flags().BoolVar(&regArgs.Insecure, "insecure", false, "If check server's certificate if it's https protocol")
registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "Runner token")
registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "Runner name")
registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "Runner tags, comma separated")
rootCmd.AddCommand(registerCmd)
// ./act_runner daemon
daemonCmd := &cobra.Command{
Use: "daemon",
Short: "Run as a runner daemon",
Args: cobra.MaximumNArgs(1),
RunE: runDaemon(ctx, gArgs.EnvFile),
}
// add all command
rootCmd.AddCommand(daemonCmd)
// hide completion command
rootCmd.CompletionOptions.HiddenDefaultCmd = true
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -1,112 +0,0 @@
package cmd
import (
"context"
"os"
"strings"
"gitea.com/gitea/act_runner/client"
"gitea.com/gitea/act_runner/config"
"gitea.com/gitea/act_runner/engine"
"gitea.com/gitea/act_runner/poller"
"gitea.com/gitea/act_runner/runtime"
"github.com/joho/godotenv"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
log.Infoln("Starting runner daemon")
_ = godotenv.Load(envFile)
cfg, err := config.FromEnviron()
if err != nil {
log.WithError(err).
Fatalln("invalid configuration")
}
initLogging(cfg)
// require docker if a runner label uses a docker backend
needsDocker := false
for _, l := range cfg.Runner.Labels {
splits := strings.SplitN(l, ":", 2)
if len(splits) == 2 && strings.HasPrefix(splits[1], "docker://") {
needsDocker = true
break
}
}
if needsDocker {
// try to connect to docker daemon
// if failed, exit with error
if err := engine.Start(ctx); err != nil {
log.WithError(err).Fatalln("failed to connect docker daemon engine")
}
}
var g errgroup.Group
cli := client.New(
cfg.Client.Address,
cfg.Client.Insecure,
cfg.Runner.UUID,
cfg.Runner.Token,
)
runner := &runtime.Runner{
Client: cli,
Machine: cfg.Runner.Name,
ForgeInstance: cfg.Client.Address,
Environ: cfg.Runner.Environ,
Labels: cfg.Runner.Labels,
}
poller := poller.New(
cli,
runner.Run,
cfg.Runner.Capacity,
)
g.Go(func() error {
l := log.WithField("capacity", cfg.Runner.Capacity).
WithField("endpoint", cfg.Client.Address).
WithField("os", cfg.Platform.OS).
WithField("arch", cfg.Platform.Arch)
l.Infoln("polling the remote server")
if err := poller.Poll(ctx); err != nil {
l.Errorf("poller error: %v", err)
}
poller.Wait()
return nil
})
err = g.Wait()
if err != nil {
log.WithError(err).
Errorln("shutting down the server")
}
return err
}
}
// initLogging setup the global logrus logger.
func initLogging(cfg config.Config) {
isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{
DisableColors: !isTerm,
FullTimestamp: true,
})
if cfg.Debug {
log.SetLevel(log.DebugLevel)
}
if cfg.Trace {
log.SetLevel(log.TraceLevel)
}
}

View File

@ -1,305 +0,0 @@
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"os/signal"
"runtime"
"strings"
"time"
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"gitea.com/gitea/act_runner/client"
"gitea.com/gitea/act_runner/config"
"gitea.com/gitea/act_runner/register"
"github.com/bufbuild/connect-go"
"github.com/joho/godotenv"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// runRegister registers a runner to the server
func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(false)
isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{
DisableColors: !isTerm,
DisableTimestamp: true,
})
log.SetLevel(log.DebugLevel)
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
runtime.GOARCH, runtime.GOOS, version)
// runner always needs root permission
if os.Getuid() != 0 {
// TODO: use a better way to check root permission
log.Warnf("Runner in user-mode.")
}
if regArgs.NoInteractive {
if err := registerNoInteractive(envFile, regArgs); err != nil {
return err
}
} else {
go func() {
if err := registerInteractive(envFile); err != nil {
// log.Errorln(err)
os.Exit(2)
return
}
os.Exit(0)
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
return nil
}
}
// registerArgs represents the arguments for register command
type registerArgs struct {
NoInteractive bool
InstanceAddr string
Insecure bool
Token string
RunnerName string
Labels string
}
type registerStage int8
const (
StageUnknown registerStage = -1
StageOverwriteLocalConfig registerStage = iota + 1
StageInputInstance
StageInputToken
StageInputRunnerName
StageInputCustomLabels
StageWaitingForRegistration
StageExit
)
var defaultLabels = []string{
"ubuntu-latest:docker://node:16-bullseye",
"ubuntu-22.04:docker://node:16-bullseye", // There's no node:16-bookworm yet
"ubuntu-20.04:docker://node:16-bullseye",
"ubuntu-18.04:docker://node:16-buster",
}
type registerInputs struct {
InstanceAddr string
Insecure bool
Token string
RunnerName string
CustomLabels []string
}
func (r *registerInputs) validate() error {
if r.InstanceAddr == "" {
return fmt.Errorf("instance address is empty")
}
if r.Token == "" {
return fmt.Errorf("token is empty")
}
if len(r.CustomLabels) > 0 {
return validateLabels(r.CustomLabels)
}
return nil
}
func validateLabels(labels []string) error {
for _, label := range labels {
values := strings.SplitN(label, ":", 2)
if len(values) > 2 {
return fmt.Errorf("Invalid label: %s", label)
}
// len(values) == 1, label for non docker execution environment
// TODO: validate value format, like docker://node:16-buster
}
return nil
}
func (r *registerInputs) assignToNext(stage registerStage, value string) registerStage {
// must set instance address and token.
// if empty, keep current stage.
if stage == StageInputInstance || stage == StageInputToken {
if value == "" {
return stage
}
}
// set hostname for runner name if empty
if stage == StageInputRunnerName && value == "" {
value, _ = os.Hostname()
}
switch stage {
case StageOverwriteLocalConfig:
if value == "Y" || value == "y" {
return StageInputInstance
}
return StageExit
case StageInputInstance:
r.InstanceAddr = value
return StageInputToken
case StageInputToken:
r.Token = value
return StageInputRunnerName
case StageInputRunnerName:
r.RunnerName = value
return StageInputCustomLabels
case StageInputCustomLabels:
r.CustomLabels = defaultLabels
if value != "" {
r.CustomLabels = strings.Split(value, ",")
}
if validateLabels(r.CustomLabels) != nil {
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster)")
return StageInputCustomLabels
}
return StageWaitingForRegistration
}
return StageUnknown
}
func registerInteractive(envFile string) error {
var (
reader = bufio.NewReader(os.Stdin)
stage = StageInputInstance
inputs = new(registerInputs)
)
// check if overwrite local config
_ = godotenv.Load(envFile)
cfg, _ := config.FromEnviron()
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
stage = StageOverwriteLocalConfig
}
for {
printStageHelp(stage)
cmdString, err := reader.ReadString('\n')
if err != nil {
return err
}
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString))
if stage == StageWaitingForRegistration {
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
if err := doRegister(&cfg, inputs); err != nil {
log.Errorf("Failed to register runner: %v", err)
} else {
log.Infof("Runner registered successfully.")
}
return nil
}
if stage == StageExit {
return nil
}
if stage <= StageUnknown {
log.Errorf("Invalid input, please re-run act command.")
return nil
}
}
}
func printStageHelp(stage registerStage) {
switch stage {
case StageOverwriteLocalConfig:
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
case StageInputInstance:
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
case StageInputToken:
log.Infoln("Enter the runner token:")
case StageInputRunnerName:
hostname, _ := os.Hostname()
log.Infof("Enter the runner name (if set empty, use hostname:%s ):\n", hostname)
case StageInputCustomLabels:
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, self-hosted,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster):")
case StageWaitingForRegistration:
log.Infoln("Waiting for registration...")
}
}
func registerNoInteractive(envFile string, regArgs *registerArgs) error {
_ = godotenv.Load(envFile)
cfg, _ := config.FromEnviron()
inputs := &registerInputs{
InstanceAddr: regArgs.InstanceAddr,
Insecure: regArgs.Insecure,
Token: regArgs.Token,
RunnerName: regArgs.RunnerName,
CustomLabels: defaultLabels,
}
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
if regArgs.Labels != "" {
inputs.CustomLabels = strings.Split(regArgs.Labels, ",")
}
if inputs.RunnerName == "" {
inputs.RunnerName, _ = os.Hostname()
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
}
if err := inputs.validate(); err != nil {
log.WithError(err).Errorf("Invalid input, please re-run act command.")
return nil
}
if err := doRegister(&cfg, inputs); err != nil {
log.Errorf("Failed to register runner: %v", err)
return nil
}
log.Infof("Runner registered successfully.")
return nil
}
func doRegister(cfg *config.Config, inputs *registerInputs) error {
ctx := context.Background()
// initial http client
cli := client.New(
inputs.InstanceAddr,
inputs.Insecure,
"", "",
)
for {
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
Data: inputs.RunnerName,
}))
select {
case <-ctx.Done():
return nil
default:
}
if ctx.Err() != nil {
break
}
if err != nil {
log.WithError(err).
Errorln("Cannot ping the Gitea instance server")
// TODO: if ping failed, retry or exit
time.Sleep(time.Second)
} else {
log.Debugln("Successfully pinged the Gitea instance server")
break
}
}
cfg.Runner.Name = inputs.RunnerName
cfg.Runner.Token = inputs.Token
cfg.Runner.Labels = inputs.CustomLabels
_, err := register.New(cli).Register(ctx, cfg.Runner)
return err
}

View File

@ -1,10 +0,0 @@
package cmd
import "testing"
func TestValidateLabels(t *testing.T) {
labels := []string{"ubuntu-latest:docker://node:16-buster", "self-hosted"}
if err := validateLabels(labels); err != nil {
t.Errorf("validateLabels() error = %v", err)
}
}

View File

@ -1,114 +0,0 @@
package config
import (
"encoding/json"
"io"
"os"
"runtime"
"strconv"
"gitea.com/gitea/act_runner/core"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
)
type (
// Config provides the system configuration.
Config struct {
Debug bool `envconfig:"GITEA_DEBUG"`
Trace bool `envconfig:"GITEA_TRACE"`
Client Client
Runner Runner
Platform Platform
}
Client struct {
Address string `ignored:"true"`
Insecure bool
}
Runner struct {
UUID string `ignored:"true"`
Name string `envconfig:"GITEA_RUNNER_NAME"`
Token string `ignored:"true"`
Capacity int `envconfig:"GITEA_RUNNER_CAPACITY" default:"1"`
File string `envconfig:"GITEA_RUNNER_FILE" default:".runner"`
Environ map[string]string `envconfig:"GITEA_RUNNER_ENVIRON"`
EnvFile string `envconfig:"GITEA_RUNNER_ENV_FILE"`
Labels []string `envconfig:"GITEA_RUNNER_LABELS"`
}
Platform struct {
OS string `envconfig:"GITEA_PLATFORM_OS"`
Arch string `envconfig:"GITEA_PLATFORM_ARCH"`
}
)
// FromEnviron returns the settings from the environment.
func FromEnviron() (Config, error) {
cfg := Config{}
if err := envconfig.Process("", &cfg); err != nil {
return cfg, err
}
// check runner config exist
f, err := os.Stat(cfg.Runner.File)
if err == nil && !f.IsDir() {
jsonFile, _ := os.Open(cfg.Runner.File)
defer jsonFile.Close()
byteValue, _ := io.ReadAll(jsonFile)
var runner core.Runner
if err := json.Unmarshal(byteValue, &runner); err != nil {
return cfg, err
}
if runner.UUID != "" {
cfg.Runner.UUID = runner.UUID
}
if runner.Token != "" {
cfg.Runner.Token = runner.Token
}
if len(runner.Labels) != 0 {
cfg.Runner.Labels = runner.Labels
}
if runner.Address != "" {
cfg.Client.Address = runner.Address
}
if runner.Insecure != "" {
cfg.Client.Insecure, _ = strconv.ParseBool(runner.Insecure)
}
} else if err != nil {
return cfg, err
}
// runner config
if cfg.Runner.Environ == nil {
cfg.Runner.Environ = map[string]string{
"GITHUB_API_URL": cfg.Client.Address + "/api/v1",
"GITHUB_SERVER_URL": cfg.Client.Address,
}
}
if cfg.Runner.Name == "" {
cfg.Runner.Name, _ = os.Hostname()
}
// platform config
if cfg.Platform.OS == "" {
cfg.Platform.OS = runtime.GOOS
}
if cfg.Platform.Arch == "" {
cfg.Platform.Arch = runtime.GOARCH
}
if file := cfg.Runner.EnvFile; file != "" {
envs, err := godotenv.Read(file)
if err != nil {
return cfg, err
}
for k, v := range envs {
cfg.Runner.Environ[k] = v
}
}
return cfg, nil
}

View File

@ -1,17 +0,0 @@
package core
const (
UUIDHeader = "x-runner-uuid"
TokenHeader = "x-runner-token"
)
// Runner struct
type Runner struct {
ID int64 `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Token string `json:"token"`
Address string `json:"address"`
Insecure string `json:"insecure"`
Labels []string `json:"labels"`
}

View File

@ -1,37 +0,0 @@
package engine
import (
"context"
"github.com/docker/docker/client"
)
type Docker struct {
client client.APIClient
hidePull bool
}
func New(opts ...Option) (*Docker, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
srv := &Docker{
client: cli,
}
// Loop through each option
for _, opt := range opts {
// Call the option giving the instantiated
opt.Apply(srv)
}
return srv, nil
}
// Ping pings the Docker daemon.
func (e *Docker) Ping(ctx context.Context) error {
_, err := e.client.Ping(ctx)
return err
}

View File

@ -1,43 +0,0 @@
package engine
import (
"context"
"fmt"
"time"
log "github.com/sirupsen/logrus"
)
// Start start docker engine api loop
func Start(ctx context.Context) error {
engine, err := New()
if err != nil {
return err
}
count := 0
for {
err := engine.Ping(ctx)
if err == context.Canceled {
break
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err != nil {
log.WithError(err).
Errorln("cannot ping the docker daemon")
count++
if count == 5 {
return fmt.Errorf("retry connect to docker daemon failed: %d times", count)
}
time.Sleep(time.Second)
} else {
log.Infoln("successfully ping the docker daemon")
break
}
}
return nil
}

View File

@ -1,30 +0,0 @@
package engine
import "github.com/docker/docker/client"
// An Option configures a mutex.
type Option interface {
Apply(*Docker)
}
// OptionFunc is a function that configure a value.
type OptionFunc func(*Docker)
// Apply calls f(option)
func (f OptionFunc) Apply(docker *Docker) {
f(docker)
}
// WithClient set custom client
func WithClient(c client.APIClient) Option {
return OptionFunc(func(q *Docker) {
q.client = c
})
}
// WithHidePull hide pull event.
func WithHidePull(v bool) Option {
return OptionFunc(func(q *Docker) {
q.hidePull = v
})
}

12
examples/README.md Normal file
View File

@ -0,0 +1,12 @@
# `loong_runner` 使用示例
欢迎来到我们专为 Gitea 设置设计的使用和部署示例集合。无论您是初学者还是经验丰富的用户,您都会在这里找到可以直接应用以增强 Gitea 体验的实用资源。我们鼓励您贡献自己的见解和知识,使这个集合更加全面和有价值。
| 部分 | 描述 |
|-----|------|
| [`docker`](docker) | 本部分为您提供适用于在工作站或安装了 Docker 的服务器上运行容器的脚本和说明。它简化了使用 Docker 设置和管理 Gitea 部署的过程。 |
| [`docker-compose`](docker-compose) | 在本部分中,您将发现如何利用 `docker-compose` 来高效处理 Gitea 部署的示例。它提供了一个直接的方法来管理 Gitea 设置的多个容器化组件。 |
| [`kubernetes`](kubernetes) | 如果您正在使用 Kubernetes 集群作为基础设施,本部分专门为您设计。它展示了在 Kubernetes 集群内配置 Gitea 部署的示例和指南,使您能够利用 Kubernetes 的可扩展性和灵活性。 |
| [`vm`](vm) | 本部分致力于协助您在虚拟或物理服务器上设置 Gitea 的示例。无论您是在使用虚拟机还是物理硬件,您都会找到有用的资源来指导您完成部署过程。 |
我们希望这些资源为您的 Gitea 设置提供有价值的见解和解决方案。请随意探索、贡献和调整这些示例以满足您的具体需求。

View File

@ -0,0 +1,59 @@
### 使用`docker-compose`运行`loong_runner`
```yml
...
gitea:
image: gitea/gitea
...
healthcheck:
# 使用curl检查仓库前端是否可用
test: ["CMD", "curl", "-f", "<instance_url>"]
interval: 10s
retries: 3
start_period: 30s
timeout: 10s
environment:
# GITEA_RUNNER_REGISTRATION_TOKEN可用于设置全局运行器注册令牌。
# Gitea版本必须为v1.23或更高版本。
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
# - GITEA_RUNNER_REGISTRATION_TOKEN=<用户定义的注册令牌>
runner:
image: gitea/loong_runner
restart: always
depends_on:
gitea:
# 需要(下述配置),以便运行器能够连接到仓库,请参阅“健康检查(healthcheck)”
condition: service_healthy
restart: true
volumes:
- ./data/loong_runner:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=<instance url>
# 当使用Docker Secrets时
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
# 环境变量优先, 仅在首次启动时需要。
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```
### 使用 Docker-in-Docker (DIND) 运行 `loong_runner`
```yml
...
runner:
image: gitea/loong_runner:latest-dind-rootless
restart: always
privileged: true
depends_on:
- gitea
volumes:
- ./data/loong_runner:/data
environment:
- GITEA_INSTANCE_URL=<instance url>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
# 使用Docker Secrets时
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
# 环境变量优先, 仅在首次启动时需要。
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```

View File

@ -0,0 +1,8 @@
### 在Docker容器中运行`loong_runner`
```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/loong_runner:nightly
```
docker 容器内的 `/data` 目录在注册后包含运行器 API 密钥。
必须对其进行持久化存储,否则运行器将尝试再次注册,并使用相同的(现已失效的)注册令牌。

View File

@ -0,0 +1,19 @@
## 在 Kubernetes 中使用 `act_runner` 部署 Docker-in-Docker(DinD)
注意:Docker-in-Docker(DinD)在 Kubernetes 中需要提升权限。目前的实现方式是将 Pod 的 `SecurityContext`(安全上下文)设置为 `privileged`(特权模式)。请注意这存在潜在安全风险,恶意应用可能突破容器隔离上下文。
本目录包含文件:
- [`dind-docker.yaml`](dind-docker.yaml)
用于在 Kubernetes 中创建作为运行器的 Deployment(部署)和 Persistent Volume(持久卷)。Docker 凭证会在每次 Pod 连接时重新生成,无需持久化存储。
- [`rootless-docker.yaml`](rootless-docker.yaml)
用于在 Kubernetes 中创建 rootless(非特权)模式的 Deployment 和 Persistent Volume 作为运行器。Docker 凭证会在每次 Pod 连接时重新生成,无需持久化存储。
关键术语说明:
1. privileged 模式:容器获得与宿主机 root 用户几乎相同的权限
2. SecurityContext:Kubernetes 中定义 Pod/容器权限和安全配置的对象
3. rootless 模式:以非特权用户身份运行 Docker 守护进程,安全性更高
4. act_runner:Gitea 的 CI/CD 运行器组件
安全建议:在生产环境中,建议优先考虑 rootless 模式,若必须使用 DinD,建议通过 Pod 安全策略、网络策略和审计日志加强安全监控。

View File

@ -0,0 +1,81 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: act-runner-vol
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
---
apiVersion: v1
data:
# 注册令牌可从Web UI、API或命令行获取。
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
# 为Gitea实例设置预定义的全局运行器注册令牌。
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 '等待docker守护进程..'; sleep 5; done; /sbin/tini -- 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

View File

@ -0,0 +1,73 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: act-runner-vol
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
---
apiVersion: v1
data:
# 注册令牌可从Web UI、API或命令行获取。
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
# 为Gitea实例设置预定义的全局运行器注册令牌。
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: runner-data
persistentVolumeClaim:
claimName: act-runner-vol
securityContext:
fsGroup: 1000
containers:
- name: runner
image: gitea/act_runner:nightly-dind-rootless
imagePullPolicy: Always
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo '等待docker守护进程..'; 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
securityContext:
privileged: true
volumeMounts:
- name: runner-data
mountPath: /data

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

@ -0,0 +1,6 @@
# 在虚拟或物理服务器上使用 `loong_runner`
此目录中的文件:
- [`rootless-docker.md`](rootless-docker.md)
在非Root用户下使用Docker运行`loong_runner`

View File

@ -0,0 +1,96 @@
# 在非特权模式下使用Docker
以下是如何在在非特权模式下使用 Docker 中设置 `loong_runner` 的简单示例。实例是基于 Debian 编写的,其他 Linux 发行版不会有太大区别。
注意:此过程需要一个真实的登录 shell -- 使用 `sudo su` 或其他访问账户的方法将无法完成下面的一些步骤。
使用 `root` 用户:
- 创建一个用户来运行 `docker``loong_runner`。在这个例子中,我们使用了一个名为 `rootless` 的非特权账户。
```bash
useradd -m rootless
passwd rootless
apt-get install -y uidmap # 对 docker 非Root用户使用是必需的。
```
- 安装 [`docker-ce`](https://docs.docker.com/engine/install/)
- (推荐)禁用系统范围的 Docker 守护进程
``systemctl disable --now docker.service docker.socket``
作为 `rootless` 用户:
- 按照 [启用非特权模式](https://docs.docker.com/engine/security/rootless/) 的说明进行操作
- 将以下行添加到 `/home/rootless/.bashrc`:
```bash
for f in ./.bashrc.d/*.bash; do echo "处理 $f 文件..."; . "$f"; done
```
- 创建 `.bashrc.d` 目录 `mkdir ~/.bashrc.d`
- 将以下行添加到 `/home/rootless/.bashrc.d/rootless-docker.bash`:
```bash
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
export PATH=/home/rootless/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
```
- 重启。确保 Docker 进程正在工作。
- 为保存 `loong_runner` 数据创建一个目录
`mkdir /home/rootless/loong_runner`
- 从数据目录注册 runner
```bash
cd /home/rootless/loong_runner
loong_runner register
```
- 在数据目录中生成 `loong_runner` 配置文件。编辑文件以适应系统。
```bash
loong_runner generate-config >/home/rootless/loong_runner/config
```
- 创建一个新的用户级 `systemd` 单元文件 `/home/rootless/.config/systemd/user/loong_runner.service`,内容如下:
```bash
Description=龙架构代码仓库运行时
Documentation=https://git.whlug.cn/LoongArchActions/loong_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/loong_runner daemon -c /home/rootless/loong_runner/config
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/rootless/loong_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
```
- 重启
系统重启后,检查 `loong_runner` 是否正常工作,并且 运行时 是否已连接到 Gitea。
````bash
systemctl --user status loong_runner
journalctl --user -xeu loong_runner
```

135
go.mod
View File

@ -1,80 +1,101 @@
module gitea.com/gitea/act_runner
module git.whlug.cn/LAA/loong_runner
go 1.18
go 1.24
require (
code.gitea.io/actions-proto-go v0.2.0
github.com/avast/retry-go/v4 v4.3.1
github.com/bufbuild/connect-go v1.3.1
github.com/docker/docker v20.10.21+incompatible
github.com/joho/godotenv v1.4.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/mattn/go-isatty v0.0.16
github.com/nektos/act v0.0.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
google.golang.org/protobuf v1.28.1
code.gitea.io/actions-proto-go v0.4.1
code.gitea.io/gitea-vet v0.2.3
connectrpc.com/connect v1.16.2
github.com/avast/retry-go/v4 v4.6.0
github.com/docker/docker v25.0.5+incompatible
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.20
github.com/nektos/act v0.0.0 // will be replaced
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/term v0.22.0
golang.org/x/time v0.5.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.3 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/containerd/cgroups v1.0.3 // indirect
github.com/containerd/containerd v1.6.6 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/docker/cli v20.10.21+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/containerd/containerd v1.7.13 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/cyphar/filepath-securejoin v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/cli v25.0.3+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/buildkit v0.10.6 // indirect
github.com/moby/sys/mount v0.3.1 // indirect
github.com/moby/sys/mountinfo v0.6.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/buildkit v0.12.5 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/opencontainers/selinux v1.10.2 // indirect
github.com/opencontainers/image-spec v1.1.0 // 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/rhysd/actionlint v1.6.22 // indirect
github.com/rivo/uniseg v0.3.4 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rhysd/actionlint v1.7.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/nektos/act => gitea.com/gitea/act v0.234.3-0.20230224062034-1252e551b867
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4

1316
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"os/signal"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"github.com/nektos/act/pkg/artifactcache"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type cacheServerArgs struct {
Dir string // 缓存目录
Host string // 主机地址
Port uint16 // 端口号
}
func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadDefault(*configFile)
if err != nil {
return fmt.Errorf("配置无效: %w", err)
}
initLogging(cfg)
var (
dir = cfg.Cache.Dir
host = cfg.Cache.Host
port = cfg.Cache.Port
)
// cacheArgs 优先级更高
if cacheArgs.Dir != "" {
dir = cacheArgs.Dir
}
if cacheArgs.Host != "" {
host = cacheArgs.Host
}
if cacheArgs.Port != 0 {
port = cacheArgs.Port
}
cacheHandler, err := artifactcache.StartHandler(
dir,
host,
port,
log.StandardLogger().WithField("module", "cache_request"),
)
if err != nil {
return err
}
log.Infof("缓存服务器正在监听 %v", cacheHandler.ExternalURL())
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
return nil
}
}

88
internal/app/cmd/cmd.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
)
func Execute(ctx context.Context) {
// ./act_runner
rootCmd := &cobra.Command{
Use: "act_runner [运行事件名称]\n如果没有传递事件名称, 默认为 \"on: push\"",
Short: "通过指定事件名称(例如 `push`)或直接指定操作名称来本地运行 GitHub Actions。",
Args: cobra.MaximumNArgs(1),
Version: ver.Version(),
SilenceUsage: true,
}
configFile := ""
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径")
// ./act_runner register
var regArgs registerArgs
registerCmd := &cobra.Command{
Use: "register",
Short: "将运行器注册到服务器",
Args: cobra.MaximumNArgs(0),
RunE: runRegister(ctx, &regArgs, &configFile), // 必须使用 regArgs 的指针
}
registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "禁用交互模式")
registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Gitea 实例地址")
registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "运行器令牌")
registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "运行器名称")
registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "运行器标签,逗号分隔")
registerCmd.Flags().BoolVar(&regArgs.Ephemeral, "ephemeral", false, "配置运行器为临时运行器,只能选择单个作业(比 --once 更严格)")
rootCmd.AddCommand(registerCmd)
// ./act_runner daemon
var daemArgs daemonArgs
daemonCmd := &cobra.Command{
Use: "daemon",
Short: "作为运行器守护进程运行",
Args: cobra.MaximumNArgs(0),
RunE: runDaemon(ctx, &daemArgs, &configFile),
}
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "运行一个作业然后退出")
rootCmd.AddCommand(daemonCmd)
// ./act_runner exec
rootCmd.AddCommand(loadExecCmd(ctx))
// ./act_runner config
rootCmd.AddCommand(&cobra.Command{
Use: "generate-config",
Short: "生成示例配置文件",
Args: cobra.MaximumNArgs(0),
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("%s", config.Example)
},
})
// ./act_runner cache-server
var cacheArgs cacheServerArgs
cacheCmd := &cobra.Command{
Use: "cache-server",
Short: "启动缓存服务器用于缓存操作",
Args: cobra.MaximumNArgs(0),
RunE: runCacheServer(ctx, &configFile, &cacheArgs),
}
cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "缓存目录")
cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "缓存服务器主机")
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "缓存服务器端口")
rootCmd.AddCommand(cacheCmd)
// 隐藏自动补全命令
rootCmd.CompletionOptions.HiddenDefaultCmd = true
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

230
internal/app/cmd/daemon.go Normal file
View File

@ -0,0 +1,230 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"connectrpc.com/connect"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/app/poll"
"git.whlug.cn/LAA/loong_runner/internal/app/run"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/envcheck"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
)
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadDefault(*configFile)
if err != nil {
return fmt.Errorf("无效的配置: %w", err)
}
initLogging(cfg)
log.Infoln("启动运行器守护进程")
reg, err := config.LoadRegistration(cfg.Runner.File)
if os.IsNotExist(err) {
log.Error("未找到注册文件,请先注册运行器")
return err
} else if err != nil {
return fmt.Errorf("加载注册文件失败: %w", err)
}
lbls := reg.Labels
if len(cfg.Runner.Labels) > 0 {
lbls = cfg.Runner.Labels
}
ls := labels.Labels{}
for _, l := range lbls {
label, err := labels.Parse(l)
if err != nil {
log.WithError(err).Warnf("忽略无效标签 %q", l)
continue
}
ls = append(ls, label)
}
if len(ls) == 0 {
log.Warn("未配置任何标签,运行器可能无法接取作业")
}
if ls.RequireDocker() {
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
if err != nil {
return err
}
if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil {
return err
}
// 如果 dockerSocketPath 通过检查,用 dockerSocketPath 覆盖 DOCKER_HOST
os.Setenv("DOCKER_HOST", dockerSocketPath)
// 如果 cfg.Container.DockerHost 为空,意味着 act_runner 需要自动查找可用的 docker 主机
// 并将路径分配给 cfg.Container.DockerHost
if cfg.Container.DockerHost == "" {
cfg.Container.DockerHost = dockerSocketPath
}
// 检查方案,如果方案不是 npipe 或 unix
// 将 cfg.Container.DockerHost 设置为 "-",因为它不能挂载到作业容器
if protoIndex := strings.Index(cfg.Container.DockerHost, "://"); protoIndex != -1 {
scheme := cfg.Container.DockerHost[:protoIndex]
if !strings.EqualFold(scheme, "npipe") && !strings.EqualFold(scheme, "unix") {
cfg.Container.DockerHost = "-"
}
}
}
if !slices.Equal(reg.Labels, ls.ToStrings()) {
reg.Labels = ls.ToStrings()
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
return fmt.Errorf("保存运行器配置失败: %w", err)
}
log.Infof("标签更新为: %v", reg.Labels)
}
cli := client.New(
reg.Address,
cfg.Runner.Insecure,
reg.UUID,
reg.Token,
ver.Version(),
)
runner := run.NewRunner(cfg, reg, cli)
// 在获取任务之前声明运行器的标签
resp, err := runner.Declare(ctx, ls.Names())
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
log.Errorf("您的 Gitea 版本太旧,不支持运行器声明,请升级到 v1.21 或更高版本")
return err
} else if err != nil {
log.WithError(err).Error("调用 Declare 失败")
return err
} else {
log.Infof("运行器: %s, 版本: %s, 标签: %v, 声明成功",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
}
poller := poll.New(cfg, cli, runner)
if daemArgs.Once || reg.Ephemeral {
done := make(chan struct{})
go func() {
defer close(done)
poller.PollOnce()
}()
// 当完成一个作业或请求取消时关闭
select {
case <-ctx.Done():
case <-done:
}
} else {
go poller.Poll()
<-ctx.Done()
}
log.Infof("运行器: %s 关闭开始,等待 %s 让正在运行的作业完成后再关闭", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
defer cancel()
err = poller.Shutdown(ctx)
if err != nil {
log.Warnf("运行器: %s 在关闭期间取消了正在进行的作业", resp.Msg.Runner.Name)
}
return nil
}
}
type daemonArgs struct {
Once bool // 是否只运行一次
}
// initLogging 设置全局 logrus 日志记录器。
func initLogging(cfg *config.Config) {
isTerm := isatty.IsTerminal(os.Stdout.Fd())
format := &log.TextFormatter{
DisableColors: !isTerm,
FullTimestamp: true,
}
log.SetFormatter(format)
if l := cfg.Log.Level; l != "" {
level, err := log.ParseLevel(l)
if err != nil {
log.WithError(err).
Errorf("无效的日志级别: %q", l)
}
// 调试级别
if level == log.DebugLevel {
log.SetReportCaller(true)
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
// 获取函数名
s := strings.Split(f.Function, ".")
funcname := "[" + s[len(s)-1] + "]"
// 获取文件名和行号
_, filename := path.Split(f.File)
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
return funcname, filename
}
log.SetFormatter(format)
}
if log.GetLevel() != level {
log.Infof("日志级别更改为 %v", level)
log.SetLevel(level)
}
}
}
var commonSocketPaths = []string{
"/var/run/docker.sock",
"/run/podman/podman.sock",
"$HOME/.colima/docker.sock",
"$XDG_RUNTIME_DIR/docker.sock",
"$XDG_RUNTIME_DIR/podman/podman.sock",
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
func getDockerSocketPath(configDockerHost string) (string, error) {
// `-` 表示不要将 docker 套接字挂载到作业容器
if configDockerHost != "" && configDockerHost != "-" {
return configDockerHost, nil
}
socket, found := os.LookupEnv("DOCKER_HOST")
if found {
return socket, nil
}
for _, p := range commonSocketPaths {
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
if strings.HasPrefix(p, `\\.\`) {
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
}
return "", fmt.Errorf("守护进程 Docker 引擎套接字未找到且 docker_host 配置无效")
}

491
internal/app/cmd/exec.go Normal file
View File

@ -0,0 +1,491 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2019 nektos
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"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"
"github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
)
type executeArgs struct {
runList bool // 是否列出工作流
job string // 特定作业 ID
event string // 事件名称
workdir string // 工作目录
workflowsPath string // 工作流文件路径
noWorkflowRecurse bool // 是否禁止递归运行子目录中的工作流
autodetectEvent bool // 自动检测事件
forcePull bool // 即使存在也拉取 Docker 镜像
forceRebuild bool // 即使存在也重建本地动作 Docker 镜像
jsonLogger bool // 以 JSON 格式输出日志
envs []string // 环境变量
envfile string // 环境文件
secrets []string // 机密
defaultActionsURL string // 默认动作 URL
insecureSecrets bool // 不建议!打印日志时不隐藏机密
privileged bool // 使用特权模式
usernsMode string // 用户命名空间
containerArchitecture string // 容器架构
containerDaemonSocket string // 容器守护进程套接字
useGitIgnore bool // 控制是否将 .gitignore 中指定的路径复制到容器中
containerCapAdd []string // 添加到工作流容器的内核能力
containerCapDrop []string // 从工作流容器中删除的内核能力
containerOptions string // 容器选项
artifactServerPath string // 存储上传和检索下载的路径
artifactServerAddr string // 监听地址
artifactServerPort string // 监听端口
noSkipCheckout bool // 不跳过 actions/checkout
debug bool // 启用调试日志
dryrun bool // dryrun 模式
image string // 使用的 Docker 镜像
cacheHandler *artifactcache.Handler // 缓存处理器
network string // 容器连接的网络
githubInstance string // 使用的 Gitea 实例
}
// WorkflowsPath 返回工作流文件的路径
func (i *executeArgs) WorkflowsPath() string {
return i.resolve(i.workflowsPath)
}
// Envfile 返回 .env 文件的路径
func (i *executeArgs) Envfile() string {
return i.resolve(i.envfile)
}
func (i *executeArgs) LoadSecrets() map[string]string {
s := make(map[string]string)
for _, secretPair := range i.secrets {
secretPairParts := strings.SplitN(secretPair, "=", 2)
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
log.Errorf("机密 %s 已经定义(机密不区分大小写)", secretPairParts[0])
}
if len(secretPairParts) == 2 {
s[secretPairParts[0]] = secretPairParts[1]
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
s[secretPairParts[0]] = env
} else {
fmt.Printf("为 '%s' 提供值: ", secretPairParts[0])
val, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
log.Errorf("读取输入失败: %v", err)
os.Exit(1)
}
s[secretPairParts[0]] = string(val)
}
}
return s
}
func readEnvs(path string, envs map[string]string) bool {
if _, err := os.Stat(path); err == nil {
env, err := godotenv.Read(path)
if err != nil {
log.Fatalf("从 %s 加载失败: %v", path, err)
}
for k, v := range env {
envs[k] = v
}
return true
}
return false
}
func (i *executeArgs) LoadEnvs() map[string]string {
envs := make(map[string]string)
if i.envs != nil {
for _, envVar := range i.envs {
e := strings.SplitN(envVar, `=`, 2)
if len(e) == 2 {
envs[e[0]] = e[1]
} else {
envs[e[0]] = ""
}
}
}
_ = readEnvs(i.Envfile(), envs)
envs["ACTIONS_CACHE_URL"] = i.cacheHandler.ExternalURL() + "/"
return envs
}
// Workdir 返回工作目录的路径
func (i *executeArgs) Workdir() string {
return i.resolve(".")
}
func (i *executeArgs) resolve(path string) string {
basedir, err := filepath.Abs(i.workdir)
if err != nil {
log.Fatal(err)
}
if path == "" {
return path
}
if !filepath.IsAbs(path) {
path = filepath.Join(basedir, path)
}
return path
}
func printList(plan *model.Plan) error {
type lineInfoDef struct {
jobID string // 作业 ID
jobName string // 作业名称
stage string // 阶段
wfName string // 工作流名称
wfFile string // 工作流文件
events string // 事件
}
lineInfos := []lineInfoDef{}
header := lineInfoDef{
jobID: "作业 ID",
jobName: "作业名称",
stage: "阶段",
wfName: "工作流名称",
wfFile: "工作流文件",
events: "事件",
}
jobs := map[string]bool{}
duplicateJobIDs := false
jobIDMaxWidth := len(header.jobID)
jobNameMaxWidth := len(header.jobName)
stageMaxWidth := len(header.stage)
wfNameMaxWidth := len(header.wfName)
wfFileMaxWidth := len(header.wfFile)
eventsMaxWidth := len(header.events)
for i, stage := range plan.Stages {
for _, r := range stage.Runs {
jobID := r.JobID
line := lineInfoDef{
jobID: jobID,
jobName: r.String(),
stage: strconv.Itoa(i),
wfName: r.Workflow.Name,
wfFile: r.Workflow.File,
events: strings.Join(r.Workflow.On(), `,`),
}
if _, ok := jobs[jobID]; ok {
duplicateJobIDs = true
} else {
jobs[jobID] = true
}
lineInfos = append(lineInfos, line)
if jobIDMaxWidth < len(line.jobID) {
jobIDMaxWidth = len(line.jobID)
}
if jobNameMaxWidth < len(line.jobName) {
jobNameMaxWidth = len(line.jobName)
}
if stageMaxWidth < len(line.stage) {
stageMaxWidth = len(line.stage)
}
if wfNameMaxWidth < len(line.wfName) {
wfNameMaxWidth = len(line.wfName)
}
if wfFileMaxWidth < len(line.wfFile) {
wfFileMaxWidth = len(line.wfFile)
}
if eventsMaxWidth < len(line.events) {
eventsMaxWidth = len(line.events)
}
}
}
jobIDMaxWidth += 2
jobNameMaxWidth += 2
stageMaxWidth += 2
wfNameMaxWidth += 2
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
-stageMaxWidth, header.stage,
-jobIDMaxWidth, header.jobID,
-jobNameMaxWidth, header.jobName,
-wfNameMaxWidth, header.wfName,
-wfFileMaxWidth, header.wfFile,
-eventsMaxWidth, header.events,
)
for _, line := range lineInfos {
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
-stageMaxWidth, line.stage,
-jobIDMaxWidth, line.jobID,
-jobNameMaxWidth, line.jobName,
-wfNameMaxWidth, line.wfName,
-wfFileMaxWidth, line.wfFile,
-eventsMaxWidth, line.events,
)
}
if duplicateJobIDs {
fmt.Print("\n检测到多个作业具有相同的作业名称,请使用 `-W` 指定特定工作流的路径。\n")
}
return nil
}
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
// 计划带有过滤的作业 - 仅用于过滤
var filterPlan *model.Plan
// 确定要过滤的事件名称
var filterEventName string
if len(execArgs.event) > 0 {
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
filterEventName = execArgs.event
} else if execArgs.autodetectEvent {
// 收集所有加载的工作流中的事件
events := planner.GetEvents()
// 将默认事件类型设置为第一个可用的事件
// 这样用户就不必指定事件。
log.Infof("使用检测到的第一个工作流事件进行过滤: %s", events[0])
filterEventName = events[0]
}
var err error
if execArgs.job != "" {
log.Infof("准备带有作业的计划: %s", execArgs.job)
filterPlan, err = planner.PlanJob(execArgs.job)
if err != nil {
return err
}
} else if filterEventName != "" {
log.Infof("准备事件的计划: %s", filterEventName)
filterPlan, err = planner.PlanEvent(filterEventName)
if err != nil {
return err
}
} else {
log.Infof("准备所有作业的计划")
filterPlan, err = planner.PlanAll()
if err != nil {
return err
}
}
_ = printList(filterPlan)
return nil
}
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
if err != nil {
return err
}
if execArgs.runList {
return runExecList(ctx, planner, execArgs)
}
// 计划触发作业
var plan *model.Plan
// 确定要触发的事件名称
var eventName string
// 收集所有加载的工作流中的事件
events := planner.GetEvents()
if len(execArgs.event) > 0 {
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
eventName = execArgs.event
} else if len(events) == 1 && len(events[0]) > 0 {
log.Infof("使用唯一检测到的工作流事件: %s", events[0])
eventName = events[0]
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
// 将默认事件类型设置为第一个可用的事件
// 这样用户就不必指定事件。
log.Infof("使用检测到的第一个工作流事件: %s", events[0])
eventName = events[0]
} else {
log.Infof("使用默认工作流事件: push")
eventName = "push"
}
// 为此运行构建计划
if execArgs.job != "" {
log.Infof("规划作业: %s", execArgs.job)
plan, err = planner.PlanJob(execArgs.job)
if err != nil {
return err
}
} else {
log.Infof("规划事件的作业: %s", eventName)
plan, err = planner.PlanEvent(eventName)
if err != nil {
return err
}
}
maxLifetime := 3 * time.Hour
if deadline, ok := ctx.Deadline(); ok {
maxLifetime = time.Until(deadline)
}
// 初始化缓存服务器
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
if err != nil {
return err
}
log.Infof("缓存处理器监听于: %v", handler.ExternalURL())
execArgs.cacheHandler = handler
if len(execArgs.artifactServerAddr) == 0 {
ip := common.GetOutboundIP()
if ip == nil {
return fmt.Errorf("无法确定出站 IP 地址")
}
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
}
// 运行计划
config := &runner.Config{
Workdir: execArgs.Workdir(),
BindWorkdir: false,
ReuseContainers: false,
ForcePull: execArgs.forcePull,
ForceRebuild: execArgs.forceRebuild,
LogOutput: true,
JSONLogger: execArgs.jsonLogger,
Env: execArgs.LoadEnvs(),
Secrets: execArgs.LoadSecrets(),
InsecureSecrets: execArgs.insecureSecrets,
Privileged: execArgs.privileged,
UsernsMode: execArgs.usernsMode,
ContainerArchitecture: execArgs.containerArchitecture,
ContainerDaemonSocket: execArgs.containerDaemonSocket,
UseGitIgnore: execArgs.useGitIgnore,
GitHubInstance: execArgs.githubInstance,
ContainerCapAdd: execArgs.containerCapAdd,
ContainerCapDrop: execArgs.containerCapDrop,
ContainerOptions: execArgs.containerOptions,
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: container.NetworkMode(execArgs.network),
DefaultActionInstance: execArgs.defaultActionsURL,
PlatformPicker: func(_ []string) string {
return execArgs.image
},
ValidVolumes: []string{"​**​"}, // 所有挂载的卷(volumes)都允许被 exec 命令访问
}
config.Env["ACT_EXEC"] = "true"
if t := config.Secrets["GITEA_TOKEN"]; t != "" {
config.Token = t
} else if t := config.Secrets["GITHUB_TOKEN"]; t != "" {
config.Token = t
}
if !execArgs.debug {
logLevel := log.InfoLevel
config.JobLoggerLevel = &logLevel
}
r, err := runner.New(config)
if err != nil {
return err
}
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
log.Debugf("artifacts 服务器启动于 %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
ctx = common.WithDryrun(ctx, execArgs.dryrun)
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
artifactCancel()
return nil
})
return executor(ctx)
}
}
func loadExecCmd(ctx context.Context) *cobra.Command {
execArg := executeArgs{}
execCmd := &cobra.Command{
Use: "exec",
Short: "本地运行工作流",
Args: cobra.MaximumNArgs(20),
RunE: runExec(ctx, &execArg),
}
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "本地运行工作流")
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "运行特定作业 ID")
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "运行事件名称")
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "工作流文件路径")
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "工作目录")
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "禁用运行指定路径的子目录中的工作流")
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "使用工作流中的第一个事件类型作为触发工作流的事件")
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "即使已经存在也拉取 Docker 镜像")
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "即使已经存在也重建本地动作 Docker 镜像")
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "以 JSON 格式输出日志")
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "使环境变量对动作可用,可选值(例如 --env myenv=foo 或 --env myenv)")
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "读取并用作容器中的环境的环境文件")
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "为 Action 提供密钥,可带可选值(例如 -s mysecret=foo 或 -s mysecret)")
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "不推荐!打印日志时不会隐藏密钥信息")
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "使用特权模式")
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "要使用的用户命名空间")
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "运行容器使用的架构(如 linux/loong64)。未指定时使用宿主机默认架构。需要 Docker 服务端 API 版本 1.41+,更低版本的 Docker 平台会忽略此参数")
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "挂载到容器的 Docker 守护进程 socket 路径")
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "控制是否将 .gitignore 中指定的路径复制到容器中")
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "为工作流容器添加的内核能力(例如 --container-cap-add SYS_PTRACE)")
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "从工作流容器移除的内核能力(例如 --container-cap-drop SYS_PTRACE)")
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "容器选项")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "定义构建物服务器存储上传和下载的路径。未指定时构建物服务器不会启动")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "定义构建物服务器的监听地址")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "定义构建物服务器的监听端口(例如 --container-cap-drop SYS_PTRACE)")
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "定义 Action 实例的默认 URL")
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "不跳过 actions/checkout")
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "启用调试日志")
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun 模式")
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "lcr.loongnix.cn/library/debian:latest", "使用的 Docker 镜像。使用 \"-self-hosted\" 直接在主机上运行")
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "容器连接的网络")
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "使用的 Gitea 实例")
return execCmd
}

View File

@ -0,0 +1,394 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"os/signal"
goruntime "runtime"
"strings"
"time"
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
)
// runRegister 将运行器注册到服务器
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(false)
isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{
DisableColors: !isTerm,
DisableTimestamp: true,
})
log.SetLevel(log.DebugLevel)
log.Infof("注册运行器,架构=%s,操作系统=%s,版本=%s。",
goruntime.GOARCH, goruntime.GOOS, ver.Version())
// 运行器始终需要 root 权限
if os.Getuid() != 0 {
// TODO: 使用更好的方法检查 root 权限
log.Warnf("运行器处于用户模式。")
}
if regArgs.NoInteractive {
if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {
return err
}
} else {
go func() {
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
log.Fatal(err)
return
}
os.Exit(0)
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
return nil
}
}
// registerArgs 代表 register 命令的参数
type registerArgs struct {
NoInteractive bool // 是否非交互模式
InstanceAddr string // 实例地址
Token string // 令牌
RunnerName string // 运行器名称
Labels string // 标签
Ephemeral bool // 是否临时
}
type registerStage int8
const (
StageUnknown registerStage = -1
StageOverwriteLocalConfig registerStage = iota + 1
StageInputInstance
StageInputToken
StageInputRunnerName
StageInputLabels
StageWaitingForRegistration
StageExit
)
var defaultLabels = []string{
"debian-latest:docker://lcr.loongnix.cn/library/debian:latest",
"anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest",
"anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2",
}
type registerInputs struct {
InstanceAddr string
Token string
RunnerName string
Labels []string
Ephemeral bool
}
func (r *registerInputs) validate() error {
if r.InstanceAddr == "" {
return fmt.Errorf("实例地址为空")
}
if r.Token == "" {
return fmt.Errorf("令牌为空")
}
if len(r.Labels) > 0 {
return validateLabels(r.Labels)
}
return nil
}
func validateLabels(ls []string) error {
for _, label := range ls {
if _, err := labels.Parse(label); err != nil {
return err
}
}
return nil
}
func (r *registerInputs) stageValue(stage registerStage) string {
switch stage {
case StageInputInstance:
return r.InstanceAddr
case StageInputToken:
return r.Token
case StageInputRunnerName:
return r.RunnerName
case StageInputLabels:
if len(r.Labels) > 0 {
return strings.Join(r.Labels, ",")
}
}
return ""
}
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
// 必须设置实例地址和令牌。
// 如果为空,保持当前阶段。
if stage == StageInputInstance || stage == StageInputToken {
if value == "" {
return stage
}
}
// 如果运行器名称为空,设置为主机名
if stage == StageInputRunnerName && value == "" {
value, _ = os.Hostname()
}
switch stage {
case StageOverwriteLocalConfig:
if value == "Y" || value == "y" {
return StageInputInstance
}
return StageExit
case StageInputInstance:
r.InstanceAddr = value
return StageInputToken
case StageInputToken:
r.Token = value
return StageInputRunnerName
case StageInputRunnerName:
r.RunnerName = value
// 如果配置文件中有标签配置,跳过输入标签阶段
if len(cfg.Runner.Labels) > 0 {
ls := make([]string, 0, len(cfg.Runner.Labels))
for _, l := range cfg.Runner.Labels {
_, err := labels.Parse(l)
if err != nil {
log.WithError(err).Warnf("忽略无效标签 %q", l)
continue
}
ls = append(ls, l)
}
if len(ls) == 0 {
log.Warn("配置文件中没有有效的标签配置,运行器可能无法接取作业")
}
r.Labels = ls
return StageWaitingForRegistration
}
return StageInputLabels
case StageInputLabels:
r.Labels = defaultLabels
if value != "" {
r.Labels = strings.Split(value, ",")
}
if validateLabels(r.Labels) != nil {
log.Infoln("无效的标签, 请重新输入, 留空以使用默认标签 (例如, debian-latest:lcr.loongnix.cn/library/debian:latest) ")
r.Labels = nil
return StageInputLabels
}
return StageWaitingForRegistration
}
return StageUnknown
}
func initInputs(regArgs *registerArgs) *registerInputs {
inputs := &registerInputs{
InstanceAddr: regArgs.InstanceAddr,
Token: regArgs.Token,
RunnerName: regArgs.RunnerName,
Ephemeral: regArgs.Ephemeral,
}
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
// command line flag.
if regArgs.Labels != "" {
inputs.Labels = strings.Split(regArgs.Labels, ",")
}
return inputs
}
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
var (
reader = bufio.NewReader(os.Stdin)
stage = StageInputInstance
)
cfg, err := config.LoadDefault(configFile)
if err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
stage = StageOverwriteLocalConfig
}
inputs := initInputs(regArgs)
for {
cmdString := inputs.stageValue(stage)
if cmdString == "" {
printStageHelp(stage)
var err error
cmdString, err = reader.ReadString('\n')
if err != nil {
return err
}
}
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
if stage == StageWaitingForRegistration {
log.Infof("注册运行器,名称=%s, 实例=%s, 标签=%v。", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("注册运行器失败: %w", err)
}
log.Infof("运行器注册成功。")
return nil
}
if stage == StageExit {
return nil
}
if stage <= StageUnknown {
log.Errorf("无效输入,请重新运行命令。")
return nil
}
}
}
func printStageHelp(stage registerStage) {
switch stage {
case StageOverwriteLocalConfig:
log.Infoln("运行器已注册,是否覆盖本地配置?[y/N]")
case StageInputInstance:
log.Infoln("请输入 Gitea 实例 URL(例如, https://gitea.com/):")
case StageInputToken:
log.Infoln("请输入运行器令牌:")
case StageInputRunnerName:
hostname, _ := os.Hostname()
log.Infof("请输入运行器名称(如果留空,使用主机名:%s):\n", hostname)
case StageInputLabels:
log.Infoln("请输入运行器标签, 留空以使用默认标签(逗号分隔, 例如, debian-latest:docker://lcr.loongnix.cn/library/debian:latest):")
case StageWaitingForRegistration:
log.Infoln("等待注册...")
}
}
func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
cfg, err := config.LoadDefault(configFile)
if err != nil {
return err
}
inputs := initInputs(regArgs)
// 配置文件中指定的标签。
if len(cfg.Runner.Labels) > 0 {
if regArgs.Labels != "" {
log.Warn("命令行中的标签将被忽略,使用配置文件中定义的标签。")
}
inputs.Labels = cfg.Runner.Labels
}
if len(inputs.Labels) == 0 {
inputs.Labels = defaultLabels
}
if inputs.RunnerName == "" {
inputs.RunnerName, _ = os.Hostname()
log.Infof("运行器名称为空,使用主机名 '%s'。", inputs.RunnerName)
}
if err := inputs.validate(); err != nil {
log.WithError(err).Errorf("无效输入,请重新运行命令。")
return err
}
if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("注册运行器失败: %w", err)
}
log.Infof("运行器注册成功。")
return nil
}
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
// 初始化 http 客户端
cli := client.New(
inputs.InstanceAddr,
cfg.Runner.Insecure,
"",
"",
ver.Version(),
)
for {
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
Data: inputs.RunnerName,
}))
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if ctx.Err() != nil {
break
}
if err != nil {
log.WithError(err).
Errorln("无法 ping 到 Gitea 实例服务器")
// TODO: 如果 ping 失败,重试或退出
time.Sleep(time.Second)
} else {
log.Debugln("成功 ping 到 Gitea 实例服务器")
break
}
}
reg := &config.Registration{
Name: inputs.RunnerName,
Token: inputs.Token,
Address: inputs.InstanceAddr,
Labels: inputs.Labels,
Ephemeral: inputs.Ephemeral,
}
ls := make([]string, len(reg.Labels))
for i, v := range reg.Labels {
l, _ := labels.Parse(v)
ls[i] = l.Name
}
// 注册新的运行器。
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
Name: reg.Name,
Token: reg.Token,
Version: ver.Version(),
AgentLabels: ls, // 在 Gitea 1.20 之后可能会被移除
Labels: ls,
Ephemeral: reg.Ephemeral,
}))
if err != nil {
log.WithError(err).Error("poller: 无法注册新运行器")
return err
}
reg.ID = resp.Msg.Runner.Id
reg.UUID = resp.Msg.Runner.Uuid
reg.Name = resp.Msg.Runner.Name
reg.Token = resp.Msg.Runner.Token
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
// TODO 我们不能通过 runner api 移除配置,如果在这里返回错误,我们只是填充数据库
log.Error("poller: 无法注册新的临时运行器,升级 Gitea 以获得安全性,自动使用 run-once")
}
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
return fmt.Errorf("保存运行器配置失败: %w", err)
}
return nil
}

View File

@ -0,0 +1,19 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"gotest.tools/v3/assert"
)
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
err := registerNoInteractive(t.Context(), "", &registerArgs{
Labels: "标签:无效",
Token: "token",
InstanceAddr: "http://localhost:3000",
})
assert.Error(t, err, "不支持的标签: 无效")
}

208
internal/app/poll/poller.go Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package poll
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"git.whlug.cn/LAA/loong_runner/internal/app/run"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
)
type Poller struct {
client client.Client // Gitea 客户端,用于与服务器通信
runner *run.Runner // 任务执行器
cfg *config.Config // 配置信息
tasksVersion atomic.Int64 // 任务版本号,用于增量同步
pollingCtx context.Context // 轮询上下文
shutdownPolling context.CancelFunc // 轮询关闭函数
jobsCtx context.Context // 任务执行上下文
shutdownJobs context.CancelFunc // 任务执行关闭函数
done chan struct{} // 完成信号通道
}
// New 构造函数,创建并初始化 Poller 实例
// 接受配置、客户端和服务运行器作为参数,并初始化必要的上下文和通道以便于后续操作。
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
done := make(chan struct{})
return &Poller{
client: client,
runner: runner,
cfg: cfg,
pollingCtx: pollingCtx,
shutdownPolling: shutdownPolling,
jobsCtx: jobsCtx,
shutdownJobs: shutdownJobs,
done: done,
}
}
// Poll 持续轮询模式
// 启动多个 goroutine 来并发地轮询任务。每个 goroutine 调用 poll 方法进行实际的工作。在所有工作完成之后,通过关闭 done 通道发出信号。
func (p *Poller) Poll() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
wg := &sync.WaitGroup{}
for i := 0; i < p.cfg.Runner.Capacity; i++ {
wg.Add(1)
go p.poll(wg, limiter)
}
wg.Wait()
// 发出我们正在关闭的信号
close(p.done)
}
// PollOnce 单次轮询模式
// 类似于 Poll,但是只执行一次任务轮询。同样会在完成后关闭 done 通道。
func (p *Poller) PollOnce() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
p.pollOnce(limiter)
// 发出我们已经完成的信号
close(p.done)
}
// Shutdown 关闭
// 关闭轮询过程。首先取消轮询上下文,然后等待所有任务完成。如果超时发生,则强制关闭所有正在运行的任务并确保状态报告给 Gitea。
func (p *Poller) Shutdown(ctx context.Context) error {
p.shutdownPolling()
select {
// 优雅地完成关闭
case <-p.done:
return nil
// 关闭超时
case <-ctx.Done():
// 当超时和优雅关闭同时发生时,
// 这个分支可能会被触发。这里进行非阻塞检查,
// 避免在不必要的情况下发送错误。
_, ok := <-p.done
if !ok {
return nil
}
// 强制关闭所有运行中的任务
p.shutdownJobs()
// 等待运行中的任务向Gitea报告其状态
_, _ = <-p.done
return ctx.Err()
}
}
// poll 轮询循环
// 这是实际执行轮询逻辑的地方。它会持续调用 pollOnce 方法来获取并处理任务,直到上下文被取消。
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
defer wg.Done()
for {
p.pollOnce(limiter)
select {
case <-p.pollingCtx.Done():
return
default:
continue
}
}
}
// pollOnce 单次轮询执行
// 负责单次任务轮询。它首先等待速率限制器允许继续,然后尝试获取一个新任务。一旦获取到任务,就调用 runTaskWithRecover 方法来执行任务。
func (p *Poller) pollOnce(limiter *rate.Limiter) {
for {
if err := limiter.Wait(p.pollingCtx); err != nil {
if p.pollingCtx.Err() != nil {
log.WithError(err).Debug("速率限制等待失败")
}
return
}
task, ok := p.fetchTask(p.pollingCtx)
if !ok {
continue
}
p.runTaskWithRecover(p.jobsCtx, task)
return
}
}
// runTaskWithRecover 安全执行任务
// 执行给定的任务,并提供 panic 恢复机制以防止意外崩溃。如果任务执行过程中出现错误或 panic,都会记录相应的日志信息。
func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
// 建立错误恢复机制
defer func() {
if r := recover(); r != nil {
err := fmt.Errorf("panic: %v", r)
log.WithError(err).Error("runTaskWithRecover中发生panic")
}
}()
if err := p.runner.Run(ctx, task); err != nil {
log.WithError(err).Error("运行任务失败")
}
}
// fetchTask 获取任务
// 尝试从 Gitea 获取一个新任务。如果成功获取到任务且该任务的版本号比本地缓存的大,则更新本地缓存的版本号。
// 如果获取失败或没有可用的任务,返回 false;否则返回 true 和任务数据。
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
defer cancel()
// 加载发送请求时缓存中的版本值
v := p.tasksVersion.Load()
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: v,
}))
if errors.Is(err, context.DeadlineExceeded) {
err = nil
}
if err != nil {
log.WithError(err).Error("获取任务失败")
return nil, false
}
if resp == nil || resp.Msg == nil {
return nil, false
}
if resp.Msg.TasksVersion > v {
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
}
if resp.Msg.Task == nil {
return nil, false
}
// 收到一个任务,将`tasksVersion`设置为零,以便在下一个请求中创建查询数据库。
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
return resp.Msg.Task, true
}

View File

@ -0,0 +1,23 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"io"
log "github.com/sirupsen/logrus"
)
// NullLogger用于创建一个新的JobLogger以丢弃日志。
// 这将防止这些日志被记录到标准输出,但会通过其钩子将它们转发给Reporter。
type NullLogger struct{}
// WithJobLogger 创建一个新的 logrus.Logger,它将丢弃所有日志。
func (n NullLogger) WithJobLogger() *log.Logger {
logger := log.New()
logger.SetOutput(io.Discard)
logger.SetLevel(log.TraceLevel)
return logger
}

255
internal/app/run/runner.go Normal file
View File

@ -0,0 +1,255 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"sync"
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"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"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/report"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
)
// Runner 运行流水线
type Runner struct {
name string // Runner 名称
cfg *config.Config // 配置信息
client client.Client // Gitea 客户端
labels labels.Labels // 标签集合
envs map[string]string // 环境变量
// 正在运行的任务
runningTasks sync.Map // 使用 sync.Map 来存储正在运行的任务 ID
}
// NewRunner 使用提供的配置、注册信息和客户端创建一个新的 Runner 实例
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
ls := labels.Labels{}
for _, v := range reg.Labels {
if l, err := labels.Parse(v); err == nil {
ls = append(ls, l)
}
}
envs := make(map[string]string, len(cfg.Runner.Envs))
for k, v := range cfg.Runner.Envs {
envs[k] = v
}
// 初始化缓存环境变量
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
if cfg.Cache.ExternalServer != "" {
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
} else {
cacheHandler, err := artifactcache.StartHandler(
cfg.Cache.Dir,
cfg.Cache.Host,
cfg.Cache.Port,
log.StandardLogger().WithField("module", "cache_request"),
)
if err != nil {
log.Errorf("无法初始化缓存服务器,将禁用它:%v", err)
// 继续执行
} else {
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
}
}
}
// 设置 artifact gitea api
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/")
// 设置特定环境变量以区分 Gitea 和 GitHub
envs["GITEA_ACTIONS"] = "true"
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
return &Runner{
name: reg.Name,
cfg: cfg,
client: cli,
labels: ls,
envs: envs,
}
}
// Run 在给定的上下文中执行任务,确保同一时间仅运行一个任务
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
// 检查任务是否已在运行
if _, ok := r.runningTasks.Load(task.Id); ok {
return fmt.Errorf("任务 %d 已经在运行", task.Id)
}
r.runningTasks.Store(task.Id, struct{}{})
defer r.runningTasks.Delete(task.Id)
// 创建带超时的子上下文
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
defer cancel()
reporter := report.NewReporter(ctx, cancel, r.client, task)
var runErr error
defer func() {
lastWords := ""
if runErr != nil {
lastWords = runErr.Error()
}
_ = reporter.Close(lastWords)
}()
reporter.RunDaemon()
runErr = r.run(ctx, task, reporter)
return nil
}
// run 执行具体的任务逻辑
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// 记录任务接收日志
reporter.Logf("%s(版本:%s) 收到任务 %v 的作业 %v,由事件触发: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
workflow, jobID, err := generateWorkflow(task)
if err != nil {
return err
}
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
if err != nil {
return err
}
job := workflow.GetJob(jobID)
reporter.ResetSteps(len(job.Steps))
taskContext := task.Context.Fields
log.Infof("任务 %v 仓库是 %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
taskContext["gitea_default_actions_url"].GetStringValue(),
r.client.Address())
// 构建预设的 GitHub 上下文环境
preset := &model.GithubContext{
Event: taskContext["event"].GetStructValue().AsMap(),
RunID: taskContext["run_id"].GetStringValue(),
RunNumber: taskContext["run_number"].GetStringValue(),
Actor: taskContext["actor"].GetStringValue(),
Repository: taskContext["repository"].GetStringValue(),
EventName: taskContext["event_name"].GetStringValue(),
Sha: taskContext["sha"].GetStringValue(),
Ref: taskContext["ref"].GetStringValue(),
RefName: taskContext["ref_name"].GetStringValue(),
RefType: taskContext["ref_type"].GetStringValue(),
HeadRef: taskContext["head_ref"].GetStringValue(),
BaseRef: taskContext["base_ref"].GetStringValue(),
Token: taskContext["token"].GetStringValue(),
RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
RetentionDays: taskContext["retention_days"].GetStringValue(),
}
// 优先使用 GITEA_TOKEN
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
preset.Token = t
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
preset.Token = t
}
if actionsIdTokenRequestUrl := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIdTokenRequestUrl != "" {
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIdTokenRequestUrl
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
}
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
if giteaRuntimeToken == "" {
// 兼容旧版本 Gitea Server
giteaRuntimeToken = preset.Token
}
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
eventJSON, err := json.Marshal(preset.Event)
if err != nil {
return err
}
maxLifetime := 3 * time.Hour
if deadline, ok := ctx.Deadline(); ok {
maxLifetime = time.Until(deadline)
}
// 创建 Runner 配置
runnerConfig := &runner.Config{
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
BindWorkdir: false,
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
ReuseContainers: false,
ForcePull: r.cfg.Container.ForcePull,
ForceRebuild: r.cfg.Container.ForceRebuild,
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,
ContainerDaemonSocket: r.cfg.Container.DockerHost,
Privileged: r.cfg.Container.Privileged,
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
PlatformPicker: r.labels.PickPlatform,
Vars: task.Vars,
ValidVolumes: r.cfg.Container.ValidVolumes,
InsecureSkipTLS: r.cfg.Runner.Insecure,
}
rr, err := runner.New(runnerConfig)
if err != nil {
return err
}
executor := rr.NewPlanExecutor(plan)
reporter.Logf("工作流程已准备就绪")
// 添加日志记录器
ctx = common.WithLoggerHook(ctx, reporter)
if !log.IsLevelEnabled(log.DebugLevel) {
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
}
execErr := executor(ctx)
reporter.SetOutputs(job.Outputs)
return execErr
}
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
Version: ver.Version(),
Labels: labels,
}))
}

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("找到多个工作: %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_生成工作流(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: "有需求",
args: args{
task: &runnerv1.Task{
WorkflowPayload: []byte(`
name: 构建部署测试
on: push
jobs:
job9:
needs: build
runs-on: linux-loong64
steps:
- uses: actions/checkout@v4
- 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": "输出1值",
},
Result: runnerv1.Result_RESULT_SUCCESS,
},
"job2": {
Outputs: map[string]string{
"output2": "输出2值",
},
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

@ -1,3 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package client
import (
@ -6,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,11 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package client
const (
UUIDHeader = "x-runner-uuid"
TokenHeader = "x-runner-token"
// 已弃用: 可以在Gitea 1.20发布后删除
VersionHeader = "x-runner-version"
)

View File

@ -1,3 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package client
import (
@ -8,11 +11,10 @@ import (
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"gitea.com/gitea/act_runner/core"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
)
func getHttpClient(endpoint string, insecure bool) *http.Client {
func getHTTPClient(endpoint string, insecure bool) *http.Client {
if strings.HasPrefix(endpoint, "https://") && insecure {
return &http.Client{
Transport: &http.Transport{
@ -25,17 +27,21 @@ func getHttpClient(endpoint string, insecure bool) *http.Client {
return http.DefaultClient
}
// New returns a new runner client.
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
// New返回一个新的runner客户端。
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
if uuid != "" {
req.Header().Set(core.UUIDHeader, uuid)
req.Header().Set(UUIDHeader, uuid)
}
if token != "" {
req.Header().Set(core.TokenHeader, token)
req.Header().Set(TokenHeader, token)
}
// TODO:version将在Gitea 1.20发布后从请求标头中删除。
if version != "" {
req.Header().Set(VersionHeader, version)
}
return next(ctx, req)
}
@ -43,12 +49,12 @@ func New(endpoint string, insecure bool, uuid, token string, opts ...connect.Cli
return &HTTPClient{
PingServiceClient: pingv1connect.NewPingServiceClient(
getHttpClient(endpoint, insecure),
getHTTPClient(endpoint, insecure),
baseURL,
opts...,
),
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
getHttpClient(endpoint, insecure),
getHTTPClient(endpoint, insecure),
baseURL,
opts...,
),
@ -67,7 +73,7 @@ func (c *HTTPClient) Insecure() bool {
var _ Client = (*HTTPClient)(nil)
// An HTTPClient manages communication with the runner API.
// HTTPClient管理与runner API的通信。
type HTTPClient struct {
pingv1connect.PingServiceClient
runnerv1connect.RunnerServiceClient

View File

@ -0,0 +1,251 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package mocks
import (
context "context"
connect "connectrpc.com/connect"
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()
if len(ret) == 0 {
panic("no return value specified for Address")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Declare provides a mock function with given fields: _a0, _a1
func (_m *Client) Declare(_a0 context.Context, _a1 *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Declare")
}
var r0 *connect.Response[runnerv1.DeclareResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) *connect.Response[runnerv1.DeclareResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.DeclareResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// 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)
if len(ret) == 0 {
panic("no return value specified for FetchTask")
}
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()
if len(ret) == 0 {
panic("no return value specified for Insecure")
}
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)
if len(ret) == 0 {
panic("no return value specified for Ping")
}
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)
if len(ret) == 0 {
panic("no return value specified for Register")
}
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)
if len(ret) == 0 {
panic("no return value specified for UpdateLog")
}
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)
if len(ret) == 0 {
panic("no return value specified for UpdateTask")
}
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
}
// 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.
// The first argument is typically a *testing.T value.
func NewClient(t interface {
mock.TestingT
Cleanup(func())
},
) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,101 @@
# 示例配置文件,可以安全地将其复制为默认配置文件而无需任何修改。
# 您不必将此文件复制到您的实例,
# 只需运行 `./act_runner generate-config > config.yaml` 来生成配置文件。
log:
# 日志级别,可以是 trace、debug、info、warn、error、fatal
level: info
runner:
# 存储注册结果的路径。
file: .runner
# 同时执行多少个任务。
capacity: 1
# 运行作业的额外环境变量。
envs:
A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2
# 从文件中运行作业的额外环境变量。
# 如果为空或文件不存在,则会被忽略。
env_file: .env
# 作业完成的超时时间。
# 请注意,Gitea 实例也有一个作业超时时间(默认为 3 小时)。
# 如果这个超时时间比 Gitea 实例的超时时间短,作业可能会被 Gitea 实例停止。
timeout: 3h
# 运行器在关闭时等待运行作业完成的超时时间。
# 在这个超时时间之后仍未完成的任何运行作业将被取消。
shutdown_timeout: 0s
# 是否跳过验证 Gitea 实例的 TLS 证书。
insecure: false
# 从 Gitea 实例获取作业的超时时间。
fetch_timeout: 5s
# 从 Gitea 实例获取作业的时间间隔。
fetch_interval: 2s
# 运行器的标签用于确定运行器可以运行哪些作业以及如何运行它们。
# 例如:"linux-loong64.abi2:host" 或 "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
# 在 https://gitea.com/docker.gitea.com/runner-images 查找 Gitea 提供的更多镜像。
# 如果在注册时为空,它会要求输入标签。
# 如果在执行 `daemon` 时为空,将使用 `.runner` 文件中的标签。
labels:
- "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
- "anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest"
- "anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2"
cache:
# 启用缓存服务器以使用 actions/cache。
enabled: true
# 存储缓存数据的目录。
# 如果为空,缓存数据将存储在 $HOME/.cache/actcache。
dir: ""
# 缓存服务器的主机。
# 它不是用于监听的地址,而是用于作业容器连接的地址。
# 所以 0.0.0.0 是一个糟糕的选择,留空以自动检测。
host: ""
# 缓存服务器的端口。
# 0 表示使用随机可用端口。
port: 0
# 外部缓存服务器 URL。仅在启用时有效。
# 如果指定了它,act_runner 将使用此 URL 作为 ACTIONS_CACHE_URL 而不是自己启动一个服务器。
# URL 通常应该以 "/" 结尾。
external_server: ""
container:
# 指定容器将连接的网络。
# 可以是 host、bridge 或自定义网络的名称。
# 如果为空,act_runner 将自动创建一个网络。
network: ""
# 启动任务容器时是否使用特权模式(特权模式对于 Docker-in-Docker 是必需的)。
privileged: false
# 容器启动时使用的其他选项(例如,--add-host=my.gitea.url:host-gateway)。
options:
# 作业工作目录的父目录。
# 注意:不需要在路径前添加第一个 '/',因为 act_runner 会自动添加。
# 如果路径以 '/' 开头,'/' 将被修剪。
# 例如,如果父目录是 /path/to/my/dir,workdir_parent 应该是 path/to/my/dir
# 如果为空,将使用 /workspace。
workdir_parent:
# 可以挂载到容器的卷(包括绑定挂载)。支持 glob 语法,参见 https://github.com/gobwas/glob
# 您可以指定多个卷。如果序列为空,则不能挂载任何卷。
# 例如,如果您只允许容器挂载 `data` 卷和 `/src` 中的所有 json 文件,您应该将配置更改为:
# valid_volumes:
# - data
# - /src/*.json
# 如果您想允许任何卷,请使用以下配置:
# valid_volumes:
# - '​**​'
valid_volumes: []
# 用指定的主机覆盖 docker 客户端主机。
# 如果为空,act_runner 将自动查找可用的 docker 主机。
# 如果是 "-",act_runner 将自动查找可用的 docker 主机,但 docker 主机不会挂载到作业容器和服务容器。
# 如果不为空或 "-",将使用指定的 docker 主机。如果不起作用,将返回错误。
docker_host: ""
# 即使已经存在也拉取 Docker 镜像
force_pull: true
# 即使已经存在也重建 Docker 镜像
force_rebuild: false
host:
# 作业工作目录的父目录。
# 如果为空,将使用 $HOME/.cache/act/。
workdir_parent:

View File

@ -0,0 +1,151 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
// Log 代表日志的配置。
type Log struct {
Level string `yaml:"level"` // Level 表示日志级别。
}
// Runner 代表运行器的配置。
type Runner struct {
File string `yaml:"file"` // File 指定运行器的文件路径。
Capacity int `yaml:"capacity"` // Capacity 指定运行器的容量。
Envs map[string]string `yaml:"envs"` // Envs 存储运行器的环境变量。
EnvFile string `yaml:"env_file"` // EnvFile 指定包含运行器环境变量的文件路径。
Timeout time.Duration `yaml:"timeout"` // Timeout 指定运行器超时的持续时间。
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout 指定在运行器关闭期间等待运行作业完成的持续时间。
Insecure bool `yaml:"insecure"` // Insecure 表示运行器是否在不安全模式下运行。
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout 指定获取资源的超时持续时间。
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval 指定获取资源的间隔持续时间。
Labels []string `yaml:"labels"` // Labels 指定运行器的标签。每个启动时声明标签。
}
// Cache 代表缓存的配置。
type Cache struct {
Enabled *bool `yaml:"enabled"` // Enabled 表示是否启用缓存。它是指针,用于区分 false 和未设置。如果未设置,它将为 true。
Dir string `yaml:"dir"` // Dir 指定缓存的目录路径。
Host string `yaml:"host"` // Host 指定缓存主机。
Port uint16 `yaml:"port"` // Port 指定缓存端口。
ExternalServer string `yaml:"external_server"` // ExternalServer 指定外部缓存服务器的 URL。
}
// Container 代表容器的配置。
type Container struct {
Network string `yaml:"network"` // Network 指定容器的网络。
NetworkMode string `yaml:"network_mode"` // 已弃用:使用 Network 替代。可能在 Gitea 1.20 之后被移除。
Privileged bool `yaml:"privileged"` // Privileged 表示容器是否以特权模式运行。
Options string `yaml:"options"` // Options 指定容器的其他选项。
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定容器工作目录的父目录。
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes 指定可以挂载到容器的卷(包括绑定挂载)。
DockerHost string `yaml:"docker_host"` // DockerHost 指定 Docker 主机。它覆盖环境变量 DOCKER_HOST 中指定的值。
ForcePull bool `yaml:"force_pull"` // 即使已经存在也拉取 Docker 镜像。
ForceRebuild bool `yaml:"force_rebuild"` // 即使已经存在也重建 Docker 镜像。
}
// Host 代表主机的配置。
type Host struct {
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定主机工作目录的父目录。
}
// Config 代表整体配置。
type Config struct {
Log Log `yaml:"log"` // Log 代表日志的配置。
Runner Runner `yaml:"runner"` // Runner 代表运行器的配置。
Cache Cache `yaml:"cache"` // Cache 代表缓存的配置。
Container Container `yaml:"container"` // Container 代表容器的配置。
Host Host `yaml:"host"` // Host 代表主机的配置。
}
// LoadDefault 返回默认配置。
// 如果文件不为空,它将被用来加载配置。
func LoadDefault(file string) (*Config, error) {
cfg := &Config{}
if file != "" {
content, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("打开配置文件 %q: %w", file, err)
}
if err := yaml.Unmarshal(content, cfg); err != nil {
return nil, fmt.Errorf("解析配置文件 %q: %w", file, err)
}
}
compatibleWithOldEnvs(file != "", cfg)
if cfg.Runner.EnvFile != "" {
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
envs, err := godotenv.Read(cfg.Runner.EnvFile)
if err != nil {
return nil, fmt.Errorf("读取环境文件 %q: %w", cfg.Runner.EnvFile, err)
}
if cfg.Runner.Envs == nil {
cfg.Runner.Envs = map[string]string{}
}
for k, v := range envs {
cfg.Runner.Envs[k] = v
}
}
}
if cfg.Log.Level == "" {
cfg.Log.Level = "info"
}
if cfg.Runner.File == "" {
cfg.Runner.File = ".runner"
}
if cfg.Runner.Capacity <= 0 {
cfg.Runner.Capacity = 1
}
if cfg.Runner.Timeout <= 0 {
cfg.Runner.Timeout = 3 * time.Hour
}
if cfg.Cache.Enabled == nil {
b := true
cfg.Cache.Enabled = &b
}
if *cfg.Cache.Enabled {
if cfg.Cache.Dir == "" {
home, _ := os.UserHomeDir()
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
}
}
if cfg.Container.WorkdirParent == "" {
cfg.Container.WorkdirParent = "workspace"
}
if cfg.Host.WorkdirParent == "" {
home, _ := os.UserHomeDir()
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
}
if cfg.Runner.FetchTimeout <= 0 {
cfg.Runner.FetchTimeout = 5 * time.Second
}
if cfg.Runner.FetchInterval <= 0 {
cfg.Runner.FetchInterval = 2 * time.Second
}
// 虽然 `container.network_mode` 将被弃用,但我们现在必须与它兼容。
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
log.Warn("您正在尝试使用已弃用的配置项 `container.network_mode`,请使用 `container.network` 替代。")
if cfg.Container.NetworkMode == "bridge" {
// 以前,如果 `container.network_mode` 的值是 `bridge`,我们会为作业创建一个新的网络。
// 但是,“bridge”很容易与 Docker 默认创建的桥接网络混淆。
// 所以我们将 `container.network` 的值设置为空字符串,以使 `act_runner` 为作业自动创建一个新的网络。
} else {
cfg.Container.Network = cfg.Container.NetworkMode
}
}
return cfg, nil
}

View File

@ -0,0 +1,62 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import (
"os"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
// 已弃用:未来可能会移除。TODO: 在 Gitea 1.20.0 发布时将其移除。
// 兼容旧环境。
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
handleEnv := func(key string) (string, bool) {
if v, ok := os.LookupEnv(key); ok {
if fileUsed {
log.Warnf("环境 %s 已被忽略,因为使用了配置文件", key)
return "", false
}
log.Warnf("环境变量 %s 将被弃用,请改用配置文件。", key)
return v, true
}
return "", false
}
if v, ok := handleEnv("GITEA_DEBUG"); ok {
if b, _ := strconv.ParseBool(v); b {
cfg.Log.Level = "debug"
}
}
if v, ok := handleEnv("GITEA_TRACE"); ok {
if b, _ := strconv.ParseBool(v); b {
cfg.Log.Level = "trace"
}
}
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
if i, _ := strconv.Atoi(v); i > 0 {
cfg.Runner.Capacity = i
}
}
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
cfg.Runner.File = v
}
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
splits := strings.Split(v, ",")
if cfg.Runner.Envs == nil {
cfg.Runner.Envs = map[string]string{}
}
for _, split := range splits {
kv := strings.SplitN(split, ":", 2)
if len(kv) == 2 && kv[0] != "" {
cfg.Runner.Envs[kv[0]] = kv[1]
}
}
}
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
cfg.Runner.EnvFile = v
}
}

View File

@ -0,0 +1,9 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import _ "embed"
//go:embed config.example.yaml
var Example []byte

View File

@ -0,0 +1,55 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import (
"encoding/json"
"os"
)
const registrationWarning = "此文件由`Loong Runner`自动生成。除非你知道自己在做什么, 否则不要手动编辑它。删除此文件将导致`Loong Runner`重新注册为新的运行器。"
// Registration 表示运行器的注册信息
type Registration struct {
Warning string `json:"WARNING"` // 警告信息,始终为 registrationWarning 常量
ID int64 `json:"id"` // 唯一标识符(整数类型)
UUID string `json:"uuid"` // 全局唯一标识符(字符串类型)
Name string `json:"name"` // 运行器名称
Token string `json:"token"` // 认证令牌,用于与调度器通信
Address string `json:"address"` // 运行器的网络地址(如IP或域名)
Labels []string `json:"labels"` // 运行器关联的标签列表(用于任务匹配)
Ephemeral bool `json:"ephemeral"` // 是否为临时实例(完成任务后自动销毁)
}
func LoadRegistration(file string) (*Registration, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
var reg Registration
if err := json.NewDecoder(f).Decode(&reg); err != nil {
return nil, err
}
reg.Warning = ""
return &reg, nil
}
func SaveRegistration(file string, reg *Registration) error {
f, err := os.Create(file)
if err != nil {
return err
}
defer f.Close()
reg.Warning = registrationWarning
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(reg)
}

View File

@ -0,0 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// envcheck包中提供了一种简单的方法, 用来检查环境是否准备好执行工作。
package envcheck

View File

@ -0,0 +1,34 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package envcheck
import (
"context"
"fmt"
"github.com/docker/docker/client"
)
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
opts := []client.Opt{
client.FromEnv,
}
if configDockerHost != "" {
opts = append(opts, client.WithHost(configDockerHost))
}
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
return err
}
defer cli.Close()
_, err = cli.Ping(ctx)
if err != nil {
return fmt.Errorf("无法ping通docker守护进程, 它是否在运行? %w", err)
}
return nil
}

View File

@ -0,0 +1,113 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package labels
import (
"fmt"
"strings"
)
const (
// SchemeHost 表示主机模式
SchemeHost = "host"
// SchemeDocker 表示 Docker 模式
SchemeDocker = "docker"
)
type Label struct {
Name string
Schema string
Arg string
}
// Parse 解析标签字符串并返回 Label 结构体。
func Parse(str string) (*Label, error) {
splits := strings.SplitN(str, ":", 3)
label := &Label{
Name: splits[0],
Schema: "host",
Arg: "",
}
if len(splits) >= 2 {
label.Schema = splits[1]
}
if len(splits) >= 3 {
label.Arg = splits[2]
}
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
return nil, fmt.Errorf("不支持的标签: %s", label.Schema)
}
return label, nil
}
type Labels []*Label
// RequireDocker 检查 Labels 是否需要 Docker。
func (l Labels) RequireDocker() bool {
for _, label := range l {
if label.Schema == SchemeDocker {
return true
}
}
return false
}
// PickPlatform 根据 runsOn 列表选择一个平台。
func (l Labels) PickPlatform(runsOn []string) string {
platforms := make(map[string]string, len(l))
for _, label := range l {
switch label.Schema {
case SchemeDocker:
// 忽略 "//"
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
case SchemeHost:
platforms[label.Name] = "-self-hosted"
default:
// 这不应该发生,因为 Parse 已经检查过了。
continue
}
}
for _, v := range runsOn {
if v, ok := platforms[v]; ok {
return v
}
}
// TODO: 支持多个标签
// 例如:
// ["debian-12"] => "debian:12"
// ["with-gpu"] => "linux:with-gpu"
// ["debian-12", "with-gpu"] => "debian:12_with-gpu"
// 返回默认值。
// 因此,当运行器收到一个它没有的标签的任务时,
// 这发生在用户在 Web UI 中编辑了运行器的标签时。
// TODO: 这可能不正确,如果运行器仅作为主机模式使用呢?
return "lcr.loongnix.cn/library/debian:latest"
}
// Names 返回所有标签的名称。
func (l Labels) Names() []string {
names := make([]string, 0, len(l))
for _, label := range l {
names = append(names, label.Name)
}
return names
}
// ToStrings 将 Labels 转换为字符串数组。
func (l Labels) ToStrings() []string {
ls := make([]string, 0, len(l))
for _, label := range l {
lbl := label.Name
if label.Schema != "" {
lbl += ":" + label.Schema
if label.Arg != "" {
lbl += ":" + label.Arg
}
}
ls = append(ls, lbl)
}
return ls
}

View File

@ -0,0 +1,63 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package labels
import (
"testing"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
)
func Test解析(t *testing.T) {
tests := []struct {
args string
want *Label
wantErr bool
}{
{
args: "debian:docker://node:18",
want: &Label{
Name: "debian",
Schema: "docker",
Arg: "//node:18",
},
wantErr: false,
},
{
args: "aosc:host",
want: &Label{
Name: "aosc",
Schema: "host",
Arg: "",
},
wantErr: false,
},
{
args: "aosc",
want: &Label{
Name: "aosc",
Schema: "host",
Arg: "",
},
wantErr: false,
},
{
args: "debian:vm:debian12",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) {
got, err := Parse(tt.args)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.DeepEqual(t, got, tt.want)
})
}
}

View File

@ -0,0 +1,451 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Reporter 组件是负责任务执行状态报告和日志记录的核心模块
package report
import (
"context"
"fmt"
"regexp"
"strings"
"sync"
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/avast/retry-go/v4"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
)
type Reporter struct {
ctx context.Context // 上下文,用于控制生命周期
cancel context.CancelFunc // 取消函数
closed bool // 是否已关闭
client client.Client // Gitea 客户端
clientM sync.Mutex // 客户端访问互斥锁
logOffset int // 日志偏移量
logRows []*runnerv1.LogRow // 日志行缓存
logReplacer *strings.Replacer // 日志内容替换器
oldnew []string // 需要替换的敏感信息
state *runnerv1.TaskState // 任务状态
stateMu sync.RWMutex // 状态访问读写锁
outputs sync.Map // 输出参数存储
debugOutputEnabled bool // 是否启用调试输出
stopCommandEndToken string // 停止命令结束标记
}
// NewReporter 构造函数 初始化日志脱敏规则、状态等信息
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
var oldnew []string
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
oldnew = append(oldnew, v, "***")
}
if v := task.Context.Fields["gitea_runtime_token"].GetStringValue(); v != "" {
oldnew = append(oldnew, v, "***")
}
for _, v := range task.Secrets {
oldnew = append(oldnew, v, "***")
}
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
}
// ResetSteps 重置任务状态中的步骤状态数组,为即将开始的新任务做准备
func (r *Reporter) ResetSteps(l int) {
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),
})
}
}
// Levels 返回所有支持的日志级别(通常是为了兼容 logger 接口)
func (r *Reporter) Levels() []log.Level {
return log.AllLevels
}
// appendIfNotNil 通用函数,如果传入指针不为空,则将其添加到切片中。
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.stateMu.Lock()
defer r.stateMu.Unlock()
log.WithFields(entry.Data).Trace(entry.Message)
timestamp := entry.Time
if r.state.StartedAt == nil {
r.state.StartedAt = timestamppb.New(timestamp)
}
stage := entry.Data["stage"]
if stage != "Main" {
if v, ok := entry.Data["jobResult"]; ok {
if jobResult, ok := r.parseResult(v); ok {
r.state.Result = jobResult
r.state.StoppedAt = timestamppb.New(timestamp)
for _, s := range r.state.Steps {
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
s.Result = runnerv1.Result_RESULT_CANCELLED
if jobResult == runnerv1.Result_RESULT_SKIPPED {
s.Result = runnerv1.Result_RESULT_SKIPPED
}
}
}
}
}
if !r.duringSteps() {
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
return nil
}
var step *runnerv1.StepState
if v, ok := entry.Data["stepNumber"]; ok {
if v, ok := v.(int); ok && len(r.state.Steps) > v {
step = r.state.Steps[v]
}
}
if step == nil {
if !r.duringSteps() {
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
return nil
}
if step.StartedAt == nil {
step.StartedAt = timestamppb.New(timestamp)
}
if v, ok := entry.Data["raw_output"]; ok {
if rawOutput, ok := v.(bool); ok && rawOutput {
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)
}
}
} else if !r.duringSteps() {
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
}
if v, ok := entry.Data["stepResult"]; ok {
if stepResult, ok := r.parseResult(v); ok {
if step.LogLength == 0 {
step.LogIndex = int64(r.logOffset + len(r.logRows))
}
step.Result = stepResult
step.StoppedAt = timestamppb.New(timestamp)
}
}
return nil
}
// RunDaemon 定时运行后台任务,定期将缓存中的日志和状态上报给 Gitea
func (r *Reporter) RunDaemon() {
if r.closed {
return
}
if r.ctx.Err() != nil {
return
}
_ = r.ReportLog(false)
_ = r.ReportState()
time.AfterFunc(time.Second, r.RunDaemon)
}
// 自定义日志记录方法,格式化输出并保存到 logRows 中
func (r *Reporter) Logf(format string, a ...interface{}) {
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(),
Content: fmt.Sprintf(format, a...),
})
}
}
// SetOutputs 设置输出参数,过滤无效或超限的键值对
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("忽略超长键值对的键: %q", k)
continue
}
if l := len(v); l > 1024*1024 {
log.Println("忽略超长键值对的值:", k, l)
r.logf("忽略超长键值对的值 %q: %d 字节", k, l)
}
if _, ok := r.outputs.Load(k); ok {
continue
}
r.outputs.Store(k, v)
}
}
// Close 关闭 reporter,确保最后一批日志和最终状态被上报
func (r *Reporter) Close(lastWords string) error {
r.closed = true
r.stateMu.Lock()
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
if lastWords == "" {
lastWords = "Early termination"
}
for _, v := range r.state.Steps {
if v.Result == runnerv1.Result_RESULT_UNSPECIFIED {
v.Result = runnerv1.Result_RESULT_CANCELLED
}
}
r.state.Result = runnerv1.Result_RESULT_FAILURE
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: lastWords,
})
r.state.StoppedAt = timestamppb.Now()
} else if lastWords != "" {
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: lastWords,
})
}
r.stateMu.Unlock()
return retry.Do(func() error {
if err := r.ReportLog(true); err != nil {
return err
}
return r.ReportState()
}, retry.Context(r.ctx))
}
// ReportLog 将当前缓存的日志批量上报到 Gitea
func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateMu.RLock()
rows := r.logRows
r.stateMu.RUnlock()
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
TaskId: r.state.Id,
Index: int64(r.logOffset),
Rows: rows,
NoMore: noMore,
}))
if err != nil {
return err
}
ack := int(resp.Msg.AckIndex)
if ack < r.logOffset {
return fmt.Errorf("已提交的日志丢失了")
}
r.stateMu.Lock()
r.logRows = r.logRows[ack-r.logOffset:]
r.logOffset = ack
r.stateMu.Unlock()
if noMore && ack < r.logOffset+len(rows) {
return fmt.Errorf("并非所有日志都已提交")
}
return nil
}
// ReportState 将当前任务状态上报到 Gitea
func (r *Reporter) ReportState() error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateMu.RLock()
state := proto.Clone(r.state).(*runnerv1.TaskState)
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,
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("仍有一些输出尚未发送: %v", noSent)
}
return nil
}
// duringSteps 判断当前是否正在执行某个步骤
func (r *Reporter) duringSteps() bool {
if steps := r.state.Steps; len(steps) == 0 {
return false
} else if first := steps[0]; first.Result == runnerv1.Result_RESULT_UNSPECIFIED && first.LogLength == 0 {
return false
} else if last := steps[len(steps)-1]; last.Result != runnerv1.Result_RESULT_UNSPECIFIED {
return false
}
return true
}
var stringToResult = map[string]runnerv1.Result{
"success": runnerv1.Result_RESULT_SUCCESS,
"failure": runnerv1.Result_RESULT_FAILURE,
"skipped": runnerv1.Result_RESULT_SKIPPED,
"cancelled": runnerv1.Result_RESULT_CANCELLED,
}
// parseResult 将字符串或 Stringer 类型的结果解析为 runnerv1.Result 类型
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
str := ""
if v, ok := result.(string); ok { // 对于作业结果
str = v
} else if v, ok := result.(fmt.Stringer); ok { // 对于步骤结果
str = v.String()
}
ret, ok := stringToResult[str]
return ret, ok
}
// 匹配 GitHub Actions/Gitea Actions 特殊命令,如 ::set-output::, ::add-mask:: 等
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
// handleCommand 处理日志中的特殊指令命令(如 ::add-mask::),根据命令执行对应操作
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":
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "warning":
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "error":
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "group":
// 返回原始内容,因为我认为前端在渲染输出时会使用它。
return &originalContent
case "endgroup":
// 同上
return &originalContent
case "stop-commands":
r.stopCommandEndToken = value
return nil
case r.stopCommandEndToken:
r.stopCommandEndToken = ""
return nil
}
return &originalContent
}
// parseLogRow 将日志条目转换为 LogRow 结构,用于后续上报
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: strings.ToValidUTF8(content, "?"),
}
}
// addMask 将指定字符串加入脱敏列表,之后所有日志中出现该字符串都会被替换成 ***
func (r *Reporter) addMask(msg string) {
r.oldnew = append(r.oldnew, msg, "***")
r.logReplacer = strings.NewReplacer(r.oldnew...)
}

View File

@ -0,0 +1,223 @@
// 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 "connectrpc.com/connect"
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"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks"
)
func Test记录器_解析输出日志(t *testing.T) {
tests := []struct {
name string // 测试用例名称
debugOutputEnabled bool // 是否启用调试输出
args []string // 输入的日志行
want []string // 期望的输出结果
}{
{
name: "无命令",
debugOutputEnabled: false,
args: []string{"你好,世界!"},
want: []string{"你好,世界!"},
},
{
name: "添加掩码",
debugOutputEnabled: false,
args: []string{
"foo 我的密钥 bar", // 输入日志:普通日志行
"::add-mask::我的密钥", // 输入命令:添加掩码
"foo 我的密钥 bar", // 输入日志:再次普通日志行
},
want: []string{
"foo 我的密钥 bar", // 原始日志直接输出
"<nil>", // 添加掩码命令处理结果(无输出内容)
"foo *** bar", // 掩码替换后的日志行
},
},
{
name: "启用调试",
debugOutputEnabled: true,
args: []string{
"::debug::GitHub Actions 运行时令牌访问控制",
},
want: []string{
"GitHub Actions 运行时令牌访问控制", // 调试信息直接输出
},
},
{
name: "禁用调试",
debugOutputEnabled: false,
args: []string{
"::debug::GitHub Actions 运行时令牌访问控制",
},
want: []string{
"<nil>", // 调试信息被忽略
},
},
{
name: "通知",
debugOutputEnabled: false,
args: []string{
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
want: []string{
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 通知日志原样输出
},
},
{
name: "警告",
debugOutputEnabled: false,
args: []string{
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
want: []string{
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 警告日志原样输出
},
},
{
name: "错误",
debugOutputEnabled: false,
args: []string{
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
want: []string{
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 错误日志原样输出
},
},
{
name: "分组",
debugOutputEnabled: false,
args: []string{
"::group::", // 开始分组
"::endgroup::", // 结束分组
},
want: []string{
"::group::", // 分组开始标记原样输出
"::endgroup::", // 分组结束标记原样输出
},
},
{
name: "停止命令",
debugOutputEnabled: false,
args: []string{
"::add-mask::foo", // 添加掩码命令
"::stop-commands::我的停止令牌", // 停止命令标记
"::add-mask::bar", // 被忽略的添加掩码命令
"::debug::调试信息", // 被忽略的调试信息
"我的停止令牌", // 停止命令标记结束
"::add-mask::baz", // 恢复处理的添加掩码命令
"::我的停止令牌::", // 另一种停止命令标记
"::add-mask::wibble", // 被忽略的添加掩码命令
"foo bar baz wibble", // 普通日志行
},
want: []string{
"<nil>", // 第一个添加掩码命令处理结果
"<nil>", // 停止命令标记处理结果
"::add-mask::bar", // 被忽略的命令原样输出
"::debug::调试信息", // 被忽略的调试信息原样输出
"我的停止令牌", // 停止标记结束原样输出
"::add-mask::baz", // 恢复处理的添加掩码命令
"<nil>", // 无效停止命令标记处理结果
"<nil>", // 被忽略的添加掩码命令处理结果
"*** bar baz ***", // 掩码替换后的日志行
},
},
{
name: "未知命令",
debugOutputEnabled: false,
args: []string{
"::set-mask::foo", // 未知命令
},
want: []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)
}
})
}
}
// 测试 Reporter 的 Fire 方法(验证命令行忽略逻辑)
func Test记录器_触发(t *testing.T) {
t.Run("忽略命令行", func(t *testing.T) { // 测试场景:验证是否正确处理需要忽略的命令行日志
// 创建模拟客户端
client := mocks.NewClient(t)
// 模拟 UpdateLog 接口调用,记录请求内容并返回响应
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("收到 UpdateLog 请求:%s", req.Msg.String()) // 记录日志请求内容
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)), // 计算确认索引
}), nil
})
// 模拟 UpdateTask 接口调用
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("收到 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 实例
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
Context: taskCtx, // 注入任务上下文
})
defer func() {
assert.NoError(t, reporter.Close("")) // 测试结束关闭 Reporter
}()
reporter.ResetSteps(5) // 初始化5个步骤的日志存储
// 定义步骤0的日志元数据
dataStep0 := map[string]interface{}{
"stage": "Main", // 阶段名称
"stepNumber": 0, // 步骤编号
"raw_output": true, // 启用原始输出模式
}
// 发送混合类型的日志条目 ---------------------------------------------------
// 预期:普通日志被记录,调试日志被忽略
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
// 验证结果:步骤0应只有3条普通日志(调试日志被过滤)
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength, "普通日志数量不符预期")
})
}

View File

@ -0,0 +1,11 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package ver
// go build -ldflags "-X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=1.2.3"
var version = "dev"
func Version() string {
return version
}

29
main.go
View File

@ -1,34 +1,19 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package main
import (
"context"
"os"
"os/signal"
"syscall"
"gitea.com/gitea/act_runner/cmd"
"git.whlug.cn/LAA/loong_runner/internal/app/cmd"
)
func withContextFunc(ctx context.Context, f func()) context.Context {
ctx, cancel := context.WithCancel(ctx)
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(c)
select {
case <-ctx.Done():
case <-c:
cancel()
f()
}
}()
return ctx
}
func main() {
ctx := withContextFunc(context.Background(), func() {})
// run the command
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// 运行命令
cmd.Execute(ctx)
}

View File

@ -1,33 +0,0 @@
package poller
import "sync/atomic"
// Metric interface
type Metric interface {
IncBusyWorker() int64
DecBusyWorker() int64
BusyWorkers() int64
}
var _ Metric = (*metric)(nil)
type metric struct {
busyWorkers int64
}
// NewMetric for default metric structure
func NewMetric() Metric {
return &metric{}
}
func (m *metric) IncBusyWorker() int64 {
return atomic.AddInt64(&m.busyWorkers, 1)
}
func (m *metric) DecBusyWorker() int64 {
return atomic.AddInt64(&m.busyWorkers, -1)
}
func (m *metric) BusyWorkers() int64 {
return atomic.LoadInt64(&m.busyWorkers)
}

View File

@ -1,146 +0,0 @@
package poller
import (
"context"
"errors"
"sync"
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"gitea.com/gitea/act_runner/client"
"github.com/bufbuild/connect-go"
log "github.com/sirupsen/logrus"
)
var ErrDataLock = errors.New("Data Lock Error")
func New(cli client.Client, dispatch func(context.Context, *runnerv1.Task) error, workerNum int) *Poller {
return &Poller{
Client: cli,
Dispatch: dispatch,
routineGroup: newRoutineGroup(),
metric: &metric{},
workerNum: workerNum,
ready: make(chan struct{}, 1),
}
}
type Poller struct {
Client client.Client
Dispatch func(context.Context, *runnerv1.Task) error
sync.Mutex
routineGroup *routineGroup
metric *metric
ready chan struct{}
workerNum int
}
func (p *Poller) schedule() {
p.Lock()
defer p.Unlock()
if int(p.metric.BusyWorkers()) >= p.workerNum {
return
}
select {
case p.ready <- struct{}{}:
default:
}
}
func (p *Poller) Wait() {
p.routineGroup.Wait()
}
func (p *Poller) Poll(ctx context.Context) error {
l := log.WithField("func", "Poll")
for {
// check worker number
p.schedule()
select {
// wait worker ready
case <-p.ready:
case <-ctx.Done():
return nil
}
LOOP:
for {
select {
case <-ctx.Done():
break LOOP
default:
task, err := p.pollTask(ctx)
if task == nil || err != nil {
if err != nil {
l.Errorf("can't find the task: %v", err.Error())
}
time.Sleep(5 * time.Second)
break
}
p.metric.IncBusyWorker()
p.routineGroup.Run(func() {
defer p.schedule()
defer p.metric.DecBusyWorker()
if err := p.dispatchTask(ctx, task); err != nil {
l.Errorf("execute task: %v", err.Error())
}
})
break LOOP
}
}
}
}
func (p *Poller) pollTask(ctx context.Context) (*runnerv1.Task, error) {
l := log.WithField("func", "pollTask")
l.Info("poller: request stage from remote server")
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// request a new build stage for execution from the central
// build server.
resp, err := p.Client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
if err == context.Canceled || err == context.DeadlineExceeded {
l.WithError(err).Trace("poller: no stage returned")
return nil, nil
}
if err != nil && err == ErrDataLock {
l.WithError(err).Info("task accepted by another runner")
return nil, nil
}
if err != nil {
l.WithError(err).Error("cannot accept task")
return nil, err
}
// exit if a nil or empty stage is returned from the system
// and allow the runner to retry.
if resp.Msg.Task == nil || resp.Msg.Task.Id == 0 {
return nil, nil
}
return resp.Msg.Task, nil
}
func (p *Poller) dispatchTask(ctx context.Context, task *runnerv1.Task) error {
l := log.WithField("func", "dispatchTask")
defer func() {
e := recover()
if e != nil {
l.Errorf("panic error: %v", e)
}
}()
runCtx, cancel := context.WithTimeout(ctx, time.Hour)
defer cancel()
return p.Dispatch(runCtx, task)
}

View File

@ -1,24 +0,0 @@
package poller
import "sync"
type routineGroup struct {
waitGroup sync.WaitGroup
}
func newRoutineGroup() *routineGroup {
return new(routineGroup)
}
func (g *routineGroup) Run(fn func()) {
g.waitGroup.Add(1)
go func() {
defer g.waitGroup.Done()
fn()
}()
}
func (g *routineGroup) Wait() {
g.waitGroup.Wait()
}

View File

@ -1,63 +0,0 @@
package register
import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"gitea.com/gitea/act_runner/client"
"gitea.com/gitea/act_runner/config"
"gitea.com/gitea/act_runner/core"
"github.com/bufbuild/connect-go"
log "github.com/sirupsen/logrus"
)
func New(cli client.Client) *Register {
return &Register{
Client: cli,
}
}
type Register struct {
Client client.Client
}
func (p *Register) Register(ctx context.Context, cfg config.Runner) (*core.Runner, error) {
labels := make([]string, len(cfg.Labels))
for i, v := range cfg.Labels {
labels[i] = strings.SplitN(v, ":", 2)[0]
}
// register new runner.
resp, err := p.Client.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
Name: cfg.Name,
Token: cfg.Token,
AgentLabels: labels,
}))
if err != nil {
log.WithError(err).Error("poller: cannot register new runner")
return nil, err
}
data := &core.Runner{
ID: resp.Msg.Runner.Id,
UUID: resp.Msg.Runner.Uuid,
Name: resp.Msg.Runner.Name,
Token: resp.Msg.Runner.Token,
Address: p.Client.Address(),
Insecure: strconv.FormatBool(p.Client.Insecure()),
Labels: cfg.Labels,
}
file, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.WithError(err).Error("poller: cannot marshal the json input")
return data, err
}
// store runner config in .runner file
return data, os.WriteFile(cfg.File, file, 0o644)
}

6
renovate.json5 Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>gitea/renovate-config"
]
}

View File

@ -1,294 +0,0 @@
package runtime
import (
"context"
"fmt"
"strings"
"sync"
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"gitea.com/gitea/act_runner/client"
retry "github.com/avast/retry-go/v4"
"github.com/bufbuild/connect-go"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
type Reporter struct {
ctx context.Context
cancel context.CancelFunc
closed bool
client client.Client
clientM sync.Mutex
logOffset int
logRows []*runnerv1.LogRow
logReplacer *strings.Replacer
state *runnerv1.TaskState
stateM sync.RWMutex
}
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
var oldnew []string
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
oldnew = append(oldnew, v, "***")
}
for _, v := range task.Secrets {
oldnew = append(oldnew, v, "***")
}
return &Reporter{
ctx: ctx,
cancel: cancel,
client: client,
logReplacer: strings.NewReplacer(oldnew...),
state: &runnerv1.TaskState{
Id: task.Id,
},
}
}
func (r *Reporter) ResetSteps(l int) {
r.stateM.Lock()
defer r.stateM.Unlock()
for i := 0; i < l; i++ {
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
Id: int64(i),
})
}
}
func (r *Reporter) Levels() []log.Level {
return log.AllLevels
}
func (r *Reporter) Fire(entry *log.Entry) error {
r.stateM.Lock()
defer r.stateM.Unlock()
log.WithFields(entry.Data).Trace(entry.Message)
timestamp := entry.Time
if r.state.StartedAt == nil {
r.state.StartedAt = timestamppb.New(timestamp)
}
stage := entry.Data["stage"]
if stage != "Main" {
if v, ok := entry.Data["jobResult"]; ok {
if jobResult, ok := r.parseResult(v); ok {
r.state.Result = jobResult
r.state.StoppedAt = timestamppb.New(timestamp)
for _, s := range r.state.Steps {
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
s.Result = runnerv1.Result_RESULT_CANCELLED
}
}
}
}
if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
}
return nil
}
var step *runnerv1.StepState
if v, ok := entry.Data["stepNumber"]; ok {
if v, ok := v.(int); ok {
step = r.state.Steps[v]
}
}
if step == nil {
if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
}
return nil
}
if step.StartedAt == nil {
step.StartedAt = timestamppb.New(timestamp)
}
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))
}
step.LogLength++
r.logRows = append(r.logRows, r.parseLogRow(entry))
}
} else if !r.duringSteps() {
r.logRows = append(r.logRows, r.parseLogRow(entry))
}
if v, ok := entry.Data["stepResult"]; ok {
if stepResult, ok := r.parseResult(v); ok {
if step.LogLength == 0 {
step.LogIndex = int64(r.logOffset + len(r.logRows))
}
step.Result = stepResult
step.StoppedAt = timestamppb.New(timestamp)
}
}
return nil
}
func (r *Reporter) RunDaemon() {
if r.closed {
return
}
if r.ctx.Err() != nil {
return
}
_ = r.ReportLog(false)
_ = r.ReportState()
time.AfterFunc(time.Second, r.RunDaemon)
}
func (r *Reporter) Logf(format string, a ...interface{}) {
r.stateM.Lock()
defer r.stateM.Unlock()
if !r.duringSteps() {
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: fmt.Sprintf(format, a...),
})
}
}
func (r *Reporter) Close(lastWords string) error {
r.closed = true
r.stateM.Lock()
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
if lastWords == "" {
lastWords = "Early termination"
}
for _, v := range r.state.Steps {
if v.Result == runnerv1.Result_RESULT_UNSPECIFIED {
v.Result = runnerv1.Result_RESULT_CANCELLED
}
}
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: lastWords,
})
return nil
} else if lastWords != "" {
r.logRows = append(r.logRows, &runnerv1.LogRow{
Time: timestamppb.Now(),
Content: lastWords,
})
}
r.stateM.Unlock()
return retry.Do(func() error {
if err := r.ReportLog(true); err != nil {
return err
}
return r.ReportState()
}, retry.Context(r.ctx))
}
func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateM.RLock()
rows := r.logRows
r.stateM.RUnlock()
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
TaskId: r.state.Id,
Index: int64(r.logOffset),
Rows: rows,
NoMore: noMore,
}))
if err != nil {
return err
}
ack := int(resp.Msg.AckIndex)
if ack < r.logOffset {
return fmt.Errorf("submitted logs are lost")
}
r.stateM.Lock()
r.logRows = r.logRows[ack-r.logOffset:]
r.logOffset = ack
r.stateM.Unlock()
if noMore && ack < r.logOffset+len(rows) {
return fmt.Errorf("not all logs are submitted")
}
return nil
}
func (r *Reporter) ReportState() error {
r.clientM.Lock()
defer r.clientM.Unlock()
r.stateM.RLock()
state := proto.Clone(r.state).(*runnerv1.TaskState)
r.stateM.RUnlock()
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
State: state,
}))
if err != nil {
return err
}
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
r.cancel()
}
return nil
}
func (r *Reporter) duringSteps() bool {
if steps := r.state.Steps; len(steps) == 0 {
return false
} else if first := steps[0]; first.Result == runnerv1.Result_RESULT_UNSPECIFIED && first.LogLength == 0 {
return false
} else if last := steps[len(steps)-1]; last.Result != runnerv1.Result_RESULT_UNSPECIFIED {
return false
}
return true
}
var stringToResult = map[string]runnerv1.Result{
"success": runnerv1.Result_RESULT_SUCCESS,
"failure": runnerv1.Result_RESULT_FAILURE,
"skipped": runnerv1.Result_RESULT_SKIPPED,
"cancelled": runnerv1.Result_RESULT_CANCELLED,
}
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
str := ""
if v, ok := result.(string); ok { // for jobResult
str = v
} else if v, ok := result.(fmt.Stringer); ok { // for stepResult
str = v.String()
}
ret, ok := stringToResult[str]
return ret, ok
}
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
content = r.logReplacer.Replace(content)
return &runnerv1.LogRow{
Time: timestamppb.New(entry.Time),
Content: content,
}
}

View File

@ -1,64 +0,0 @@
package runtime
import (
"context"
"strings"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"gitea.com/gitea/act_runner/client"
)
// Runner runs the pipeline.
type Runner struct {
Machine string
ForgeInstance string
Environ map[string]string
Client client.Client
Labels []string
}
// Run runs the pipeline stage.
func (s *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
return NewTask(s.ForgeInstance, task.Id, s.Client, s.Environ, s.platformPicker).Run(ctx, task)
}
func (s *Runner) platformPicker(labels []string) string {
// "ubuntu-18.04:docker://node:16-buster"
// "self-hosted"
platforms := make(map[string]string, len(labels))
for _, l := range s.Labels {
// "ubuntu-18.04:docker://node:16-buster"
splits := strings.SplitN(l, ":", 2)
if len(splits) == 1 {
// identifier for non docker execution environment
platforms[splits[0]] = "-self-hosted"
continue
}
// ["ubuntu-18.04", "docker://node:16-buster"]
k, v := splits[0], splits[1]
if prefix := "docker://"; !strings.HasPrefix(v, prefix) {
continue
} else {
v = strings.TrimPrefix(v, prefix)
}
// ubuntu-18.04 => node:16-buster
platforms[k] = v
}
for _, label := range labels {
if v, ok := platforms[label]; ok {
return v
}
}
// TODO: support multiple labels
// like:
// ["ubuntu-22.04"] => "ubuntu:22.04"
// ["with-gpu"] => "linux:with-gpu"
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
// return default
return "node:16-bullseye"
}

View File

@ -1,265 +0,0 @@
package runtime
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"gitea.com/gitea/act_runner/client"
"github.com/nektos/act/pkg/artifacts"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus"
)
var globalTaskMap sync.Map
type TaskInput struct {
repoDirectory string
// actor string
// workdir string
// workflowsPath string
// autodetectEvent bool
// eventPath string
// reuseContainers bool
// bindWorkdir bool
// secrets []string
envs map[string]string
// platforms []string
// dryrun bool
forcePull bool
forceRebuild bool
// noOutput bool
// envfile string
// secretfile string
insecureSecrets bool
// defaultBranch string
privileged bool
usernsMode string
containerArchitecture string
containerDaemonSocket string
// noWorkflowRecurse bool
useGitIgnore bool
containerCapAdd []string
containerCapDrop []string
// autoRemove bool
artifactServerPath string
artifactServerPort string
jsonLogger bool
// noSkipCheckout bool
// remoteName string
EnvFile string
containerNetworkMode string
}
type Task struct {
BuildID int64
Input *TaskInput
client client.Client
log *log.Entry
platformPicker func([]string) string
}
// NewTask creates a new task
func NewTask(forgeInstance string, buildID int64, client client.Client, runnerEnvs map[string]string, picker func([]string) string) *Task {
task := &Task{
Input: &TaskInput{
envs: runnerEnvs,
containerNetworkMode: "bridge", // TODO should be configurable
},
BuildID: buildID,
client: client,
log: log.WithField("buildID", buildID),
platformPicker: picker,
}
task.Input.repoDirectory, _ = os.Getwd()
return task
}
// getWorkflowsPath return the workflows directory, it will try .gitea first and then fallback to .github
func getWorkflowsPath(dir string) (string, error) {
p := filepath.Join(dir, ".gitea/workflows")
_, err := os.Stat(p)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
return filepath.Join(dir, ".github/workflows"), nil
}
return p, nil
}
func getToken(task *runnerv1.Task) string {
token := task.Secrets["GITHUB_TOKEN"]
if task.Secrets["GITEA_TOKEN"] != "" {
token = task.Secrets["GITEA_TOKEN"]
}
if task.Context.Fields["token"].GetStringValue() != "" {
token = task.Context.Fields["token"].GetStringValue()
}
return token
}
func (t *Task) Run(ctx context.Context, task *runnerv1.Task) (lastErr error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
_, exist := globalTaskMap.Load(task.Id)
if exist {
return fmt.Errorf("task %d already exists", task.Id)
}
// set task ve to global map
// when task is done or canceled, it will be removed from the map
globalTaskMap.Store(task.Id, t)
defer globalTaskMap.Delete(task.Id)
lastWords := ""
reporter := NewReporter(ctx, cancel, t.client, task)
defer func() {
// set the job to failed on an error return value
if lastErr != nil {
reporter.Fire(&log.Entry{
Data: log.Fields{
"jobResult": "failure",
},
})
}
_ = reporter.Close(lastWords)
}()
reporter.RunDaemon()
reporter.Logf("received task %v of job %v", task.Id, task.Context.Fields["job"].GetStringValue())
workflowsPath, err := getWorkflowsPath(t.Input.repoDirectory)
if err != nil {
lastWords = err.Error()
return err
}
t.log.Debugf("workflows path: %s", workflowsPath)
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
if err != nil {
lastWords = err.Error()
return err
}
var plan *model.Plan
jobIDs := workflow.GetJobIDs()
if len(jobIDs) != 1 {
err := fmt.Errorf("multiple jobs found: %v", jobIDs)
lastWords = err.Error()
return err
}
jobID := jobIDs[0]
plan = model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
job := workflow.GetJob(jobID)
reporter.ResetSteps(len(job.Steps))
log.Infof("plan: %+v", plan.Stages[0].Runs)
token := getToken(task)
dataContext := task.Context.Fields
log.Infof("task %v repo is %v %v %v", task.Id, dataContext["repository"].GetStringValue(),
dataContext["gitea_default_actions_url"].GetStringValue(),
t.client.Address())
preset := &model.GithubContext{
Event: dataContext["event"].GetStructValue().AsMap(),
RunID: dataContext["run_id"].GetStringValue(),
RunNumber: dataContext["run_number"].GetStringValue(),
Actor: dataContext["actor"].GetStringValue(),
Repository: dataContext["repository"].GetStringValue(),
EventName: dataContext["event_name"].GetStringValue(),
Sha: dataContext["sha"].GetStringValue(),
Ref: dataContext["ref"].GetStringValue(),
RefName: dataContext["ref_name"].GetStringValue(),
RefType: dataContext["ref_type"].GetStringValue(),
HeadRef: dataContext["head_ref"].GetStringValue(),
BaseRef: dataContext["base_ref"].GetStringValue(),
Token: token,
RepositoryOwner: dataContext["repository_owner"].GetStringValue(),
RetentionDays: dataContext["retention_days"].GetStringValue(),
}
eventJSON, err := json.Marshal(preset.Event)
if err != nil {
lastWords = err.Error()
return err
}
maxLifetime := 3 * time.Hour
if deadline, ok := ctx.Deadline(); ok {
maxLifetime = time.Until(deadline)
}
input := t.Input
config := &runner.Config{
Workdir: "/" + preset.Repository,
BindWorkdir: false,
ReuseContainers: false,
ForcePull: input.forcePull,
ForceRebuild: input.forceRebuild,
LogOutput: true,
JSONLogger: input.jsonLogger,
Env: input.envs,
Secrets: task.Secrets,
InsecureSecrets: input.insecureSecrets,
Privileged: input.privileged,
UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture,
ContainerDaemonSocket: input.containerDaemonSocket,
UseGitIgnore: input.useGitIgnore,
GitHubInstance: t.client.Address(),
ContainerCapAdd: input.containerCapAdd,
ContainerCapDrop: input.containerCapDrop,
AutoRemove: true,
ArtifactServerPath: input.artifactServerPath,
ArtifactServerPort: input.artifactServerPort,
NoSkipCheckout: true,
PresetGitHubContext: preset,
EventJSON: string(eventJSON),
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
ContainerMaxLifetime: maxLifetime,
ContainerNetworkMode: input.containerNetworkMode,
DefaultActionInstance: dataContext["gitea_default_actions_url"].GetStringValue(),
PlatformPicker: t.platformPicker,
}
r, err := runner.New(config)
if err != nil {
lastWords = err.Error()
return err
}
artifactCancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort)
t.log.Debugf("artifacts server started at %s:%s", input.artifactServerPath, input.artifactServerPort)
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
artifactCancel()
return nil
})
t.log.Infof("workflow prepared")
reporter.Logf("workflow prepared")
// add logger recorders
ctx = common.WithLoggerHook(ctx, reporter)
if err := executor(ctx); err != nil {
lastWords = err.Error()
return err
}
return nil
}

70
scripts/run.sh Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
# 如果 /data 目录不存在,则创建它
if [[ ! -d /data ]]; then
mkdir -p /data
fi
cd /data
# 运行器状态文件,默认为 '.runner'
RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-'.runner'}
CONFIG_ARG=""
# 如果设置了 CONFIG_FILE,则添加配置文件参数
if [[ ! -z "${CONFIG_FILE}" ]]; then
CONFIG_ARG="--config ${CONFIG_FILE}"
fi
EXTRA_ARGS=""
# 如果设置了 GITEA_RUNNER_LABELS,则添加标签参数
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
EXTRA_ARGS="${EXTRA_ARGS} --labels ${GITEA_RUNNER_LABELS}"
fi
if [[ ! -z "${GITEA_RUNNER_EPHEMERAL}" ]]; then
EXTRA_ARGS="${EXTRA_ARGS} --ephemeral"
fi
RUN_ARGS=""
if [[ ! -z "${GITEA_RUNNER_ONCE}" ]]; then
RUN_ARGS="${RUN_ARGS} --once"
fi
# 如果没有设置令牌,可以从文件中读取令牌,例如从 Docker Secret
if [[ -z "${GITEA_RUNNER_REGISTRATION_TOKEN}" ]] && [[ -f "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}" ]]; then
GITEA_RUNNER_REGISTRATION_TOKEN=$(cat "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}")
fi
# 使用与 https://github.com/vegardit/docker-gitea-act-runner 相同的 ENV 变量名称
test -f "$RUNNER_STATE_FILE" || echo "$RUNNER_STATE_FILE 丢失或不是常规文件"
# 如果运行器状态文件不存在或为空
if [[ ! -s "$RUNNER_STATE_FILE" ]]; then
try=$((try + 1))
success=0
# 此循环的目的是使其简单,当在 docker 中同时运行 act_runner 和 gitea 时,
# 使 act_runner 在出错之前等待一段时间以等待 gitea 变为可用。在单个 docker-compose 的上下文中,
# 可以通过健康检查做类似的事情,但这更灵活。
while [[ $success -eq 0 ]] && [[ $try -lt ${GITEA_MAX_REG_ATTEMPTS:-10} ]]; do
act_runner register \
--instance "${GITEA_INSTANCE_URL}" \
--token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \
--name "${GITEA_RUNNER_NAME:-`hostname`}" \
${CONFIG_ARG} ${EXTRA_ARGS} --no-interactive 2>&1 | tee /tmp/reg.log
# 检查输出中是否有成功注册的消息
cat /tmp/reg.log | grep 'Runner registered successfully' > /dev/null
if [[ $? -eq 0 ]]; then
echo "成功"
success=1
else
echo "等待重试..."
sleep 5
fi
done
fi
# 防止从 act_runner 进程中读取令牌
unset GITEA_RUNNER_REGISTRATION_TOKEN
unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE
# 启动 act_runner 守护进程
exec act_runner daemon ${CONFIG_ARG} ${RUN_ARGS}

3
scripts/s6/act_runner/finish Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
exec s6-svscanctl -t /etc/s6

5
scripts/s6/act_runner/run Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
s6-svwait -U /etc/s6/docker
exec run.sh

6
scripts/s6/docker/data/check Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
if ! docker info &> /dev/null; then
echo "等待Docker守护进程启动……"
exit 1
fi

4
scripts/s6/docker/finish Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
exec s6-svscanctl -t /etc/s6

View File

@ -0,0 +1 @@
3

3
scripts/s6/docker/run Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
exec s6-notifyoncheck dockerd-entrypoint.sh