26 Commits

Author SHA1 Message Date
f58a6daa6c [汉化] 汉化测试用例、修复部分龙架构测试用例失效的问题
Some checks failed
checks / check and test (push) Has been cancelled
release-nightly / goreleaser (push) Has been cancelled
release-nightly / release-image (push) Has been cancelled
2025-05-09 15:17:16 +08:00
54308690d0 [冲突修复] 修复和上游的冲突文件
Some checks are pending
release-nightly / goreleaser (push) Waiting to run
release-nightly / release-image (push) Waiting to run
checks / check and test (push) Waiting to run
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)
Some checks failed
checks / check and test (pull_request) Has been cancelled
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 [汉化] 添加一些运行时候调用函数的注释, 帮助理解运行流程
Some checks failed
release-nightly / goreleaser (push) Has been cancelled
release-nightly / release-image (push) Has been cancelled
checks / check and test (push) Has been cancelled
2025-05-07 18:15:42 +08:00
19bcf7e525 [汉化] 新增汉化两个文件修复poller中的汉化导致的一些异
Some checks are pending
release-nightly / goreleaser (push) Waiting to run
release-nightly / release-image (push) Waiting to run
checks / check and test (push) Waiting to run
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
52 changed files with 1089 additions and 812 deletions

View File

@ -66,6 +66,7 @@ jobs:
with:
context: .
file: ./Dockerfile
target: basic
platforms: |
linux/amd64
linux/arm64
@ -73,13 +74,25 @@ jobs:
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
- name: Build and push dind-rootless
- name: Build and push dind
uses: docker/build-push-action@v5
env:
ACTIONS_RUNTIME_TOKEN: "" # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile.rootless
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

View File

