9 Commits
main ... main

Author SHA1 Message Date
8920c4a170 Timeout to wait for and optionally require docker always (#741)
* do not require docker cli in the runner image for waiting for a sidecar dockerd
* allow to specify that `:host` labels would also only be accepted if docker is reachable

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/741
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-08-28 17:28:08 +00:00
bbf9d7e90f feat: support github mirror (#716)
ref: https://github.com/go-gitea/gitea/issues/34858

when github_mirror='https://ghfast.top/https://github.com'

it will clone from the github mirror

However, there is an exception: because the cache is hashed using the string, if the same cache has been used before, it will still be pulled from github, only for the newly deployed act_ruuner

In the following two scenarios, it will solve the problem encountered:

1. some github mirror is  https://xxx.com/https://github.com/actions/checkout@v4, it will report error `Expected format {org}/{repo}[/path]@ref. Actual ‘https://xxx.com/https://github.com/actions/checkout@v4’ Input string was not in a correct format`
2. If I use an action that has a dependency on another action, even if I configure the url of the action I want to use, the action that the action introduces will still pull from github.
for example 02882cc2d9/action.yml (L127-L132)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/716
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Akkuman <akkumans@qq.com>
Co-committed-by: Akkuman <akkumans@qq.com>
2025-08-28 16:57:01 +00:00
46f471a900 refac(workflow): use matrix to compile different docker images (#740)
I have made this to speed up and make it more robust.

The matrix executes the jobs in parallel, doing some things perhaps double. But making overall management easier due to the simple defined variables at the top of the matrix declaration.

Co-authored-by: Daan Selen <dselen@systemec.nl>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/740
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Daan Selen <dselen@nerthus.nl>
Co-committed-by: Daan Selen <dselen@nerthus.nl>
2025-08-22 23:49:20 +00:00
aa28f8d99c feat(docker): add TZ env variable working (#738)
Relevant: https://gitea.com/gitea/act_runner/issues/735

See my example below, `edit` is my PR, `non-edit` is the origin/main.
```
dselen@N-DESKTOP1:~/development/act_runner$ docker images
REPOSITORY   TAG        IMAGE ID       CREATED          SIZE
runner       edit       b12322f8c3f0   26 seconds ago   43.5MB
runner       non-edit   e5593ad32c16   34 minutes ago   43.1MB

dselen@N-DESKTOP1:~/development/act_runner$ docker run -d -e TZ=Europe/Amsterdam runner:non-edit
5f26979515f461a2a7e342aa586d7b91224d2d3c3dcf1ed0c1e7293ff00645a4

dselen@N-DESKTOP1:~/development/act_runner$ docker run -d -e TZ=Europe/Amsterdam runner:edit
9cc5fc6b364cf07776d97c6c60c03f23372eb2c93c7da8d3d80f4f6dc2a6b10e

dselen@N-DESKTOP1:~/development/act_runner$ docker ps
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS     NAMES
9cc5fc6b364c   runner:edit       "/sbin/tini -- run.sh"   2 seconds ago   Up 2 seconds             serene_bardeen
5f26979515f4   runner:non-edit   "/sbin/tini -- run.sh"   5 seconds ago   Up 5 seconds             jovial_euler

dselen@N-DESKTOP1:~/development/act_runner$ docker exec -it jovial_euler bash
5f26979515f4:/# date
Thu Aug 21 16:40:35 UTC 2025

dselen@N-DESKTOP1:~/development/act_runner$ docker exec -it serene_bardeen bash
9cc5fc6b364c:/# date
Thu Aug 21 18:40:42 CEST 2025
```

I do not see why this would not be acceptable, its only 400KB

Regards.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/738
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Co-authored-by: DaanSelen <daanselen@noreply.gitea.com>
Co-committed-by: DaanSelen <daanselen@noreply.gitea.com>
2025-08-21 23:56:32 +00:00
6a7e18b124 Allow node24 actions (#737)
* `node` Tool is used regardless of this change
* upgrade images with `node` = `node24` if required
* actions/checkout@v5 now passing validation

Fixes #729

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/737
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-08-21 17:47:26 +00:00
53329c46ff Add ubuntu-24.04 label to defaults. (#724)
Simple change to include ubuntu-24.04 in the default list.  While ubuntu-latest already points to the same image (at this time) it is appropriate to have it by version as well.

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/724
Co-authored-by: telackey <telackey@noreply.gitea.com>
Co-committed-by: telackey <telackey@noreply.gitea.com>
2025-07-22 14:47:30 +00:00
6b1aea9c04 Upgrade github.com/go-git/go-git/v5 to v5.16.2 (#706)
Fix #695

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/706
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
2025-06-11 17:59:35 +00:00
edec9a8f91 Add a way to specify vars in act_runner exec (#704)
Before this commit, when running locally `act_runner exec` to test workflows, we could only fill env and secrets but not vars
This commit add a new exec option `--var` based on what is done for env and secret

Example:

`act_runner exec --env MY_ENV=testenv -s MY_SECRET=testsecret --var MY_VAR=testvariable`

 workflow
```
name: Gitea Actions test
on: [push]

jobs:
  TestAction:
    runs-on: ubuntu-latest
    steps:
      - run: echo "VAR -> ${{ vars.MY_VAR }}"
      - run: echo "ENV -> ${{ env.MY_ENV }}"
      - run: echo "SECRET -> ${{ secrets.MY_SECRET }}"
```

Will echo var, env and secret values sent in the command line

Fixes gitea/act_runner#692

Co-authored-by: Lautriva <gitlactr@dbn.re>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/704
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: lautriva <lautriva@noreply.gitea.com>
Co-committed-by: lautriva <lautriva@noreply.gitea.com>
2025-06-11 17:38:29 +00:00
6a9a447f86 Report errors by setting raw_output when it's error level (#645)
This solves #643 by setting the "raw_output" entry attribute when the log level is error.  This results in the log line being shipped to the Gitea UI.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/645
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Co-authored-by: Pablo Carranza <pcarranza@gmail.com>
Co-committed-by: Pablo Carranza <pcarranza@gmail.com>
2025-06-05 17:53:13 +00:00
43 changed files with 941 additions and 981 deletions

View File

@ -1,10 +1,17 @@
---
name: release-nightly name: release-nightly
on: on:
workflow_dispatch:
push: push:
branches: [main] branches:
- 'main'
tags: tags:
- "*" - '*'
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
jobs: jobs:
goreleaser: goreleaser:
@ -12,7 +19,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
@ -30,16 +37,22 @@ jobs:
S3_BUCKET: ${{ secrets.AWS_BUCKET }} S3_BUCKET: ${{ secrets.AWS_BUCKET }}
GORELEASER_FORCE_TOKEN: "gitea" GORELEASER_FORCE_TOKEN: "gitea"
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-image: release-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: strategy:
image: catthehacker/ubuntu:act-latest matrix:
env: variant:
DOCKER_ORG: gitea - target: basic
DOCKER_LATEST: nightly tag_suffix: ""
- target: dind
tag_suffix: "-dind"
- target: dind-rootless
tag_suffix: "-dind-rootless"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
@ -55,47 +68,18 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta - name: Echo the tag
id: meta run: echo "${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}"
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
target: basic target: ${{ matrix.variant.target }}
platforms: | platforms: |
linux/amd64 linux/amd64
linux/arm64 linux/arm64
push: true push: true
tags: | tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }} ${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}
- name: Build and push dind
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
- name: Build and push dind-rootless
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
target: dind-rootless
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless

3
.gitignore vendored
View File

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

View File

@ -62,7 +62,7 @@ builds:
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w -X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version={{ .Summary }} - -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
binary: >- binary: >-
{{ .ProjectName }}- {{ .ProjectName }}-
{{- .Version }}- {{- .Version }}-

View File

@ -1,3 +1,6 @@
### BUILDER STAGE
#
#
FROM golang:1.24-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` # Do not remove `git` here, it is required for getting runner version when executing `make build`
@ -11,9 +14,12 @@ WORKDIR /opt/src/act_runner
RUN make clean && make build RUN make clean && make build
### DIND VARIANT
#
#
FROM docker:dind AS dind FROM docker:dind AS dind
RUN apk add --no-cache s6 bash git RUN apk add --no-cache s6 bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner 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/run.sh /usr/local/bin/run.sh
@ -23,10 +29,13 @@ VOLUME /data
ENTRYPOINT ["s6-svscan","/etc/s6"] ENTRYPOINT ["s6-svscan","/etc/s6"]
### DIND-ROOTLESS VARIANT
#
#
FROM docker:dind-rootless AS dind-rootless FROM docker:dind-rootless AS dind-rootless
USER root USER root
RUN apk add --no-cache s6 bash git RUN apk add --no-cache s6 bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner 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/run.sh /usr/local/bin/run.sh
@ -41,8 +50,11 @@ ENV DOCKER_HOST=unix:///run/user/1000/docker.sock
USER rootless USER rootless
ENTRYPOINT ["s6-svscan","/etc/s6"] ENTRYPOINT ["s6-svscan","/etc/s6"]
### BASIC VARIANT
#
#
FROM alpine AS basic FROM alpine AS basic
RUN apk add --no-cache tini bash git RUN apk add --no-cache tini bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner 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/run.sh /usr/local/bin/run.sh

View File

@ -1,5 +1,5 @@
DIST := dist DIST := dist
EXECUTABLE := loong_runner EXECUTABLE := act_runner
GOFMT ?= gofumpt -l GOFMT ?= gofumpt -l
DIST := dist DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release DIST_DIRS := $(DIST)/binaries $(DIST)/release
@ -16,7 +16,7 @@ WINDOWS_ARCHS ?= windows/amd64
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*") GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*") GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
DOCKER_IMAGE ?= gitea/loong_runner DOCKER_IMAGE ?= gitea/act_runner
DOCKER_TAG ?= nightly DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
@ -66,11 +66,11 @@ else
endif endif
endif endif
GO_PACKAGES_TO_VET ?= $(filter-out git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...)) GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
TAGS ?= TAGS ?=
LDFLAGS ?= -X "git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=v$(RELASE_VERSION)" LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
all: build all: build
@ -86,7 +86,7 @@ go-check:
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' '))) $(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 '.' ' ');)) $(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 \ @if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
echo "Act Runner 需要 Go $(MIN_GO_VERSION_STR) 或更高版本才能构建。您可以从 https://go.dev/dl/ 获取。"; \ echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
exit 1; \ exit 1; \
fi fi
@ -97,17 +97,17 @@ fmt-check:
fi fi
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \ @diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "请运行'make fmt'并提交结果"; \ echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \ echo "$${diff}"; \
exit 1; \ exit 1; \
fi; fi;
test: fmt-check test: fmt-check
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo -e "\n===> \e[32mOk\e[m\n" || exit 1 @$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: vet .PHONY: vet
vet: vet:
@echo "运行 go vet..." @echo "Running go vet..."
@$(GO) build code.gitea.io/gitea-vet @$(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET) @$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)

View File

@ -1,111 +1,108 @@
# Loong Runner # act runner
Loong Runner 是基于 [Gitea派生(fock)的act](https://gitea.com/gitea/act) [二次派生](https://git.whlug.cn/LoongArchActions/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).
> 上游的Gitea派生是可以直接在龙架构上编译使用的, 此派生仓库主要是解决上游的Docker是基于乌班图系统制作的, 目前龙架构暂无乌班图系统 ## Installation
> * 当前计划使用`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
在 Docker 模式下需要 `Docker Engine Community` 版本。要安装 `Docker CE`,请遵循官方 [安装说明](https://docs.docker.com/engine/install/)。 Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
### 下载预构建的二进制文件 ### Build from source
访问 [这里](https://git.whlug.cn/LoongArchActions/loong_runner) 仅提供龙架构版本。
### 从源码构建
```bash ```bash
make build make build
``` ```
### 构建 Docker 容器镜像 ### Build a docker image
```bash ```bash
make docker 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 ```ini
[actions] [actions]
ENABLED=true ENABLED=true
``` ```
### 注册 ### Register
```bash ```bash
./loong_runner register ./act_runner register
``` ```
系统将提示您输入: And you will be asked to input:
1. Gitea 实例 URL,例如 `http://192.168.8.8:3000/`。您应该使用您的 Gitea 实例的 ROOT_URL 作为实例参数,并且不应使用 `localhost``127.0.0.1` 作为实例 IP; 1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
2. 运行器令牌,您可以从 `http://192.168.8.8:3000/admin/actions/runners` 获取; and you should not use `localhost` or `127.0.0.1` as instance IP;
3. 运行器名称,您可以留空; 2. Runner token, you can get it from `http://192.168.8.8:3000/admin/actions/runners`;
4. 运行器标签,您可以留空。 3. Runner name, you can just leave it blank;
4. Runner labels, you can just leave it blank.
过程如下: The process looks like:
```text ```text
INFO 注册运行器,架构=loong64,操作系统=linux,版本=0.1.5 INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
WARN 运行器处于用户模式。 WARN Runner in user-mode.
INFO 输入 Gitea 实例 URL(例如,https://gitea.com/): INFO Enter the Gitea instance URL (for example, https://gitea.com/):
http://192.168.8.8:3000/ http://192.168.8.8:3000/
INFO 输入运行器令牌: INFO Enter the runner token:
fe884e8027dc292970d4e0303fe82b14xxxxxxxx fe884e8027dc292970d4e0303fe82b14xxxxxxxx
INFO 输入运行器名称(如果设置为空,则使用主机名:Test.local): INFO Enter the runner name (if set empty, use hostname: Test.local):
INFO 输入运行器标签,留空以使用默认标签(逗号分隔,例如,debian-latest:docker://docker.gitea.com/runner-images:latest INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):
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]。 INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04 ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04].
DEBU 成功 ping 到 Gitea 实例服务器 DEBU Successfully pinged the Gitea instance server
INFO 运行器注册成功。 INFO Runner registered successfully.
``` ```
您也可以使用命令行参数进行注册。 You can also register with command line arguments.
```bash ```bash
./loong_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive ./act_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 ```bash
./loong_runner daemon ./act_runner daemon
``` ```
### 使用 Docker 运行 ### Run with docker
```bash ```bash
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/loong_runner:nightly 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
``` ```
### 配置 ### Configuration
您还可以使用配置文件配置运行器。 You can also configure the runner with a configuration file.
配置文件是一个 YAML 文件,您可以使用 `./loong_runner generate-config` 生成一个示例配置文件。 The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
```bash ```bash
./loong_runner generate-config > config.yaml ./act_runner generate-config > config.yaml
``` ```
您可以使用 `-c`/`--config` 参数指定配置文件路径。 You can specify the configuration file path with `-c`/`--config` argument.
```bash ```bash
./loong_runner -c config.yaml register # 使用配置文件注册 ./act_runner -c config.yaml register # register with config file
./loong_runner -c config.yaml daemon # 使用配置文件运行 ./act_runner -c config.yaml daemon # run with config file
``` ```
您可以在 [config.example.yaml](internal/pkg/config/config.example.yaml) 上在线查看配置文件的最新版本。 You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml).
### 示例部署 ### Example Deployments
查看 [examples](examples) 目录中的示例部署类型。 Check out the [examples](examples) directory for sample deployment types.

View File

@ -1,12 +1,12 @@
# `loong_runner` 使用示例 # Usage Examples for `act_runner`
欢迎来到我们专为 Gitea 设置设计的使用和部署示例集合。无论您是初学者还是经验丰富的用户,您都会在这里找到可以直接应用以增强 Gitea 体验的实用资源。我们鼓励您贡献自己的见解和知识,使这个集合更加全面和有价值。 Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
| 部分 | 描述 | | Section | Description |
|-----|------| |-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`docker`](docker) | 本部分为您提供适用于在工作站或安装了 Docker 的服务器上运行容器的脚本和说明。它简化了使用 Docker 设置和管理 Gitea 部署的过程。 | | [`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) | 在本部分中,您将发现如何利用 `docker-compose` 来高效处理 Gitea 部署的示例。它提供了一个直接的方法来管理 Gitea 设置的多个容器化组件。 | | [`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) | 如果您正在使用 Kubernetes 集群作为基础设施,本部分专门为您设计。它展示了在 Kubernetes 集群内配置 Gitea 部署的示例和指南,使您能够利用 Kubernetes 的可扩展性和灵活性。 | | [`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) | 本部分致力于协助您在虚拟或物理服务器上设置 Gitea 的示例。无论您是在使用虚拟机还是物理硬件,您都会找到有用的资源来指导您完成部署过程。 | | [`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. |
我们希望这些资源为您的 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.

View File

@ -1,4 +1,4 @@
### 使用`docker-compose`运行`loong_runner` ### Running `act_runner` using `docker-compose`
```yml ```yml
... ...
@ -6,54 +6,56 @@
image: gitea/gitea image: gitea/gitea
... ...
healthcheck: healthcheck:
# 使用curl检查仓库前端是否可用 # checks availability of Gitea's front-end with curl
test: ["CMD", "curl", "-f", "<instance_url>"] test: ["CMD", "curl", "-f", "<instance_url>"]
interval: 10s interval: 10s
retries: 3 retries: 3
start_period: 30s start_period: 30s
timeout: 10s timeout: 10s
environment: environment:
# GITEA_RUNNER_REGISTRATION_TOKEN可用于设置全局运行器注册令牌。 # GITEA_RUNNER_REGISTRATION_TOKEN can be used to set a global runner registration token.
# Gitea版本必须为v1.23或更高版本。 # The Gitea version must be v1.23 or higher.
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。 # It's also possible to use GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# - GITEA_RUNNER_REGISTRATION_TOKEN=<用户定义的注册令牌> # - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
runner: runner:
image: gitea/loong_runner image: gitea/act_runner
restart: always restart: always
depends_on: depends_on:
gitea: gitea:
# 需要(下述配置),以便运行器能够连接到仓库,请参阅“健康检查(healthcheck)” # required so runner can attach to gitea, see "healthcheck"
condition: service_healthy condition: service_healthy
restart: true restart: true
volumes: volumes:
- ./data/loong_runner:/data - ./data/act_runner:/data
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
- GITEA_INSTANCE_URL=<instance url> - GITEA_INSTANCE_URL=<instance url>
# 当使用Docker Secrets # When using Docker Secrets, it's also possible to use
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。 # GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# 环境变量优先, 仅在首次启动时需要。 # The env var takes precedence.
# Needed only for the first start.
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token> - GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
``` ```
### 使用 Docker-in-Docker (DIND) 运行 `loong_runner` ### Running `act_runner` using Docker-in-Docker (DIND)
```yml ```yml
... ...
runner: runner:
image: gitea/loong_runner:latest-dind-rootless image: gitea/act_runner:latest-dind-rootless
restart: always restart: always
privileged: true privileged: true
depends_on: depends_on:
- gitea - gitea
volumes: volumes:
- ./data/loong_runner:/data - ./data/act_runner:/data
environment: environment:
- GITEA_INSTANCE_URL=<instance url> - GITEA_INSTANCE_URL=<instance url>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock - DOCKER_HOST=unix:///var/run/user/1000/docker.sock
# 使用Docker Secrets # When using Docker Secrets, it's also possible to use
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。 # GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# 环境变量优先, 仅在首次启动时需要。 # The env var takes precedence.
# Needed only for the first start.
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token> - GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
``` ```

View File

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

View File

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

View File

@ -12,9 +12,9 @@ spec:
--- ---
apiVersion: v1 apiVersion: v1
data: data:
# 注册令牌可从Web UIAPI或命令行获取。 # The registration token can be obtained from the web UI, API or command-line.
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量 # You can also set a pre-defined global runner registration token for the Gitea instance via
# 为Gitea实例设置预定义的全局运行器注册令牌。 # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >> token: << base64 encoded registration token >>
kind: Secret kind: Secret
metadata: metadata:
@ -49,7 +49,7 @@ spec:
containers: containers:
- name: runner - name: runner
image: gitea/act_runner:nightly image: gitea/act_runner:nightly
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo '等待docker守护进程..'; sleep 5; done; /sbin/tini -- run.sh"] command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
env: env:
- name: DOCKER_HOST - name: DOCKER_HOST
value: tcp://localhost:2376 value: tcp://localhost:2376

View File

@ -12,9 +12,9 @@ spec:
--- ---
apiVersion: v1 apiVersion: v1
data: data:
# 注册令牌可从Web UIAPI或命令行获取。 # The registration token can be obtained from the web UI, API or command-line.
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量 # You can also set a pre-defined global runner registration token for the Gitea instance via
# 为Gitea实例设置预定义的全局运行器注册令牌。 # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >> token: << base64 encoded registration token >>
kind: Secret kind: Secret
metadata: metadata:
@ -50,7 +50,7 @@ spec:
- name: runner - name: runner
image: gitea/act_runner:nightly-dind-rootless image: gitea/act_runner:nightly-dind-rootless
imagePullPolicy: Always imagePullPolicy: Always
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo '等待docker守护进程..'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"] # command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
env: env:
- name: DOCKER_HOST - name: DOCKER_HOST
value: tcp://localhost:2376 value: tcp://localhost:2376

View File

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

View File

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

32
go.mod
View File

@ -1,4 +1,4 @@
module git.whlug.cn/LAA/loong_runner module gitea.com/gitea/act_runner
go 1.24 go 1.24
@ -13,8 +13,8 @@ require (
github.com/nektos/act v0.0.0 // will be replaced github.com/nektos/act v0.0.0 // will be replaced
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.10.0
golang.org/x/term v0.22.0 golang.org/x/term v0.31.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
google.golang.org/protobuf v1.34.2 google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@ -26,12 +26,12 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.3.9 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/containerd v1.7.13 // indirect github.com/containerd/containerd v1.7.13 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/creack/pty v1.1.21 // indirect github.com/creack/pty v1.1.21 // indirect
github.com/cyphar/filepath-securejoin v0.3.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect github.com/distribution/reference v0.5.0 // indirect
github.com/docker/cli v25.0.3+incompatible // indirect github.com/docker/cli v25.0.3+incompatible // indirect
@ -43,14 +43,14 @@ require (
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/imdario/mergo v0.3.16 // indirect github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
@ -69,14 +69,14 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rhysd/actionlint v1.7.1 // indirect github.com/rhysd/actionlint v1.7.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
@ -89,13 +89,15 @@ require (
go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/tools v0.23.0 // indirect golang.org/x/tools v0.23.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4 replace github.com/nektos/act => gitea.com/gitea/act v0.261.7
replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.16.2

116
go.sum
View File

@ -6,8 +6,8 @@ connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= 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 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8= gitea.com/gitea/act v0.261.7 h1:0tX0EdWo5uZUgsN/iJGyEAtagqYURrbOuWxyx+LV1Wk=
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/act v0.261.7/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 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 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= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
@ -19,20 +19,18 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is= github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is=
github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4= github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@ -40,8 +38,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.3.0 h1:tXpmbiaeBrS/K2US8nhgwdKYnfAOnVfkcLPKFgFHeA0= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.3.0/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -59,24 +57,24 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -86,12 +84,12 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
@ -142,16 +140,16 @@ github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWam
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -163,16 +161,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -189,8 +187,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 h1:zjNCuOOhh1TKRU0Ru3PPPJt80z7eReswCao91gBLk00= github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 h1:zjNCuOOhh1TKRU0Ru3PPPJt80z7eReswCao91gBLk00=
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928/go.mod h1:PCFYfAEfKT+Nd6zWvUpsXduMR1bXFLf0uGSlEF05MCI= github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928/go.mod h1:PCFYfAEfKT+Nd6zWvUpsXduMR1bXFLf0uGSlEF05MCI=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@ -205,7 +203,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
@ -229,35 +226,25 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -269,34 +256,21 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -304,8 +278,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

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

View File

@ -10,47 +10,47 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver" "gitea.com/gitea/act_runner/internal/pkg/ver"
) )
func Execute(ctx context.Context) { func Execute(ctx context.Context) {
// ./act_runner // ./act_runner
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "act_runner [运行事件名称]\n如果没有传递事件名称, 默认为 \"on: push\"", Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
Short: "通过指定事件名称(例如 `push`)或直接指定操作名称来本地运行 GitHub Actions", Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
Version: ver.Version(), Version: ver.Version(),
SilenceUsage: true, SilenceUsage: true,
} }
configFile := "" configFile := ""
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径") rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
// ./act_runner register // ./act_runner register
var regArgs registerArgs var regArgs registerArgs
registerCmd := &cobra.Command{ registerCmd := &cobra.Command{
Use: "register", Use: "register",
Short: "将运行器注册到服务器", Short: "Register a runner to the server",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
RunE: runRegister(ctx, &regArgs, &configFile), // 必须使用 regArgs 的指针 RunE: runRegister(ctx, &regArgs, &configFile), // must use a pointer to regArgs
} }
registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "禁用交互模式") registerCmd.Flags().BoolVar(&regArgs.NoInteractive, "no-interactive", false, "Disable interactive mode")
registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Gitea 实例地址") registerCmd.Flags().StringVar(&regArgs.InstanceAddr, "instance", "", "Gitea instance address")
registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "运行器令牌") registerCmd.Flags().StringVar(&regArgs.Token, "token", "", "Runner token")
registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "运行器名称") registerCmd.Flags().StringVar(&regArgs.RunnerName, "name", "", "Runner name")
registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "运行器标签,逗号分隔") registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "Runner tags, comma separated")
registerCmd.Flags().BoolVar(&regArgs.Ephemeral, "ephemeral", false, "配置运行器为临时运行器,只能选择单个作业(比 --once 更严格)") registerCmd.Flags().BoolVar(&regArgs.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
rootCmd.AddCommand(registerCmd) rootCmd.AddCommand(registerCmd)
// ./act_runner daemon // ./act_runner daemon
var daemArgs daemonArgs var daemArgs daemonArgs
daemonCmd := &cobra.Command{ daemonCmd := &cobra.Command{
Use: "daemon", Use: "daemon",
Short: "作为运行器守护进程运行", Short: "Run as a runner daemon",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
RunE: runDaemon(ctx, &daemArgs, &configFile), RunE: runDaemon(ctx, &daemArgs, &configFile),
} }
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "运行一个作业然后退出") daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit")
rootCmd.AddCommand(daemonCmd) rootCmd.AddCommand(daemonCmd)
// ./act_runner exec // ./act_runner exec
@ -59,7 +59,7 @@ func Execute(ctx context.Context) {
// ./act_runner config // ./act_runner config
rootCmd.AddCommand(&cobra.Command{ rootCmd.AddCommand(&cobra.Command{
Use: "generate-config", Use: "generate-config",
Short: "生成示例配置文件", Short: "Generate an example config file",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("%s", config.Example) fmt.Printf("%s", config.Example)
@ -70,16 +70,16 @@ func Execute(ctx context.Context) {
var cacheArgs cacheServerArgs var cacheArgs cacheServerArgs
cacheCmd := &cobra.Command{ cacheCmd := &cobra.Command{
Use: "cache-server", Use: "cache-server",
Short: "启动缓存服务器用于缓存操作", Short: "Start a cache server for the cache action",
Args: cobra.MaximumNArgs(0), Args: cobra.MaximumNArgs(0),
RunE: runCacheServer(ctx, &configFile, &cacheArgs), RunE: runCacheServer(ctx, &configFile, &cacheArgs),
} }
cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "缓存目录") cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "Cache directory")
cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "缓存服务器主机") cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "Host of the cache server")
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "缓存服务器端口") cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server")
rootCmd.AddCommand(cacheCmd) rootCmd.AddCommand(cacheCmd)
// 隐藏自动补全命令 // hide completion command
rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.CompletionOptions.HiddenDefaultCmd = true
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {

View File

@ -5,6 +5,7 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
@ -13,37 +14,38 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/app/poll" "gitea.com/gitea/act_runner/internal/app/poll"
"git.whlug.cn/LAA/loong_runner/internal/app/run" "gitea.com/gitea/act_runner/internal/app/run"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client" "gitea.com/gitea/act_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/envcheck" "gitea.com/gitea/act_runner/internal/pkg/envcheck"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels" "gitea.com/gitea/act_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver" "gitea.com/gitea/act_runner/internal/pkg/ver"
) )
func runDaemon(ctx context.Context, daemArgs *daemonArgs, 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 { return func(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadDefault(*configFile) cfg, err := config.LoadDefault(*configFile)
if err != nil { if err != nil {
return fmt.Errorf("无效的配置: %w", err) return fmt.Errorf("invalid configuration: %w", err)
} }
initLogging(cfg) initLogging(cfg)
log.Infoln("启动运行器守护进程") log.Infoln("Starting runner daemon")
reg, err := config.LoadRegistration(cfg.Runner.File) reg, err := config.LoadRegistration(cfg.Runner.File)
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Error("未找到注册文件,请先注册运行器") log.Error("registration file not found, please register the runner first")
return err return err
} else if err != nil { } else if err != nil {
return fmt.Errorf("加载注册文件失败: %w", err) return fmt.Errorf("failed to load registration file: %w", err)
} }
lbls := reg.Labels lbls := reg.Labels
@ -55,16 +57,43 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
for _, l := range lbls { for _, l := range lbls {
label, err := labels.Parse(l) label, err := labels.Parse(l)
if err != nil { if err != nil {
log.WithError(err).Warnf("忽略无效标签 %q", l) log.WithError(err).Warnf("ignored invalid label %q", l)
continue continue
} }
ls = append(ls, label) ls = append(ls, label)
} }
if len(ls) == 0 { if len(ls) == 0 {
log.Warn("未配置任何标签,运行器可能无法接取作业") log.Warn("no labels configured, runner may not be able to pick up jobs")
} }
if ls.RequireDocker() { if ls.RequireDocker() || cfg.Container.RequireDocker {
// Wait for dockerd be ready
if timeout := cfg.Container.DockerTimeout; timeout > 0 {
tctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
keepRunning := true
for keepRunning {
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
if err != nil {
log.Errorf("Failed to get socket path: %s", err.Error())
} else if err = envcheck.CheckIfDockerRunning(tctx, dockerSocketPath); errors.Is(err, context.Canceled) {
log.Infof("Docker wait timeout of %s expired", timeout.String())
break
} else if err != nil {
log.Errorf("Docker connection failed: %s", err.Error())
} else {
log.Infof("Docker is ready")
break
}
select {
case <-time.After(time.Second):
case <-tctx.Done():
log.Infof("Docker wait timeout of %s expired", timeout.String())
keepRunning = false
}
}
}
// Require dockerd be ready
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost) dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
if err != nil { if err != nil {
return err return err
@ -72,15 +101,15 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil { if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil {
return err return err
} }
// 如果 dockerSocketPath 通过检查,用 dockerSocketPath 覆盖 DOCKER_HOST // if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath
os.Setenv("DOCKER_HOST", dockerSocketPath) os.Setenv("DOCKER_HOST", dockerSocketPath)
// 如果 cfg.Container.DockerHost 为空,意味着 act_runner 需要自动查找可用的 docker 主机 // empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
// 并将路径分配给 cfg.Container.DockerHost // and assign the path to cfg.Container.DockerHost
if cfg.Container.DockerHost == "" { if cfg.Container.DockerHost == "" {
cfg.Container.DockerHost = dockerSocketPath cfg.Container.DockerHost = dockerSocketPath
} }
// 检查方案,如果方案不是 npipe unix // check the scheme, if the scheme is not npipe or unix
// cfg.Container.DockerHost 设置为 "-",因为它不能挂载到作业容器 // set cfg.Container.DockerHost to "-" because it can't be mounted to the job container
if protoIndex := strings.Index(cfg.Container.DockerHost, "://"); protoIndex != -1 { if protoIndex := strings.Index(cfg.Container.DockerHost, "://"); protoIndex != -1 {
scheme := cfg.Container.DockerHost[:protoIndex] scheme := cfg.Container.DockerHost[:protoIndex]
if !strings.EqualFold(scheme, "npipe") && !strings.EqualFold(scheme, "unix") { if !strings.EqualFold(scheme, "npipe") && !strings.EqualFold(scheme, "unix") {
@ -92,9 +121,9 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
if !slices.Equal(reg.Labels, ls.ToStrings()) { if !slices.Equal(reg.Labels, ls.ToStrings()) {
reg.Labels = ls.ToStrings() reg.Labels = ls.ToStrings()
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil { if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
return fmt.Errorf("保存运行器配置失败: %w", err) return fmt.Errorf("failed to save runner config: %w", err)
} }
log.Infof("标签更新为: %v", reg.Labels) log.Infof("labels updated to: %v", reg.Labels)
} }
cli := client.New( cli := client.New(
@ -107,16 +136,16 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
runner := run.NewRunner(cfg, reg, cli) runner := run.NewRunner(cfg, reg, cli)
// 在获取任务之前声明运行器的标签 // declare the labels of the runner before fetching tasks
resp, err := runner.Declare(ctx, ls.Names()) resp, err := runner.Declare(ctx, ls.Names())
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented { if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
log.Errorf("您的 Gitea 版本太旧,不支持运行器声明,请升级到 v1.21 或更高版本") log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
return err return err
} else if err != nil { } else if err != nil {
log.WithError(err).Error("调用 Declare 失败") log.WithError(err).Error("fail to invoke Declare")
return err return err
} else { } else {
log.Infof("运行器: %s, 版本: %s, 标签: %v, 声明成功", log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels) resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
} }
@ -129,7 +158,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
poller.PollOnce() poller.PollOnce()
}() }()
// 当完成一个作业或请求取消时关闭 // shutdown when we complete a job or cancel is requested
select { select {
case <-ctx.Done(): case <-ctx.Done():
case <-done: case <-done:
@ -140,14 +169,14 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
<-ctx.Done() <-ctx.Done()
} }
log.Infof("运行器: %s 关闭开始,等待 %s 让正在运行的作业完成后再关闭", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout) log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout) ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
defer cancel() defer cancel()
err = poller.Shutdown(ctx) err = poller.Shutdown(ctx)
if err != nil { if err != nil {
log.Warnf("运行器: %s 在关闭期间取消了正在进行的作业", resp.Msg.Runner.Name) log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
} }
return nil return nil
@ -155,44 +184,51 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
} }
type daemonArgs struct { type daemonArgs struct {
Once bool // 是否只运行一次 Once bool
} }
// initLogging 设置全局 logrus 日志记录器。 // initLogging setup the global logrus logger.
func initLogging(cfg *config.Config) { func initLogging(cfg *config.Config) {
callPrettyfier := 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
}
isTerm := isatty.IsTerminal(os.Stdout.Fd()) isTerm := isatty.IsTerminal(os.Stdout.Fd())
format := &log.TextFormatter{ format := &log.TextFormatter{
DisableColors: !isTerm, DisableColors: !isTerm,
FullTimestamp: true, FullTimestamp: true,
CallerPrettyfier: callPrettyfier,
} }
log.SetFormatter(format) log.SetFormatter(format)
if l := cfg.Log.Level; l != "" { l := cfg.Log.Level
level, err := log.ParseLevel(l) if l == "" {
if err != nil { log.Infof("Log level not set, sticking to info")
log.WithError(err). return
Errorf("无效的日志级别: %q", l) }
}
// 调试级别 level, err := log.ParseLevel(l)
if level == log.DebugLevel { if err != nil {
log.SetReportCaller(true) log.WithError(err).
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) { Errorf("invalid log level: %q", l)
// 获取函数名 }
s := strings.Split(f.Function, ".")
funcname := "[" + s[len(s)-1] + "]"
// 获取文件名和行号
_, filename := path.Split(f.File)
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
return funcname, filename
}
log.SetFormatter(format)
}
if log.GetLevel() != level { // debug level
log.Infof("日志级别更改为 %v", level) switch level {
log.SetLevel(level) case log.DebugLevel, log.TraceLevel:
} log.SetReportCaller(true) // Only in debug or trace because it takes a performance toll
log.Infof("Log level %s requested, setting up report caller for further debugging", level)
}
if log.GetLevel() != level {
log.Infof("log level set to %v", level)
log.SetLevel(level)
} }
} }
@ -207,7 +243,7 @@ var commonSocketPaths = []string{
} }
func getDockerSocketPath(configDockerHost string) (string, error) { func getDockerSocketPath(configDockerHost string) (string, error) {
// `-` 表示不要将 docker 套接字挂载到作业容器 // a `-` means don't mount the docker socket to job containers
if configDockerHost != "" && configDockerHost != "-" { if configDockerHost != "" && configDockerHost != "-" {
return configDockerHost, nil return configDockerHost, nil
} }
@ -226,5 +262,5 @@ func getDockerSocketPath(configDockerHost string) (string, error) {
} }
} }
return "", fmt.Errorf("守护进程 Docker 引擎套接字未找到且 docker_host 配置无效") return "", fmt.Errorf("daemon Docker Engine socket not found and docker_host config was invalid")
} }

View File

@ -26,47 +26,48 @@ import (
) )
type executeArgs struct { type executeArgs struct {
runList bool // 是否列出工作流 runList bool
job string // 特定作业 ID job string
event string // 事件名称 event string
workdir string // 工作目录 workdir string
workflowsPath string // 工作流文件路径 workflowsPath string
noWorkflowRecurse bool // 是否禁止递归运行子目录中的工作流 noWorkflowRecurse bool
autodetectEvent bool // 自动检测事件 autodetectEvent bool
forcePull bool // 即使存在也拉取 Docker 镜像 forcePull bool
forceRebuild bool // 即使存在也重建本地动作 Docker 镜像 forceRebuild bool
jsonLogger bool // 以 JSON 格式输出日志 jsonLogger bool
envs []string // 环境变量 envs []string
envfile string // 环境文件 envfile string
secrets []string // 机密 secrets []string
defaultActionsURL string // 默认动作 URL vars []string
insecureSecrets bool // 不建议!打印日志时不隐藏机密 defaultActionsURL string
privileged bool // 使用特权模式 insecureSecrets bool
usernsMode string // 用户命名空间 privileged bool
containerArchitecture string // 容器架构 usernsMode string
containerDaemonSocket string // 容器守护进程套接字 containerArchitecture string
useGitIgnore bool // 控制是否将 .gitignore 中指定的路径复制到容器中 containerDaemonSocket string
containerCapAdd []string // 添加到工作流容器的内核能力 useGitIgnore bool
containerCapDrop []string // 从工作流容器中删除的内核能力 containerCapAdd []string
containerOptions string // 容器选项 containerCapDrop []string
artifactServerPath string // 存储上传和检索下载的路径 containerOptions string
artifactServerAddr string // 监听地址 artifactServerPath string
artifactServerPort string // 监听端口 artifactServerAddr string
noSkipCheckout bool // 不跳过 actions/checkout artifactServerPort string
debug bool // 启用调试日志 noSkipCheckout bool
dryrun bool // dryrun 模式 debug bool
image string // 使用的 Docker 镜像 dryrun bool
cacheHandler *artifactcache.Handler // 缓存处理器 image string
network string // 容器连接的网络 cacheHandler *artifactcache.Handler
githubInstance string // 使用的 Gitea 实例 network string
githubInstance string
} }
// WorkflowsPath 返回工作流文件的路径 // WorkflowsPath returns path to workflow file(s)
func (i *executeArgs) WorkflowsPath() string { func (i *executeArgs) WorkflowsPath() string {
return i.resolve(i.workflowsPath) return i.resolve(i.workflowsPath)
} }
// Envfile 返回 .env 文件的路径 // Envfile returns path to .env
func (i *executeArgs) Envfile() string { func (i *executeArgs) Envfile() string {
return i.resolve(i.envfile) return i.resolve(i.envfile)
} }
@ -77,18 +78,18 @@ func (i *executeArgs) LoadSecrets() map[string]string {
secretPairParts := strings.SplitN(secretPair, "=", 2) secretPairParts := strings.SplitN(secretPair, "=", 2)
secretPairParts[0] = strings.ToUpper(secretPairParts[0]) secretPairParts[0] = strings.ToUpper(secretPairParts[0])
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] { if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
log.Errorf("机密 %s 已经定义(机密不区分大小写)", secretPairParts[0]) log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
} }
if len(secretPairParts) == 2 { if len(secretPairParts) == 2 {
s[secretPairParts[0]] = secretPairParts[1] s[secretPairParts[0]] = secretPairParts[1]
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" { } else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
s[secretPairParts[0]] = env s[secretPairParts[0]] = env
} else { } else {
fmt.Printf(" '%s' 提供值: ", secretPairParts[0]) fmt.Printf("Provide value for '%s': ", secretPairParts[0])
val, err := term.ReadPassword(int(os.Stdin.Fd())) val, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println() fmt.Println()
if err != nil { if err != nil {
log.Errorf("读取输入失败: %v", err) log.Errorf("failed to read input: %v", err)
os.Exit(1) os.Exit(1)
} }
s[secretPairParts[0]] = string(val) s[secretPairParts[0]] = string(val)
@ -101,7 +102,7 @@ func readEnvs(path string, envs map[string]string) bool {
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
env, err := godotenv.Read(path) env, err := godotenv.Read(path)
if err != nil { if err != nil {
log.Fatalf("从 %s 加载失败: %v", path, err) log.Fatalf("Error loading from %s: %v", path, err)
} }
for k, v := range env { for k, v := range env {
envs[k] = v envs[k] = v
@ -130,7 +131,23 @@ func (i *executeArgs) LoadEnvs() map[string]string {
return envs return envs
} }
// Workdir 返回工作目录的路径 func (i *executeArgs) LoadVars() map[string]string {
vars := make(map[string]string)
if i.vars != nil {
for _, runVar := range i.vars {
e := strings.SplitN(runVar, `=`, 2)
if len(e) == 2 {
vars[e[0]] = e[1]
} else {
vars[e[0]] = ""
}
}
}
return vars
}
// Workdir returns path to workdir
func (i *executeArgs) Workdir() string { func (i *executeArgs) Workdir() string {
return i.resolve(".") return i.resolve(".")
} }
@ -151,22 +168,22 @@ func (i *executeArgs) resolve(path string) string {
func printList(plan *model.Plan) error { func printList(plan *model.Plan) error {
type lineInfoDef struct { type lineInfoDef struct {
jobID string // 作业 ID jobID string
jobName string // 作业名称 jobName string
stage string // 阶段 stage string
wfName string // 工作流名称 wfName string
wfFile string // 工作流文件 wfFile string
events string // 事件 events string
} }
lineInfos := []lineInfoDef{} lineInfos := []lineInfoDef{}
header := lineInfoDef{ header := lineInfoDef{
jobID: "作业 ID", jobID: "Job ID",
jobName: "作业名称", jobName: "Job name",
stage: "阶段", stage: "Stage",
wfName: "工作流名称", wfName: "Workflow name",
wfFile: "工作流文件", wfFile: "Workflow file",
events: "事件", events: "Events",
} }
jobs := map[string]bool{} jobs := map[string]bool{}
@ -221,6 +238,7 @@ func printList(plan *model.Plan) error {
jobNameMaxWidth += 2 jobNameMaxWidth += 2
stageMaxWidth += 2 stageMaxWidth += 2
wfNameMaxWidth += 2 wfNameMaxWidth += 2
wfFileMaxWidth += 2
fmt.Printf("%*s%*s%*s%*s%*s%*s\n", fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
-stageMaxWidth, header.stage, -stageMaxWidth, header.stage,
@ -241,47 +259,47 @@ func printList(plan *model.Plan) error {
) )
} }
if duplicateJobIDs { if duplicateJobIDs {
fmt.Print("\n检测到多个作业具有相同的作业名称,请使用 `-W` 指定特定工作流的路径。\n") fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
} }
return nil return nil
} }
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error { 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 var filterPlan *model.Plan
// 确定要过滤的事件名称 // Determine the event name to be filtered
var filterEventName string var filterEventName string
if len(execArgs.event) > 0 { if len(execArgs.event) > 0 {
log.Infof("使用选择的事件进行过滤: %s", execArgs.event) log.Infof("Using chosed event for filtering: %s", execArgs.event)
filterEventName = execArgs.event filterEventName = execArgs.event
} else if execArgs.autodetectEvent { } else if execArgs.autodetectEvent {
// 收集所有加载的工作流中的事件 // collect all events from loaded workflows
events := planner.GetEvents() events := planner.GetEvents()
// 将默认事件类型设置为第一个可用的事件 // set default event type to first event from many available
// 这样用户就不必指定事件。 // this way user dont have to specify the event.
log.Infof("使用检测到的第一个工作流事件进行过滤: %s", events[0]) log.Infof("Using first detected workflow event for filtering: %s", events[0])
filterEventName = events[0] filterEventName = events[0]
} }
var err error var err error
if execArgs.job != "" { if execArgs.job != "" {
log.Infof("准备带有作业的计划: %s", execArgs.job) log.Infof("Preparing plan with a job: %s", execArgs.job)
filterPlan, err = planner.PlanJob(execArgs.job) filterPlan, err = planner.PlanJob(execArgs.job)
if err != nil { if err != nil {
return err return err
} }
} else if filterEventName != "" { } else if filterEventName != "" {
log.Infof("准备事件的计划: %s", filterEventName) log.Infof("Preparing plan for a event: %s", filterEventName)
filterPlan, err = planner.PlanEvent(filterEventName) filterPlan, err = planner.PlanEvent(filterEventName)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
log.Infof("准备所有作业的计划") log.Infof("Preparing plan with all jobs")
filterPlan, err = planner.PlanAll() filterPlan, err = planner.PlanAll()
if err != nil { if err != nil {
return err return err
@ -304,40 +322,40 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
return runExecList(ctx, planner, execArgs) return runExecList(ctx, planner, execArgs)
} }
// 计划触发作业 // plan with triggered jobs
var plan *model.Plan var plan *model.Plan
// 确定要触发的事件名称 // Determine the event name to be triggered
var eventName string var eventName string
// 收集所有加载的工作流中的事件 // collect all events from loaded workflows
events := planner.GetEvents() events := planner.GetEvents()
if len(execArgs.event) > 0 { if len(execArgs.event) > 0 {
log.Infof("使用选择的事件进行过滤: %s", execArgs.event) log.Infof("Using chosed event for filtering: %s", execArgs.event)
eventName = execArgs.event eventName = execArgs.event
} else if len(events) == 1 && len(events[0]) > 0 { } else if len(events) == 1 && len(events[0]) > 0 {
log.Infof("使用唯一检测到的工作流事件: %s", events[0]) log.Infof("Using the only detected workflow event: %s", events[0])
eventName = events[0] eventName = events[0]
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 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("使用检测到的第一个工作流事件: %s", events[0]) log.Infof("Using first detected workflow event: %s", events[0])
eventName = events[0] eventName = events[0]
} else { } else {
log.Infof("使用默认工作流事件: push") log.Infof("Using default workflow event: push")
eventName = "push" eventName = "push"
} }
// 为此运行构建计划 // build the plan for this run
if execArgs.job != "" { if execArgs.job != "" {
log.Infof("规划作业: %s", execArgs.job) log.Infof("Planning job: %s", execArgs.job)
plan, err = planner.PlanJob(execArgs.job) plan, err = planner.PlanJob(execArgs.job)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
log.Infof("规划事件的作业: %s", eventName) log.Infof("Planning jobs for event: %s", eventName)
plan, err = planner.PlanEvent(eventName) plan, err = planner.PlanEvent(eventName)
if err != nil { if err != nil {
return err return err
@ -349,18 +367,18 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
maxLifetime = time.Until(deadline) maxLifetime = time.Until(deadline)
} }
// 初始化缓存服务器 // init a cache server
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request")) handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
if err != nil { if err != nil {
return err return err
} }
log.Infof("缓存处理器监听于: %v", handler.ExternalURL()) log.Infof("cache handler listens on: %v", handler.ExternalURL())
execArgs.cacheHandler = handler execArgs.cacheHandler = handler
if len(execArgs.artifactServerAddr) == 0 { if len(execArgs.artifactServerAddr) == 0 {
ip := common.GetOutboundIP() ip := common.GetOutboundIP()
if ip == nil { if ip == nil {
return fmt.Errorf("无法确定出站 IP 地址") return fmt.Errorf("unable to determine outbound IP address")
} }
execArgs.artifactServerAddr = ip.String() execArgs.artifactServerAddr = ip.String()
} }
@ -375,7 +393,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
execArgs.artifactServerPath = tempDir execArgs.artifactServerPath = tempDir
} }
// 运行计划 // run the plan
config := &runner.Config{ config := &runner.Config{
Workdir: execArgs.Workdir(), Workdir: execArgs.Workdir(),
BindWorkdir: false, BindWorkdir: false,
@ -385,6 +403,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
LogOutput: true, LogOutput: true,
JSONLogger: execArgs.jsonLogger, JSONLogger: execArgs.jsonLogger,
Env: execArgs.LoadEnvs(), Env: execArgs.LoadEnvs(),
Vars: execArgs.LoadVars(),
Secrets: execArgs.LoadSecrets(), Secrets: execArgs.LoadSecrets(),
InsecureSecrets: execArgs.insecureSecrets, InsecureSecrets: execArgs.insecureSecrets,
Privileged: execArgs.privileged, Privileged: execArgs.privileged,
@ -410,7 +429,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
PlatformPicker: func(_ []string) string { PlatformPicker: func(_ []string) string {
return execArgs.image return execArgs.image
}, },
ValidVolumes: []string{"**"}, // 所有挂载的卷(volumes)都允许被 exec 命令访问 ValidVolumes: []string{"**"}, // All volumes are allowed for `exec` command
} }
config.Env["ACT_EXEC"] = "true" config.Env["ACT_EXEC"] = "true"
@ -432,7 +451,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
} }
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort) artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
log.Debugf("artifacts 服务器启动于 %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort) log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
ctx = common.WithDryrun(ctx, execArgs.dryrun) ctx = common.WithDryrun(ctx, execArgs.dryrun)
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
@ -449,43 +468,44 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
execCmd := &cobra.Command{ execCmd := &cobra.Command{
Use: "exec", Use: "exec",
Short: "本地运行工作流", Short: "Run workflow locally.",
Args: cobra.MaximumNArgs(20), Args: cobra.MaximumNArgs(20),
RunE: runExec(ctx, &execArg), RunE: runExec(ctx, &execArg),
} }
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "本地运行工作流") execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "运行特定作业 ID") execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "运行事件名称") execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "工作流文件路径") execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "工作目录") execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "禁用运行指定路径的子目录中的工作流") 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, "使用工作流中的第一个事件类型作为触发工作流的事件") 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, "即使已经存在也拉取 Docker 镜像") execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "即使已经存在也重建本地动作 Docker 镜像") execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "以 JSON 格式输出日志") execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "使环境变量对动作可用,可选值(例如 --env myenv=foo --env myenv)") 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", "读取并用作容器中的环境的环境文件") 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{}, "为 Action 提供密钥,可带可选值(例如 -s mysecret=foo -s mysecret)") 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, "不推荐!打印日志时不会隐藏密钥信息") execCmd.Flags().StringArrayVarP(&execArg.vars, "var", "", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)")
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "使用特权模式") execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "要使用的用户命名空间") execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "运行容器使用的架构(如 linux/loong64)。未指定时使用宿主机默认架构。需要 Docker 服务端 API 版本 1.41+,更低版本的 Docker 平台会忽略此参数") execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "挂载到容器的 Docker 守护进程 socket 路径") 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.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "控制是否将 .gitignore 中指定的路径复制到容器中") 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().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "为工作流容器添加的内核能力(例如 --container-cap-add SYS_PTRACE)") execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "从工作流容器移除的内核能力(例如 --container-cap-drop SYS_PTRACE)") 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().StringVarP(&execArg.containerOptions, "container-opts", "", "", "容器选项") 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.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "定义构建物服务器存储上传和下载的路径。未指定时构建物服务器不会启动") execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "定义构建物服务器的监听地址") 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.artifactServerPort, "artifact-server-port", "", "34567", "定义构建物服务器的监听端口(例如 --container-cap-drop SYS_PTRACE)") execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "Defines the address where the artifact server listens")
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "定义 Action 实例的默认 URL") execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "不跳过 actions/checkout") execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "Defines the default url of action instance.")
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "启用调试日志") execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun 模式") execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "lcr.loongnix.cn/library/debian:latest", "使用的 Docker 镜像。使用 \"-self-hosted\" 直接在主机上运行") execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "容器连接的网络") execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "docker.gitea.com/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "使用的 Gitea 实例") 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.")
return execCmd return execCmd
} }

View File

@ -20,13 +20,13 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client" "gitea.com/gitea/act_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels" "gitea.com/gitea/act_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver" "gitea.com/gitea/act_runner/internal/pkg/ver"
) )
// runRegister 将运行器注册到服务器 // runRegister registers a runner to the server
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error { func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(false) log.SetReportCaller(false)
@ -37,13 +37,13 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
}) })
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
log.Infof("注册运行器,架构=%s,操作系统=%s,版本=%s", log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
goruntime.GOARCH, goruntime.GOOS, ver.Version()) goruntime.GOARCH, goruntime.GOOS, ver.Version())
// 运行器始终需要 root 权限 // runner always needs root permission
if os.Getuid() != 0 { if os.Getuid() != 0 {
// TODO: 使用更好的方法检查 root 权限 // TODO: use a better way to check root permission
log.Warnf("运行器处于用户模式。") log.Warnf("Runner in user-mode.")
} }
if regArgs.NoInteractive { if regArgs.NoInteractive {
@ -68,14 +68,14 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
} }
} }
// registerArgs 代表 register 命令的参数 // registerArgs represents the arguments for register command
type registerArgs struct { type registerArgs struct {
NoInteractive bool // 是否非交互模式 NoInteractive bool
InstanceAddr string // 实例地址 InstanceAddr string
Token string // 令牌 Token string
RunnerName string // 运行器名称 RunnerName string
Labels string // 标签 Labels string
Ephemeral bool // 是否临时 Ephemeral bool
} }
type registerStage int8 type registerStage int8
@ -92,9 +92,9 @@ const (
) )
var defaultLabels = []string{ var defaultLabels = []string{
"debian-latest:docker://lcr.loongnix.cn/library/debian:latest", "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest",
"anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest", "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04",
"anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2", "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04",
} }
type registerInputs struct { type registerInputs struct {
@ -107,10 +107,10 @@ type registerInputs struct {
func (r *registerInputs) validate() error { func (r *registerInputs) validate() error {
if r.InstanceAddr == "" { if r.InstanceAddr == "" {
return fmt.Errorf("实例地址为空") return fmt.Errorf("instance address is empty")
} }
if r.Token == "" { if r.Token == "" {
return fmt.Errorf("令牌为空") return fmt.Errorf("token is empty")
} }
if len(r.Labels) > 0 { if len(r.Labels) > 0 {
return validateLabels(r.Labels) return validateLabels(r.Labels)
@ -144,15 +144,15 @@ func (r *registerInputs) stageValue(stage registerStage) string {
} }
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage { 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 stage == StageInputInstance || stage == StageInputToken {
if value == "" { if value == "" {
return stage return stage
} }
} }
// 如果运行器名称为空,设置为主机名 // set hostname for runner name if empty
if stage == StageInputRunnerName && value == "" { if stage == StageInputRunnerName && value == "" {
value, _ = os.Hostname() value, _ = os.Hostname()
} }
@ -171,19 +171,19 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
return StageInputRunnerName return StageInputRunnerName
case StageInputRunnerName: case StageInputRunnerName:
r.RunnerName = value r.RunnerName = value
// 如果配置文件中有标签配置,跳过输入标签阶段 // if there are some labels configured in config file, skip input labels stage
if len(cfg.Runner.Labels) > 0 { if len(cfg.Runner.Labels) > 0 {
ls := make([]string, 0, len(cfg.Runner.Labels)) ls := make([]string, 0, len(cfg.Runner.Labels))
for _, l := range cfg.Runner.Labels { for _, l := range cfg.Runner.Labels {
_, err := labels.Parse(l) _, err := labels.Parse(l)
if err != nil { if err != nil {
log.WithError(err).Warnf("忽略无效标签 %q", l) log.WithError(err).Warnf("ignored invalid label %q", l)
continue continue
} }
ls = append(ls, l) ls = append(ls, l)
} }
if len(ls) == 0 { if len(ls) == 0 {
log.Warn("配置文件中没有有效的标签配置,运行器可能无法接取作业") log.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
} }
r.Labels = ls r.Labels = ls
return StageWaitingForRegistration return StageWaitingForRegistration
@ -196,7 +196,7 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
} }
if validateLabels(r.Labels) != nil { if validateLabels(r.Labels) != nil {
log.Infoln("无效的标签, 请重新输入, 留空以使用默认标签 (例如, debian-latest:lcr.loongnix.cn/library/debian:latest) ") log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest)")
r.Labels = nil r.Labels = nil
return StageInputLabels return StageInputLabels
} }
@ -228,7 +228,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
cfg, err := config.LoadDefault(configFile) cfg, err := config.LoadDefault(configFile)
if err != nil { if err != nil {
return fmt.Errorf("加载配置失败: %v", err) return fmt.Errorf("failed to load config: %v", err)
} }
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() { if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
stage = StageOverwriteLocalConfig stage = StageOverwriteLocalConfig
@ -248,11 +248,11 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg) stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
if stage == StageWaitingForRegistration { if stage == StageWaitingForRegistration {
log.Infof("注册运行器,名称=%s, 实例=%s, 标签=%v", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels) log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
if err := doRegister(ctx, cfg, inputs); err != nil { if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("注册运行器失败: %w", err) return fmt.Errorf("Failed to register runner: %w", err)
} }
log.Infof("运行器注册成功。") log.Infof("Runner registered successfully.")
return nil return nil
} }
@ -261,7 +261,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
} }
if stage <= StageUnknown { if stage <= StageUnknown {
log.Errorf("无效输入,请重新运行命令。") log.Errorf("Invalid input, please re-run act command.")
return nil return nil
} }
} }
@ -270,18 +270,18 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
func printStageHelp(stage registerStage) { func printStageHelp(stage registerStage) {
switch stage { switch stage {
case StageOverwriteLocalConfig: case StageOverwriteLocalConfig:
log.Infoln("运行器已注册,是否覆盖本地配置?[y/N]") log.Infoln("Runner is already registered, overwrite local config? [y/N]")
case StageInputInstance: case StageInputInstance:
log.Infoln("请输入 Gitea 实例 URL(例如, https://gitea.com/):") log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
case StageInputToken: case StageInputToken:
log.Infoln("请输入运行器令牌:") log.Infoln("Enter the runner token:")
case StageInputRunnerName: case StageInputRunnerName:
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
log.Infof("请输入运行器名称(如果留空,使用主机名:%s):\n", hostname) log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
case StageInputLabels: case StageInputLabels:
log.Infoln("请输入运行器标签, 留空以使用默认标签(逗号分隔, 例如, debian-latest:docker://lcr.loongnix.cn/library/debian:latest):") log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):")
case StageWaitingForRegistration: case StageWaitingForRegistration:
log.Infoln("等待注册...") log.Infoln("Waiting for registration...")
} }
} }
@ -291,10 +291,10 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
return err return err
} }
inputs := initInputs(regArgs) inputs := initInputs(regArgs)
// 配置文件中指定的标签。 // specify labels in config file.
if len(cfg.Runner.Labels) > 0 { if len(cfg.Runner.Labels) > 0 {
if regArgs.Labels != "" { if regArgs.Labels != "" {
log.Warn("命令行中的标签将被忽略,使用配置文件中定义的标签。") log.Warn("Labels from command will be ignored, use labels defined in config file.")
} }
inputs.Labels = cfg.Runner.Labels inputs.Labels = cfg.Runner.Labels
} }
@ -304,21 +304,21 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
if inputs.RunnerName == "" { if inputs.RunnerName == "" {
inputs.RunnerName, _ = os.Hostname() inputs.RunnerName, _ = os.Hostname()
log.Infof("运行器名称为空,使用主机名 '%s'", inputs.RunnerName) log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
} }
if err := inputs.validate(); err != nil { if err := inputs.validate(); err != nil {
log.WithError(err).Errorf("无效输入,请重新运行命令。") log.WithError(err).Errorf("Invalid input, please re-run act command.")
return err return err
} }
if err := doRegister(ctx, cfg, inputs); err != nil { if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("注册运行器失败: %w", err) return fmt.Errorf("Failed to register runner: %w", err)
} }
log.Infof("运行器注册成功。") log.Infof("Runner registered successfully.")
return nil return nil
} }
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error { func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
// 初始化 http 客户端 // initial http client
cli := client.New( cli := client.New(
inputs.InstanceAddr, inputs.InstanceAddr,
cfg.Runner.Insecure, cfg.Runner.Insecure,
@ -341,11 +341,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
} }
if err != nil { if err != nil {
log.WithError(err). log.WithError(err).
Errorln("无法 ping Gitea 实例服务器") Errorln("Cannot ping the Gitea instance server")
// TODO: 如果 ping 失败,重试或退出 // TODO: if ping failed, retry or exit
time.Sleep(time.Second) time.Sleep(time.Second)
} else { } else {
log.Debugln("成功 ping 到 Gitea 实例服务器") log.Debugln("Successfully pinged the Gitea instance server")
break break
} }
} }
@ -363,17 +363,17 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
l, _ := labels.Parse(v) l, _ := labels.Parse(v)
ls[i] = l.Name ls[i] = l.Name
} }
// 注册新的运行器。 // register new runner.
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{ resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
Name: reg.Name, Name: reg.Name,
Token: reg.Token, Token: reg.Token,
Version: ver.Version(), Version: ver.Version(),
AgentLabels: ls, // Gitea 1.20 之后可能会被移除 AgentLabels: ls, // Could be removed after Gitea 1.20
Labels: ls, Labels: ls,
Ephemeral: reg.Ephemeral, Ephemeral: reg.Ephemeral,
})) }))
if err != nil { if err != nil {
log.WithError(err).Error("poller: 无法注册新运行器") log.WithError(err).Error("poller: cannot register new runner")
return err return err
} }
@ -383,12 +383,12 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
reg.Token = resp.Msg.Runner.Token reg.Token = resp.Msg.Runner.Token
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral { if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
// TODO 我们不能通过 runner api 移除配置,如果在这里返回错误,我们只是填充数据库 // TODO we cannot remove the configuration via runner api, if we return an error here we just fill the database
log.Error("poller: 无法注册新的临时运行器,升级 Gitea 以获得安全性,自动使用 run-once") log.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
} }
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil { if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
return fmt.Errorf("保存运行器配置失败: %w", err) return fmt.Errorf("failed to save runner config: %w", err)
} }
return nil return nil
} }

View File

@ -11,9 +11,9 @@ import (
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) { func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
err := registerNoInteractive(t.Context(), "", &registerArgs{ err := registerNoInteractive(t.Context(), "", &registerArgs{
Labels: "标签:无效", Labels: "label:invalid",
Token: "token", Token: "token",
InstanceAddr: "http://localhost:3000", InstanceAddr: "http://localhost:3000",
}) })
assert.Error(t, err, "不支持的标签: 无效") assert.Error(t, err, "unsupported schema: invalid")
} }

View File

@ -15,29 +15,26 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"git.whlug.cn/LAA/loong_runner/internal/app/run" "gitea.com/gitea/act_runner/internal/app/run"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client" "gitea.com/gitea/act_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/config"
) )
type Poller struct { type Poller struct {
client client.Client // Gitea 客户端,用于与服务器通信 client client.Client
runner *run.Runner // 任务执行器 runner *run.Runner
cfg *config.Config // 配置信息 cfg *config.Config
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
tasksVersion atomic.Int64 // 任务版本号,用于增量同步 pollingCtx context.Context
shutdownPolling context.CancelFunc
pollingCtx context.Context // 轮询上下文 jobsCtx context.Context
shutdownPolling context.CancelFunc // 轮询关闭函数 shutdownJobs context.CancelFunc
jobsCtx context.Context // 任务执行上下文 done chan struct{}
shutdownJobs context.CancelFunc // 任务执行关闭函数
done chan struct{} // 完成信号通道
} }
// New 构造函数,创建并初始化 Poller 实例
// 接受配置、客户端和服务运行器作为参数,并初始化必要的上下文和通道以便于后续操作。
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller { func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
pollingCtx, shutdownPolling := context.WithCancel(context.Background()) pollingCtx, shutdownPolling := context.WithCancel(context.Background())
@ -60,8 +57,6 @@ func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
} }
} }
// Poll 持续轮询模式
// 启动多个 goroutine 来并发地轮询任务。每个 goroutine 调用 poll 方法进行实际的工作。在所有工作完成之后,通过关闭 done 通道发出信号。
func (p *Poller) Poll() { func (p *Poller) Poll() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1) limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
@ -71,54 +66,48 @@ func (p *Poller) Poll() {
} }
wg.Wait() wg.Wait()
// 发出我们正在关闭的信号 // signal that we shutdown
close(p.done) close(p.done)
} }
// PollOnce 单次轮询模式
// 类似于 Poll,但是只执行一次任务轮询。同样会在完成后关闭 done 通道。
func (p *Poller) PollOnce() { func (p *Poller) PollOnce() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1) limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
p.pollOnce(limiter) p.pollOnce(limiter)
// 发出我们已经完成的信号 // signal that we're done
close(p.done) close(p.done)
} }
// Shutdown 关闭
// 关闭轮询过程。首先取消轮询上下文,然后等待所有任务完成。如果超时发生,则强制关闭所有正在运行的任务并确保状态报告给 Gitea。
func (p *Poller) Shutdown(ctx context.Context) error { func (p *Poller) Shutdown(ctx context.Context) error {
p.shutdownPolling() p.shutdownPolling()
select { select {
// graceful shutdown completed succesfully
// 优雅地完成关闭
case <-p.done: case <-p.done:
return nil return nil
// 关闭超时 // our timeout for shutting down ran out
case <-ctx.Done(): 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 _, ok := <-p.done
if !ok { if !ok {
return nil return nil
} }
// 强制关闭所有运行中的任务 // force a shutdown of all running jobs
p.shutdownJobs() p.shutdownJobs()
// 等待运行中的任务向Gitea报告其状态 // wait for running jobs to report their status to Gitea
_, _ = <-p.done _, _ = <-p.done
return ctx.Err() return ctx.Err()
} }
} }
// poll 轮询循环
// 这是实际执行轮询逻辑的地方。它会持续调用 pollOnce 方法来获取并处理任务,直到上下文被取消。
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) { func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
defer wg.Done() defer wg.Done()
for { for {
@ -133,13 +122,11 @@ func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
} }
} }
// pollOnce 单次轮询执行
// 负责单次任务轮询。它首先等待速率限制器允许继续,然后尝试获取一个新任务。一旦获取到任务,就调用 runTaskWithRecover 方法来执行任务。
func (p *Poller) pollOnce(limiter *rate.Limiter) { func (p *Poller) pollOnce(limiter *rate.Limiter) {
for { for {
if err := limiter.Wait(p.pollingCtx); err != nil { if err := limiter.Wait(p.pollingCtx); err != nil {
if p.pollingCtx.Err() != nil { if p.pollingCtx.Err() != nil {
log.WithError(err).Debug("速率限制等待失败") log.WithError(err).Debug("limiter wait failed")
} }
return return
} }
@ -153,30 +140,24 @@ func (p *Poller) pollOnce(limiter *rate.Limiter) {
} }
} }
// runTaskWithRecover 安全执行任务
// 执行给定的任务,并提供 panic 恢复机制以防止意外崩溃。如果任务执行过程中出现错误或 panic,都会记录相应的日志信息。
func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) { func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
// 建立错误恢复机制
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err := fmt.Errorf("panic: %v", r) err := fmt.Errorf("panic: %v", r)
log.WithError(err).Error("runTaskWithRecover中发生panic") log.WithError(err).Error("panic in runTaskWithRecover")
} }
}() }()
if err := p.runner.Run(ctx, task); err != nil { if err := p.runner.Run(ctx, task); err != nil {
log.WithError(err).Error("运行任务失败") log.WithError(err).Error("failed to run task")
} }
} }
// fetchTask 获取任务
// 尝试从 Gitea 获取一个新任务。如果成功获取到任务且该任务的版本号比本地缓存的大,则更新本地缓存的版本号。
// 如果获取失败或没有可用的任务,返回 false;否则返回 true 和任务数据。
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) { func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout) reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
defer cancel() defer cancel()
// 加载发送请求时缓存中的版本值 // Load the version value that was in the cache when the request was sent.
v := p.tasksVersion.Load() v := p.tasksVersion.Load()
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{ resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: v, TasksVersion: v,
@ -185,7 +166,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
err = nil err = nil
} }
if err != nil { if err != nil {
log.WithError(err).Error("获取任务失败") log.WithError(err).Error("failed to fetch task")
return nil, false return nil, false
} }
@ -201,7 +182,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
return nil, false return nil, false
} }
// 收到一个任务,将`tasksVersion`设置为零,以便在下一个请求中创建查询数据库。 // got a task, set `tasksVersion` to zero to focre query db in next request.
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0) p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
return resp.Msg.Task, true return resp.Msg.Task, true

View File

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

View File

@ -21,28 +21,26 @@ import (
"github.com/nektos/act/pkg/runner" "github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client" "gitea.com/gitea/act_runner/internal/pkg/client"
"git.whlug.cn/LAA/loong_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/config"
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels" "gitea.com/gitea/act_runner/internal/pkg/labels"
"git.whlug.cn/LAA/loong_runner/internal/pkg/report" "gitea.com/gitea/act_runner/internal/pkg/report"
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver" "gitea.com/gitea/act_runner/internal/pkg/ver"
) )
// Runner 运行流水线 // Runner runs the pipeline.
type Runner struct { type Runner struct {
name string // Runner 名称 name string
cfg *config.Config // 配置信息 cfg *config.Config
client client.Client // Gitea 客户端 client client.Client
labels labels.Labels // 标签集合 labels labels.Labels
envs map[string]string // 环境变量 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 { func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
ls := labels.Labels{} ls := labels.Labels{}
for _, v := range reg.Labels { for _, v := range reg.Labels {
@ -54,7 +52,6 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
for k, v := range cfg.Runner.Envs { for k, v := range cfg.Runner.Envs {
envs[k] = v envs[k] = v
} }
// 初始化缓存环境变量
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled { if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
if cfg.Cache.ExternalServer != "" { if cfg.Cache.ExternalServer != "" {
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
@ -66,20 +63,20 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
log.StandardLogger().WithField("module", "cache_request"), log.StandardLogger().WithField("module", "cache_request"),
) )
if err != nil { if err != nil {
log.Errorf("无法初始化缓存服务器,将禁用它:%v", err) log.Errorf("cannot init cache server, it will be disabled: %v", err)
// 继续执行 // go on
} else { } else {
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/" envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
} }
} }
} }
// 设置 artifact gitea api // set artifact gitea api
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/" artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/") envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/")
// 设置特定环境变量以区分 Gitea GitHub // Set specific environments to distinguish between Gitea and GitHub
envs["GITEA_ACTIONS"] = "true" envs["GITEA_ACTIONS"] = "true"
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version() envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
@ -92,16 +89,13 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
} }
} }
// Run 在给定的上下文中执行任务,确保同一时间仅运行一个任务
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error { func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
// 检查任务是否已在运行
if _, ok := r.runningTasks.Load(task.Id); ok { if _, ok := r.runningTasks.Load(task.Id); ok {
return fmt.Errorf("任务 %d 已经在运行", task.Id) return fmt.Errorf("task %d is already running", task.Id)
} }
r.runningTasks.Store(task.Id, struct{}{}) r.runningTasks.Store(task.Id, struct{}{})
defer r.runningTasks.Delete(task.Id) defer r.runningTasks.Delete(task.Id)
// 创建带超时的子上下文
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout) ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
defer cancel() defer cancel()
reporter := report.NewReporter(ctx, cancel, r.client, task) reporter := report.NewReporter(ctx, cancel, r.client, task)
@ -119,7 +113,17 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
return nil return nil
} }
// run 执行具体的任务逻辑 // getDefaultActionsURL
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
// it should be set to GithubMirror first.
func (r *Runner) getDefaultActionsURL(ctx context.Context, task *runnerv1.Task) string {
giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue()
if giteaDefaultActionsURL == "https://github.com" && r.cfg.Runner.GithubMirror != "" {
return r.cfg.Runner.GithubMirror
}
return giteaDefaultActionsURL
}
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) { func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -127,8 +131,7 @@ 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) workflow, jobID, err := generateWorkflow(task)
if err != nil { if err != nil {
@ -144,11 +147,10 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
taskContext := task.Context.Fields taskContext := task.Context.Fields
log.Infof("任务 %v 仓库是 %v %v %v", task.Id, taskContext["repository"].GetStringValue(), log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
taskContext["gitea_default_actions_url"].GetStringValue(), r.getDefaultActionsURL(ctx, task),
r.client.Address()) r.client.Address())
// 构建预设的 GitHub 上下文环境
preset := &model.GithubContext{ preset := &model.GithubContext{
Event: taskContext["event"].GetStructValue().AsMap(), Event: taskContext["event"].GetStructValue().AsMap(),
RunID: taskContext["run_id"].GetStringValue(), RunID: taskContext["run_id"].GetStringValue(),
@ -166,7 +168,6 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
RepositoryOwner: taskContext["repository_owner"].GetStringValue(), RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
RetentionDays: taskContext["retention_days"].GetStringValue(), RetentionDays: taskContext["retention_days"].GetStringValue(),
} }
// 优先使用 GITEA_TOKEN
if t := task.Secrets["GITEA_TOKEN"]; t != "" { if t := task.Secrets["GITEA_TOKEN"]; t != "" {
preset.Token = t preset.Token = t
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" { } else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
@ -181,7 +182,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue() giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
if giteaRuntimeToken == "" { if giteaRuntimeToken == "" {
// 兼容旧版本 Gitea Server // use task token to action api token for previous Gitea Server Versions
giteaRuntimeToken = preset.Token giteaRuntimeToken = preset.Token
} }
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
@ -196,8 +197,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
maxLifetime = time.Until(deadline) maxLifetime = time.Until(deadline)
} }
// 创建 Runner 配置
runnerConfig := &runner.Config{ 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)), Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
BindWorkdir: false, BindWorkdir: false,
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent), ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
@ -220,7 +222,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
ContainerOptions: r.cfg.Container.Options, ContainerOptions: r.cfg.Container.Options,
ContainerDaemonSocket: r.cfg.Container.DockerHost, ContainerDaemonSocket: r.cfg.Container.DockerHost,
Privileged: r.cfg.Container.Privileged, Privileged: r.cfg.Container.Privileged,
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(), DefaultActionInstance: r.getDefaultActionsURL(ctx, task),
PlatformPicker: r.labels.PickPlatform, PlatformPicker: r.labels.PickPlatform,
Vars: task.Vars, Vars: task.Vars,
ValidVolumes: r.cfg.Container.ValidVolumes, ValidVolumes: r.cfg.Container.ValidVolumes,
@ -233,9 +235,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
} }
executor := rr.NewPlanExecutor(plan) executor := rr.NewPlanExecutor(plan)
reporter.Logf("工作流程已准备就绪") reporter.Logf("workflow prepared")
// 添加日志记录器 // add logger recorders
ctx = common.WithLoggerHook(ctx, reporter) ctx = common.WithLoggerHook(ctx, reporter)
if !log.IsLevelEnabled(log.DebugLevel) { if !log.IsLevelEnabled(log.DebugLevel) {

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client {
return http.DefaultClient return http.DefaultClient
} }
// New返回一个新的runner客户端。 // New returns a new runner client.
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient { func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions" baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
@ -39,7 +39,7 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
if token != "" { if token != "" {
req.Header().Set(TokenHeader, token) req.Header().Set(TokenHeader, token)
} }
// TODOversion将在Gitea 1.20发布后从请求标头中删除。 // TODO: version will be removed from request header after Gitea 1.20 released.
if version != "" { if version != "" {
req.Header().Set(VersionHeader, version) req.Header().Set(VersionHeader, version)
} }
@ -73,7 +73,7 @@ func (c *HTTPClient) Insecure() bool {
var _ Client = (*HTTPClient)(nil) var _ Client = (*HTTPClient)(nil)
// HTTPClient管理与runner API的通信。 // An HTTPClient manages communication with the runner API.
type HTTPClient struct { type HTTPClient struct {
pingv1connect.PingServiceClient pingv1connect.PingServiceClient
runnerv1connect.RunnerServiceClient runnerv1connect.RunnerServiceClient

View File

@ -1,101 +1,110 @@
# 示例配置文件,可以安全地将其复制为默认配置文件而无需任何修改。 # 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,
# 只需运行 `./act_runner generate-config > config.yaml` 来生成配置文件。 # just run `./act_runner generate-config > config.yaml` to generate a config file.
log: log:
# 日志级别,可以是 tracedebuginfowarnerrorfatal # The level of logging, can be trace, debug, info, warn, error, fatal
level: info level: info
runner: runner:
# 存储注册结果的路径。 # Where to store the registration result.
file: .runner file: .runner
# 同时执行多少个任务。 # Execute how many tasks concurrently at the same time.
capacity: 1 capacity: 1
# 运行作业的额外环境变量。 # Extra environment variables to run jobs.
envs: envs:
A_TEST_ENV_NAME_1: a_test_env_value_1 A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2 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 env_file: .env
# 作业完成的超时时间。 # The timeout for a job to be finished.
# 请注意,Gitea 实例也有一个作业超时时间(默认为 3 小时)。 # Please note that the Gitea instance also has a timeout (3h by default) for the job.
# 如果这个超时时间比 Gitea 实例的超时时间短,作业可能会被 Gitea 实例停止。 # So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
timeout: 3h 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 shutdown_timeout: 0s
# 是否跳过验证 Gitea 实例的 TLS 证书。 # Whether skip verifying the TLS certificate of the Gitea instance.
insecure: false insecure: false
# 从 Gitea 实例获取作业的超时时间。 # The timeout for fetching the job from the Gitea instance.
fetch_timeout: 5s fetch_timeout: 5s
# 从 Gitea 实例获取作业的时间间隔。 # The interval for fetching the job from the Gitea instance.
fetch_interval: 2s fetch_interval: 2s
# 运行器的标签用于确定运行器可以运行哪些作业以及如何运行它们。 # The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
# 例如:"linux-loong64.abi2:host" 或 "debian-latest:docker://lcr.loongnix.cn/library/debian:latest" # It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
# 在 https://gitea.com/docker.gitea.com/runner-images 查找 Gitea 提供的更多镜像。 # and github_mirror is not empty. In this case,
# 如果在注册时为空,它会要求输入标签。 # it replaces https://github.com with the value here, which is useful for some special network environments.
# 如果在执行 `daemon` 时为空,将使用 `.runner` 文件中的标签。 github_mirror: ''
# 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://docker.gitea.com/runner-images:ubuntu-latest"
# Find more images provided by Gitea at https://gitea.com/docker.gitea.com/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.
labels: labels:
- "debian-latest:docker://lcr.loongnix.cn/library/debian:latest" - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest" - "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
- "anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2" - "ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04"
cache: cache:
# 启用缓存服务器以使用 actions/cache # Enable cache server to use actions/cache.
enabled: true enabled: true
# 存储缓存数据的目录。 # The directory to store the cache data.
# 如果为空,缓存数据将存储在 $HOME/.cache/actcache # If it's empty, the cache data will be stored in $HOME/.cache/actcache.
dir: "" dir: ""
# 缓存服务器的主机。 # The host of the cache server.
# 它不是用于监听的地址,而是用于作业容器连接的地址。 # It's not for the address to listen, but the address to connect from job containers.
# 所以 0.0.0.0 是一个糟糕的选择,留空以自动检测。 # So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
host: "" host: ""
# 缓存服务器的端口。 # The port of the cache server.
# 0 表示使用随机可用端口。 # 0 means to use a random available port.
port: 0 port: 0
# 外部缓存服务器 URL。仅在启用时有效。 # The external cache server URL. Valid only when enable is true.
# 如果指定了它,act_runner 将使用此 URL 作为 ACTIONS_CACHE_URL 而不是自己启动一个服务器。 # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
# URL 通常应该以 "/" 结尾。 # The URL should generally end with "/".
external_server: "" external_server: ""
container: container:
# 指定容器将连接的网络。 # Specifies the network to which the container will connect.
# 可以是 hostbridge 或自定义网络的名称。 # Could be host, bridge or the name of a custom network.
# 如果为空,act_runner 将自动创建一个网络。 # If it's empty, act_runner will create a network automatically.
network: "" network: ""
# 启动任务容器时是否使用特权模式(特权模式对于 Docker-in-Docker 是必需的)。 # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
privileged: false privileged: false
# 容器启动时使用的其他选项(例如,--add-host=my.gitea.url:host-gateway)。 # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
options: options:
# 作业工作目录的父目录。 # The parent directory of a job's working directory.
# 注意:不需要在路径前添加第一个 '/',因为 act_runner 会自动添加。 # 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.
# 例如,如果父目录是 /path/to/my/dirworkdir_parent 应该是 path/to/my/dir # For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
# 如果为空,将使用 /workspace。 # If it's empty, /workspace will be used.
workdir_parent: workdir_parent:
# 可以挂载到容器的卷(包括绑定挂载)。支持 glob 语法,参见 https://github.com/gobwas/glob # 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.
# 例如,如果您只允许容器挂载 `data` 卷和 `/src` 中的所有 json 文件,您应该将配置更改为: # 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:
# valid_volumes: # valid_volumes:
# - data # - data
# - /src/*.json # - /src/*.json
# 如果您想允许任何卷,请使用以下配置: # If you want to allow any volume, please use the following configuration:
# valid_volumes: # valid_volumes:
# - '**' # - '**'
valid_volumes: [] valid_volumes: []
# 用指定的主机覆盖 docker 客户端主机。 # overrides the docker client host with the specified one.
# 如果为空,act_runner 将自动查找可用的 docker 主机。 # If it's empty, act_runner will find an available docker host automatically.
# 如果是 "-"act_runner 将自动查找可用的 docker 主机,但 docker 主机不会挂载到作业容器和服务容器。 # 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.
# 如果不为空或 "-",将使用指定的 docker 主机。如果不起作用,将返回错误。 # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
docker_host: "" docker_host: ""
# 即使已经存在也拉取 Docker 镜像 # Pull docker image(s) even if already present
force_pull: true force_pull: true
# 即使已经存在也重建 Docker 镜像 # Rebuild docker image(s) even if already present
force_rebuild: false force_rebuild: false
# Always require a reachable docker daemon, even if not required by act_runner
require_docker: false
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
docker_timeout: 0s
host: host:
# 作业工作目录的父目录。 # The parent directory of a job's working directory.
# 如果为空,将使用 $HOME/.cache/act/ # If it's empty, $HOME/.cache/act/ will be used.
workdir_parent: workdir_parent:

View File

@ -14,72 +14,75 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// Log 代表日志的配置。 // Log represents the configuration for logging.
type Log struct { type Log struct {
Level string `yaml:"level"` // Level 表示日志级别。 Level string `yaml:"level"` // Level indicates the logging level.
} }
// Runner 代表运行器的配置。 // Runner represents the configuration for the runner.
type Runner struct { type Runner struct {
File string `yaml:"file"` // File 指定运行器的文件路径。 File string `yaml:"file"` // File specifies the file path for the runner.
Capacity int `yaml:"capacity"` // Capacity 指定运行器的容量。 Capacity int `yaml:"capacity"` // Capacity specifies the capacity of the runner.
Envs map[string]string `yaml:"envs"` // Envs 存储运行器的环境变量。 Envs map[string]string `yaml:"envs"` // Envs stores environment variables for the runner.
EnvFile string `yaml:"env_file"` // EnvFile 指定包含运行器环境变量的文件路径。 EnvFile string `yaml:"env_file"` // EnvFile specifies the path to the file containing environment variables for the runner.
Timeout time.Duration `yaml:"timeout"` // Timeout 指定运行器超时的持续时间。 Timeout time.Duration `yaml:"timeout"` // Timeout specifies the duration for runner timeout.
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout 指定在运行器关闭期间等待运行作业完成的持续时间。 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 表示运行器是否在不安全模式下运行。 Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout 指定获取资源的超时持续时间。 FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval 指定获取资源的间隔持续时间。 FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
Labels []string `yaml:"labels"` // Labels 指定运行器的标签。每个启动时声明标签。 Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
} }
// Cache 代表缓存的配置。 // Cache represents the configuration for caching.
type Cache struct { type Cache struct {
Enabled *bool `yaml:"enabled"` // Enabled 表示是否启用缓存。它是指针,用于区分 false 和未设置。如果未设置,它将为 true 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 指定缓存的目录路径。 Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
Host string `yaml:"host"` // Host 指定缓存主机。 Host string `yaml:"host"` // Host specifies the caching host.
Port uint16 `yaml:"port"` // Port 指定缓存端口。 Port uint16 `yaml:"port"` // Port specifies the caching port.
ExternalServer string `yaml:"external_server"` // ExternalServer 指定外部缓存服务器的 URL。 ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
} }
// Container 代表容器的配置。 // Container represents the configuration for the container.
type Container struct { type Container struct {
Network string `yaml:"network"` // Network 指定容器的网络。 Network string `yaml:"network"` // Network specifies the network for the container.
NetworkMode string `yaml:"network_mode"` // 已弃用:使用 Network 替代。可能在 Gitea 1.20 之后被移除。 NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
Privileged bool `yaml:"privileged"` // Privileged 表示容器是否以特权模式运行。 Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
Options string `yaml:"options"` // Options 指定容器的其他选项。 Options string `yaml:"options"` // Options specifies additional options for the container.
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定容器工作目录的父目录。 WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes 指定可以挂载到容器的卷(包括绑定挂载)。 ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers.
DockerHost string `yaml:"docker_host"` // DockerHost 指定 Docker 主机。它覆盖环境变量 DOCKER_HOST 中指定的值。 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"` // 即使已经存在也拉取 Docker 镜像。 ForcePull bool `yaml:"force_pull"` // Pull docker image(s) even if already present
ForceRebuild bool `yaml:"force_rebuild"` // 即使已经存在也重建 Docker 镜像。 ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present
RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by act_runner
DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
} }
// Host 代表主机的配置。 // Host represents the configuration for the host.
type Host struct { type Host struct {
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定主机工作目录的父目录。 WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the host's working directory.
} }
// Config 代表整体配置。 // Config represents the overall configuration.
type Config struct { type Config struct {
Log Log `yaml:"log"` // Log 代表日志的配置。 Log Log `yaml:"log"` // Log represents the configuration for logging.
Runner Runner `yaml:"runner"` // Runner 代表运行器的配置。 Runner Runner `yaml:"runner"` // Runner represents the configuration for the runner.
Cache Cache `yaml:"cache"` // Cache 代表缓存的配置。 Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
Container Container `yaml:"container"` // Container 代表容器的配置。 Container Container `yaml:"container"` // Container represents the configuration for the container.
Host Host `yaml:"host"` // Host 代表主机的配置。 Host Host `yaml:"host"` // Host represents the configuration for the host.
} }
// LoadDefault 返回默认配置。 // LoadDefault returns the default configuration.
// 如果文件不为空,它将被用来加载配置。 // If file is not empty, it will be used to load the configuration.
func LoadDefault(file string) (*Config, error) { func LoadDefault(file string) (*Config, error) {
cfg := &Config{} cfg := &Config{}
if file != "" { if file != "" {
content, err := os.ReadFile(file) content, err := os.ReadFile(file)
if err != nil { if err != nil {
return nil, fmt.Errorf("打开配置文件 %q: %w", file, err) return nil, fmt.Errorf("open config file %q: %w", file, err)
} }
if err := yaml.Unmarshal(content, cfg); err != nil { if err := yaml.Unmarshal(content, cfg); err != nil {
return nil, fmt.Errorf("解析配置文件 %q: %w", file, err) return nil, fmt.Errorf("parse config file %q: %w", file, err)
} }
} }
compatibleWithOldEnvs(file != "", cfg) compatibleWithOldEnvs(file != "", cfg)
@ -88,7 +91,7 @@ func LoadDefault(file string) (*Config, error) {
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() { if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
envs, err := godotenv.Read(cfg.Runner.EnvFile) envs, err := godotenv.Read(cfg.Runner.EnvFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("读取环境文件 %q: %w", cfg.Runner.EnvFile, err) return nil, fmt.Errorf("read env file %q: %w", cfg.Runner.EnvFile, err)
} }
if cfg.Runner.Envs == nil { if cfg.Runner.Envs == nil {
cfg.Runner.Envs = map[string]string{} cfg.Runner.Envs = map[string]string{}
@ -135,13 +138,14 @@ func LoadDefault(file string) (*Config, error) {
cfg.Runner.FetchInterval = 2 * time.Second cfg.Runner.FetchInterval = 2 * time.Second
} }
// 虽然 `container.network_mode` 将被弃用,但我们现在必须与它兼容。 // although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" { if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
log.Warn("您正在尝试使用已弃用的配置项 `container.network_mode`,请使用 `container.network` 替代。") log.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
if cfg.Container.NetworkMode == "bridge" { if cfg.Container.NetworkMode == "bridge" {
// 以前,如果 `container.network_mode` 的值是 `bridge`,我们会为作业创建一个新的网络。 // Previously, if the value of `container.network_mode` is `bridge`, we will create a new network for job.
// 但是,“bridge”很容易与 Docker 默认创建的桥接网络混淆。 // But “bridge” is easily confused with the bridge network created by Docker by default.
// 所以我们将 `container.network` 的值设置为空字符串,以使 `act_runner` 为作业自动创建一个新的网络。 // So we set the value of `container.network` to empty string to make `act_runner` automatically create a new network for job.
cfg.Container.Network = ""
} else { } else {
cfg.Container.Network = cfg.Container.NetworkMode cfg.Container.Network = cfg.Container.NetworkMode
} }

View File

@ -11,16 +11,16 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// 已弃用:未来可能会移除。TODO: 在 Gitea 1.20.0 发布时将其移除。 // Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
// 兼容旧环境。 // Be compatible with old envs.
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) { func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
handleEnv := func(key string) (string, bool) { handleEnv := func(key string) (string, bool) {
if v, ok := os.LookupEnv(key); ok { if v, ok := os.LookupEnv(key); ok {
if fileUsed { if fileUsed {
log.Warnf("环境 %s 已被忽略,因为使用了配置文件", key) log.Warnf("env %s has been ignored because config file is used", key)
return "", false return "", false
} }
log.Warnf("环境变量 %s 将被弃用,请改用配置文件。", key) log.Warnf("env %s will be deprecated, please use config file instead", key)
return v, true return v, true
} }
return "", false return "", false

View File

@ -8,19 +8,19 @@ import (
"os" "os"
) )
const registrationWarning = "此文件由`Loong Runner`自动生成。除非你知道自己在做什么, 否则不要手动编辑它。删除此文件将导致`Loong Runner`重新注册为新的运行器。" 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."
// Registration 表示运行器的注册信息 // Registration is the registration information for a runner
type Registration struct { type Registration struct {
Warning string `json:"WARNING"` // 警告信息,始终为 registrationWarning 常量 Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
ID int64 `json:"id"` // 唯一标识符(整数类型) ID int64 `json:"id"`
UUID string `json:"uuid"` // 全局唯一标识符(字符串类型) UUID string `json:"uuid"`
Name string `json:"name"` // 运行器名称 Name string `json:"name"`
Token string `json:"token"` // 认证令牌,用于与调度器通信 Token string `json:"token"`
Address string `json:"address"` // 运行器的网络地址(如IP或域名) Address string `json:"address"`
Labels []string `json:"labels"` // 运行器关联的标签列表(用于任务匹配) Labels []string `json:"labels"`
Ephemeral bool `json:"ephemeral"` // 是否为临时实例(完成任务后自动销毁) Ephemeral bool `json:"ephemeral"`
} }
func LoadRegistration(file string) (*Registration, error) { func LoadRegistration(file string) (*Registration, error) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Reporter 组件是负责任务执行状态报告和日志记录的核心模块
package report package report
@ -19,30 +18,30 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"git.whlug.cn/LAA/loong_runner/internal/pkg/client" "gitea.com/gitea/act_runner/internal/pkg/client"
) )
type Reporter struct { type Reporter struct {
ctx context.Context // 上下文,用于控制生命周期 ctx context.Context
cancel context.CancelFunc // 取消函数 cancel context.CancelFunc
closed bool // 是否已关闭
client client.Client // Gitea 客户端
clientM sync.Mutex // 客户端访问互斥锁
logOffset int // 日志偏移量 closed bool
logRows []*runnerv1.LogRow // 日志行缓存 client client.Client
logReplacer *strings.Replacer // 日志内容替换器 clientM sync.Mutex
oldnew []string // 需要替换的敏感信息
state *runnerv1.TaskState // 任务状态 logOffset int
stateMu sync.RWMutex // 状态访问读写锁 logRows []*runnerv1.LogRow
outputs sync.Map // 输出参数存储 logReplacer *strings.Replacer
oldnew []string
debugOutputEnabled bool // 是否启用调试输出 state *runnerv1.TaskState
stopCommandEndToken string // 停止命令结束标记 stateMu sync.RWMutex
outputs sync.Map
debugOutputEnabled bool
stopCommandEndToken string
} }
// NewReporter 构造函数 初始化日志脱敏规则、状态等信息
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter { func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
var oldnew []string var oldnew []string
if v := task.Context.Fields["token"].GetStringValue(); v != "" { if v := task.Context.Fields["token"].GetStringValue(); v != "" {
@ -73,7 +72,6 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
return rv return rv
} }
// ResetSteps 重置任务状态中的步骤状态数组,为即将开始的新任务做准备
func (r *Reporter) ResetSteps(l int) { func (r *Reporter) ResetSteps(l int) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@ -84,12 +82,10 @@ func (r *Reporter) ResetSteps(l int) {
} }
} }
// Levels 返回所有支持的日志级别(通常是为了兼容 logger 接口)
func (r *Reporter) Levels() []log.Level { func (r *Reporter) Levels() []log.Level {
return log.AllLevels return log.AllLevels
} }
// appendIfNotNil 通用函数,如果传入指针不为空,则将其添加到切片中。
func appendIfNotNil[T any](s []*T, v *T) []*T { func appendIfNotNil[T any](s []*T, v *T) []*T {
if v != nil { if v != nil {
return append(s, v) return append(s, v)
@ -97,7 +93,6 @@ func appendIfNotNil[T any](s []*T, v *T) []*T {
return s return s
} }
// 接收日志条目,处理日志内容,并更新任务状态。
func (r *Reporter) Fire(entry *log.Entry) error { func (r *Reporter) Fire(entry *log.Entry) error {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@ -148,6 +143,12 @@ func (r *Reporter) Fire(entry *log.Entry) error {
if step.StartedAt == nil { if step.StartedAt == nil {
step.StartedAt = timestamppb.New(timestamp) step.StartedAt = timestamppb.New(timestamp)
} }
// Force reporting log errors as raw output to prevent silent failures
if entry.Level == log.ErrorLevel {
entry.Data["raw_output"] = true
}
if v, ok := entry.Data["raw_output"]; ok { if v, ok := entry.Data["raw_output"]; ok {
if rawOutput, ok := v.(bool); ok && rawOutput { if rawOutput, ok := v.(bool); ok && rawOutput {
if row := r.parseLogRow(entry); row != nil { if row := r.parseLogRow(entry); row != nil {
@ -174,7 +175,6 @@ func (r *Reporter) Fire(entry *log.Entry) error {
return nil return nil
} }
// RunDaemon 定时运行后台任务,定期将缓存中的日志和状态上报给 Gitea
func (r *Reporter) RunDaemon() { func (r *Reporter) RunDaemon() {
if r.closed { if r.closed {
return return
@ -189,7 +189,6 @@ func (r *Reporter) RunDaemon() {
time.AfterFunc(time.Second, r.RunDaemon) time.AfterFunc(time.Second, r.RunDaemon)
} }
// 自定义日志记录方法,格式化输出并保存到 logRows 中
func (r *Reporter) Logf(format string, a ...interface{}) { func (r *Reporter) Logf(format string, a ...interface{}) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@ -197,7 +196,6 @@ func (r *Reporter) Logf(format string, a ...interface{}) {
r.logf(format, a...) r.logf(format, a...)
} }
// 内部使用的日志记录方法,仅在任务执行期间记录日志
func (r *Reporter) logf(format string, a ...interface{}) { func (r *Reporter) logf(format string, a ...interface{}) {
if !r.duringSteps() { if !r.duringSteps() {
r.logRows = append(r.logRows, &runnerv1.LogRow{ r.logRows = append(r.logRows, &runnerv1.LogRow{
@ -207,19 +205,18 @@ func (r *Reporter) logf(format string, a ...interface{}) {
} }
} }
// SetOutputs 设置输出参数,过滤无效或超限的键值对
func (r *Reporter) SetOutputs(outputs map[string]string) { func (r *Reporter) SetOutputs(outputs map[string]string) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
for k, v := range outputs { for k, v := range outputs {
if len(k) > 255 { if len(k) > 255 {
r.logf("忽略超长键值对的键: %q", k) r.logf("ignore output because the key is too long: %q", k)
continue continue
} }
if l := len(v); l > 1024*1024 { if l := len(v); l > 1024*1024 {
log.Println("忽略超长键值对的值:", k, l) log.Println("ignore output because the value is too long:", k, l)
r.logf("忽略超长键值对的值 %q: %d 字节", k, l) r.logf("ignore output because the value %q is too long: %d", k, l)
} }
if _, ok := r.outputs.Load(k); ok { if _, ok := r.outputs.Load(k); ok {
continue continue
@ -228,7 +225,6 @@ func (r *Reporter) SetOutputs(outputs map[string]string) {
} }
} }
// Close 关闭 reporter,确保最后一批日志和最终状态被上报
func (r *Reporter) Close(lastWords string) error { func (r *Reporter) Close(lastWords string) error {
r.closed = true r.closed = true
@ -264,7 +260,6 @@ func (r *Reporter) Close(lastWords string) error {
}, retry.Context(r.ctx)) }, retry.Context(r.ctx))
} }
// ReportLog 将当前缓存的日志批量上报到 Gitea
func (r *Reporter) ReportLog(noMore bool) error { func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock() r.clientM.Lock()
defer r.clientM.Unlock() defer r.clientM.Unlock()
@ -285,7 +280,7 @@ func (r *Reporter) ReportLog(noMore bool) error {
ack := int(resp.Msg.AckIndex) ack := int(resp.Msg.AckIndex)
if ack < r.logOffset { if ack < r.logOffset {
return fmt.Errorf("已提交的日志丢失了") return fmt.Errorf("submitted logs are lost")
} }
r.stateMu.Lock() r.stateMu.Lock()
@ -294,13 +289,12 @@ func (r *Reporter) ReportLog(noMore bool) error {
r.stateMu.Unlock() r.stateMu.Unlock()
if noMore && ack < r.logOffset+len(rows) { if noMore && ack < r.logOffset+len(rows) {
return fmt.Errorf("并非所有日志都已提交") return fmt.Errorf("not all logs are submitted")
} }
return nil return nil
} }
// ReportState 将当前任务状态上报到 Gitea
func (r *Reporter) ReportState() error { func (r *Reporter) ReportState() error {
r.clientM.Lock() r.clientM.Lock()
defer r.clientM.Unlock() defer r.clientM.Unlock()
@ -341,13 +335,12 @@ func (r *Reporter) ReportState() error {
return true return true
}) })
if len(noSent) > 0 { if len(noSent) > 0 {
return fmt.Errorf("仍有一些输出尚未发送: %v", noSent) return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
} }
return nil return nil
} }
// duringSteps 判断当前是否正在执行某个步骤
func (r *Reporter) duringSteps() bool { func (r *Reporter) duringSteps() bool {
if steps := r.state.Steps; len(steps) == 0 { if steps := r.state.Steps; len(steps) == 0 {
return false return false
@ -366,12 +359,11 @@ var stringToResult = map[string]runnerv1.Result{
"cancelled": runnerv1.Result_RESULT_CANCELLED, "cancelled": runnerv1.Result_RESULT_CANCELLED,
} }
// parseResult 将字符串或 Stringer 类型的结果解析为 runnerv1.Result 类型
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) { func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
str := "" str := ""
if v, ok := result.(string); ok { // 对于作业结果 if v, ok := result.(string); ok { // for jobResult
str = v str = v
} else if v, ok := result.(fmt.Stringer); ok { // 对于步骤结果 } else if v, ok := result.(fmt.Stringer); ok { // for stepResult
str = v.String() str = v.String()
} }
@ -379,10 +371,8 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
return ret, ok return ret, ok
} }
// 匹配 GitHub Actions/Gitea Actions 特殊命令,如 ::set-output::, ::add-mask:: 等
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`) var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
// handleCommand 处理日志中的特殊指令命令(如 ::add-mask::),根据命令执行对应操作
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string { func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken { if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
return &originalContent return &originalContent
@ -399,19 +389,20 @@ func (r *Reporter) handleCommand(originalContent, command, parameters, value str
return nil return nil
case "notice": case "notice":
// 尚未实现,因此仅返回原始内容。 // Not implemented yet, so just return the original content.
return &originalContent return &originalContent
case "warning": case "warning":
// 尚未实现,因此仅返回原始内容。 // Not implemented yet, so just return the original content.
return &originalContent return &originalContent
case "error": case "error":
// 尚未实现,因此仅返回原始内容。 // Not implemented yet, so just return the original content.
return &originalContent return &originalContent
case "group": case "group":
// 返回原始内容,因为我认为前端在渲染输出时会使用它。 // Returning the original content, because I think the frontend
// will use it when rendering the output.
return &originalContent return &originalContent
case "endgroup": case "endgroup":
// 同上 // Ditto
return &originalContent return &originalContent
case "stop-commands": case "stop-commands":
r.stopCommandEndToken = value r.stopCommandEndToken = value
@ -423,7 +414,6 @@ func (r *Reporter) handleCommand(originalContent, command, parameters, value str
return &originalContent return &originalContent
} }
// parseLogRow 将日志条目转换为 LogRow 结构,用于后续上报
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow { func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' }) content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
@ -444,7 +434,6 @@ func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
} }
} }
// addMask 将指定字符串加入脱敏列表,之后所有日志中出现该字符串都会被替换成 ***
func (r *Reporter) addMask(msg string) { func (r *Reporter) addMask(msg string) {
r.oldnew = append(r.oldnew, msg, "***") r.oldnew = append(r.oldnew, msg, "***")
r.logReplacer = strings.NewReplacer(r.oldnew...) r.logReplacer = strings.NewReplacer(r.oldnew...)

View File

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

View File

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

View File

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

View File

@ -1,22 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 如果 /data 目录不存在,则创建它
if [[ ! -d /data ]]; then if [[ ! -d /data ]]; then
mkdir -p /data mkdir -p /data
fi fi
cd /data cd /data
# 运行器状态文件,默认为 '.runner'
RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-'.runner'} RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-'.runner'}
CONFIG_ARG="" CONFIG_ARG=""
# 如果设置了 CONFIG_FILE,则添加配置文件参数
if [[ ! -z "${CONFIG_FILE}" ]]; then if [[ ! -z "${CONFIG_FILE}" ]]; then
CONFIG_ARG="--config ${CONFIG_FILE}" CONFIG_ARG="--config ${CONFIG_FILE}"
fi fi
EXTRA_ARGS="" EXTRA_ARGS=""
# 如果设置了 GITEA_RUNNER_LABELS,则添加标签参数
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
EXTRA_ARGS="${EXTRA_ARGS} --labels ${GITEA_RUNNER_LABELS}" EXTRA_ARGS="${EXTRA_ARGS} --labels ${GITEA_RUNNER_LABELS}"
fi fi
@ -28,22 +24,22 @@ if [[ ! -z "${GITEA_RUNNER_ONCE}" ]]; then
RUN_ARGS="${RUN_ARGS} --once" RUN_ARGS="${RUN_ARGS} --once"
fi fi
# 如果没有设置令牌,可以从文件中读取令牌,例如从 Docker Secret # In case no token is set, it's possible to read the token from a file, i.e. a Docker Secret
if [[ -z "${GITEA_RUNNER_REGISTRATION_TOKEN}" ]] && [[ -f "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}" ]]; then if [[ -z "${GITEA_RUNNER_REGISTRATION_TOKEN}" ]] && [[ -f "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}" ]]; then
GITEA_RUNNER_REGISTRATION_TOKEN=$(cat "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}") GITEA_RUNNER_REGISTRATION_TOKEN=$(cat "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}")
fi fi
# 使用与 https://github.com/vegardit/docker-gitea-act-runner 相同的 ENV 变量名称 # Use the same ENV variable names as https://github.com/vegardit/docker-gitea-act-runner
test -f "$RUNNER_STATE_FILE" || echo "$RUNNER_STATE_FILE 丢失或不是常规文件" test -f "$RUNNER_STATE_FILE" || echo "$RUNNER_STATE_FILE is missing or not a regular file"
# 如果运行器状态文件不存在或为空
if [[ ! -s "$RUNNER_STATE_FILE" ]]; then if [[ ! -s "$RUNNER_STATE_FILE" ]]; then
try=$((try + 1)) try=$((try + 1))
success=0 success=0
# 此循环的目的是使其简单,当在 docker 中同时运行 act_runner gitea 时, # The point of this loop is to make it simple, when running both act_runner and gitea in docker,
# 使 act_runner 在出错之前等待一段时间以等待 gitea 变为可用。在单个 docker-compose 的上下文中, # 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.
while [[ $success -eq 0 ]] && [[ $try -lt ${GITEA_MAX_REG_ATTEMPTS:-10} ]]; do while [[ $success -eq 0 ]] && [[ $try -lt ${GITEA_MAX_REG_ATTEMPTS:-10} ]]; do
act_runner register \ act_runner register \
--instance "${GITEA_INSTANCE_URL}" \ --instance "${GITEA_INSTANCE_URL}" \
@ -51,20 +47,18 @@ if [[ ! -s "$RUNNER_STATE_FILE" ]]; then
--name "${GITEA_RUNNER_NAME:-`hostname`}" \ --name "${GITEA_RUNNER_NAME:-`hostname`}" \
${CONFIG_ARG} ${EXTRA_ARGS} --no-interactive 2>&1 | tee /tmp/reg.log ${CONFIG_ARG} ${EXTRA_ARGS} --no-interactive 2>&1 | tee /tmp/reg.log
# 检查输出中是否有成功注册的消息
cat /tmp/reg.log | grep 'Runner registered successfully' > /dev/null cat /tmp/reg.log | grep 'Runner registered successfully' > /dev/null
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
echo "成功" echo "SUCCESS"
success=1 success=1
else else
echo "等待重试..." echo "Waiting to retry ..."
sleep 5 sleep 5
fi fi
done done
fi fi
# 防止从 act_runner 进程中读取令牌 # Prevent reading the token from the act_runner process
unset GITEA_RUNNER_REGISTRATION_TOKEN unset GITEA_RUNNER_REGISTRATION_TOKEN
unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE
# 启动 act_runner 守护进程
exec act_runner daemon ${CONFIG_ARG} ${RUN_ARGS} exec act_runner daemon ${CONFIG_ARG} ${RUN_ARGS}

View File

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