@ -73,6 +73,7 @@ jobs:
with:
context: .
file: ./Dockerfile
target: basic
platforms: |
linux/amd64
linux/arm64
@ -81,13 +82,26 @@ jobs:
${{ 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-rootless
- name: Build and push dind
uses: docker/build-push-action@v5
env:
ACTIONS_RUNTIME_TOKEN: "" # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile.rootless
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

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
act_runner
/act_runner
/loong_runner
.env
.runner
coverage.txt
@ -12,3 +13,5 @@ coverage.txt
__debug_bin
# gorelease binary folder
dist
# vim
*.sw*

View File

@ -16,6 +16,8 @@ builds:
- amd64
- arm
- arm64
- s390x
- riscv64
goarm:
- "5"
- "6"
@ -60,7 +62,7 @@ builds:
flags:
- -trimpath
ldflags:
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
- -s -w -X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version={{ .Summary }}
binary: >-
{{ .ProjectName }}-
{{- .Version }}-

View File

@ -1,16 +1,54 @@
FROM golang:1.23-alpine AS builder
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 alpine
RUN apk add --no-cache git bash tini
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 /opt/act/run.sh
COPY scripts/run.sh /usr/local/bin/run.sh
COPY scripts/s6 /etc/s6
ENTRYPOINT ["/sbin/tini","--","/opt/act/run.sh"]
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,24 +0,0 @@
FROM golang:1.23-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
COPY . /opt/src/act_runner
WORKDIR /opt/src/act_runner
RUN make clean && make build
FROM docker:dind-rootless
USER root
RUN apk add --no-cache \
git bash supervisor
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
COPY /scripts/supervisord.conf /etc/supervisord.conf
COPY /scripts/run.sh /opt/act/run.sh
COPY /scripts/rootless.sh /opt/act/rootless.sh
RUN mkdir /data \
&& chown rootless:rootless /data
USER rootless
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

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,7 +7,7 @@ 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
@ -16,7 +16,7 @@ WINDOWS_ARCHS ?= windows/amd64
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
DOCKER_IMAGE ?= gitea/act_runner
DOCKER_IMAGE ?= gitea/loong_runner
DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
@ -66,11 +66,11 @@ else
endif
endif
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
GO_PACKAGES_TO_VET ?= $(filter-out git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
TAGS ?=
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
LDFLAGS ?= -X "git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
all: build
@ -86,7 +86,7 @@ go-check:
$(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 requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
echo "Act Runner 需要 Go $(MIN_GO_VERSION_STR) 或更高版本才能构建。您可以从 https://go.dev/dl/ 获取。"; \
exit 1; \
fi
@ -97,17 +97,17 @@ fmt-check:
fi
@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 "Running go vet..."
@echo "运行 go vet..."
@$(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
@ -170,7 +170,6 @@ docker:
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
fi; \
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_ROOTLESS_REF) -f Dockerfile.rootless .
clean:
$(GO) clean -x -i ./...

View File

@ -1,108 +1,111 @@
# act runner
# Loong Runner
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act).
Loong Runner 是基于 [Gitea派生(fock)的act](https://gitea.com/gitea/act) [二次派生](https://git.whlug.cn/LoongArchActions/loong_runner) 主要适用于当前龙架构的运行时。
## Installation
> 上游的Gitea派生是可以直接在龙架构上编译使用的, 此派生仓库主要是解决上游的Docker是基于乌班图系统制作的, 目前龙架构暂无乌班图系统
> * 当前计划使用`Debian`来代替乌班图用于构建容器镜像
### Prerequisites
Docker Engine Community version is required for docker mode. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
## 安装
### Download pre-built binary
### 前提条件
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
在 Docker 模式下需要 `Docker Engine Community` 版本。要安装 `Docker CE`,请遵循官方 [安装说明](https://docs.docker.com/engine/install/)。
### Build from source
### 下载预构建的二进制文件
访问 [这里](https://git.whlug.cn/LoongArchActions/loong_runner) 仅提供龙架构版本。
### 从源码构建
```bash
make build
```
### Build a docker image
### 构建 Docker 容器镜像
```bash
make docker
```
## Quickstart
## 快速入门
默认情况下,操作是禁用的,因此您需要在 Gitea 实例的配置文件中添加以下内容以启用它:
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
```ini
[actions]
ENABLED=true
```
### Register
### 注册
```bash
./act_runner register
./loong_runner register
```
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/actions/runners`;
3. Runner name, you can just leave it blank;
4. Runner labels, you can just leave it blank.
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. 运行器标签,您可以留空。
The process looks like:
过程如下:
```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, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest):
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://gitea/runner-images:ubuntu-latest ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04 ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04].
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
```
### Run with docker
### 使用 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/act_runner:nightly
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
```
### Configuration
### 配置
You can also configure the runner with a configuration file.
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
您还可以使用配置文件配置运行器。
配置文件是一个 YAML 文件,您可以使用 `./loong_runner generate-config` 生成一个示例配置文件。
```bash
./act_runner generate-config > config.yaml
./loong_runner generate-config > config.yaml
```
You can specify the configuration file path with `-c`/`--config` argument.
您可以使用 `-c`/`--config` 参数指定配置文件路径。
```bash
./act_runner -c config.yaml register # register with config file
./act_runner -c config.yaml daemon # run with config file
./loong_runner -c config.yaml register # 使用配置文件注册
./loong_runner -c config.yaml daemon # 使用配置文件运行
```
You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml).
您可以在 [config.example.yaml](internal/pkg/config/config.example.yaml) 上在线查看配置文件的最新版本。
### Example Deployments
### 示例部署
Check out the [examples](examples) directory for sample deployment types.
查看 [examples](examples) 目录中的示例部署类型。

View File

@ -1,12 +1,12 @@
# Usage Examples for `act_runner`
# `loong_runner` 使用示例
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
欢迎来到我们专为 Gitea 设置设计的使用和部署示例集合。无论您是初学者还是经验丰富的用户,您都会在这里找到可以直接应用以增强 Gitea 体验的实用资源。我们鼓励您贡献自己的见解和知识,使这个集合更加全面和有价值。
| Section | Description |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`docker`](docker) | This section provides you with scripts and instructions tailored for running containers on a workstation or server where Docker is installed. It simplifies the process of setting up and managing your Gitea deployment using Docker. |
| [`docker-compose`](docker-compose) | In this section, you'll discover examples demonstrating how to utilize docker-compose to efficiently handle your Gitea deployments. It offers a straightforward approach to managing multiple containerized components of your Gitea setup. |
| [`kubernetes`](kubernetes) | If you're utilizing Kubernetes clusters for your infrastructure, this section is specifically designed for you. It presents examples and guidelines for configuring Gitea deployments within Kubernetes clusters, enabling you to leverage the scalability and flexibility of Kubernetes. |
| [`vm`](vm) | This section is dedicated to examples that assist you in setting up Gitea on virtual or physical servers. Whether you're working with virtual machines or physical hardware, you'll find helpful resources to guide you through the deployment process. |
| 部分 | 描述 |
|-----|------|
| [`docker`](docker) | 本部分为您提供适用于在工作站或安装了 Docker 的服务器上运行容器的脚本和说明。它简化了使用 Docker 设置和管理 Gitea 部署的过程。 |
| [`docker-compose`](docker-compose) | 在本部分中,您将发现如何利用 `docker-compose` 来高效处理 Gitea 部署的示例。它提供了一个直接的方法来管理 Gitea 设置的多个容器化组件。 |
| [`kubernetes`](kubernetes) | 如果您正在使用 Kubernetes 集群作为基础设施,本部分专门为您设计。它展示了在 Kubernetes 集群内配置 Gitea 部署的示例和指南,使您能够利用 Kubernetes 的可扩展性和灵活性。 |
| [`vm`](vm) | 本部分致力于协助您在虚拟或物理服务器上设置 Gitea 的示例。无论您是在使用虚拟机还是物理硬件,您都会找到有用的资源来指导您完成部署过程。 |
We hope these resources provide you with valuable insights and solutions for your Gitea setup. Feel free to explore, contribute, and adapt these examples to suit your specific requirements.
我们希望这些资源为您的 Gitea 设置提供有价值的见解和解决方案。请随意探索、贡献和调整这些示例以满足您的具体需求。

View File

@ -1,46 +1,59 @@
### Running `act_runner` using `docker-compose`
### 使用`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/act_runner
image: gitea/loong_runner
restart: always
depends_on:
- gitea
gitea:
# 需要(下述配置),以便运行器能够连接到仓库,请参阅“健康检查(healthcheck)”
condition: service_healthy
restart: true
volumes:
- ./data/act_runner:/data
- ./data/loong_runner:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=<instance url>
# When using Docker Secrets, it's also possible to use
# GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# The env var takes precedence.
# Needed only for the first start.
# 当使用Docker Secrets
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
# 环境变量优先, 仅在首次启动时需要。
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```
### Running `act_runner` using Docker-in-Docker (DIND)
### 使用 Docker-in-Docker (DIND) 运行 `loong_runner`
```yml
...
runner:
image: gitea/act_runner:latest-dind-rootless
image: gitea/loong_runner:latest-dind-rootless
restart: always
privileged: true
depends_on:
- gitea
volumes:
- ./data/act_runner:/data
- ./data/loong_runner:/data
environment:
- GITEA_INSTANCE_URL=<instance url>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
# When using Docker Secrets, it's also possible to use
# GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# The env var takes precedence.
# Needed only for the first start.
# 使用Docker Secrets
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
# 环境变量优先, 仅在首次启动时需要。
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```

View File

@ -1,8 +1,8 @@
### Run `act_runner` in a Docker Container
### 在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/act_runner:nightly
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
```
The `/data` directory inside the docker container contains the runner API keys after registration.
It must be persisted, otherwise the runner would try to register again, using the same, now defunct registration token.
docker 容器内的 `/data` 目录在注册后包含运行器 API 密钥。
必须对其进行持久化存储,否则运行器将尝试再次注册,并使用相同的(现已失效的)注册令牌。

View File

@ -1,11 +1,19 @@
## Kubernetes Docker in Docker Deployment with `act_runner`
## Kubernetes 中使用 `act_runner` 部署 Docker-in-Docker(DinD)
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
注意:Docker-in-Docker(DinD)在 Kubernetes 中需要提升权限。目前的实现方式是将 Pod 的 `SecurityContext`(安全上下文)设置为 `privileged`(特权模式)。请注意这存在潜在安全风险,恶意应用可能突破容器隔离上下文。
Files in this directory:
本目录包含文件:
- [`dind-docker.yaml`](dind-docker.yaml)
How to create a Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.
- [`dind-docker.yaml`](dind-docker.yaml)
用于在 Kubernetes 中创建作为运行器的 Deployment(部署)和 Persistent Volume(持久卷)。Docker 凭证会在每次 Pod 连接时重新生成,无需持久化存储。
- [`rootless-docker.yaml`](rootless-docker.yaml)
How to create a rootless Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.
- [`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

@ -12,6 +12,9 @@ spec:
---
apiVersion: v1
data:
# 注册令牌可从Web UI、API或命令行获取。
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
# 为Gitea实例设置预定义的全局运行器注册令牌。
token: << base64 encoded registration token >>
kind: Secret
metadata:
@ -46,7 +49,7 @@ spec:
containers:
- name: runner
image: gitea/act_runner:nightly
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
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

View File

@ -12,7 +12,10 @@ spec:
---
apiVersion: v1
data:
token: << runner registration token goes here >>
# 注册令牌可从Web UI、API或命令行获取。
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
# 为Gitea实例设置预定义的全局运行器注册令牌。
token: << base64 encoded registration token >>
kind: Secret
metadata:
name: runner-secret
@ -47,7 +50,7 @@ spec:
- name: runner
image: gitea/act_runner:nightly-dind-rootless
imagePullPolicy: Always
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
# 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

View File

@ -1,6 +1,6 @@
## `act_runner` on Virtual or Physical Servers
# 在虚拟或物理服务器上使用 `loong_runner`
Files in this directory:
此目录中的文件:
- [`rootless-docker.md`](rootless-docker.md)
How to set up a rootless docker implementation of the runner.
在非Root用户下使用Docker运行`loong_runner`

View File

@ -1,65 +1,73 @@
## Using Rootless Docker with`act_runner`
# 在非特权模式下使用Docker
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
以下是如何在在非特权模式下使用 Docker 中设置 `loong_runner` 的简单示例。实例是基于 Debian 编写的,其他 Linux 发行版不会有太大区别。
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
注意:此过程需要一个真实的登录 shell -- 使用 `sudo su` 或其他访问账户的方法将无法完成下面的一些步骤。
As `root`:
使用 `root` 用户:
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`.
- 创建一个用户来运行 `docker``loong_runner`。在这个例子中,我们使用了一个名为 `rootless` 的非特权账户。
```bash
useradd -m rootless
passwd rootless
useradd -m rootless
passwd rootless
apt-get install -y uidmap # 对 docker 非Root用户使用是必需的。
```
- Install [`docker-ce`](https://docs.docker.com/engine/install/)
- (Recommended) Disable the system-wide Docker daemon
- 安装 [`docker-ce`](https://docs.docker.com/engine/install/)
- (推荐)禁用系统范围的 Docker 守护进程
``systemctl disable --now docker.service docker.socket``
As the `rootless` user:
作为 `rootless` 用户:
- Follow the instructions for [enabling rootless mode](https://docs.docker.com/engine/security/rootless/)
- Add the following lines to the `/home/rootless/.bashrc`:
- 按照 [启用非特权模式](https://docs.docker.com/engine/security/rootless/) 的说明进行操作
- 将以下行添加到 `/home/rootless/.bashrc`
```bash
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
export PATH=/home/rootless/bin:$PATH
export DOCKER_HOST=unix:///run/user/1001/docker.sock
for f in ./.bashrc.d/*.bash; do echo "处理 $f 文件..."; . "$f"; done
```
- Reboot. Ensure that the Docker process is working.
- Create a directory for saving `act_runner` data between restarts
`mkdir /home/rootless/act_runner`
- Register the runner from the data directory
- 创建 `.bashrc.d` 目录 `mkdir ~/.bashrc.d`
- 将以下行添加到 `/home/rootless/.bashrc.d/rootless-docker.bash`:
```bash
cd /home/rootless/act_runner
act_runner register
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
export PATH=/home/rootless/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
```
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system.
- 重启。确保 Docker 进程正在工作。
- 为保存 `loong_runner` 数据创建一个目录
`mkdir /home/rootless/loong_runner`
- 从数据目录注册 runner
```bash
act_runner generate-config >/home/rootless/act_runner/config
cd /home/rootless/loong_runner
loong_runner register
```
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents:
- 在数据目录中生成 `loong_runner` 配置文件。编辑文件以适应系统。
```bash
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/act_runner
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/act_runner daemon -c /home/rootless/act_runner/config
ExecStart=/usr/bin/loong_runner daemon -c /home/rootless/loong_runner/config
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/rootless/act_runner
WorkingDirectory=/home/rootless/loong_runner
TimeoutSec=0
RestartSec=2
Restart=always
@ -78,10 +86,11 @@ As the `rootless` user:
WantedBy=default.target
```
- Reboot
- 重启
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea.
系统重启后,检查 `loong_runner` 是否正常工作,并且 运行时 是否已连接到 Gitea
````bash
systemctl --user status act_runner
journalctl --user -xeu act_runner
systemctl --user status loong_runner
journalctl --user -xeu loong_runner
```

8
go.mod
View File

@ -1,9 +1,9 @@
module gitea.com/gitea/act_runner
module git.whlug.cn/LAA/loong_runner
go 1.23
go 1.24
require (
code.gitea.io/actions-proto-go v0.4.0
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
@ -98,4 +98,4 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/nektos/act => gitea.com/gitea/act v0.261.2
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4

8
go.sum
View File

@ -1,13 +1,13 @@
code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU=
code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
gitea.com/gitea/act v0.261.2 h1:yAhxlt11gpRmF7CeVsVjgLg1Ph0xxroJ/l2fWaYyl84=
gitea.com/gitea/act v0.261.2/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=

View File

@ -9,7 +9,7 @@ import (
"os"
"os/signal"
"gitea.com/gitea/act_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
"github.com/nektos/act/pkg/artifactcache"
log "github.com/sirupsen/logrus"
@ -17,16 +17,16 @@ import (
)
type cacheServerArgs struct {
Dir string
Host string
Port uint16
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("invalid configuration: %w", err)
return fmt.Errorf("配置无效: %w", err)
}
initLogging(cfg)
@ -37,7 +37,7 @@ func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheSer
port = cfg.Cache.Port
)
// cacheArgs has higher priority
// cacheArgs 优先级更高
if cacheArgs.Dir != "" {
dir = cacheArgs.Dir
}
@ -58,7 +58,7 @@ func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheSer
return err
}
log.Infof("cache server is listening on %v", cacheHandler.ExternalURL())
log.Infof("缓存服务器正在监听 %v", cacheHandler.ExternalURL())
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)

View File

@ -10,44 +10,47 @@ import (
"github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"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 [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.",
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", "", "Config file path")
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径")
// ./act_runner register
var regArgs registerArgs
registerCmd := &cobra.Command{
Use: "register",
Short: "Register a runner to the server",
Short: "将运行器注册到服务器",
Args: cobra.MaximumNArgs(0),
RunE: runRegister(ctx, &regArgs, &configFile), // must use a pointer to regArgs
RunE: runRegister(ctx, &regArgs, &configFile), // 必须使用 regArgs 的指针
}
registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "Disable interactive mode")
registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Gitea instance address")
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")
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: "Run as a runner daemon",
Args: cobra.MaximumNArgs(1),
RunE: runDaemon(ctx, &configFile),
Short: "作为运行器守护进程运行",
Args: cobra.MaximumNArgs(0),
RunE: runDaemon(ctx, &daemArgs, &configFile),
}
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "运行一个作业然后退出")
rootCmd.AddCommand(daemonCmd)
// ./act_runner exec
@ -56,7 +59,7 @@ func Execute(ctx context.Context) {
// ./act_runner config
rootCmd.AddCommand(&cobra.Command{
Use: "generate-config",
Short: "Generate an example config file",
Short: "生成示例配置文件",
Args: cobra.MaximumNArgs(0),
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("%s", config.Example)
@ -67,16 +70,16 @@ func Execute(ctx context.Context) {
var cacheArgs cacheServerArgs
cacheCmd := &cobra.Command{
Use: "cache-server",
Short: "Start a cache server for the cache action",
Short: "启动缓存服务器用于缓存操作",
Args: cobra.MaximumNArgs(0),
RunE: runCacheServer(ctx, &configFile, &cacheArgs),
}
cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "Cache directory")
cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "Host of the cache server")
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server")
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)
// hide completion command
// 隐藏自动补全命令
rootCmd.CompletionOptions.HiddenDefaultCmd = true
if err := rootCmd.Execute(); err != nil {

View File

@ -19,31 +19,31 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/app/poll"
"gitea.com/gitea/act_runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
"gitea.com/gitea/act_runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"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, configFile *string) func(cmd *cobra.Command, args []string) error {
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("invalid configuration: %w", err)
return fmt.Errorf("无效的配置: %w", err)
}
initLogging(cfg)
log.Infoln("Starting runner daemon")
log.Infoln("启动运行器守护进程")
reg, err := config.LoadRegistration(cfg.Runner.File)
if os.IsNotExist(err) {
log.Error("registration file not found, please register the runner first")
log.Error("未找到注册文件,请先注册运行器")
return err
} else if err != nil {
return fmt.Errorf("failed to load registration file: %w", err)
return fmt.Errorf("加载注册文件失败: %w", err)
}
lbls := reg.Labels
@ -55,13 +55,13 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
for _, l := range lbls {
label, err := labels.Parse(l)
if err != nil {
log.WithError(err).Warnf("ignored invalid label %q", l)
log.WithError(err).Warnf("忽略无效标签 %q", l)
continue
}
ls = append(ls, label)
}
if len(ls) == 0 {
log.Warn("no labels configured, runner may not be able to pick up jobs")
log.Warn("未配置任何标签,运行器可能无法接取作业")
}
if ls.RequireDocker() {
@ -72,15 +72,15 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil {
return err
}
// if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath
// 如果 dockerSocketPath 通过检查,用 dockerSocketPath 覆盖 DOCKER_HOST
os.Setenv("DOCKER_HOST", dockerSocketPath)
// empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
// and assign the path to cfg.Container.DockerHost
// 如果 cfg.Container.DockerHost 为空,意味着 act_runner 需要自动查找可用的 docker 主机
// 并将路径分配给 cfg.Container.DockerHost
if cfg.Container.DockerHost == "" {
cfg.Container.DockerHost = dockerSocketPath
}
// check the scheme, if the scheme is not npipe or unix
// set cfg.Container.DockerHost to "-" because it can't be mounted to the job container
// 检查方案,如果方案不是 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") {
@ -92,9 +92,9 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
if !slices.Equal(reg.Labels, ls.ToStrings()) {
reg.Labels = ls.ToStrings()
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
return fmt.Errorf("failed to save runner config: %w", err)
return fmt.Errorf("保存运行器配置失败: %w", err)
}
log.Infof("labels updated to: %v", reg.Labels)
log.Infof("标签更新为: %v", reg.Labels)
}
cli := client.New(
@ -107,38 +107,58 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
runner := run.NewRunner(cfg, reg, cli)
// declare the labels of the runner before fetching tasks
// 在获取任务之前声明运行器的标签
resp, err := runner.Declare(ctx, ls.Names())
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
log.Errorf("您的 Gitea 版本太旧,不支持运行器声明,请升级到 v1.21 或更高版本")
return err
} else if err != nil {
log.WithError(err).Error("fail to invoke Declare")
log.WithError(err).Error("调用 Declare 失败")
return err
} else {
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
log.Infof("运行器: %s, 版本: %s, 标签: %v, 声明成功",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
}
poller := poll.New(cfg, cli, runner)
go poller.Poll()
if daemArgs.Once || reg.Ephemeral {
done := make(chan struct{})
go func() {
defer close(done)
poller.PollOnce()
}()
<-ctx.Done()
log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
// 当完成一个作业或请求取消时关闭
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("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
log.Warnf("运行器: %s 在关闭期间取消了正在进行的作业", resp.Msg.Runner.Name)
}
return nil
}
}
// initLogging setup the global logrus logger.
type daemonArgs struct {
Once bool // 是否只运行一次
}
// initLogging 设置全局 logrus 日志记录器。
func initLogging(cfg *config.Config) {
isTerm := isatty.IsTerminal(os.Stdout.Fd())
format := &log.TextFormatter{
@ -151,17 +171,17 @@ func initLogging(cfg *config.Config) {
level, err := log.ParseLevel(l)
if err != nil {
log.WithError(err).
Errorf("invalid log level: %q", l)
Errorf("无效的日志级别: %q", l)
}
// debug level
// 调试级别
if level == log.DebugLevel {
log.SetReportCaller(true)
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
// get function name
// 获取函数名
s := strings.Split(f.Function, ".")
funcname := "[" + s[len(s)-1] + "]"
// get file name and line number
// 获取文件名和行号
_, filename := path.Split(f.File)
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
return funcname, filename
@ -170,7 +190,7 @@ func initLogging(cfg *config.Config) {
}
if log.GetLevel() != level {
log.Infof("log level changed to %v", level)
log.Infof("日志级别更改为 %v", level)
log.SetLevel(level)
}
}
@ -187,7 +207,7 @@ var commonSocketPaths = []string{
}
func getDockerSocketPath(configDockerHost string) (string, error) {
// a `-` means don't mount the docker socket to job containers
// `-` 表示不要将 docker 套接字挂载到作业容器
if configDockerHost != "" && configDockerHost != "-" {
return configDockerHost, nil
}
@ -206,5 +226,5 @@ func getDockerSocketPath(configDockerHost string) (string, error) {
}
}
return "", fmt.Errorf("daemon Docker Engine socket not found and docker_host config was invalid")
return "", fmt.Errorf("守护进程 Docker 引擎套接字未找到且 docker_host 配置无效")
}

View File

@ -26,47 +26,47 @@ import (
)
type executeArgs struct {
runList bool
job string
event string
workdir string
workflowsPath string
noWorkflowRecurse bool
autodetectEvent bool
forcePull bool
forceRebuild bool
jsonLogger bool
envs []string
envfile string
secrets []string
defaultActionsURL string
insecureSecrets bool
privileged bool
usernsMode string
containerArchitecture string
containerDaemonSocket string
useGitIgnore bool
containerCapAdd []string
containerCapDrop []string
containerOptions string
artifactServerPath string
artifactServerAddr string
artifactServerPort string
noSkipCheckout bool
debug bool
dryrun bool
image string
cacheHandler *artifactcache.Handler
network string
githubInstance string
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 returns path to workflow file(s)
// WorkflowsPath 返回工作流文件的路径
func (i *executeArgs) WorkflowsPath() string {
return i.resolve(i.workflowsPath)
}
// Envfile returns path to .env
// Envfile 返回 .env 文件的路径
func (i *executeArgs) Envfile() string {
return i.resolve(i.envfile)
}
@ -77,18 +77,18 @@ func (i *executeArgs) LoadSecrets() map[string]string {
secretPairParts := strings.SplitN(secretPair, "=", 2)
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
log.Errorf("Secret %s is already defined (secrets are case insensitive)", 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("Provide value for '%s': ", secretPairParts[0])
fmt.Printf(" '%s' 提供值: ", secretPairParts[0])
val, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
log.Errorf("failed to read input: %v", err)
log.Errorf("读取输入失败: %v", err)
os.Exit(1)
}
s[secretPairParts[0]] = string(val)
@ -101,7 +101,7 @@ 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("Error loading from %s: %v", path, err)
log.Fatalf("从 %s 加载失败: %v", path, err)
}
for k, v := range env {
envs[k] = v
@ -130,7 +130,7 @@ func (i *executeArgs) LoadEnvs() map[string]string {
return envs
}
// Workdir returns path to workdir
// Workdir 返回工作目录的路径
func (i *executeArgs) Workdir() string {
return i.resolve(".")
}
@ -151,22 +151,22 @@ func (i *executeArgs) resolve(path string) string {
func printList(plan *model.Plan) error {
type lineInfoDef struct {
jobID string
jobName string
stage string
wfName string
wfFile string
events string
jobID string // 作业 ID
jobName string // 作业名称
stage string // 阶段
wfName string // 工作流名称
wfFile string // 工作流文件
events string // 事件
}
lineInfos := []lineInfoDef{}
header := lineInfoDef{
jobID: "Job ID",
jobName: "Job name",
stage: "Stage",
wfName: "Workflow name",
wfFile: "Workflow file",
events: "Events",
jobID: "作业 ID",
jobName: "作业名称",
stage: "阶段",
wfName: "工作流名称",
wfFile: "工作流文件",
events: "事件",
}
jobs := map[string]bool{}
@ -221,7 +221,6 @@ func printList(plan *model.Plan) error {
jobNameMaxWidth += 2
stageMaxWidth += 2
wfNameMaxWidth += 2
wfFileMaxWidth += 2
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
-stageMaxWidth, header.stage,
@ -242,47 +241,47 @@ func printList(plan *model.Plan) error {
)
}
if duplicateJobIDs {
fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
fmt.Print("\n检测到多个作业具有相同的作业名称,请使用 `-W` 指定特定工作流的路径。\n")
}
return nil
}
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
// plan with filtered jobs - to be used for filtering only
// 计划带有过滤的作业 - 仅用于过滤
var filterPlan *model.Plan
// Determine the event name to be filtered
// 确定要过滤的事件名称
var filterEventName string
if len(execArgs.event) > 0 {
log.Infof("Using chosed event for filtering: %s", execArgs.event)
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
filterEventName = execArgs.event
} else if execArgs.autodetectEvent {
// collect all events from loaded workflows
// 收集所有加载的工作流中的事件
events := planner.GetEvents()
// set default event type to first event from many available
// this way user dont have to specify the event.
log.Infof("Using first detected workflow event for filtering: %s", events[0])
// 将默认事件类型设置为第一个可用的事件
// 这样用户就不必指定事件。
log.Infof("使用检测到的第一个工作流事件进行过滤: %s", events[0])
filterEventName = events[0]
}
var err error
if execArgs.job != "" {
log.Infof("Preparing plan with a job: %s", execArgs.job)
log.Infof("准备带有作业的计划: %s", execArgs.job)
filterPlan, err = planner.PlanJob(execArgs.job)
if err != nil {
return err
}
} else if filterEventName != "" {
log.Infof("Preparing plan for a event: %s", filterEventName)
log.Infof("准备事件的计划: %s", filterEventName)
filterPlan, err = planner.PlanEvent(filterEventName)
if err != nil {
return err
}
} else {
log.Infof("Preparing plan with all jobs")
log.Infof("准备所有作业的计划")
filterPlan, err = planner.PlanAll()
if err != nil {
return err
@ -305,40 +304,40 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
return runExecList(ctx, planner, execArgs)
}
// plan with triggered jobs
// 计划触发作业
var plan *model.Plan
// Determine the event name to be triggered
// 确定要触发的事件名称
var eventName string
// collect all events from loaded workflows
// 收集所有加载的工作流中的事件
events := planner.GetEvents()
if len(execArgs.event) > 0 {
log.Infof("Using chosed event for filtering: %s", execArgs.event)
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
eventName = execArgs.event
} else if len(events) == 1 && len(events[0]) > 0 {
log.Infof("Using the only detected workflow event: %s", events[0])
log.Infof("使用唯一检测到的工作流事件: %s", events[0])
eventName = events[0]
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
// set default event type to first event from many available
// this way user dont have to specify the event.
log.Infof("Using first detected workflow event: %s", events[0])
// 将默认事件类型设置为第一个可用的事件
// 这样用户就不必指定事件。
log.Infof("使用检测到的第一个工作流事件: %s", events[0])
eventName = events[0]
} else {
log.Infof("Using default workflow event: push")
log.Infof("使用默认工作流事件: push")
eventName = "push"
}
// build the plan for this run
// 为此运行构建计划
if execArgs.job != "" {
log.Infof("Planning job: %s", execArgs.job)
log.Infof("规划作业: %s", execArgs.job)
plan, err = planner.PlanJob(execArgs.job)
if err != nil {
return err
}
} else {
log.Infof("Planning jobs for event: %s", eventName)
log.Infof("规划事件的作业: %s", eventName)
plan, err = planner.PlanEvent(eventName)
if err != nil {
return err
@ -350,18 +349,18 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
maxLifetime = time.Until(deadline)
}
// init a cache server
// 初始化缓存服务器
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
if err != nil {
return err
}
log.Infof("cache handler listens on: %v", handler.ExternalURL())
log.Infof("缓存处理器监听于: %v", handler.ExternalURL())
execArgs.cacheHandler = handler
if len(execArgs.artifactServerAddr) == 0 {
ip := common.GetOutboundIP()
if ip == nil {
return fmt.Errorf("unable to determine outbound IP address")
return fmt.Errorf("无法确定出站 IP 地址")
}
execArgs.artifactServerAddr = ip.String()
}
@ -376,7 +375,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
execArgs.artifactServerPath = tempDir
}
// run the plan
// 运行计划
config := &runner.Config{
Workdir: execArgs.Workdir(),
BindWorkdir: false,
@ -411,7 +410,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
PlatformPicker: func(_ []string) string {
return execArgs.image
},
ValidVolumes: []string{"**"}, // All volumes are allowed for `exec` command
ValidVolumes: []string{"**"}, // 所有挂载的卷(volumes)都允许被 exec 命令访问
}
config.Env["ACT_EXEC"] = "true"
@ -433,7 +432,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
}
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, 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 {
@ -450,43 +449,43 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
execCmd := &cobra.Command{
Use: "exec",
Short: "Run workflow locally.",
Short: "本地运行工作流",
Args: cobra.MaximumNArgs(20),
RunE: runExec(ctx, &execArg),
}
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "Defines the address where the artifact server listens")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "Defines the default url of action instance.")
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "gitea/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.")
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

@ -20,13 +20,13 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"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 registers a runner to the server
// 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)
@ -37,13 +37,13 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
})
log.SetLevel(log.DebugLevel)
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
log.Infof("注册运行器,架构=%s,操作系统=%s,版本=%s",
goruntime.GOARCH, goruntime.GOOS, ver.Version())
// runner always needs root permission
// 运行器始终需要 root 权限
if os.Getuid() != 0 {
// TODO: use a better way to check root permission
log.Warnf("Runner in user-mode.")
// TODO: 使用更好的方法检查 root 权限
log.Warnf("运行器处于用户模式。")
}
if regArgs.NoInteractive {
@ -52,7 +52,7 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
}
} else {
go func() {
if err := registerInteractive(ctx, *configFile); err != nil {
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
log.Fatal(err)
return
}
@ -68,13 +68,14 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
}
}
// registerArgs represents the arguments for register command
// registerArgs 代表 register 命令的参数
type registerArgs struct {
NoInteractive bool
InstanceAddr string
Token string
RunnerName string
Labels string
NoInteractive bool // 是否非交互模式
InstanceAddr string // 实例地址
Token string // 令牌
RunnerName string // 运行器名称
Labels string // 标签
Ephemeral bool // 是否临时
}
type registerStage int8
@ -91,9 +92,9 @@ const (
)
var defaultLabels = []string{
"ubuntu-latest:docker://gitea/runner-images:ubuntu-latest",
"ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04",
"ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04",
"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 {
@ -101,14 +102,15 @@ type registerInputs struct {
Token string
RunnerName string
Labels []string
Ephemeral bool
}
func (r *registerInputs) validate() error {
if r.InstanceAddr == "" {
return fmt.Errorf("instance address is empty")
return fmt.Errorf("实例地址为空")
}
if r.Token == "" {
return fmt.Errorf("token is empty")
return fmt.Errorf("令牌为空")
}
if len(r.Labels) > 0 {
return validateLabels(r.Labels)
@ -125,16 +127,32 @@ func validateLabels(ls []string) error {
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 {
// 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()
}
@ -153,19 +171,19 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
return StageInputRunnerName
case StageInputRunnerName:
r.RunnerName = value
// if there are some labels configured in config file, skip input labels stage
// 如果配置文件中有标签配置,跳过输入标签阶段
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("ignored invalid label %q", l)
log.WithError(err).Warnf("忽略无效标签 %q", l)
continue
}
ls = append(ls, l)
}
if len(ls) == 0 {
log.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
log.Warn("配置文件中没有有效的标签配置,运行器可能无法接取作业")
}
r.Labels = ls
return StageWaitingForRegistration
@ -178,7 +196,8 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
}
if validateLabels(r.Labels) != nil {
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest)")
log.Infoln("无效的标签, 请重新输入, 留空以使用默认标签 (例如, debian-latest:lcr.loongnix.cn/library/debian:latest) ")
r.Labels = nil
return StageInputLabels
}
return StageWaitingForRegistration
@ -186,36 +205,54 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
return StageUnknown
}
func registerInteractive(ctx context.Context, configFile string) error {
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
inputs = new(registerInputs)
)
cfg, err := config.LoadDefault(configFile)
if err != nil {
return fmt.Errorf("failed to load config: %v", err)
return fmt.Errorf("加载配置失败: %v", err)
}
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
stage = StageOverwriteLocalConfig
}
inputs := initInputs(regArgs)
for {
printStageHelp(stage)
cmdString, err := reader.ReadString('\n')
if err != nil {
return err
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("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
log.Infof("注册运行器,名称=%s, 实例=%s, 标签=%v", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("Failed to register runner: %w", err)
return fmt.Errorf("注册运行器失败: %w", err)
}
log.Infof("Runner registered successfully.")
log.Infof("运行器注册成功。")
return nil
}
@ -224,7 +261,7 @@ func registerInteractive(ctx context.Context, configFile string) error {
}
if stage <= StageUnknown {
log.Errorf("Invalid input, please re-run act command.")
log.Errorf("无效输入,请重新运行命令。")
return nil
}
}
@ -233,18 +270,18 @@ func registerInteractive(ctx context.Context, configFile string) error {
func printStageHelp(stage registerStage) {
switch stage {
case StageOverwriteLocalConfig:
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
log.Infoln("运行器已注册,是否覆盖本地配置?[y/N]")
case StageInputInstance:
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
log.Infoln("请输入 Gitea 实例 URL(例如, https://gitea.com/):")
case StageInputToken:
log.Infoln("Enter the runner token:")
log.Infoln("请输入运行器令牌:")
case StageInputRunnerName:
hostname, _ := os.Hostname()
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
log.Infof("请输入运行器名称(如果留空,使用主机名:%s):\n", hostname)
case StageInputLabels:
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest):")
log.Infoln("请输入运行器标签, 留空以使用默认标签(逗号分隔, 例如, debian-latest:docker://lcr.loongnix.cn/library/debian:latest):")
case StageWaitingForRegistration:
log.Infoln("Waiting for registration...")
log.Infoln("等待注册...")
}
}
@ -253,42 +290,35 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
if err != nil {
return err
}
inputs := &registerInputs{
InstanceAddr: regArgs.InstanceAddr,
Token: regArgs.Token,
RunnerName: regArgs.RunnerName,
Labels: defaultLabels,
}
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
// command line flag.
if regArgs.Labels != "" {
inputs.Labels = strings.Split(regArgs.Labels, ",")
}
// specify labels in config file.
inputs := initInputs(regArgs)
// 配置文件中指定的标签。
if len(cfg.Runner.Labels) > 0 {
if regArgs.Labels != "" {
log.Warn("Labels from command will be ignored, use labels defined in config file.")
log.Warn("命令行中的标签将被忽略,使用配置文件中定义的标签。")
}
inputs.Labels = cfg.Runner.Labels
}
if len(inputs.Labels) == 0 {
inputs.Labels = defaultLabels
}
if inputs.RunnerName == "" {
inputs.RunnerName, _ = os.Hostname()
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
log.Infof("运行器名称为空,使用主机名 '%s'", inputs.RunnerName)
}
if err := inputs.validate(); err != nil {
log.WithError(err).Errorf("Invalid input, please re-run act command.")
return nil
log.WithError(err).Errorf("无效输入,请重新运行命令。")
return err
}
if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("Failed to register runner: %w", err)
return fmt.Errorf("注册运行器失败: %w", err)
}
log.Infof("Runner registered successfully.")
log.Infof("运行器注册成功。")
return nil
}
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
// initial http client
// 初始化 http 客户端
cli := client.New(
inputs.InstanceAddr,
cfg.Runner.Insecure,
@ -311,20 +341,21 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
}
if err != nil {
log.WithError(err).
Errorln("Cannot ping the Gitea instance server")
// TODO: if ping failed, retry or exit
Errorln("无法 ping Gitea 实例服务器")
// TODO: 如果 ping 失败,重试或退出
time.Sleep(time.Second)
} else {
log.Debugln("Successfully pinged the Gitea instance server")
log.Debugln("成功 ping 到 Gitea 实例服务器")
break
}
}
reg := &config.Registration{
Name: inputs.RunnerName,
Token: inputs.Token,
Address: inputs.InstanceAddr,
Labels: inputs.Labels,
Name: inputs.RunnerName,
Token: inputs.Token,
Address: inputs.InstanceAddr,
Labels: inputs.Labels,
Ephemeral: inputs.Ephemeral,
}
ls := make([]string, len(reg.Labels))
@ -332,16 +363,17 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
l, _ := labels.Parse(v)
ls[i] = l.Name
}
// register new runner.
// 注册新的运行器。
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
Name: reg.Name,
Token: reg.Token,
Version: ver.Version(),
AgentLabels: ls, // Could be removed after Gitea 1.20
AgentLabels: ls, // Gitea 1.20 之后可能会被移除
Labels: ls,
Ephemeral: reg.Ephemeral,
}))
if err != nil {
log.WithError(err).Error("poller: cannot register new runner")
log.WithError(err).Error("poller: 无法注册新运行器")
return err
}
@ -350,8 +382,13 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
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("failed to save runner config: %w", err)
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, "不支持的标签: 无效")
}

View File

@ -15,26 +15,29 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"gitea.com/gitea/act_runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"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
runner *run.Runner
cfg *config.Config
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
client client.Client // Gitea 客户端,用于与服务器通信
runner *run.Runner // 任务执行器
cfg *config.Config // 配置信息
pollingCtx context.Context
shutdownPolling context.CancelFunc
tasksVersion atomic.Int64 // 任务版本号,用于增量同步
jobsCtx context.Context
shutdownJobs context.CancelFunc
pollingCtx context.Context // 轮询上下文
shutdownPolling context.CancelFunc // 轮询关闭函数
done chan struct{}
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())
@ -57,6 +60,8 @@ func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
}
}
// Poll 持续轮询模式
// 启动多个 goroutine 来并发地轮询任务。每个 goroutine 调用 poll 方法进行实际的工作。在所有工作完成之后,通过关闭 done 通道发出信号。
func (p *Poller) Poll() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
wg := &sync.WaitGroup{}
@ -66,45 +71,75 @@ func (p *Poller) Poll() {
}
wg.Wait()
// signal that we shutdown
// 发出我们正在关闭的信号
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 {
// graceful shutdown completed succesfully
// 优雅地完成关闭
case <-p.done:
return nil
// our timeout for shutting down ran out
// 关闭超时
case <-ctx.Done():
// when both the timeout fires and the graceful shutdown
// completed succsfully, this branch of the select may
// fire. Do a non-blocking check here against the graceful
// shutdown status to avoid sending an error if we don't need to.
// 当超时和优雅关闭同时发生时,
// 这个分支可能会被触发。这里进行非阻塞检查,
// 避免在不必要的情况下发送错误。
_, ok := <-p.done
if !ok {
return nil
}
// force a shutdown of all running jobs
// 强制关闭所有运行中的任务
p.shutdownJobs()
// wait for running jobs to report their status to Gitea
// 等待运行中的任务向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("limiter wait failed")
log.WithError(err).Debug("速率限制等待失败")
}
return
}
@ -114,27 +149,34 @@ func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
}
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("panic in runTaskWithRecover")
log.WithError(err).Error("runTaskWithRecover中发生panic")
}
}()
if err := p.runner.Run(ctx, task); err != nil {
log.WithError(err).Error("failed to run task")
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()
// Load the version value that was in the cache when the request was sent.
// 加载发送请求时缓存中的版本值
v := p.tasksVersion.Load()
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: v,
@ -143,7 +185,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
err = nil
}
if err != nil {
log.WithError(err).Error("failed to fetch task")
log.WithError(err).Error("获取任务失败")
return nil, false
}
@ -159,7 +201,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
return nil, false
}
// got a task, set `tasksVersion` to zero to focre query db in next request.
// 收到一个任务,将`tasksVersion`设置为零,以便在下一个请求中创建查询数据库。
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
return resp.Msg.Task, true

View File

@ -9,12 +9,11 @@ import (
log "github.com/sirupsen/logrus"
)
// NullLogger is used to create a new JobLogger to discard logs. This
// will prevent these logs from being logged to the stdout, but
// forward them to the Reporter via its hook.
// NullLogger用于创建一个新的JobLogger以丢弃日志。
// 这将防止这些日志被记录到标准输出,但会通过其钩子将它们转发给Reporter。
type NullLogger struct{}
// WithJobLogger creates a new logrus.Logger that will discard all logs.
// WithJobLogger 创建一个新的 logrus.Logger,它将丢弃所有日志。
func (n NullLogger) WithJobLogger() *log.Logger {
logger := log.New()
logger.SetOutput(io.Discard)

View File

@ -21,26 +21,28 @@ import (
"github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/report"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"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 runs the pipeline.
// Runner 运行流水线
type Runner struct {
name string
name string // Runner 名称
cfg *config.Config
cfg *config.Config // 配置信息
client client.Client
labels labels.Labels
envs map[string]string
client client.Client // Gitea 客户端
labels labels.Labels // 标签集合
envs map[string]string // 环境变量
runningTasks sync.Map
// 正在运行的任务
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 {
@ -52,6 +54,7 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
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
@ -63,20 +66,20 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
log.StandardLogger().WithField("module", "cache_request"),
)
if err != nil {
log.Errorf("cannot init cache server, it will be disabled: %v", err)
// go on
log.Errorf("无法初始化缓存服务器,将禁用它:%v", err)
// 继续执行
} else {
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
}
}
}
// set artifact gitea api
// 设置 artifact gitea api
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/")
// Set specific environments to distinguish between Gitea and GitHub
// 设置特定环境变量以区分 Gitea GitHub
envs["GITEA_ACTIONS"] = "true"
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
@ -89,13 +92,16 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
}
}
// Run 在给定的上下文中执行任务,确保同一时间仅运行一个任务
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
// 检查任务是否已在运行
if _, ok := r.runningTasks.Load(task.Id); ok {
return fmt.Errorf("task %d is already running", task.Id)
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)
@ -113,6 +119,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
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 {
@ -120,7 +127,8 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
}()
reporter.Logf("%s(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
// 记录任务接收日志
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 {
@ -136,10 +144,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
taskContext := task.Context.Fields
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
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(),
@ -157,15 +166,22 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
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 == "" {
// use task token to action api token for previous Gitea Server Versions
// 兼容旧版本 Gitea Server
giteaRuntimeToken = preset.Token
}
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
@ -180,9 +196,8 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
maxLifetime = time.Until(deadline)
}
// 创建 Runner 配置
runnerConfig := &runner.Config{
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
BindWorkdir: false,
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
@ -218,9 +233,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
executor := rr.NewPlanExecutor(plan)
reporter.Logf("workflow prepared")
reporter.Logf("工作流程已准备就绪")
// add logger recorders
// 添加日志记录器
ctx = common.WithLoggerHook(ctx, reporter)
if !log.IsLevelEnabled(log.DebugLevel) {

View File

@ -22,7 +22,7 @@ func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
jobIDs := workflow.GetJobIDs()
if len(jobIDs) != 1 {
return nil, "", fmt.Errorf("multiple jobs found: %v", jobIDs)
return nil, "", fmt.Errorf("找到多个工作: %v", jobIDs)
}
jobID := jobIDs[0]

View File

@ -12,7 +12,7 @@ import (
"gotest.tools/v3/assert"
)
func Test_generateWorkflow(t *testing.T) {
func Test_生成工作流(t *testing.T) {
type args struct {
task *runnerv1.Task
}
@ -24,32 +24,32 @@ func Test_generateWorkflow(t *testing.T) {
wantErr bool
}{
{
name: "has needs",
name: "有需求",
args: args{
task: &runnerv1.Task{
WorkflowPayload: []byte(`
name: Build and deploy
name: 构建部署测试
on: push
jobs:
job9:
needs: build
runs-on: ubuntu-latest
runs-on: linux-loong64
steps:
- uses: actions/checkout@v3
- 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": "output1 value",
"output1": "输出1值",
},
Result: runnerv1.Result_RESULT_SUCCESS,
},
"job2": {
Outputs: map[string]string{
"output2": "output2 value",
"output2": "输出2值",
},
Result: runnerv1.Result_RESULT_SUCCESS,
},

View File

@ -6,6 +6,6 @@ package client
const (
UUIDHeader = "x-runner-uuid"
TokenHeader = "x-runner-token"
// Deprecated: could be removed after Gitea 1.20 released
// 已弃用: 可以在Gitea 1.20发布后删除
VersionHeader = "x-runner-version"
)

View File

@ -27,7 +27,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client {
return http.DefaultClient
}
// New returns a new runner client.
// New返回一个新的runner客户端。
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
@ -39,7 +39,7 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
if token != "" {
req.Header().Set(TokenHeader, token)
}
// TODO: version will be removed from request header after Gitea 1.20 released.
// TODOversion将在Gitea 1.20发布后从请求标头中删除。
if version != "" {
req.Header().Set(VersionHeader, version)
}
@ -73,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

@ -1,101 +1,101 @@
# Example configuration file, it's safe to copy this as the default config file without any modification.
# 示例配置文件,可以安全地将其复制为默认配置文件而无需任何修改。
# You don't have to copy this file to your instance,
# just run `./act_runner generate-config > config.yaml` to generate a config file.
# 您不必将此文件复制到您的实例,
# 只需运行 `./act_runner generate-config > config.yaml` 来生成配置文件。
log:
# The level of logging, can be trace, debug, info, warn, error, fatal
# 日志级别,可以是 tracedebuginfowarnerrorfatal
level: info
runner:
# Where to store the registration result.
# 存储注册结果的路径。
file: .runner
# Execute how many tasks concurrently at the same time.
# 同时执行多少个任务。
capacity: 1
# Extra environment variables to run jobs.
# 运行作业的额外环境变量。
envs:
A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2
# Extra environment variables to run jobs from a file.
# It will be ignored if it's empty or the file doesn't exist.
# 从文件中运行作业的额外环境变量。
# 如果为空或文件不存在,则会被忽略。
env_file: .env
# The timeout for a job to be finished.
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
# 作业完成的超时时间。
# 请注意,Gitea 实例也有一个作业超时时间(默认为 3 小时)。
# 如果这个超时时间比 Gitea 实例的超时时间短,作业可能会被 Gitea 实例停止。
timeout: 3h
# The timeout for the runner to wait for running jobs to finish when shutting down.
# Any running jobs that haven't finished after this timeout will be cancelled.
# 运行器在关闭时等待运行作业完成的超时时间。
# 在这个超时时间之后仍未完成的任何运行作业将被取消。
shutdown_timeout: 0s
# Whether skip verifying the TLS certificate of the Gitea instance.
# 是否跳过验证 Gitea 实例的 TLS 证书。
insecure: false
# The timeout for fetching the job from the Gitea instance.
# 从 Gitea 实例获取作业的超时时间。
fetch_timeout: 5s
# The interval for fetching the job from the Gitea instance.
# 从 Gitea 实例获取作业的时间间隔。
fetch_interval: 2s
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
# Like: "macos-arm64:host" or "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
# If it's empty when registering, it will ask for inputting labels.
# If it's empty when execute `daemon`, will use labels in `.runner` file.
# 运行器的标签用于确定运行器可以运行哪些作业以及如何运行它们。
# 例如:"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:
- "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
- "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
- "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"
- "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:
# Enable cache server to use actions/cache.
# 启用缓存服务器以使用 actions/cache
enabled: true
# The directory to store the cache data.
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
# 存储缓存数据的目录。
# 如果为空,缓存数据将存储在 $HOME/.cache/actcache
dir: ""
# The host of the cache server.
# It's not for the address to listen, but the address to connect from job containers.
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
# 缓存服务器的主机。
# 它不是用于监听的地址,而是用于作业容器连接的地址。
# 所以 0.0.0.0 是一个糟糕的选择,留空以自动检测。
host: ""
# The port of the cache server.
# 0 means to use a random available port.
# 缓存服务器的端口。
# 0 表示使用随机可用端口。
port: 0
# The external cache server URL. Valid only when enable is true.
# If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
# The URL should generally end with "/".
# 外部缓存服务器 URL。仅在启用时有效。
# 如果指定了它,act_runner 将使用此 URL 作为 ACTIONS_CACHE_URL 而不是自己启动一个服务器。
# URL 通常应该以 "/" 结尾。
external_server: ""
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.
# 指定容器将连接的网络。
# 可以是 hostbridge 或自定义网络的名称。
# 如果为空,act_runner 将自动创建一个网络。
network: ""
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
# 启动任务容器时是否使用特权模式(特权模式对于 Docker-in-Docker 是必需的)。
privileged: false
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
# 容器启动时使用的其他选项(例如,--add-host=my.gitea.url:host-gateway)。
options:
# The parent directory of a job's working directory.
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
# If the path starts with '/', the '/' will be trimmed.
# For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
# If it's empty, /workspace will be used.
# 作业工作目录的父目录。
# 注意:不需要在路径前添加第一个 '/',因为 act_runner 会自动添加。
# 如果路径以 '/' 开头,'/' 将被修剪。
# 例如,如果父目录是 /path/to/my/dirworkdir_parent 应该是 path/to/my/dir
# 如果为空,将使用 /workspace。
workdir_parent:
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
# 可以挂载到容器的卷(包括绑定挂载)。支持 glob 语法,参见 https://github.com/gobwas/glob
# 您可以指定多个卷。如果序列为空,则不能挂载任何卷。
# 例如,如果您只允许容器挂载 `data` 卷和 `/src` 中的所有 json 文件,您应该将配置更改为:
# valid_volumes:
# - data
# - /src/*.json
# If you want to allow any volume, please use the following configuration:
# 如果您想允许任何卷,请使用以下配置:
# valid_volumes:
# - '**'
# - '**'
valid_volumes: []
# overrides the docker client host with the specified one.
# If it's empty, act_runner will find an available docker host automatically.
# If it's "-", 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 it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
# 用指定的主机覆盖 docker 客户端主机。
# 如果为空,act_runner 将自动查找可用的 docker 主机。
# 如果是 "-"act_runner 将自动查找可用的 docker 主机,但 docker 主机不会挂载到作业容器和服务容器。
# 如果不为空或 "-",将使用指定的 docker 主机。如果不起作用,将返回错误。
docker_host: ""
# Pull docker image(s) even if already present
# 即使已经存在也拉取 Docker 镜像
force_pull: true
# Rebuild docker image(s) even if already present
# 即使已经存在也重建 Docker 镜像
force_rebuild: false
host:
# The parent directory of a job's working directory.
# If it's empty, $HOME/.cache/act/ will be used.
workdir_parent:
# 作业工作目录的父目录。
# 如果为空,将使用 $HOME/.cache/act/
workdir_parent:

View File

@ -14,72 +14,72 @@ import (
"gopkg.in/yaml.v3"
)
// Log represents the configuration for logging.
// Log 代表日志的配置。
type Log struct {
Level string `yaml:"level"` // Level indicates the logging level.
Level string `yaml:"level"` // Level 表示日志级别。
}
// Runner represents the configuration for the runner.
// Runner 代表运行器的配置。
type Runner struct {
File string `yaml:"file"` // File specifies the file path for the runner.
Capacity int `yaml:"capacity"` // Capacity specifies the capacity of the runner.
Envs map[string]string `yaml:"envs"` // Envs stores environment variables for the runner.
EnvFile string `yaml:"env_file"` // EnvFile specifies the path to the file containing environment variables for the runner.
Timeout time.Duration `yaml:"timeout"` // Timeout specifies the duration for runner timeout.
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout specifies the duration to wait for running jobs to complete during a shutdown of the runner.
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
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 represents the configuration for caching.
// Cache 代表缓存的配置。
type Cache struct {
Enabled *bool `yaml:"enabled"` // Enabled indicates whether caching is enabled. It is a pointer to distinguish between false and not set. If not set, it will be true.
Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
Host string `yaml:"host"` // Host specifies the caching host.
Port uint16 `yaml:"port"` // Port specifies the caching port.
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
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 represents the configuration for the container.
// Container 代表容器的配置。
type Container struct {
Network string `yaml:"network"` // Network specifies the network for the container.
NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
Options string `yaml:"options"` // Options specifies additional options for the container.
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers.
DockerHost string `yaml:"docker_host"` // DockerHost specifies the Docker host. It overrides the value specified in environment variable DOCKER_HOST.
ForcePull bool `yaml:"force_pull"` // Pull docker image(s) even if already present
ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present
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 represents the configuration for the host.
// Host 代表主机的配置。
type Host struct {
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the host's working directory.
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定主机工作目录的父目录。
}
// Config represents the overall configuration.
// Config 代表整体配置。
type Config struct {
Log Log `yaml:"log"` // Log represents the configuration for logging.
Runner Runner `yaml:"runner"` // Runner represents the configuration for the runner.
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
Container Container `yaml:"container"` // Container represents the configuration for the container.
Host Host `yaml:"host"` // Host represents the configuration for the host.
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 returns the default configuration.
// If file is not empty, it will be used to load the configuration.
// LoadDefault 返回默认配置。
// 如果文件不为空,它将被用来加载配置。
func LoadDefault(file string) (*Config, error) {
cfg := &Config{}
if file != "" {
content, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("open config file %q: %w", file, err)
return nil, fmt.Errorf("打开配置文件 %q: %w", file, err)
}
if err := yaml.Unmarshal(content, cfg); err != nil {
return nil, fmt.Errorf("parse config file %q: %w", file, err)
return nil, fmt.Errorf("解析配置文件 %q: %w", file, err)
}
}
compatibleWithOldEnvs(file != "", cfg)
@ -88,7 +88,7 @@ func LoadDefault(file string) (*Config, error) {
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("read env file %q: %w", cfg.Runner.EnvFile, err)
return nil, fmt.Errorf("读取环境文件 %q: %w", cfg.Runner.EnvFile, err)
}
if cfg.Runner.Envs == nil {
cfg.Runner.Envs = map[string]string{}
@ -135,14 +135,13 @@ func LoadDefault(file string) (*Config, error) {
cfg.Runner.FetchInterval = 2 * time.Second
}
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
// 虽然 `container.network_mode` 将被弃用,但我们现在必须与它兼容。
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
log.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
log.Warn("您正在尝试使用已弃用的配置项 `container.network_mode`,请使用 `container.network` 替代。")
if cfg.Container.NetworkMode == "bridge" {
// Previously, if the value of `container.network_mode` is `bridge`, we will create a new network for job.
// But “bridge” is easily confused with the bridge network created by Docker by default.
// So we set the value of `container.network` to empty string to make `act_runner` automatically create a new network for job.
cfg.Container.Network = ""
// 以前,如果 `container.network_mode` 的值是 `bridge`,我们会为作业创建一个新的网络。
// 但是,“bridge”很容易与 Docker 默认创建的桥接网络混淆。
// 所以我们将 `container.network` 的值设置为空字符串,以使 `act_runner` 为作业自动创建一个新的网络。
} else {
cfg.Container.Network = cfg.Container.NetworkMode
}

View File

@ -11,16 +11,16 @@ import (
log "github.com/sirupsen/logrus"
)
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
// Be compatible with old envs.
// 已弃用:未来可能会移除。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("env %s has been ignored because config file is used", key)
log.Warnf("环境 %s 已被忽略,因为使用了配置文件", key)
return "", false
}
log.Warnf("env %s will be deprecated, please use config file instead", key)
log.Warnf("环境变量 %s 将被弃用,请改用配置文件。", key)
return v, true
}
return "", false

View File

@ -8,18 +8,19 @@ import (
"os"
)
const registrationWarning = "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner."
const registrationWarning = "此文件由`Loong Runner`自动生成。除非你知道自己在做什么, 否则不要手动编辑它。删除此文件将导致`Loong Runner`重新注册为新的运行器。"
// Registration is the registration information for a runner
// Registration 表示运行器的注册信息
type Registration struct {
Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
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"`
Labels []string `json:"labels"`
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) {

View File

@ -1,5 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package envcheck provides a simple way to check if the environment is ready to run jobs.
// envcheck包中提供了一种简单的方法, 用来检查环境是否准备好执行工作。
package envcheck

View File

@ -27,7 +27,7 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
_, err = cli.Ping(ctx)
if err != nil {
return fmt.Errorf("cannot ping the docker daemon, is it running? %w", err)
return fmt.Errorf("无法ping通docker守护进程, 它是否在运行? %w", err)
}
return nil

View File

@ -9,7 +9,9 @@ import (
)
const (
SchemeHost = "host"
// SchemeHost 表示主机模式
SchemeHost = "host"
// SchemeDocker 表示 Docker 模式
SchemeDocker = "docker"
)
@ -19,6 +21,7 @@ type Label struct {
Arg string
}
// Parse 解析标签字符串并返回 Label 结构体。
func Parse(str string) (*Label, error) {
splits := strings.SplitN(str, ":", 3)
label := &Label{
@ -33,13 +36,14 @@ func Parse(str string) (*Label, error) {
label.Arg = splits[2]
}
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
return nil, fmt.Errorf("unsupported schema: %s", label.Schema)
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 {
@ -49,17 +53,18 @@ func (l Labels) RequireDocker() bool {
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:
// "//" will be ignored
// 忽略 "//"
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
case SchemeHost:
platforms[label.Name] = "-self-hosted"
default:
// It should not happen, because Parse has checked it.
// 这不应该发生,因为 Parse 已经检查过了。
continue
}
}
@ -69,19 +74,20 @@ func (l Labels) PickPlatform(runsOn []string) string {
}
}
// TODO: support multiple labels
// like:
// ["ubuntu-22.04"] => "ubuntu:22.04"
// TODO: 支持多个标签
// 例如:
// ["debian-12"] => "debian:12"
// ["with-gpu"] => "linux:with-gpu"
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
// ["debian-12", "with-gpu"] => "debian:12_with-gpu"
// return default.
// So the runner receives a task with a label that the runner doesn't have,
// it happens when the user have edited the label of the runner in the web UI.
// TODO: it may be not correct, what if the runner is used as host mode only?
return "gitea/runner-images:ubuntu-latest"
// 返回默认值。
// 因此,当运行器收到一个它没有的标签的任务时,
// 这发生在用户在 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 {
@ -90,6 +96,7 @@ func (l Labels) Names() []string {
return names
}
// ToStrings 将 Labels 转换为字符串数组。
func (l Labels) ToStrings() []string {
ls := make([]string, 0, len(l))
for _, label := range l {

View File

@ -10,41 +10,41 @@ import (
"gotest.tools/v3/assert"
)
func TestParse(t *testing.T) {
func Test解析(t *testing.T) {
tests := []struct {
args string
want *Label
wantErr bool
}{
{
args: "ubuntu:docker://node:18",
args: "debian:docker://node:18",
want: &Label{
Name: "ubuntu",
Name: "debian",
Schema: "docker",
Arg: "//node:18",
},
wantErr: false,
},
{
args: "ubuntu:host",
args: "aosc:host",
want: &Label{
Name: "ubuntu",
Name: "aosc",
Schema: "host",
Arg: "",
},
wantErr: false,
},
{
args: "ubuntu",
args: "aosc",
want: &Label{
Name: "ubuntu",
Name: "aosc",
Schema: "host",
Arg: "",
},
wantErr: false,
},
{
args: "ubuntu:vm:ubuntu-18.04",
args: "debian:vm:debian12",
want: nil,
wantErr: true,
},

View File

@ -1,5 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Reporter 组件是负责任务执行状态报告和日志记录的核心模块
package report
@ -18,30 +19,30 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"gitea.com/gitea/act_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
)
type Reporter struct {
ctx context.Context
cancel context.CancelFunc
ctx context.Context // 上下文,用于控制生命周期
cancel context.CancelFunc // 取消函数
closed bool // 是否已关闭
client client.Client // Gitea 客户端
clientM sync.Mutex // 客户端访问互斥锁
closed bool
client client.Client
clientM sync.Mutex
logOffset int // 日志偏移量
logRows []*runnerv1.LogRow // 日志行缓存
logReplacer *strings.Replacer // 日志内容替换器
oldnew []string // 需要替换的敏感信息
logOffset int
logRows []*runnerv1.LogRow
logReplacer *strings.Replacer
oldnew []string
state *runnerv1.TaskState // 任务状态
stateMu sync.RWMutex // 状态访问读写锁
outputs sync.Map // 输出参数存储
state *runnerv1.TaskState
stateMu sync.RWMutex
outputs sync.Map
debugOutputEnabled bool
stopCommandEndToken string
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 != "" {
@ -72,6 +73,7 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
return rv
}
// ResetSteps 重置任务状态中的步骤状态数组,为即将开始的新任务做准备
func (r *Reporter) ResetSteps(l int) {
r.stateMu.Lock()
defer r.stateMu.Unlock()
@ -82,10 +84,12 @@ func (r *Reporter) ResetSteps(l int) {
}
}
// 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)
@ -93,6 +97,7 @@ func appendIfNotNil[T any](s []*T, v *T) []*T {
return s
}
// 接收日志条目,处理日志内容,并更新任务状态。
func (r *Reporter) Fire(entry *log.Entry) error {
r.stateMu.Lock()
defer r.stateMu.Unlock()
@ -169,6 +174,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
return nil
}
// RunDaemon 定时运行后台任务,定期将缓存中的日志和状态上报给 Gitea
func (r *Reporter) RunDaemon() {
if r.closed {
return
@ -183,6 +189,7 @@ func (r *Reporter) RunDaemon() {
time.AfterFunc(time.Second, r.RunDaemon)
}
// 自定义日志记录方法,格式化输出并保存到 logRows 中
func (r *Reporter) Logf(format string, a ...interface{}) {
r.stateMu.Lock()
defer r.stateMu.Unlock()
@ -190,6 +197,7 @@ func (r *Reporter) Logf(format string, a ...interface{}) {
r.logf(format, a...)
}
// 内部使用的日志记录方法,仅在任务执行期间记录日志
func (r *Reporter) logf(format string, a ...interface{}) {
if !r.duringSteps() {
r.logRows = append(r.logRows, &runnerv1.LogRow{
@ -199,18 +207,19 @@ func (r *Reporter) logf(format string, a ...interface{}) {
}
}
// 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("ignore output because the key is too long: %q", k)
r.logf("忽略超长键值对的键: %q", k)
continue
}
if l := len(v); l > 1024*1024 {
log.Println("ignore output because the value is too long:", k, l)
r.logf("ignore output because the value %q is too long: %d", k, l)
log.Println("忽略超长键值对的值:", k, l)
r.logf("忽略超长键值对的值 %q: %d 字节", k, l)
}
if _, ok := r.outputs.Load(k); ok {
continue
@ -219,6 +228,7 @@ func (r *Reporter) SetOutputs(outputs map[string]string) {
}
}
// Close 关闭 reporter,确保最后一批日志和最终状态被上报
func (r *Reporter) Close(lastWords string) error {
r.closed = true
@ -254,6 +264,7 @@ func (r *Reporter) Close(lastWords string) error {
}, retry.Context(r.ctx))
}
// ReportLog 将当前缓存的日志批量上报到 Gitea
func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock()
defer r.clientM.Unlock()
@ -274,7 +285,7 @@ func (r *Reporter) ReportLog(noMore bool) error {
ack := int(resp.Msg.AckIndex)
if ack < r.logOffset {
return fmt.Errorf("submitted logs are lost")
return fmt.Errorf("已提交的日志丢失了")
}
r.stateMu.Lock()
@ -283,12 +294,13 @@ func (r *Reporter) ReportLog(noMore bool) error {
r.stateMu.Unlock()
if noMore && ack < r.logOffset+len(rows) {
return fmt.Errorf("not all logs are submitted")
return fmt.Errorf("并非所有日志都已提交")
}
return nil
}
// ReportState 将当前任务状态上报到 Gitea
func (r *Reporter) ReportState() error {
r.clientM.Lock()
defer r.clientM.Unlock()
@ -329,12 +341,13 @@ func (r *Reporter) ReportState() error {
return true
})
if len(noSent) > 0 {
return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
return fmt.Errorf("仍有一些输出尚未发送: %v", noSent)
}
return nil
}
// duringSteps 判断当前是否正在执行某个步骤
func (r *Reporter) duringSteps() bool {
if steps := r.state.Steps; len(steps) == 0 {
return false
@ -353,11 +366,12 @@ var stringToResult = map[string]runnerv1.Result{
"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 { // for jobResult
if v, ok := result.(string); ok { // 对于作业结果
str = v
} else if v, ok := result.(fmt.Stringer); ok { // for stepResult
} else if v, ok := result.(fmt.Stringer); ok { // 对于步骤结果
str = v.String()
}
@ -365,8 +379,10 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
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
@ -383,20 +399,19 @@ func (r *Reporter) handleCommand(originalContent, command, parameters, value str
return nil
case "notice":
// Not implemented yet, so just return the original content.
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "warning":
// Not implemented yet, so just return the original content.
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "error":
// Not implemented yet, so just return the original content.
// 尚未实现,因此仅返回原始内容。
return &originalContent
case "group":
// Returning the original content, because I think the frontend
// will use it when rendering the output.
// 返回原始内容,因为我认为前端在渲染输出时会使用它。
return &originalContent
case "endgroup":
// Ditto
// 同上
return &originalContent
case "stop-commands":
r.stopCommandEndToken = value
@ -408,6 +423,7 @@ func (r *Reporter) handleCommand(originalContent, command, parameters, value str
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' })
@ -428,6 +444,7 @@ func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
}
}
// addMask 将指定字符串加入脱敏列表,之后所有日志中出现该字符串都会被替换成 ***
func (r *Reporter) addMask(msg string) {
r.oldnew = append(r.oldnew, msg, "***")
r.logReplacer = strings.NewReplacer(r.oldnew...)

View File

@ -16,125 +16,136 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks"
)
func TestReporter_parseLogRow(t *testing.T) {
func Test记录器_解析输出日志(t *testing.T) {
tests := []struct {
name string
debugOutputEnabled bool
args []string
want []string
name string // 测试用例名称
debugOutputEnabled bool // 是否启用调试输出
args []string // 输入的日志行
want []string // 期望的输出结果
}{
{
"No command", false,
[]string{"Hello, world!"},
[]string{"Hello, world!"},
name: "无命令",
debugOutputEnabled: false,
args: []string{"你好,世界!"},
want: []string{"你好,世界!"},
},
{
"Add-mask", false,
[]string{
"foo mysecret bar",
"::add-mask::mysecret",
"foo mysecret bar",
name: "添加掩码",
debugOutputEnabled: false,
args: []string{
"foo 我的密钥 bar", // 输入日志:普通日志行
"::add-mask::我的密钥", // 输入命令:添加掩码
"foo 我的密钥 bar", // 输入日志:再次普通日志行
},
[]string{
"foo mysecret bar",
"<nil>",
"foo *** bar",
want: []string{
"foo 我的密钥 bar", // 原始日志直接输出
"<nil>", // 添加掩码命令处理结果(无输出内容)
"foo *** bar", // 掩码替换后的日志行
},
},
{
"Debug enabled", true,
[]string{
"::debug::GitHub Actions runtime token access controls",
name: "启用调试",
debugOutputEnabled: true,
args: []string{
"::debug::GitHub Actions 运行时令牌访问控制",
},
[]string{
"GitHub Actions runtime token access controls",
want: []string{
"GitHub Actions 运行时令牌访问控制", // 调试信息直接输出
},
},
{
"Debug not enabled", false,
[]string{
"::debug::GitHub Actions runtime token access controls",
name: "禁用调试",
debugOutputEnabled: false,
args: []string{
"::debug::GitHub Actions 运行时令牌访问控制",
},
[]string{
"<nil>",
want: []string{
"<nil>", // 调试信息被忽略
},
},
{
"notice", false,
[]string{
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
name: "通知",
debugOutputEnabled: false,
args: []string{
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
[]string{
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
want: []string{
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 通知日志原样输出
},
},
{
"warning", false,
[]string{
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
name: "警告",
debugOutputEnabled: false,
args: []string{
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
[]string{
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
want: []string{
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 警告日志原样输出
},
},
{
"error", false,
[]string{
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
name: "错误",
debugOutputEnabled: false,
args: []string{
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
},
[]string{
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
want: []string{
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 错误日志原样输出
},
},
{
"group", false,
[]string{
"::group::",
"::endgroup::",
name: "分组",
debugOutputEnabled: false,
args: []string{
"::group::", // 开始分组
"::endgroup::", // 结束分组
},
[]string{
"::group::",
"::endgroup::",
want: []string{
"::group::", // 分组开始标记原样输出
"::endgroup::", // 分组结束标记原样输出
},
},
{
"stop-commands", false,
[]string{
"::add-mask::foo",
"::stop-commands::myverycoolstoptoken",
"::add-mask::bar",
"::debug::Stuff",
"myverycoolstoptoken",
"::add-mask::baz",
"::myverycoolstoptoken::",
"::add-mask::wibble",
"foo bar baz wibble",
name: "停止命令",
debugOutputEnabled: false,
args: []string{
"::add-mask::foo", // 添加掩码命令
"::stop-commands::我的停止令牌", // 停止命令标记
"::add-mask::bar", // 被忽略的添加掩码命令
"::debug::调试信息", // 被忽略的调试信息
"我的停止令牌", // 停止命令标记结束
"::add-mask::baz", // 恢复处理的添加掩码命令
"::我的停止令牌::", // 另一种停止命令标记
"::add-mask::wibble", // 被忽略的添加掩码命令
"foo bar baz wibble", // 普通日志行
},
[]string{
"<nil>",
"<nil>",
"::add-mask::bar",
"::debug::Stuff",
"myverycoolstoptoken",
"::add-mask::baz",
"<nil>",
"<nil>",
"*** bar baz ***",
want: []string{
"<nil>", // 第一个添加掩码命令处理结果
"<nil>", // 停止命令标记处理结果
"::add-mask::bar", // 被忽略的命令原样输出
"::debug::调试信息", // 被忽略的调试信息原样输出
"我的停止令牌", // 停止标记结束原样输出
"::add-mask::baz", // 恢复处理的添加掩码命令
"<nil>", // 无效停止命令标记处理结果
"<nil>", // 被忽略的添加掩码命令处理结果
"*** bar baz ***", // 掩码替换后的日志行
},
},
{
"unknown command", false,
[]string{
"::set-mask::foo",
name: "未知命令",
debugOutputEnabled: false,
args: []string{
"::set-mask::foo", // 未知命令
},
[]string{
"::set-mask::foo",
want: []string{
"::set-mask::foo", // 未知命令原样输出
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Reporter{
@ -155,43 +166,58 @@ func TestReporter_parseLogRow(t *testing.T) {
}
}
func TestReporter_Fire(t *testing.T) {
t.Run("ignore command lines", func(t *testing.T) {
// 测试 ReporterFire 方法(验证命令行忽略逻辑)
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("Received UpdateLog: %s", req.Msg.String())
t.Logf("收到 UpdateLog 请求:%s", req.Msg.String()) // 记录日志请求内容
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
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("Received UpdateTask: %s", req.Msg.String())
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{}{})
taskCtx, err := structpb.NewStruct(map[string]interface{}{}) // 创建空任务上下文
require.NoError(t, err)
// 创建 Reporter 实例
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
Context: taskCtx,
Context: taskCtx, // 注入任务上下文
})
defer func() {
assert.NoError(t, reporter.Close(""))
assert.NoError(t, reporter.Close("")) // 测试结束关闭 Reporter
}()
reporter.ResetSteps(5)
reporter.ResetSteps(5) // 初始化5个步骤的日志存储
// 定义步骤0的日志元数据
dataStep0 := map[string]interface{}{
"stage": "Main",
"stepNumber": 0,
"raw_output": true,
"stage": "Main", // 阶段名称
"stepNumber": 0, // 步骤编号
"raw_output": true, // 启用原始输出模式
}
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
// 发送混合类型的日志条目 ---------------------------------------------------
// 预期:普通日志被记录,调试日志被忽略
assert.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}))
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength)
// 验证结果:步骤0应只有3条普通日志(调试日志被过滤)
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength, "普通日志数量不符预期")
})
}

View File

@ -3,7 +3,7 @@
package ver
// go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
// go build -ldflags "-X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=1.2.3"
var version = "dev"
func Version() string {

View File

@ -8,12 +8,12 @@ import (
"os/signal"
"syscall"
"gitea.com/gitea/act_runner/internal/app/cmd"
"git.whlug.cn/LAA/loong_runner/internal/app/cmd"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// run the command
// 运行命令
cmd.Execute(ctx)
}

View File

@ -1,9 +0,0 @@
#!/usr/bin/env bash
# wait for docker daemon
while ! nc -z localhost 2376 </dev/null; do
echo 'waiting for docker daemon...'
sleep 5
done
. /opt/act/run.sh

View File

@ -1,38 +1,49 @@
#!/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
# In case no token is set, it's possible to read the token from a file, i.e. a Docker Secret
# 如果没有设置令牌,可以从文件中读取令牌,例如从 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
# Use the same ENV variable names as https://github.com/vegardit/docker-gitea-act-runner
test -f "$RUNNER_STATE_FILE" || echo "$RUNNER_STATE_FILE is missing or not a regular file"
# 使用与 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
# The point of this loop is to make it simple, when running both act_runner and gitea in docker,
# for the act_runner to wait a moment for gitea to become available before erroring out. Within
# the context of a single docker-compose, something similar could be done via healthchecks, but
# this is more flexible.
# 此循环的目的是使其简单,当在 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}" \
@ -40,18 +51,20 @@ if [[ ! -s "$RUNNER_STATE_FILE" ]]; then
--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"
echo "成功"
success=1
else
echo "Waiting to retry ..."
echo "等待重试..."
sleep 5
fi
done
fi
# Prevent reading the token from the act_runner process
# 防止从 act_runner 进程中读取令牌
unset GITEA_RUNNER_REGISTRATION_TOKEN
unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE
exec act_runner daemon ${CONFIG_ARG}
# 启动 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

View File

@ -1,17 +0,0 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:dockerd]
command=/usr/local/bin/dockerd-entrypoint.sh
[program:act_runner]
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
command=/opt/act/rootless.sh
[eventlistener:processes]
command=bash -c "echo READY && read line && kill -SIGQUIT $PPID"
events=PROCESS_STATE_STOPPED,PROCESS_STATE_EXITED,PROCESS_STATE_FATAL