mirror of
https://gitea.com/gitea/act_runner.git
synced 2025-09-05 05:42:03 +08:00
Compare commits
164 Commits
Author | SHA1 | Date | |
---|---|---|---|
f58a6daa6c | |||
54308690d0 | |||
4b90fa4325 | |||
5302c25feb | |||
a616ed1a10 | |||
f0b5aff3bb | |||
44b4736703 | |||
e9c297600c | |||
19bcf7e525 | |||
e5259a8fb7 | |||
9bed659875 | |||
b1ae30dda8 | |||
0d687268c7 | |||
425a570261 | |||
4c8179ee12 | |||
5ae13f0bd7 | |||
3510152e36 | |||
8dfb805c62 | |||
a7080f5457 | |||
8b72d1c7ae | |||
8bc0275e74 | |||
0348aaac59 | |||
9712481bed | |||
b5f901b2d9 | |||
0e2a3e00f5 | |||
b282356e9e | |||
b075e3a1d5 | |||
e27189ea32 | |||
59e478464e | |||
e1c7b20898 | |||
2f78411c3d | |||
d1d3cad4b0 | |||
1735b26e66 | |||
65ed62d2f5 | |||
ec03f19650 | |||
8567324a19 | |||
be2df361ef | |||
a5085dde0c | |||
cef86d1140 | |||
94c45acf6b | |||
1760899d27 | |||
a7eca813ea | |||
23ec12b8cf | |||
a1fc2b3ca7 | |||
5977042b86 | |||
6bc19cbc33 | |||
75006a59cc | |||
4da97f53de | |||
45270656df | |||
e14f42c40a | |||
e6630e2e36 | |||
f1f9142a3c | |||
f17cad1bbe | |||
daf52d0e62 | |||
8c8a8ce401 | |||
08c681be0c | |||
91bfe4c186 | |||
825c6f97b7 | |||
2f3e5c7125 | |||
4d9de6ca8c | |||
feb39666cc | |||
61cd71c9f8 | |||
0adfc1c7cc | |||
e3c68668fa | |||
f1b27d5274 | |||
655a39fd61 | |||
cca7d54117 | |||
934471813a | |||
1e940f028b | |||
2020ce79bf | |||
00e9b3d62b | |||
130b9f1877 | |||
4c35288175 | |||
990db1bfc0 | |||
d07fbfc8c3 | |||
10dc6fb60d | |||
ed35b09b8f | |||
03f0829d09 | |||
7fc1b91ba6 | |||
82c3c2df1a | |||
9fc823e4b1 | |||
12999b61dd | |||
49a2fcc138 | |||
8f88e4f15a | |||
a1bb3b56fd | |||
1a7ec5f339 | |||
5d01cb8904 | |||
dcf84d8a53 | |||
73adae040d | |||
db662b3690 | |||
cf92a979e2 | |||
87058716fb | |||
c701ba4787 | |||
57ff1df6e0 | |||
3dcfd6ea3d | |||
c6006ee699 | |||
f2629f2ea3 | |||
cf48ed88ba | |||
ccc27329dc | |||
b0bd503b11 | |||
8c14933e70 | |||
34d15f21c2 | |||
32d29f0813 | |||
2e2c0400c8 | |||
054c8d912f | |||
9e4a5f7363 | |||
ec38401097 | |||
45bfe0a9b2 | |||
316534996a | |||
67b1363d25 | |||
946c41cf4f | |||
341d49a24d | |||
b21d476aca | |||
a29307a9d9 | |||
4bfbfec477 | |||
fed01c9807 | |||
a83f29d5a9 | |||
69c55ee003 | |||
01ef57c667 | |||
a384adbbc6 | |||
e3271d8469 | |||
84386c1b16 | |||
fd7c8580af | |||
35596a182b | |||
c9d3f67264 | |||
94031fc198 | |||
d5e4baed54 | |||
d4caa7e065 | |||
de4160b023 | |||
609c0a0773 | |||
0c029f7e79 | |||
eef3c32eb2 | |||
c40b651873 | |||
b498341857 | |||
0d727eb262 | |||
7c71c94366 | |||
49d2cb0cb5 | |||
85626b6bbd | |||
35400f76fa | |||
0cf31b2d22 | |||
c8cc7b2448 | |||
3be962cdb3 | |||
a5edbc9ac4 | |||
66bab3d805 | |||
293926f5d5 | |||
43c5ba923f | |||
acc5afc428 | |||
27a1a90d25 | |||
83ec0ba909 | |||
ed86e2f15a | |||
d4bebccc12 | |||
c75b67e892 | |||
bc6031eff7 | |||
c69c353d93 | |||
fcc016e9b3 | |||
d5caee38f2 | |||
9e26208e13 | |||
a05c5ba3ad | |||
c248520a66 | |||
10d639cc6b | |||
5a8134410d | |||
b79c3aa1a3 | |||
9c6499ec08 | |||
d139faa40c |
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{go}]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
@ -1,26 +1,26 @@
|
||||
name: goreleaser
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --force --tags
|
||||
- uses: actions/setup-go@v3
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.20.1'
|
||||
go-version-file: "go.mod"
|
||||
- name: goreleaser
|
||||
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --nightly
|
||||
distribution: goreleaser-pro
|
||||
args: release --nightly
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
@ -28,3 +28,74 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: "gitea"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
DOCKER_ORG: gitea
|
||||
DOCKER_LATEST: nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: basic
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
|
||||
- name: Build and push dind
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
|
||||
|
||||
- name: Build and push dind-rootless
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind-rootless
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless
|
||||
|
111
.gitea/workflows/release-tag.yml
Normal file
111
.gitea/workflows/release-tag.yml
Normal file
@ -0,0 +1,111 @@
|
||||
name: release-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
args: release
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: "gitea"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
DOCKER_ORG: gitea
|
||||
DOCKER_LATEST: latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: basic
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
|
||||
- name: Build and push dind
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
|
||||
|
||||
- name: Build and push dind-rootless
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind-rootless
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind-rootless
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless
|
@ -1,43 +1,20 @@
|
||||
name: checks
|
||||
on:
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
env:
|
||||
GOPROXY: https://goproxy.io,direct
|
||||
GOPATH: /go_path
|
||||
GOCACHE: /go_cache
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: check and test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cache go path
|
||||
id: cache-go-path
|
||||
uses: https://github.com/actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
path: /go_path
|
||||
key: go_path-${{ github.repository }}-${{ github.ref_name }}
|
||||
restore-keys: |
|
||||
go_path-${{ github.repository }}-
|
||||
go_path-
|
||||
- name: cache go cache
|
||||
id: cache-go-cache
|
||||
uses: https://github.com/actions/cache@v3
|
||||
with:
|
||||
path: /go_cache
|
||||
key: go_cache-${{ github.repository }}-${{ github.ref_name }}
|
||||
restore-keys: |
|
||||
go_cache-${{ github.repository }}-
|
||||
go_cache-
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.20
|
||||
- uses: actions/checkout@v3
|
||||
go-version-file: 'go.mod'
|
||||
- name: vet checks
|
||||
run: make vet
|
||||
- name: build
|
||||
run: make build
|
||||
- name: test
|
||||
run: make test
|
||||
run: make test
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,10 +1,17 @@
|
||||
act_runner
|
||||
/act_runner
|
||||
/loong_runner
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
/gitea-vet
|
||||
/config.yaml
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
# MS VSCode
|
||||
.vscode
|
||||
__debug_bin
|
||||
# gorelease binary folder
|
||||
dist
|
||||
# vim
|
||||
*.sw*
|
||||
|
@ -1,14 +1,11 @@
|
||||
linters:
|
||||
enable:
|
||||
- gosimple
|
||||
- deadcode
|
||||
- typecheck
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- structcheck
|
||||
- varcheck
|
||||
- dupl
|
||||
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
|
||||
- gofmt
|
||||
@ -112,7 +109,6 @@ issues:
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
- deadcode
|
||||
text: "swagger"
|
||||
- path: contrib/pr/checkout.go
|
||||
linters:
|
||||
@ -154,9 +150,6 @@ issues:
|
||||
- path: cmd/dump.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: services/webhook/webhook.go
|
||||
linters:
|
||||
- structcheck
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
|
@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@ -14,6 +16,8 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- s390x
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
@ -58,7 +62,7 @@ builds:
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
- -s -w -X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- .Version }}-
|
||||
@ -71,9 +75,8 @@ builds:
|
||||
no_unique_dist_dir: true
|
||||
hooks:
|
||||
post:
|
||||
- cmd: tar -cJf {{ .Path }}.xz {{ .Path }}
|
||||
env:
|
||||
- XZ_OPT=-9
|
||||
- cmd: xz -k -9 {{ .Path }}
|
||||
dir: ./dist/
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||
|
||||
@ -82,7 +85,7 @@ blobs:
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
folder: "act_runner/{{.Version}}"
|
||||
directory: "act_runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
@ -98,10 +101,19 @@ checksum:
|
||||
- glob: ./**.xz
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Branch }}-devel"
|
||||
version_template: "{{ .Branch }}-devel"
|
||||
|
||||
nightly:
|
||||
name_template: "{{ .Branch }}"
|
||||
version_template: "nightly"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
download: https://gitea.com
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.xz.sha256
|
||||
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@ -0,0 +1,54 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-}
|
||||
|
||||
COPY . /opt/src/act_runner
|
||||
WORKDIR /opt/src/act_runner
|
||||
|
||||
RUN make clean && make build
|
||||
|
||||
FROM docker:dind AS dind
|
||||
|
||||
RUN apk add --no-cache s6 bash git
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
|
||||
FROM docker:dind-rootless AS dind-rootless
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache s6 bash git
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
VOLUME /data
|
||||
|
||||
RUN mkdir -p /data && chown -R rootless:rootless /etc/s6 /data
|
||||
|
||||
ENV DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
|
||||
USER rootless
|
||||
ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
|
||||
FROM alpine AS basic
|
||||
RUN apk add --no-cache tini bash git
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
|
||||
VOLUME /var/run/docker.sock
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["/sbin/tini","--","run.sh"]
|
31
Makefile
31
Makefile
@ -1,5 +1,5 @@
|
||||
DIST := dist
|
||||
EXECUTABLE := act_runner
|
||||
EXECUTABLE := loong_runner
|
||||
GOFMT ?= gofumpt -l
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
@ -7,7 +7,7 @@ GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
XGO_VERSION := go-1.18.x
|
||||
XGO_VERSION := go-1.24.x
|
||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||
@ -16,6 +16,11 @@ WINDOWS_ARCHS ?= windows/amd64
|
||||
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||
|
||||
DOCKER_IMAGE ?= gitea/loong_runner
|
||||
DOCKER_TAG ?= nightly
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||
|
||||
ifneq ($(shell uname), Darwin)
|
||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||
else
|
||||
@ -61,8 +66,11 @@ else
|
||||
endif
|
||||
endif
|
||||
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
|
||||
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
|
||||
LDFLAGS ?= -X "git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
|
||||
all: build
|
||||
|
||||
@ -78,7 +86,7 @@ go-check:
|
||||
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||
echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||
echo "Act Runner 需要 Go $(MIN_GO_VERSION_STR) 或更高版本才能构建。您可以从 https://go.dev/dl/ 获取。"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@ -89,19 +97,19 @@ fmt-check:
|
||||
fi
|
||||
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make fmt' and commit the result:"; \
|
||||
echo "请运行'make fmt'并提交结果"; \
|
||||
echo "$${diff}"; \
|
||||
exit 1; \
|
||||
fi;
|
||||
|
||||
test: fmt-check
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo -e "\n===> \e[32mOk\e[m\n" || exit 1
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@echo "运行 go vet..."
|
||||
@$(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet ./...
|
||||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
|
||||
|
||||
install: $(GOFILES)
|
||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||
@ -156,6 +164,13 @@ release-check: | $(DIST_DIRS)
|
||||
release-compress: | $(DIST_DIRS)
|
||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
if ! docker buildx version >/dev/null 2>&1; then \
|
||||
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||
fi; \
|
||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||
|
||||
clean:
|
||||
$(GO) clean -x -i ./...
|
||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
||||
|
111
README.md
111
README.md
@ -1,84 +1,111 @@
|
||||
# act runner
|
||||
# Loong Runner
|
||||
|
||||
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act).
|
||||
Loong Runner 是基于 [Gitea派生(fock)的act](https://gitea.com/gitea/act) 的 [二次派生](https://git.whlug.cn/LoongArchActions/loong_runner) 主要适用于当前龙架构的运行时。
|
||||
|
||||
## Installation
|
||||
> 上游的Gitea派生是可以直接在龙架构上编译使用的, 此派生仓库主要是解决上游的Docker是基于乌班图系统制作的, 目前龙架构暂无乌班图系统
|
||||
> * 当前计划使用`Debian`来代替乌班图用于构建容器镜像
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
||||
## 安装
|
||||
|
||||
### Download pre-built binary
|
||||
### 前提条件
|
||||
|
||||
Visit https://dl.gitea.com/act_runner/ and download the right version for your platform.
|
||||
在 Docker 模式下需要 `Docker Engine Community` 版本。要安装 `Docker CE`,请遵循官方 [安装说明](https://docs.docker.com/engine/install/)。
|
||||
|
||||
### Build from source
|
||||
### 下载预构建的二进制文件
|
||||
|
||||
访问 [这里](https://git.whlug.cn/LoongArchActions/loong_runner) 仅提供龙架构版本。
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Register
|
||||
### 构建 Docker 容器镜像
|
||||
|
||||
```bash
|
||||
./act_runner register
|
||||
make docker
|
||||
```
|
||||
|
||||
And you will be asked to input:
|
||||
## 快速入门
|
||||
|
||||
1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
|
||||
and you should not use `localhost` or `127.0.0.1` as instance IP;
|
||||
2. Runner token, you can get it from `http://192.168.8.8:3000/admin/runners`;
|
||||
3. Runner name, you can just leave it blank;
|
||||
4. Runner labels, you can just leave it blank.
|
||||
默认情况下,操作是禁用的,因此您需要在 Gitea 实例的配置文件中添加以下内容以启用它:
|
||||
|
||||
The process looks like:
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED=true
|
||||
```
|
||||
|
||||
### 注册
|
||||
|
||||
```bash
|
||||
./loong_runner register
|
||||
```
|
||||
|
||||
系统将提示您输入:
|
||||
|
||||
1. Gitea 实例 URL,例如 `http://192.168.8.8:3000/`。您应该使用您的 Gitea 实例的 ROOT_URL 作为实例参数,并且不应使用 `localhost` 或 `127.0.0.1` 作为实例 IP;
|
||||
2. 运行器令牌,您可以从 `http://192.168.8.8:3000/admin/actions/runners` 获取;
|
||||
3. 运行器名称,您可以留空;
|
||||
4. 运行器标签,您可以留空。
|
||||
|
||||
过程如下:
|
||||
|
||||
```text
|
||||
INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
|
||||
WARN Runner in user-mode.
|
||||
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
|
||||
INFO 注册运行器,架构=loong64,操作系统=linux,版本=0.1.5。
|
||||
WARN 运行器处于用户模式。
|
||||
INFO 输入 Gitea 实例 URL(例如,https://gitea.com/):
|
||||
http://192.168.8.8:3000/
|
||||
INFO Enter the runner token:
|
||||
INFO 输入运行器令牌:
|
||||
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
||||
INFO Enter the runner name (if set empty, use hostname: Test.local):
|
||||
INFO 输入运行器名称(如果设置为空,则使用主机名:Test.local):
|
||||
|
||||
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):
|
||||
INFO 输入运行器标签,留空以使用默认标签(逗号分隔,例如,debian-latest:docker://docker.gitea.com/runner-images:latest)
|
||||
|
||||
INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://node:16-bullseye ubuntu-22.04:docker://node:16-bullseye ubuntu-20.04:docker://node:16-bullseye ubuntu-18.04:docker://node:16-buster].
|
||||
DEBU Successfully pinged the Gitea instance server
|
||||
INFO Runner registered successfully.
|
||||
INFO 注册运行器,名称=Test.local,实例=http://192.168.8.8:3000/,标签=[debian-latest:docker://docker.gitea.com/runner-images:latest debian-sid:docker://docker.gitea.com/runner-images:debian-sid anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest]。
|
||||
DEBU 成功 ping 到 Gitea 实例服务器
|
||||
INFO 运行器注册成功。
|
||||
```
|
||||
|
||||
You can also register with command line arguments.
|
||||
您也可以使用命令行参数进行注册。
|
||||
|
||||
```bash
|
||||
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
./loong_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
```
|
||||
|
||||
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
||||
如果注册成功,它将立即运行。下次,您可以直接运行运行器。
|
||||
|
||||
### Run
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
./act_runner daemon
|
||||
./loong_runner daemon
|
||||
```
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
You can also configure the runner with a configuration file.
|
||||
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
|
||||
### 使用 Docker 运行
|
||||
|
||||
```bash
|
||||
./act_runner generate-config > config.yaml
|
||||
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
|
||||
```
|
||||
|
||||
You can specify the configuration file path with `-c`/`--config` argument.
|
||||
### 配置
|
||||
|
||||
您还可以使用配置文件配置运行器。
|
||||
配置文件是一个 YAML 文件,您可以使用 `./loong_runner generate-config` 生成一个示例配置文件。
|
||||
|
||||
```bash
|
||||
./act_runner -c config.yaml register # register with config file
|
||||
./act_runner -c config.yaml deamon # run with config file
|
||||
./loong_runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
您可以使用 `-c`/`--config` 参数指定配置文件路径。
|
||||
|
||||
```bash
|
||||
./loong_runner -c config.yaml register # 使用配置文件注册
|
||||
./loong_runner -c config.yaml daemon # 使用配置文件运行
|
||||
```
|
||||
|
||||
您可以在 [config.example.yaml](internal/pkg/config/config.example.yaml) 上在线查看配置文件的最新版本。
|
||||
|
||||
### 示例部署
|
||||
|
||||
查看 [examples](examples) 目录中的示例部署类型。
|
12
examples/README.md
Normal file
12
examples/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# `loong_runner` 使用示例
|
||||
|
||||
欢迎来到我们专为 Gitea 设置设计的使用和部署示例集合。无论您是初学者还是经验丰富的用户,您都会在这里找到可以直接应用以增强 Gitea 体验的实用资源。我们鼓励您贡献自己的见解和知识,使这个集合更加全面和有价值。
|
||||
|
||||
| 部分 | 描述 |
|
||||
|-----|------|
|
||||
| [`docker`](docker) | 本部分为您提供适用于在工作站或安装了 Docker 的服务器上运行容器的脚本和说明。它简化了使用 Docker 设置和管理 Gitea 部署的过程。 |
|
||||
| [`docker-compose`](docker-compose) | 在本部分中,您将发现如何利用 `docker-compose` 来高效处理 Gitea 部署的示例。它提供了一个直接的方法来管理 Gitea 设置的多个容器化组件。 |
|
||||
| [`kubernetes`](kubernetes) | 如果您正在使用 Kubernetes 集群作为基础设施,本部分专门为您设计。它展示了在 Kubernetes 集群内配置 Gitea 部署的示例和指南,使您能够利用 Kubernetes 的可扩展性和灵活性。 |
|
||||
| [`vm`](vm) | 本部分致力于协助您在虚拟或物理服务器上设置 Gitea 的示例。无论您是在使用虚拟机还是物理硬件,您都会找到有用的资源来指导您完成部署过程。 |
|
||||
|
||||
我们希望这些资源为您的 Gitea 设置提供有价值的见解和解决方案。请随意探索、贡献和调整这些示例以满足您的具体需求。
|
59
examples/docker-compose/README.md
Normal file
59
examples/docker-compose/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
### 使用`docker-compose`运行`loong_runner`
|
||||
|
||||
```yml
|
||||
...
|
||||
gitea:
|
||||
image: gitea/gitea
|
||||
...
|
||||
healthcheck:
|
||||
# 使用curl检查仓库前端是否可用
|
||||
test: ["CMD", "curl", "-f", "<instance_url>"]
|
||||
interval: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
environment:
|
||||
# GITEA_RUNNER_REGISTRATION_TOKEN可用于设置全局运行器注册令牌。
|
||||
# Gitea版本必须为v1.23或更高版本。
|
||||
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
|
||||
# - GITEA_RUNNER_REGISTRATION_TOKEN=<用户定义的注册令牌>
|
||||
|
||||
runner:
|
||||
image: gitea/loong_runner
|
||||
restart: always
|
||||
depends_on:
|
||||
gitea:
|
||||
# 需要(下述配置),以便运行器能够连接到仓库,请参阅“健康检查(healthcheck)”
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
volumes:
|
||||
- ./data/loong_runner:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=<instance url>
|
||||
# 当使用Docker Secrets时
|
||||
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
|
||||
# 环境变量优先, 仅在首次启动时需要。
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
||||
```
|
||||
|
||||
### 使用 Docker-in-Docker (DIND) 运行 `loong_runner`
|
||||
|
||||
```yml
|
||||
...
|
||||
runner:
|
||||
image: gitea/loong_runner:latest-dind-rootless
|
||||
restart: always
|
||||
privileged: true
|
||||
depends_on:
|
||||
- gitea
|
||||
volumes:
|
||||
- ./data/loong_runner:/data
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=<instance url>
|
||||
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
|
||||
# 使用Docker Secrets时
|
||||
# 也可以使用 GITEA_RUNNER_REGISTRATION_TOKEN_FILE 来传递位置。
|
||||
# 环境变量优先, 仅在首次启动时需要。
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
||||
```
|
8
examples/docker/README.md
Normal file
8
examples/docker/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
### 在Docker容器中运行`loong_runner`
|
||||
|
||||
```sh
|
||||
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/loong_runner:nightly
|
||||
```
|
||||
|
||||
docker 容器内的 `/data` 目录在注册后包含运行器 API 密钥。
|
||||
必须对其进行持久化存储,否则运行器将尝试再次注册,并使用相同的(现已失效的)注册令牌。
|
19
examples/kubernetes/README.md
Normal file
19
examples/kubernetes/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 在 Kubernetes 中使用 `act_runner` 部署 Docker-in-Docker(DinD)
|
||||
|
||||
注意:Docker-in-Docker(DinD)在 Kubernetes 中需要提升权限。目前的实现方式是将 Pod 的 `SecurityContext`(安全上下文)设置为 `privileged`(特权模式)。请注意这存在潜在安全风险,恶意应用可能突破容器隔离上下文。
|
||||
|
||||
本目录包含文件:
|
||||
|
||||
- [`dind-docker.yaml`](dind-docker.yaml)
|
||||
用于在 Kubernetes 中创建作为运行器的 Deployment(部署)和 Persistent Volume(持久卷)。Docker 凭证会在每次 Pod 连接时重新生成,无需持久化存储。
|
||||
|
||||
- [`rootless-docker.yaml`](rootless-docker.yaml)
|
||||
用于在 Kubernetes 中创建 rootless(非特权)模式的 Deployment 和 Persistent Volume 作为运行器。Docker 凭证会在每次 Pod 连接时重新生成,无需持久化存储。
|
||||
|
||||
关键术语说明:
|
||||
1. privileged 模式:容器获得与宿主机 root 用户几乎相同的权限
|
||||
2. SecurityContext:Kubernetes 中定义 Pod/容器权限和安全配置的对象
|
||||
3. rootless 模式:以非特权用户身份运行 Docker 守护进程,安全性更高
|
||||
4. act_runner:Gitea 的 CI/CD 运行器组件
|
||||
|
||||
安全建议:在生产环境中,建议优先考虑 rootless 模式,若必须使用 DinD,建议通过 Pod 安全策略、网络策略和审计日志加强安全监控。
|
81
examples/kubernetes/dind-docker.yaml
Normal file
81
examples/kubernetes/dind-docker.yaml
Normal file
@ -0,0 +1,81 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: act-runner-vol
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storageClassName: standard
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
# 注册令牌可从Web UI、API或命令行获取。
|
||||
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
|
||||
# 为Gitea实例设置预定义的全局运行器注册令牌。
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: runner-secret
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: act-runner
|
||||
name: act-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: act-runner
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: act-runner
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: docker-certs
|
||||
emptyDir: {}
|
||||
- name: runner-data
|
||||
persistentVolumeClaim:
|
||||
claimName: act-runner-vol
|
||||
containers:
|
||||
- name: runner
|
||||
image: gitea/act_runner:nightly
|
||||
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo '等待docker守护进程..'; sleep 5; done; /sbin/tini -- run.sh"]
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: tcp://localhost:2376
|
||||
- name: DOCKER_CERT_PATH
|
||||
value: /certs/client
|
||||
- name: DOCKER_TLS_VERIFY
|
||||
value: "1"
|
||||
- name: GITEA_INSTANCE_URL
|
||||
value: http://gitea-http.gitea.svc.cluster.local:3000
|
||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: runner-secret
|
||||
key: token
|
||||
volumeMounts:
|
||||
- name: docker-certs
|
||||
mountPath: /certs
|
||||
- name: runner-data
|
||||
mountPath: /data
|
||||
- name: daemon
|
||||
image: docker:23.0.6-dind
|
||||
env:
|
||||
- name: DOCKER_TLS_CERTDIR
|
||||
value: /certs
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: docker-certs
|
||||
mountPath: /certs
|
73
examples/kubernetes/rootless-docker.yaml
Normal file
73
examples/kubernetes/rootless-docker.yaml
Normal file
@ -0,0 +1,73 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: act-runner-vol
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storageClassName: standard
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
# 注册令牌可从Web UI、API或命令行获取。
|
||||
# 可以通过`GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE`环境变量
|
||||
# 为Gitea实例设置预定义的全局运行器注册令牌。
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: runner-secret
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: act-runner
|
||||
name: act-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: act-runner
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: act-runner
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: runner-data
|
||||
persistentVolumeClaim:
|
||||
claimName: act-runner-vol
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: runner
|
||||
image: gitea/act_runner:nightly-dind-rootless
|
||||
imagePullPolicy: Always
|
||||
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo '等待docker守护进程..'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: tcp://localhost:2376
|
||||
- name: DOCKER_CERT_PATH
|
||||
value: /certs/client
|
||||
- name: DOCKER_TLS_VERIFY
|
||||
value: "1"
|
||||
- name: GITEA_INSTANCE_URL
|
||||
value: http://gitea-http.gitea.svc.cluster.local:3000
|
||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: runner-secret
|
||||
key: token
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: runner-data
|
||||
mountPath: /data
|
||||
|
6
examples/vm/README.md
Normal file
6
examples/vm/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# 在虚拟或物理服务器上使用 `loong_runner`
|
||||
|
||||
此目录中的文件:
|
||||
|
||||
- [`rootless-docker.md`](rootless-docker.md)
|
||||
在非Root用户下使用Docker运行`loong_runner`。
|
96
examples/vm/rootless-docker.md
Normal file
96
examples/vm/rootless-docker.md
Normal file
@ -0,0 +1,96 @@
|
||||
# 在非特权模式下使用Docker
|
||||
|
||||
以下是如何在在非特权模式下使用 Docker 中设置 `loong_runner` 的简单示例。实例是基于 Debian 编写的,其他 Linux 发行版不会有太大区别。
|
||||
|
||||
注意:此过程需要一个真实的登录 shell -- 使用 `sudo su` 或其他访问账户的方法将无法完成下面的一些步骤。
|
||||
|
||||
使用 `root` 用户:
|
||||
|
||||
- 创建一个用户来运行 `docker` 和 `loong_runner`。在这个例子中,我们使用了一个名为 `rootless` 的非特权账户。
|
||||
|
||||
```bash
|
||||
useradd -m rootless
|
||||
passwd rootless
|
||||
apt-get install -y uidmap # 对 docker 非Root用户使用是必需的。
|
||||
```
|
||||
|
||||
- 安装 [`docker-ce`](https://docs.docker.com/engine/install/)
|
||||
- (推荐)禁用系统范围的 Docker 守护进程
|
||||
|
||||
``systemctl disable --now docker.service docker.socket``
|
||||
|
||||
作为 `rootless` 用户:
|
||||
|
||||
- 按照 [启用非特权模式](https://docs.docker.com/engine/security/rootless/) 的说明进行操作
|
||||
- 将以下行添加到 `/home/rootless/.bashrc`:
|
||||
|
||||
```bash
|
||||
for f in ./.bashrc.d/*.bash; do echo "处理 $f 文件..."; . "$f"; done
|
||||
```
|
||||
|
||||
- 创建 `.bashrc.d` 目录 `mkdir ~/.bashrc.d`
|
||||
- 将以下行添加到 `/home/rootless/.bashrc.d/rootless-docker.bash`:
|
||||
|
||||
```bash
|
||||
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
|
||||
export PATH=/home/rootless/bin:$PATH
|
||||
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
||||
```
|
||||
|
||||
- 重启。确保 Docker 进程正在工作。
|
||||
- 为保存 `loong_runner` 数据创建一个目录
|
||||
|
||||
`mkdir /home/rootless/loong_runner`
|
||||
|
||||
- 从数据目录注册 runner
|
||||
|
||||
```bash
|
||||
cd /home/rootless/loong_runner
|
||||
loong_runner register
|
||||
```
|
||||
|
||||
- 在数据目录中生成 `loong_runner` 配置文件。编辑文件以适应系统。
|
||||
|
||||
```bash
|
||||
loong_runner generate-config >/home/rootless/loong_runner/config
|
||||
```
|
||||
|
||||
- 创建一个新的用户级 `systemd` 单元文件 `/home/rootless/.config/systemd/user/loong_runner.service`,内容如下:
|
||||
|
||||
```bash
|
||||
Description=龙架构代码仓库运行时
|
||||
Documentation=https://git.whlug.cn/LoongArchActions/loong_runner
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
|
||||
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||
ExecStart=/usr/bin/loong_runner daemon -c /home/rootless/loong_runner/config
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
WorkingDirectory=/home/rootless/loong_runner
|
||||
TimeoutSec=0
|
||||
RestartSec=2
|
||||
Restart=always
|
||||
StartLimitBurst=3
|
||||
StartLimitInterval=60s
|
||||
LimitNOFILE=infinity
|
||||
LimitNPROC=infinity
|
||||
LimitCORE=infinity
|
||||
TasksMax=infinity
|
||||
Delegate=yes
|
||||
Type=notify
|
||||
NotifyAccess=all
|
||||
KillMode=mixed
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
- 重启
|
||||
|
||||
系统重启后,检查 `loong_runner` 是否正常工作,并且 运行时 是否已连接到 Gitea。
|
||||
|
||||
````bash
|
||||
systemctl --user status loong_runner
|
||||
journalctl --user -xeu loong_runner
|
||||
```
|
152
go.mod
152
go.mod
@ -1,111 +1,101 @@
|
||||
module gitea.com/gitea/act_runner
|
||||
module git.whlug.cn/LAA/loong_runner
|
||||
|
||||
go 1.20
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.2.0
|
||||
code.gitea.io/gitea-vet v0.2.3-0.20230113022436-2b1561217fa5
|
||||
github.com/avast/retry-go/v4 v4.3.1
|
||||
github.com/bufbuild/connect-go v1.3.1
|
||||
github.com/docker/docker v23.0.1+incompatible
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-chi/render v1.0.2
|
||||
code.gitea.io/actions-proto-go v0.4.1
|
||||
code.gitea.io/gitea-vet v0.2.3
|
||||
connectrpc.com/connect v1.16.2
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/docker/docker v25.0.5+incompatible
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/nektos/act v0.0.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/term v0.6.0
|
||||
golang.org/x/time v0.1.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/nektos/act v0.0.0 // will be replaced
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.4.0
|
||||
modernc.org/sqlite v1.14.2
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978
|
||||
xorm.io/xorm v1.3.2
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/containerd/containerd v1.6.18 // indirect
|
||||
github.com/creack/pty v1.1.18 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/containerd/containerd v1.7.13 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/cli v25.0.3+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/goccy/go-json v0.8.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/buildkit v0.11.4 // indirect
|
||||
github.com/moby/patternmatcher v0.5.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/buildkit v0.12.5 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo v1.12.1 // indirect
|
||||
github.com/onsi/gomega v1.10.3 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||
github.com/opencontainers/runc v1.1.3 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rhysd/actionlint v1.6.23 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/rhysd/actionlint v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/tools v0.1.5 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.35.18 // indirect
|
||||
modernc.org/ccgo/v3 v3.12.82 // indirect
|
||||
modernc.org/libc v1.11.87 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.5 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.243.2-0.20230323041428-929ea6df751b
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
|
||||
|
@ -1,12 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package artifactcache provides a cache handler for the runner.
|
||||
//
|
||||
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
|
||||
//
|
||||
// TODO: Authorization
|
||||
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
|
||||
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
|
||||
package artifactcache
|
@ -1,416 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const (
|
||||
urlBase = "/_apis/artifactcache"
|
||||
)
|
||||
|
||||
var logger = log.StandardLogger().WithField("module", "cache_request")
|
||||
|
||||
type Handler struct {
|
||||
engine engine
|
||||
storage *Storage
|
||||
router *chi.Mux
|
||||
listener net.Listener
|
||||
|
||||
gc atomic.Bool
|
||||
gcAt time.Time
|
||||
|
||||
outboundIP string
|
||||
}
|
||||
|
||||
func StartHandler(dir, outboundIP string, port uint16) (*Handler, error) {
|
||||
h := &Handler{}
|
||||
|
||||
if dir == "" {
|
||||
if home, err := os.UserHomeDir(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, err := xorm.NewEngine("sqlite", filepath.Join(dir, "sqlite.db"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := e.Sync(&Cache{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.engine = engine{e: e}
|
||||
|
||||
storage, err := NewStorage(filepath.Join(dir, "cache"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.storage = storage
|
||||
|
||||
if outboundIP != "" {
|
||||
h.outboundIP = outboundIP
|
||||
} else if ip, err := getOutboundIP(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
h.outboundIP = ip.String()
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}))
|
||||
router.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
go h.gcCache()
|
||||
})
|
||||
})
|
||||
router.Use(middleware.Logger)
|
||||
router.Route(urlBase, func(r chi.Router) {
|
||||
r.Get("/cache", h.find)
|
||||
r.Route("/caches", func(r chi.Router) {
|
||||
r.Post("/", h.reserve)
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Patch("/", h.upload)
|
||||
r.Post("/", h.commit)
|
||||
})
|
||||
})
|
||||
r.Get("/artifacts/{id}", h.get)
|
||||
r.Post("/clean", h.clean)
|
||||
})
|
||||
|
||||
h.router = router
|
||||
|
||||
h.gcCache()
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
if err := http.Serve(listener, h.router); err != nil {
|
||||
logger.Errorf("http serve: %v", err)
|
||||
}
|
||||
}()
|
||||
h.listener = listener
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ExternalURL() string {
|
||||
// TODO: make the external url configurable if necessary
|
||||
return fmt.Sprintf("http://%s:%d",
|
||||
h.outboundIP,
|
||||
h.listener.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
// GET /_apis/artifactcache/cache
|
||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request) {
|
||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
cache, err := h.findCache(r.Context(), keys, version)
|
||||
if err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
if cache == nil {
|
||||
responseJson(w, r, 204)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := h.storage.Exist(cache.ID); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
} else if !ok {
|
||||
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Delete(cache)
|
||||
return err
|
||||
})
|
||||
responseJson(w, r, 204)
|
||||
return
|
||||
}
|
||||
responseJson(w, r, 200, map[string]any{
|
||||
"result": "hit",
|
||||
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
||||
"cacheKey": cache.Key,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/caches
|
||||
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request) {
|
||||
cache := &Cache{}
|
||||
if err := render.Bind(r, cache); err != nil {
|
||||
responseJson(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||
return sess.Where(builder.Eq{"key": cache.Key, "version": cache.Version}).Get(&Cache{})
|
||||
}); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
} else if ok {
|
||||
responseJson(w, r, 400, fmt.Errorf("already exist"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Insert(cache)
|
||||
return err
|
||||
}); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
responseJson(w, r, 200, map[string]any{
|
||||
"cacheId": cache.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// PATCH /_apis/artifactcache/caches/:id
|
||||
func (h *Handler) upload(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
responseJson(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
cache := &Cache{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||
return sess.Get(cache)
|
||||
}); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
} else if !ok {
|
||||
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
}
|
||||
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
||||
if err != nil {
|
||||
responseJson(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
}
|
||||
h.useCache(r.Context(), id)
|
||||
responseJson(w, r, 200)
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/caches/:id
|
||||
func (h *Handler) commit(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
responseJson(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
cache := &Cache{
|
||||
ID: id,
|
||||
}
|
||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||
return sess.Get(cache)
|
||||
}); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
} else if !ok {
|
||||
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.storage.Commit(cache.ID, cache.Size); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
cache.Complete = true
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.ID(cache.ID).Cols("complete").Update(cache)
|
||||
return err
|
||||
}); err != nil {
|
||||
responseJson(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseJson(w, r, 200)
|
||||
}
|
||||
|
||||
// GET /_apis/artifactcache/artifacts/:id
|
||||
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
responseJson(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
h.useCache(r.Context(), id)
|
||||
h.storage.Serve(w, r, id)
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/clean
|
||||
func (h *Handler) clean(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: don't support force deleting cache entries
|
||||
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
|
||||
responseJson(w, r, 200)
|
||||
}
|
||||
|
||||
// if not found, return (nil, nil) instead of an error.
|
||||
func (h *Handler) findCache(ctx context.Context, keys []string, version string) (*Cache, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
key := keys[0] // the first key is for exact match.
|
||||
|
||||
cache := &Cache{}
|
||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||
return sess.Where(builder.Eq{"key": key, "version": version, "complete": true}).Get(cache)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
for _, prefix := range keys[1:] {
|
||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||
return sess.Where(builder.And(
|
||||
builder.Like{"key", prefix + "%"},
|
||||
builder.Eq{"version": version, "complete": true},
|
||||
)).OrderBy("id DESC").Get(cache)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return cache, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (h *Handler) useCache(ctx context.Context, id int64) {
|
||||
// keep quiet
|
||||
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Context(ctx).Cols("used_at").Update(&Cache{
|
||||
ID: id,
|
||||
UsedAt: time.Now().Unix(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) gcCache() {
|
||||
if h.gc.Load() {
|
||||
return
|
||||
}
|
||||
if !h.gc.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
defer h.gc.Store(false)
|
||||
|
||||
if time.Since(h.gcAt) < time.Hour {
|
||||
logger.Infof("skip gc: %v", h.gcAt.String())
|
||||
return
|
||||
}
|
||||
h.gcAt = time.Now()
|
||||
logger.Infof("gc: %v", h.gcAt.String())
|
||||
|
||||
const (
|
||||
keepUsed = 30 * 24 * time.Hour
|
||||
keepUnused = 7 * 24 * time.Hour
|
||||
keepTemp = 5 * time.Minute
|
||||
)
|
||||
|
||||
var caches []*Cache
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
return sess.Where(builder.And(builder.Lt{"used_at": time.Now().Add(-keepTemp).Unix()}, builder.Eq{"complete": false})).
|
||||
Find(&caches)
|
||||
}); err != nil {
|
||||
logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Delete(cache)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
|
||||
caches = caches[:0]
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
return sess.Where(builder.Lt{"used_at": time.Now().Add(-keepUnused).Unix()}).
|
||||
Find(&caches)
|
||||
}); err != nil {
|
||||
logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Delete(cache)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
|
||||
caches = caches[:0]
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
return sess.Where(builder.Lt{"created_at": time.Now().Add(-keepUsed).Unix()}).
|
||||
Find(&caches)
|
||||
}); err != nil {
|
||||
logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||
_, err := sess.Delete(cache)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
ID int64 `xorm:"id pk autoincr" json:"-"`
|
||||
Key string `xorm:"TEXT index unique(key_version)" json:"key"`
|
||||
Version string `xorm:"TEXT unique(key_version)" json:"version"`
|
||||
Size int64 `json:"cacheSize"`
|
||||
Complete bool `xorm:"index(complete_used_at)" json:"-"`
|
||||
UsedAt int64 `xorm:"index(complete_used_at) updated" json:"-"`
|
||||
CreatedAt int64 `xorm:"index created" json:"-"`
|
||||
}
|
||||
|
||||
// Bind implements render.Binder
|
||||
func (c *Cache) Bind(_ *http.Request) error {
|
||||
if c.Key == "" {
|
||||
return fmt.Errorf("missing key")
|
||||
}
|
||||
if c.Version == "" {
|
||||
return fmt.Errorf("missing version")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
rootDir string
|
||||
}
|
||||
|
||||
func NewStorage(rootDir string) (*Storage, error) {
|
||||
if err := os.MkdirAll(rootDir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Storage{
|
||||
rootDir: rootDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Storage) Exist(id int64) (bool, error) {
|
||||
name := s.filename(id)
|
||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Storage) Write(id int64, offset int64, reader io.Reader) error {
|
||||
name := s.tempName(id, offset)
|
||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) Commit(id int64, size int64) error {
|
||||
defer func() {
|
||||
_ = os.RemoveAll(s.tempDir(id))
|
||||
}()
|
||||
|
||||
name := s.filename(id)
|
||||
tempNames, err := s.tempNames(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var written int64
|
||||
for _, v := range tempNames {
|
||||
f, err := os.Open(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := io.Copy(file, f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
written += n
|
||||
}
|
||||
|
||||
if written != size {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(name)
|
||||
return fmt.Errorf("broken file: %v != %v", written, size)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
name := s.filename(id)
|
||||
http.ServeFile(w, r, name)
|
||||
}
|
||||
|
||||
func (s *Storage) Remove(id int64) {
|
||||
_ = os.Remove(s.filename(id))
|
||||
_ = os.RemoveAll(s.tempDir(id))
|
||||
}
|
||||
|
||||
func (s *Storage) filename(id int64) string {
|
||||
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
|
||||
}
|
||||
|
||||
func (s *Storage) tempDir(id int64) string {
|
||||
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
||||
}
|
||||
|
||||
func (s *Storage) tempName(id, offset int64) string {
|
||||
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
||||
}
|
||||
|
||||
func (s *Storage) tempNames(id int64) ([]string, error) {
|
||||
dir := s.tempDir(id)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for _, v := range files {
|
||||
if !v.IsDir() {
|
||||
names = append(names, filepath.Join(dir, v.Name()))
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func responseJson(w http.ResponseWriter, r *http.Request, code int, v ...any) {
|
||||
render.Status(r, code)
|
||||
if len(v) == 0 || v[0] == nil {
|
||||
render.JSON(w, r, struct{}{})
|
||||
} else if err, ok := v[0].(error); ok {
|
||||
logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
||||
render.JSON(w, r, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
render.JSON(w, r, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
func parseContentRange(s string) (int64, int64, error) {
|
||||
// support the format like "bytes 11-22/*" only
|
||||
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
|
||||
s1, s2, _ := strings.Cut(s, "-")
|
||||
|
||||
start, err := strconv.ParseInt(s1, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||
}
|
||||
stop, err := strconv.ParseInt(s2, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||
}
|
||||
return start, stop, nil
|
||||
}
|
||||
|
||||
func getOutboundIP() (net.IP, error) {
|
||||
// FIXME: It makes more sense to use the gateway IP address of container network
|
||||
if conn, err := net.Dial("udp", "8.8.8.8:80"); err == nil {
|
||||
defer conn.Close()
|
||||
return conn.LocalAddr().(*net.UDPAddr).IP, nil
|
||||
}
|
||||
if ifaces, err := net.Interfaces(); err == nil {
|
||||
for _, i := range ifaces {
|
||||
if addrs, err := i.Addrs(); err == nil {
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip.IsGlobalUnicast() {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no outbound IP address found")
|
||||
}
|
||||
|
||||
// engine is a wrapper of *xorm.Engine, with a lock.
|
||||
// To avoid racing of sqlite, we don't care performance here.
|
||||
type engine struct {
|
||||
e *xorm.Engine
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (e *engine) Exec(f func(*xorm.Session) error) error {
|
||||
e.m.Lock()
|
||||
defer e.m.Unlock()
|
||||
|
||||
sess := e.e.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
return f(sess)
|
||||
}
|
||||
|
||||
func (e *engine) ExecBool(f func(*xorm.Session) (bool, error)) (bool, error) {
|
||||
e.m.Lock()
|
||||
defer e.m.Unlock()
|
||||
|
||||
sess := e.e.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
return f(sess)
|
||||
}
|
69
internal/app/cmd/cache-server.go
Normal file
69
internal/app/cmd/cache-server.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cacheServerArgs struct {
|
||||
Dir string // 缓存目录
|
||||
Host string // 主机地址
|
||||
Port uint16 // 端口号
|
||||
}
|
||||
|
||||
func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("配置无效: %w", err)
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
|
||||
var (
|
||||
dir = cfg.Cache.Dir
|
||||
host = cfg.Cache.Host
|
||||
port = cfg.Cache.Port
|
||||
)
|
||||
|
||||
// cacheArgs 优先级更高
|
||||
if cacheArgs.Dir != "" {
|
||||
dir = cacheArgs.Dir
|
||||
}
|
||||
if cacheArgs.Host != "" {
|
||||
host = cacheArgs.Host
|
||||
}
|
||||
if cacheArgs.Port != 0 {
|
||||
port = cacheArgs.Port
|
||||
}
|
||||
|
||||
cacheHandler, err := artifactcache.StartHandler(
|
||||
dir,
|
||||
host,
|
||||
port,
|
||||
log.StandardLogger().WithField("module", "cache_request"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("缓存服务器正在监听 %v", cacheHandler.ExternalURL())
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@ -10,44 +10,47 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) {
|
||||
// ./act_runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Use: "act_runner [运行事件名称]\n如果没有传递事件名称, 默认为 \"on: push\"",
|
||||
Short: "通过指定事件名称(例如 `push`)或直接指定操作名称来本地运行 GitHub Actions。",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: ver.Version(),
|
||||
SilenceUsage: true,
|
||||
}
|
||||
configFile := ""
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径")
|
||||
|
||||
// ./act_runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a runner to the server",
|
||||
Short: "将运行器注册到服务器",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // 必须使用 regArgs 的指针
|
||||
}
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token")
|
||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "禁用交互模式")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea 实例地址")
|
||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "运行器令牌")
|
||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "运行器名称")
|
||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "运行器标签,逗号分隔")
|
||||
registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "配置运行器为临时运行器,只能选择单个作业(比 --once 更严格)")
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
// ./act_runner daemon
|
||||
var daemArgs daemonArgs
|
||||
daemonCmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Run as a runner daemon",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runDaemon(ctx, &configFile),
|
||||
Short: "作为运行器守护进程运行",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runDaemon(ctx, &daemArgs, &configFile),
|
||||
}
|
||||
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "运行一个作业然后退出")
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// ./act_runner exec
|
||||
@ -56,14 +59,27 @@ func Execute(ctx context.Context) {
|
||||
// ./act_runner config
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "Generate an example config file",
|
||||
Short: "生成示例配置文件",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("%s", config.Example)
|
||||
},
|
||||
})
|
||||
|
||||
// hide completion command
|
||||
// ./act_runner cache-server
|
||||
var cacheArgs cacheServerArgs
|
||||
cacheCmd := &cobra.Command{
|
||||
Use: "cache-server",
|
||||
Short: "启动缓存服务器用于缓存操作",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runCacheServer(ctx, &configFile, &cacheArgs),
|
||||
}
|
||||
cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "缓存目录")
|
||||
cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "缓存服务器主机")
|
||||
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "缓存服务器端口")
|
||||
rootCmd.AddCommand(cacheCmd)
|
||||
|
||||
// 隐藏自动补全命令
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
@ -7,56 +7,94 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/poll"
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/poll"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/run"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/envcheck"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.Infoln("Starting runner daemon")
|
||||
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
return fmt.Errorf("无效的配置: %w", err)
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
log.Infoln("启动运行器守护进程")
|
||||
|
||||
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("registration file not found, please register the runner first")
|
||||
log.Error("未找到注册文件,请先注册运行器")
|
||||
return err
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to load registration file: %w", err)
|
||||
return fmt.Errorf("加载注册文件失败: %w", err)
|
||||
}
|
||||
|
||||
lbls := reg.Labels
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
lbls = cfg.Runner.Labels
|
||||
}
|
||||
|
||||
ls := labels.Labels{}
|
||||
for _, l := range reg.Labels {
|
||||
for _, l := range lbls {
|
||||
label, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
log.WithError(err).Warnf("忽略无效标签 %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, label)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||
log.Warn("未配置任何标签,运行器可能无法接取作业")
|
||||
}
|
||||
|
||||
if ls.RequireDocker() {
|
||||
if err := envcheck.CheckIfDockerRunning(ctx); err != nil {
|
||||
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// 如果 dockerSocketPath 通过检查,用 dockerSocketPath 覆盖 DOCKER_HOST
|
||||
os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
// 如果 cfg.Container.DockerHost 为空,意味着 act_runner 需要自动查找可用的 docker 主机
|
||||
// 并将路径分配给 cfg.Container.DockerHost
|
||||
if cfg.Container.DockerHost == "" {
|
||||
cfg.Container.DockerHost = dockerSocketPath
|
||||
}
|
||||
// 检查方案,如果方案不是 npipe 或 unix
|
||||
// 将 cfg.Container.DockerHost 设置为 "-",因为它不能挂载到作业容器
|
||||
if protoIndex := strings.Index(cfg.Container.DockerHost, "://"); protoIndex != -1 {
|
||||
scheme := cfg.Container.DockerHost[:protoIndex]
|
||||
if !strings.EqualFold(scheme, "npipe") && !strings.EqualFold(scheme, "unix") {
|
||||
cfg.Container.DockerHost = "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(reg.Labels, ls.ToStrings()) {
|
||||
reg.Labels = ls.ToStrings()
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("保存运行器配置失败: %w", err)
|
||||
}
|
||||
log.Infof("标签更新为: %v", reg.Labels)
|
||||
}
|
||||
|
||||
cli := client.New(
|
||||
@ -68,31 +106,125 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||
)
|
||||
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
|
||||
// 在获取任务之前声明运行器的标签
|
||||
resp, err := runner.Declare(ctx, ls.Names())
|
||||
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||
log.Errorf("您的 Gitea 版本太旧,不支持运行器声明,请升级到 v1.21 或更高版本")
|
||||
return err
|
||||
} else if err != nil {
|
||||
log.WithError(err).Error("调用 Declare 失败")
|
||||
return err
|
||||
} else {
|
||||
log.Infof("运行器: %s, 版本: %s, 标签: %v, 声明成功",
|
||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||
}
|
||||
|
||||
poller := poll.New(cfg, cli, runner)
|
||||
|
||||
poller.Poll(ctx)
|
||||
if daemArgs.Once || reg.Ephemeral {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
poller.PollOnce()
|
||||
}()
|
||||
|
||||
// 当完成一个作业或请求取消时关闭
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-done:
|
||||
}
|
||||
} else {
|
||||
go poller.Poll()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
log.Infof("运行器: %s 关闭开始,等待 %s 让正在运行的作业完成后再关闭", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = poller.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Warnf("运行器: %s 在关闭期间取消了正在进行的作业", resp.Msg.Runner.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// initLogging setup the global logrus logger.
|
||||
type daemonArgs struct {
|
||||
Once bool // 是否只运行一次
|
||||
}
|
||||
|
||||
// initLogging 设置全局 logrus 日志记录器。
|
||||
func initLogging(cfg *config.Config) {
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
format := &log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
|
||||
if l := cfg.Log.Level; l != "" {
|
||||
level, err := log.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorf("invalid log level: %q", l)
|
||||
Errorf("无效的日志级别: %q", l)
|
||||
}
|
||||
|
||||
// 调试级别
|
||||
if level == log.DebugLevel {
|
||||
log.SetReportCaller(true)
|
||||
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||
// 获取函数名
|
||||
s := strings.Split(f.Function, ".")
|
||||
funcname := "[" + s[len(s)-1] + "]"
|
||||
// 获取文件名和行号
|
||||
_, filename := path.Split(f.File)
|
||||
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
|
||||
return funcname, filename
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
}
|
||||
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("log level changed to %v", level)
|
||||
log.Infof("日志级别更改为 %v", level)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var commonSocketPaths = []string{
|
||||
"/var/run/docker.sock",
|
||||
"/run/podman/podman.sock",
|
||||
"$HOME/.colima/docker.sock",
|
||||
"$XDG_RUNTIME_DIR/docker.sock",
|
||||
"$XDG_RUNTIME_DIR/podman/podman.sock",
|
||||
`\\.\pipe\docker_engine`,
|
||||
"$HOME/.docker/run/docker.sock",
|
||||
}
|
||||
|
||||
func getDockerSocketPath(configDockerHost string) (string, error) {
|
||||
// `-` 表示不要将 docker 套接字挂载到作业容器
|
||||
if configDockerHost != "" && configDockerHost != "-" {
|
||||
return configDockerHost, nil
|
||||
}
|
||||
|
||||
socket, found := os.LookupEnv("DOCKER_HOST")
|
||||
if found {
|
||||
return socket, nil
|
||||
}
|
||||
|
||||
for _, p := range commonSocketPaths {
|
||||
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
|
||||
if strings.HasPrefix(p, `\\.\`) {
|
||||
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
||||
}
|
||||
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("守护进程 Docker 引擎套接字未找到且 docker_host 配置无效")
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
"github.com/nektos/act/pkg/artifacts"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
@ -21,49 +23,50 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||
)
|
||||
|
||||
type executeArgs struct {
|
||||
runList bool
|
||||
job string
|
||||
event string
|
||||
workdir string
|
||||
workflowsPath string
|
||||
noWorkflowRecurse bool
|
||||
autodetectEvent bool
|
||||
forcePull bool
|
||||
forceRebuild bool
|
||||
jsonLogger bool
|
||||
envs []string
|
||||
envfile string
|
||||
secrets []string
|
||||
defaultActionsUrl string
|
||||
insecureSecrets bool
|
||||
privileged bool
|
||||
usernsMode string
|
||||
containerArchitecture string
|
||||
containerDaemonSocket string
|
||||
useGitIgnore bool
|
||||
containerCapAdd []string
|
||||
containerCapDrop []string
|
||||
artifactServerPath string
|
||||
artifactServerAddr string
|
||||
artifactServerPort string
|
||||
noSkipCheckout bool
|
||||
debug bool
|
||||
dryrun bool
|
||||
image string
|
||||
cacheHandler *artifactcache.Handler
|
||||
runList bool // 是否列出工作流
|
||||
job string // 特定作业 ID
|
||||
event string // 事件名称
|
||||
workdir string // 工作目录
|
||||
workflowsPath string // 工作流文件路径
|
||||
noWorkflowRecurse bool // 是否禁止递归运行子目录中的工作流
|
||||
autodetectEvent bool // 自动检测事件
|
||||
forcePull bool // 即使存在也拉取 Docker 镜像
|
||||
forceRebuild bool // 即使存在也重建本地动作 Docker 镜像
|
||||
jsonLogger bool // 以 JSON 格式输出日志
|
||||
envs []string // 环境变量
|
||||
envfile string // 环境文件
|
||||
secrets []string // 机密
|
||||
defaultActionsURL string // 默认动作 URL
|
||||
insecureSecrets bool // 不建议!打印日志时不隐藏机密
|
||||
privileged bool // 使用特权模式
|
||||
usernsMode string // 用户命名空间
|
||||
containerArchitecture string // 容器架构
|
||||
containerDaemonSocket string // 容器守护进程套接字
|
||||
useGitIgnore bool // 控制是否将 .gitignore 中指定的路径复制到容器中
|
||||
containerCapAdd []string // 添加到工作流容器的内核能力
|
||||
containerCapDrop []string // 从工作流容器中删除的内核能力
|
||||
containerOptions string // 容器选项
|
||||
artifactServerPath string // 存储上传和检索下载的路径
|
||||
artifactServerAddr string // 监听地址
|
||||
artifactServerPort string // 监听端口
|
||||
noSkipCheckout bool // 不跳过 actions/checkout
|
||||
debug bool // 启用调试日志
|
||||
dryrun bool // dryrun 模式
|
||||
image string // 使用的 Docker 镜像
|
||||
cacheHandler *artifactcache.Handler // 缓存处理器
|
||||
network string // 容器连接的网络
|
||||
githubInstance string // 使用的 Gitea 实例
|
||||
}
|
||||
|
||||
// WorkflowsPath returns path to workflow file(s)
|
||||
// WorkflowsPath 返回工作流文件的路径
|
||||
func (i *executeArgs) WorkflowsPath() string {
|
||||
return i.resolve(i.workflowsPath)
|
||||
}
|
||||
|
||||
// Envfile returns path to .env
|
||||
// Envfile 返回 .env 文件的路径
|
||||
func (i *executeArgs) Envfile() string {
|
||||
return i.resolve(i.envfile)
|
||||
}
|
||||
@ -74,18 +77,18 @@ func (i *executeArgs) LoadSecrets() map[string]string {
|
||||
secretPairParts := strings.SplitN(secretPair, "=", 2)
|
||||
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
|
||||
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
|
||||
log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||
log.Errorf("机密 %s 已经定义(机密不区分大小写)", secretPairParts[0])
|
||||
}
|
||||
if len(secretPairParts) == 2 {
|
||||
s[secretPairParts[0]] = secretPairParts[1]
|
||||
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||
s[secretPairParts[0]] = env
|
||||
} else {
|
||||
fmt.Printf("Provide value for '%s': ", secretPairParts[0])
|
||||
fmt.Printf("为 '%s' 提供值: ", secretPairParts[0])
|
||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read input: %v", err)
|
||||
log.Errorf("读取输入失败: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s[secretPairParts[0]] = string(val)
|
||||
@ -98,7 +101,7 @@ func readEnvs(path string, envs map[string]string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
env, err := godotenv.Read(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading from %s: %v", path, err)
|
||||
log.Fatalf("从 %s 加载失败: %v", path, err)
|
||||
}
|
||||
for k, v := range env {
|
||||
envs[k] = v
|
||||
@ -127,7 +130,7 @@ func (i *executeArgs) LoadEnvs() map[string]string {
|
||||
return envs
|
||||
}
|
||||
|
||||
// Workdir returns path to workdir
|
||||
// Workdir 返回工作目录的路径
|
||||
func (i *executeArgs) Workdir() string {
|
||||
return i.resolve(".")
|
||||
}
|
||||
@ -148,22 +151,22 @@ func (i *executeArgs) resolve(path string) string {
|
||||
|
||||
func printList(plan *model.Plan) error {
|
||||
type lineInfoDef struct {
|
||||
jobID string
|
||||
jobName string
|
||||
stage string
|
||||
wfName string
|
||||
wfFile string
|
||||
events string
|
||||
jobID string // 作业 ID
|
||||
jobName string // 作业名称
|
||||
stage string // 阶段
|
||||
wfName string // 工作流名称
|
||||
wfFile string // 工作流文件
|
||||
events string // 事件
|
||||
}
|
||||
lineInfos := []lineInfoDef{}
|
||||
|
||||
header := lineInfoDef{
|
||||
jobID: "Job ID",
|
||||
jobName: "Job name",
|
||||
stage: "Stage",
|
||||
wfName: "Workflow name",
|
||||
wfFile: "Workflow file",
|
||||
events: "Events",
|
||||
jobID: "作业 ID",
|
||||
jobName: "作业名称",
|
||||
stage: "阶段",
|
||||
wfName: "工作流名称",
|
||||
wfFile: "工作流文件",
|
||||
events: "事件",
|
||||
}
|
||||
|
||||
jobs := map[string]bool{}
|
||||
@ -218,7 +221,6 @@ func printList(plan *model.Plan) error {
|
||||
jobNameMaxWidth += 2
|
||||
stageMaxWidth += 2
|
||||
wfNameMaxWidth += 2
|
||||
wfFileMaxWidth += 2
|
||||
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, header.stage,
|
||||
@ -239,54 +241,54 @@ func printList(plan *model.Plan) error {
|
||||
)
|
||||
}
|
||||
if duplicateJobIDs {
|
||||
fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
||||
fmt.Print("\n检测到多个作业具有相同的作业名称,请使用 `-W` 指定特定工作流的路径。\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
// plan with filtered jobs - to be used for filtering only
|
||||
// 计划带有过滤的作业 - 仅用于过滤
|
||||
var filterPlan *model.Plan
|
||||
|
||||
// Determine the event name to be filtered
|
||||
var filterEventName string = ""
|
||||
// 确定要过滤的事件名称
|
||||
var filterEventName string
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
|
||||
filterEventName = execArgs.event
|
||||
} else if execArgs.autodetectEvent {
|
||||
// collect all events from loaded workflows
|
||||
// 收集所有加载的工作流中的事件
|
||||
events := planner.GetEvents()
|
||||
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event for filtering: %s", events[0])
|
||||
// 将默认事件类型设置为第一个可用的事件
|
||||
// 这样用户就不必指定事件。
|
||||
log.Infof("使用检测到的第一个工作流事件进行过滤: %s", events[0])
|
||||
|
||||
filterEventName = events[0]
|
||||
}
|
||||
|
||||
var err error
|
||||
if execArgs.job != "" {
|
||||
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
log.Infof("准备带有作业的计划: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if filterEventName != "" {
|
||||
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
log.Infof("准备事件的计划: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Preparing plan with all jobs")
|
||||
log.Infof("准备所有作业的计划")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printList(filterPlan)
|
||||
_ = printList(filterPlan)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -302,40 +304,40 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
return runExecList(ctx, planner, execArgs)
|
||||
}
|
||||
|
||||
// plan with triggered jobs
|
||||
// 计划触发作业
|
||||
var plan *model.Plan
|
||||
|
||||
// Determine the event name to be triggered
|
||||
// 确定要触发的事件名称
|
||||
var eventName string
|
||||
|
||||
// collect all events from loaded workflows
|
||||
// 收集所有加载的工作流中的事件
|
||||
events := planner.GetEvents()
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
eventName = args[0]
|
||||
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
|
||||
eventName = execArgs.event
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||
log.Infof("使用唯一检测到的工作流事件: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event: %s", events[0])
|
||||
// 将默认事件类型设置为第一个可用的事件
|
||||
// 这样用户就不必指定事件。
|
||||
log.Infof("使用检测到的第一个工作流事件: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
log.Infof("Using default workflow event: push")
|
||||
log.Infof("使用默认工作流事件: push")
|
||||
eventName = "push"
|
||||
}
|
||||
|
||||
// build the plan for this run
|
||||
// 为此运行构建计划
|
||||
if execArgs.job != "" {
|
||||
log.Infof("Planning job: %s", execArgs.job)
|
||||
log.Infof("规划作业: %s", execArgs.job)
|
||||
plan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Planning jobs for event: %s", eventName)
|
||||
log.Infof("规划事件的作业: %s", eventName)
|
||||
plan, err = planner.PlanEvent(eventName)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -347,15 +349,33 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// init a cache server
|
||||
handler, err := artifactcache.StartHandler("", "", 0)
|
||||
// 初始化缓存服务器
|
||||
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||
log.Infof("缓存处理器监听于: %v", handler.ExternalURL())
|
||||
execArgs.cacheHandler = handler
|
||||
|
||||
// run the plan
|
||||
if len(execArgs.artifactServerAddr) == 0 {
|
||||
ip := common.GetOutboundIP()
|
||||
if ip == nil {
|
||||
return fmt.Errorf("无法确定出站 IP 地址")
|
||||
}
|
||||
execArgs.artifactServerAddr = ip.String()
|
||||
}
|
||||
|
||||
if len(execArgs.artifactServerPath) == 0 {
|
||||
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
|
||||
// 运行计划
|
||||
config := &runner.Config{
|
||||
Workdir: execArgs.Workdir(),
|
||||
BindWorkdir: false,
|
||||
@ -372,48 +392,47 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
ContainerArchitecture: execArgs.containerArchitecture,
|
||||
ContainerDaemonSocket: execArgs.containerDaemonSocket,
|
||||
UseGitIgnore: execArgs.useGitIgnore,
|
||||
// GitHubInstance: t.client.Address(),
|
||||
ContainerCapAdd: execArgs.containerCapAdd,
|
||||
ContainerCapDrop: execArgs.containerCapDrop,
|
||||
AutoRemove: true,
|
||||
ArtifactServerPath: execArgs.artifactServerPath,
|
||||
ArtifactServerPort: execArgs.artifactServerPort,
|
||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||
GitHubInstance: execArgs.githubInstance,
|
||||
ContainerCapAdd: execArgs.containerCapAdd,
|
||||
ContainerCapDrop: execArgs.containerCapDrop,
|
||||
ContainerOptions: execArgs.containerOptions,
|
||||
AutoRemove: true,
|
||||
ArtifactServerPath: execArgs.artifactServerPath,
|
||||
ArtifactServerPort: execArgs.artifactServerPort,
|
||||
ArtifactServerAddr: execArgs.artifactServerAddr,
|
||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||
// PresetGitHubContext: preset,
|
||||
// EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: "bridge",
|
||||
DefaultActionInstance: execArgs.defaultActionsUrl,
|
||||
ContainerNetworkMode: container.NetworkMode(execArgs.network),
|
||||
DefaultActionInstance: execArgs.defaultActionsURL,
|
||||
PlatformPicker: func(_ []string) string {
|
||||
return execArgs.image
|
||||
},
|
||||
ValidVolumes: []string{"**"}, // 所有挂载的卷(volumes)都允许被 exec 命令访问
|
||||
}
|
||||
|
||||
// TODO: handle log level config
|
||||
// waiting https://gitea.com/gitea/act/pulls/19
|
||||
// if !execArgs.debug {
|
||||
// logLevel := log.Level(log.InfoLevel)
|
||||
// config.JobLoggerLevel = &logLevel
|
||||
// }
|
||||
config.Env["ACT_EXEC"] = "true"
|
||||
|
||||
if t := config.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
config.Token = t
|
||||
} else if t := config.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
config.Token = t
|
||||
}
|
||||
|
||||
if !execArgs.debug {
|
||||
logLevel := log.InfoLevel
|
||||
config.JobLoggerLevel = &logLevel
|
||||
}
|
||||
|
||||
r, err := runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(execArgs.artifactServerPath) == 0 {
|
||||
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
|
||||
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
log.Debugf("artifacts 服务器启动于 %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
@ -430,39 +449,43 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
|
||||
execCmd := &cobra.Command{
|
||||
Use: "exec",
|
||||
Short: "Run workflow locally.",
|
||||
Short: "本地运行工作流",
|
||||
Args: cobra.MaximumNArgs(20),
|
||||
RunE: runExec(ctx, &execArg),
|
||||
}
|
||||
|
||||
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
|
||||
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
|
||||
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
|
||||
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
|
||||
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
|
||||
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
|
||||
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
|
||||
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
|
||||
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.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", "Defines the port where the artifact server listens (will only bind to localhost).")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsUrl, "default-actions-url", "", "https://gitea.com", "Defines the default url of action instance.")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
|
||||
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "本地运行工作流")
|
||||
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "运行特定作业 ID")
|
||||
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "运行事件名称")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "工作流文件路径")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "工作目录")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "禁用运行指定路径的子目录中的工作流")
|
||||
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "使用工作流中的第一个事件类型作为触发工作流的事件")
|
||||
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "即使已经存在也拉取 Docker 镜像")
|
||||
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "即使已经存在也重建本地动作 Docker 镜像")
|
||||
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "以 JSON 格式输出日志")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "使环境变量对动作可用,可选值(例如 --env myenv=foo 或 --env myenv)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "读取并用作容器中的环境的环境文件")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "为 Action 提供密钥,可带可选值(例如 -s mysecret=foo 或 -s mysecret)")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "不推荐!打印日志时不会隐藏密钥信息")
|
||||
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "使用特权模式")
|
||||
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "要使用的用户命名空间")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "运行容器使用的架构(如 linux/loong64)。未指定时使用宿主机默认架构。需要 Docker 服务端 API 版本 1.41+,更低版本的 Docker 平台会忽略此参数")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "挂载到容器的 Docker 守护进程 socket 路径")
|
||||
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "控制是否将 .gitignore 中指定的路径复制到容器中")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "为工作流容器添加的内核能力(例如 --container-cap-add SYS_PTRACE)")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "从工作流容器移除的内核能力(例如 --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "容器选项")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "定义构建物服务器存储上传和下载的路径。未指定时构建物服务器不会启动")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "定义构建物服务器的监听地址")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "定义构建物服务器的监听端口(例如 --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "定义 Action 实例的默认 URL")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "不跳过 actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "启用调试日志")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun 模式")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "lcr.loongnix.cn/library/debian:latest", "使用的 Docker 镜像。使用 \"-self-hosted\" 直接在主机上运行")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "容器连接的网络")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "使用的 Gitea 实例")
|
||||
|
||||
return execCmd
|
||||
}
|
||||
|
@ -15,18 +15,18 @@ import (
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
// runRegister 将运行器注册到服务器
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.SetReportCaller(false)
|
||||
@ -37,22 +37,22 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||
log.Infof("注册运行器,架构=%s,操作系统=%s,版本=%s。",
|
||||
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
||||
|
||||
// runner always needs root permission
|
||||
// 运行器始终需要 root 权限
|
||||
if os.Getuid() != 0 {
|
||||
// TODO: use a better way to check root permission
|
||||
log.Warnf("Runner in user-mode.")
|
||||
// TODO: 使用更好的方法检查 root 权限
|
||||
log.Warnf("运行器处于用户模式。")
|
||||
}
|
||||
|
||||
if regArgs.NoInteractive {
|
||||
if err := registerNoInteractive(*configFile, regArgs); err != nil {
|
||||
if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(*configFile); err != nil {
|
||||
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
@ -68,13 +68,14 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
|
||||
}
|
||||
}
|
||||
|
||||
// registerArgs represents the arguments for register command
|
||||
// registerArgs 代表 register 命令的参数
|
||||
type registerArgs struct {
|
||||
NoInteractive bool
|
||||
InstanceAddr string
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels string
|
||||
NoInteractive bool // 是否非交互模式
|
||||
InstanceAddr string // 实例地址
|
||||
Token string // 令牌
|
||||
RunnerName string // 运行器名称
|
||||
Labels string // 标签
|
||||
Ephemeral bool // 是否临时
|
||||
}
|
||||
|
||||
type registerStage int8
|
||||
@ -85,34 +86,34 @@ const (
|
||||
StageInputInstance
|
||||
StageInputToken
|
||||
StageInputRunnerName
|
||||
StageInputCustomLabels
|
||||
StageInputLabels
|
||||
StageWaitingForRegistration
|
||||
StageExit
|
||||
)
|
||||
|
||||
var defaultLabels = []string{
|
||||
"ubuntu-latest:docker://node:16-bullseye",
|
||||
"ubuntu-22.04:docker://node:16-bullseye", // There's no node:16-bookworm yet
|
||||
"ubuntu-20.04:docker://node:16-bullseye",
|
||||
"ubuntu-18.04:docker://node:16-buster",
|
||||
"debian-latest:docker://lcr.loongnix.cn/library/debian:latest",
|
||||
"anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest",
|
||||
"anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2",
|
||||
}
|
||||
|
||||
type registerInputs struct {
|
||||
InstanceAddr string
|
||||
Token string
|
||||
RunnerName string
|
||||
CustomLabels []string
|
||||
Labels []string
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
func (r *registerInputs) validate() error {
|
||||
if r.InstanceAddr == "" {
|
||||
return fmt.Errorf("instance address is empty")
|
||||
return fmt.Errorf("实例地址为空")
|
||||
}
|
||||
if r.Token == "" {
|
||||
return fmt.Errorf("token is empty")
|
||||
return fmt.Errorf("令牌为空")
|
||||
}
|
||||
if len(r.CustomLabels) > 0 {
|
||||
return validateLabels(r.CustomLabels)
|
||||
if len(r.Labels) > 0 {
|
||||
return validateLabels(r.Labels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -126,16 +127,32 @@ func validateLabels(ls []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string) registerStage {
|
||||
// must set instance address and token.
|
||||
// if empty, keep current stage.
|
||||
func (r *registerInputs) stageValue(stage registerStage) string {
|
||||
switch stage {
|
||||
case StageInputInstance:
|
||||
return r.InstanceAddr
|
||||
case StageInputToken:
|
||||
return r.Token
|
||||
case StageInputRunnerName:
|
||||
return r.RunnerName
|
||||
case StageInputLabels:
|
||||
if len(r.Labels) > 0 {
|
||||
return strings.Join(r.Labels, ",")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
|
||||
// 必须设置实例地址和令牌。
|
||||
// 如果为空,保持当前阶段。
|
||||
if stage == StageInputInstance || stage == StageInputToken {
|
||||
if value == "" {
|
||||
return stage
|
||||
}
|
||||
}
|
||||
|
||||
// set hostname for runner name if empty
|
||||
// 如果运行器名称为空,设置为主机名
|
||||
if stage == StageInputRunnerName && value == "" {
|
||||
value, _ = os.Hostname()
|
||||
}
|
||||
@ -154,53 +171,88 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe
|
||||
return StageInputRunnerName
|
||||
case StageInputRunnerName:
|
||||
r.RunnerName = value
|
||||
return StageInputCustomLabels
|
||||
case StageInputCustomLabels:
|
||||
r.CustomLabels = defaultLabels
|
||||
// 如果配置文件中有标签配置,跳过输入标签阶段
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
ls := make([]string, 0, len(cfg.Runner.Labels))
|
||||
for _, l := range cfg.Runner.Labels {
|
||||
_, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("忽略无效标签 %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, l)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("配置文件中没有有效的标签配置,运行器可能无法接取作业")
|
||||
}
|
||||
r.Labels = ls
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageInputLabels
|
||||
case StageInputLabels:
|
||||
r.Labels = defaultLabels
|
||||
if value != "" {
|
||||
r.CustomLabels = strings.Split(value, ",")
|
||||
r.Labels = strings.Split(value, ",")
|
||||
}
|
||||
|
||||
if validateLabels(r.CustomLabels) != nil {
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host)")
|
||||
return StageInputCustomLabels
|
||||
if validateLabels(r.Labels) != nil {
|
||||
log.Infoln("无效的标签, 请重新输入, 留空以使用默认标签 (例如, debian-latest:lcr.loongnix.cn/library/debian:latest) ")
|
||||
r.Labels = nil
|
||||
return StageInputLabels
|
||||
}
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageUnknown
|
||||
}
|
||||
|
||||
func registerInteractive(configFile string) error {
|
||||
func initInputs(regArgs *registerArgs) *registerInputs {
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
Ephemeral: regArgs.Ephemeral,
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
// command line flag.
|
||||
if regArgs.Labels != "" {
|
||||
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
||||
var (
|
||||
reader = bufio.NewReader(os.Stdin)
|
||||
stage = StageInputInstance
|
||||
inputs = new(registerInputs)
|
||||
)
|
||||
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %v", err)
|
||||
return fmt.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||
stage = StageOverwriteLocalConfig
|
||||
}
|
||||
inputs := initInputs(regArgs)
|
||||
|
||||
for {
|
||||
printStageHelp(stage)
|
||||
|
||||
cmdString, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
cmdString := inputs.stageValue(stage)
|
||||
if cmdString == "" {
|
||||
printStageHelp(stage)
|
||||
var err error
|
||||
cmdString, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString))
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
|
||||
if err := doRegister(cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
} else {
|
||||
log.Infof("Runner registered successfully.")
|
||||
log.Infof("注册运行器,名称=%s, 实例=%s, 标签=%v。", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("注册运行器失败: %w", err)
|
||||
}
|
||||
log.Infof("运行器注册成功。")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -209,7 +261,7 @@ func registerInteractive(configFile string) error {
|
||||
}
|
||||
|
||||
if stage <= StageUnknown {
|
||||
log.Errorf("Invalid input, please re-run act command.")
|
||||
log.Errorf("无效输入,请重新运行命令。")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -218,56 +270,55 @@ func registerInteractive(configFile string) error {
|
||||
func printStageHelp(stage registerStage) {
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
log.Infoln("运行器已注册,是否覆盖本地配置?[y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
log.Infoln("请输入 Gitea 实例 URL(例如, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
log.Infoln("请输入运行器令牌:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||
case StageInputCustomLabels:
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):")
|
||||
log.Infof("请输入运行器名称(如果留空,使用主机名:%s):\n", hostname)
|
||||
case StageInputLabels:
|
||||
log.Infoln("请输入运行器标签, 留空以使用默认标签(逗号分隔, 例如, debian-latest:docker://lcr.loongnix.cn/library/debian:latest):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("Waiting for registration...")
|
||||
log.Infoln("等待注册...")
|
||||
}
|
||||
}
|
||||
|
||||
func registerNoInteractive(configFile string, regArgs *registerArgs) error {
|
||||
func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
CustomLabels: defaultLabels,
|
||||
inputs := initInputs(regArgs)
|
||||
// 配置文件中指定的标签。
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
if regArgs.Labels != "" {
|
||||
log.Warn("命令行中的标签将被忽略,使用配置文件中定义的标签。")
|
||||
}
|
||||
inputs.Labels = cfg.Runner.Labels
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
if regArgs.Labels != "" {
|
||||
inputs.CustomLabels = strings.Split(regArgs.Labels, ",")
|
||||
if len(inputs.Labels) == 0 {
|
||||
inputs.Labels = defaultLabels
|
||||
}
|
||||
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
||||
log.Infof("运行器名称为空,使用主机名 '%s'。", inputs.RunnerName)
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
log.WithError(err).Errorf("无效输入,请重新运行命令。")
|
||||
return err
|
||||
}
|
||||
if err := doRegister(cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
return nil
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("注册运行器失败: %w", err)
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
log.Infof("运行器注册成功。")
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// initial http client
|
||||
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
|
||||
// 初始化 http 客户端
|
||||
cli := client.New(
|
||||
inputs.InstanceAddr,
|
||||
cfg.Runner.Insecure,
|
||||
@ -282,7 +333,7 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
}))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
@ -290,20 +341,21 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("Cannot ping the Gitea instance server")
|
||||
// TODO: if ping failed, retry or exit
|
||||
Errorln("无法 ping 到 Gitea 实例服务器")
|
||||
// TODO: 如果 ping 失败,重试或退出
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
log.Debugln("Successfully pinged the Gitea instance server")
|
||||
log.Debugln("成功 ping 到 Gitea 实例服务器")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
reg := &config.Registration{
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.CustomLabels,
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.Labels,
|
||||
Ephemeral: inputs.Ephemeral,
|
||||
}
|
||||
|
||||
ls := make([]string, len(reg.Labels))
|
||||
@ -311,14 +363,17 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
l, _ := labels.Parse(v)
|
||||
ls[i] = l.Name
|
||||
}
|
||||
// register new runner.
|
||||
// 注册新的运行器。
|
||||
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: reg.Name,
|
||||
Token: reg.Token,
|
||||
AgentLabels: ls,
|
||||
Version: ver.Version(),
|
||||
AgentLabels: ls, // 在 Gitea 1.20 之后可能会被移除
|
||||
Labels: ls,
|
||||
Ephemeral: reg.Ephemeral,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
log.WithError(err).Error("poller: 无法注册新运行器")
|
||||
return err
|
||||
}
|
||||
|
||||
@ -327,8 +382,13 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
reg.Name = resp.Msg.Runner.Name
|
||||
reg.Token = resp.Msg.Runner.Token
|
||||
|
||||
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
|
||||
// TODO 我们不能通过 runner api 移除配置,如果在这里返回错误,我们只是填充数据库
|
||||
log.Error("poller: 无法注册新的临时运行器,升级 Gitea 以获得安全性,自动使用 run-once")
|
||||
}
|
||||
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("failed to save runner config: %w", err)
|
||||
return fmt.Errorf("保存运行器配置失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
19
internal/app/cmd/register_test.go
Normal file
19
internal/app/cmd/register_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
||||
err := registerNoInteractive(t.Context(), "", ®isterArgs{
|
||||
Labels: "标签:无效",
|
||||
Token: "token",
|
||||
InstanceAddr: "http://localhost:3000",
|
||||
})
|
||||
assert.Error(t, err, "不支持的标签: 无效")
|
||||
}
|
@ -6,77 +6,203 @@ package poll
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"sync/atomic"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/run"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
)
|
||||
|
||||
type Poller struct {
|
||||
client client.Client
|
||||
runner *run.Runner
|
||||
capacity int
|
||||
client client.Client // Gitea 客户端,用于与服务器通信
|
||||
runner *run.Runner // 任务执行器
|
||||
cfg *config.Config // 配置信息
|
||||
|
||||
tasksVersion atomic.Int64 // 任务版本号,用于增量同步
|
||||
|
||||
pollingCtx context.Context // 轮询上下文
|
||||
shutdownPolling context.CancelFunc // 轮询关闭函数
|
||||
|
||||
jobsCtx context.Context // 任务执行上下文
|
||||
shutdownJobs context.CancelFunc // 任务执行关闭函数
|
||||
|
||||
done chan struct{} // 完成信号通道
|
||||
}
|
||||
|
||||
// New 构造函数,创建并初始化 Poller 实例
|
||||
// 接受配置、客户端和服务运行器作为参数,并初始化必要的上下文和通道以便于后续操作。
|
||||
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
|
||||
|
||||
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
return &Poller{
|
||||
client: client,
|
||||
runner: runner,
|
||||
capacity: cfg.Runner.Capacity,
|
||||
client: client,
|
||||
runner: runner,
|
||||
cfg: cfg,
|
||||
|
||||
pollingCtx: pollingCtx,
|
||||
shutdownPolling: shutdownPolling,
|
||||
|
||||
jobsCtx: jobsCtx,
|
||||
shutdownJobs: shutdownJobs,
|
||||
|
||||
done: done,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) Poll(ctx context.Context) {
|
||||
limiter := rate.NewLimiter(rate.Every(2*time.Second), 1)
|
||||
// Poll 持续轮询模式
|
||||
// 启动多个 goroutine 来并发地轮询任务。每个 goroutine 调用 poll 方法进行实际的工作。在所有工作完成之后,通过关闭 done 通道发出信号。
|
||||
func (p *Poller) Poll() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < p.capacity; i++ {
|
||||
for i := 0; i < p.cfg.Runner.Capacity; i++ {
|
||||
wg.Add(1)
|
||||
go p.poll(ctx, wg, limiter)
|
||||
go p.poll(wg, limiter)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 发出我们正在关闭的信号
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
func (p *Poller) poll(ctx context.Context, wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
// PollOnce 单次轮询模式
|
||||
// 类似于 Poll,但是只执行一次任务轮询。同样会在完成后关闭 done 通道。
|
||||
func (p *Poller) PollOnce() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
|
||||
p.pollOnce(limiter)
|
||||
|
||||
// 发出我们已经完成的信号
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// Shutdown 关闭
|
||||
// 关闭轮询过程。首先取消轮询上下文,然后等待所有任务完成。如果超时发生,则强制关闭所有正在运行的任务并确保状态报告给 Gitea。
|
||||
func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownPolling()
|
||||
|
||||
select {
|
||||
|
||||
// 优雅地完成关闭
|
||||
case <-p.done:
|
||||
return nil
|
||||
|
||||
// 关闭超时
|
||||
case <-ctx.Done():
|
||||
|
||||
// 当超时和优雅关闭同时发生时,
|
||||
// 这个分支可能会被触发。这里进行非阻塞检查,
|
||||
// 避免在不必要的情况下发送错误。
|
||||
_, ok := <-p.done
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 强制关闭所有运行中的任务
|
||||
p.shutdownJobs()
|
||||
|
||||
// 等待运行中的任务向Gitea报告其状态
|
||||
_, _ = <-p.done
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// poll 轮询循环
|
||||
// 这是实际执行轮询逻辑的地方。它会持续调用 pollOnce 方法来获取并处理任务,直到上下文被取消。
|
||||
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.WithError(err).Debug("limiter wait failed")
|
||||
p.pollOnce(limiter)
|
||||
|
||||
select {
|
||||
case <-p.pollingCtx.Done():
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollOnce 单次轮询执行
|
||||
// 负责单次任务轮询。它首先等待速率限制器允许继续,然后尝试获取一个新任务。一旦获取到任务,就调用 runTaskWithRecover 方法来执行任务。
|
||||
func (p *Poller) pollOnce(limiter *rate.Limiter) {
|
||||
for {
|
||||
if err := limiter.Wait(p.pollingCtx); err != nil {
|
||||
if p.pollingCtx.Err() != nil {
|
||||
log.WithError(err).Debug("速率限制等待失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
task, ok := p.fetchTask(ctx)
|
||||
task, ok := p.fetchTask(p.pollingCtx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := p.runner.Run(ctx, task); err != nil {
|
||||
log.WithError(err).Error("failed to run task")
|
||||
}
|
||||
|
||||
p.runTaskWithRecover(p.jobsCtx, task)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// runTaskWithRecover 安全执行任务
|
||||
// 执行给定的任务,并提供 panic 恢复机制以防止意外崩溃。如果任务执行过程中出现错误或 panic,都会记录相应的日志信息。
|
||||
func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
|
||||
// 建立错误恢复机制
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := fmt.Errorf("panic: %v", r)
|
||||
log.WithError(err).Error("runTaskWithRecover中发生panic")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := p.runner.Run(ctx, task); err != nil {
|
||||
log.WithError(err).Error("运行任务失败")
|
||||
}
|
||||
}
|
||||
|
||||
// fetchTask 获取任务
|
||||
// 尝试从 Gitea 获取一个新任务。如果成功获取到任务且该任务的版本号比本地缓存的大,则更新本地缓存的版本号。
|
||||
// 如果获取失败或没有可用的任务,返回 false;否则返回 true 和任务数据。
|
||||
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
|
||||
// 加载发送请求时缓存中的版本值
|
||||
v := p.tasksVersion.Load()
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: v,
|
||||
}))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to fetch task")
|
||||
log.WithError(err).Error("获取任务失败")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if resp == nil || resp.Msg == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if resp.Msg.TasksVersion > v {
|
||||
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
||||
}
|
||||
|
||||
if resp.Msg.Task == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 收到一个任务,将`tasksVersion`设置为零,以便在下一个请求中创建查询数据库。
|
||||
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
|
||||
|
||||
return resp.Msg.Task, true
|
||||
}
|
||||
|
23
internal/app/run/logging.go
Normal file
23
internal/app/run/logging.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NullLogger用于创建一个新的JobLogger以丢弃日志。
|
||||
// 这将防止这些日志被记录到标准输出,但会通过其钩子将它们转发给Reporter。
|
||||
type NullLogger struct{}
|
||||
|
||||
// WithJobLogger 创建一个新的 logrus.Logger,它将丢弃所有日志。
|
||||
func (n NullLogger) WithJobLogger() *log.Logger {
|
||||
logger := log.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
logger.SetLevel(log.TraceLevel)
|
||||
|
||||
return logger
|
||||
}
|
@ -4,41 +4,45 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/report"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Runner runs the pipeline.
|
||||
// Runner 运行流水线
|
||||
type Runner struct {
|
||||
name string
|
||||
name string // Runner 名称
|
||||
|
||||
cfg *config.Config
|
||||
cfg *config.Config // 配置信息
|
||||
|
||||
client client.Client
|
||||
labels labels.Labels
|
||||
envs map[string]string
|
||||
client client.Client // Gitea 客户端
|
||||
labels labels.Labels // 标签集合
|
||||
envs map[string]string // 环境变量
|
||||
|
||||
runningTasks sync.Map
|
||||
// 正在运行的任务
|
||||
runningTasks sync.Map // 使用 sync.Map 来存储正在运行的任务 ID
|
||||
}
|
||||
|
||||
// NewRunner 使用提供的配置、注册信息和客户端创建一个新的 Runner 实例
|
||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||
ls := labels.Labels{}
|
||||
for _, v := range reg.Labels {
|
||||
@ -50,16 +54,35 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
||||
for k, v := range cfg.Runner.Envs {
|
||||
envs[k] = v
|
||||
}
|
||||
// 初始化缓存环境变量
|
||||
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
||||
cacheHandler, err := artifactcache.StartHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port)
|
||||
if err != nil {
|
||||
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||
// go on
|
||||
if cfg.Cache.ExternalServer != "" {
|
||||
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
|
||||
} else {
|
||||
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
|
||||
cacheHandler, err := artifactcache.StartHandler(
|
||||
cfg.Cache.Dir,
|
||||
cfg.Cache.Host,
|
||||
cfg.Cache.Port,
|
||||
log.StandardLogger().WithField("module", "cache_request"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("无法初始化缓存服务器,将禁用它:%v", err)
|
||||
// 继续执行
|
||||
} else {
|
||||
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 artifact gitea api
|
||||
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
||||
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
||||
envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/")
|
||||
|
||||
// 设置特定环境变量以区分 Gitea 和 GitHub
|
||||
envs["GITEA_ACTIONS"] = "true"
|
||||
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
|
||||
|
||||
return &Runner{
|
||||
name: reg.Name,
|
||||
cfg: cfg,
|
||||
@ -69,14 +92,16 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
||||
}
|
||||
}
|
||||
|
||||
// Run 在给定的上下文中执行任务,确保同一时间仅运行一个任务
|
||||
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
// 检查任务是否已在运行
|
||||
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||
return fmt.Errorf("task %d is already running", task.Id)
|
||||
} else {
|
||||
r.runningTasks.Store(task.Id, struct{}{})
|
||||
defer r.runningTasks.Delete(task.Id)
|
||||
return fmt.Errorf("任务 %d 已经在运行", task.Id)
|
||||
}
|
||||
r.runningTasks.Store(task.Id, struct{}{})
|
||||
defer r.runningTasks.Delete(task.Id)
|
||||
|
||||
// 创建带超时的子上下文
|
||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||
defer cancel()
|
||||
reporter := report.NewReporter(ctx, cancel, r.client, task)
|
||||
@ -94,6 +119,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// run 执行具体的任务逻辑
|
||||
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -101,18 +127,14 @@ 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, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||
workflow, jobID, err := generateWorkflow(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobIDs := workflow.GetJobIDs()
|
||||
if len(jobIDs) != 1 {
|
||||
return fmt.Errorf("multiple jobs found: %v", jobIDs)
|
||||
}
|
||||
jobID := jobIDs[0]
|
||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -122,10 +144,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
|
||||
taskContext := task.Context.Fields
|
||||
|
||||
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||
log.Infof("任务 %v 仓库是 %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||
taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
r.client.Address())
|
||||
|
||||
// 构建预设的 GitHub 上下文环境
|
||||
preset := &model.GithubContext{
|
||||
Event: taskContext["event"].GetStructValue().AsMap(),
|
||||
RunID: taskContext["run_id"].GetStringValue(),
|
||||
@ -143,12 +166,26 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
|
||||
RetentionDays: taskContext["retention_days"].GetStringValue(),
|
||||
}
|
||||
// 优先使用 GITEA_TOKEN
|
||||
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
}
|
||||
|
||||
if actionsIdTokenRequestUrl := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIdTokenRequestUrl != "" {
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIdTokenRequestUrl
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
||||
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
||||
}
|
||||
|
||||
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
|
||||
if giteaRuntimeToken == "" {
|
||||
// 兼容旧版本 Gitea Server
|
||||
giteaRuntimeToken = preset.Token
|
||||
}
|
||||
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
||||
|
||||
eventJSON, err := json.Marshal(preset.Event)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -159,29 +196,35 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// 创建 Runner 配置
|
||||
runnerConfig := &runner.Config{
|
||||
// On Linux, Workdir will be like "/<owner>/<repo>"
|
||||
// On Windows, Workdir will be like "\<owner>\<repo>"
|
||||
Workdir: filepath.FromSlash(string(filepath.Separator) + preset.Repository),
|
||||
BindWorkdir: false,
|
||||
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
|
||||
BindWorkdir: false,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: false,
|
||||
ForceRebuild: false,
|
||||
ForcePull: r.cfg.Container.ForcePull,
|
||||
ForceRebuild: r.cfg.Container.ForceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: false,
|
||||
Env: r.envs,
|
||||
Secrets: task.Secrets,
|
||||
GitHubInstance: r.client.Address(),
|
||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||
AutoRemove: true,
|
||||
NoSkipCheckout: true,
|
||||
PresetGitHubContext: preset,
|
||||
EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: r.cfg.Container.NetworkMode,
|
||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||
ContainerOptions: r.cfg.Container.Options,
|
||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||
Privileged: r.cfg.Container.Privileged,
|
||||
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
PlatformPicker: r.labels.PickPlatform,
|
||||
Vars: task.Vars,
|
||||
ValidVolumes: r.cfg.Container.ValidVolumes,
|
||||
InsecureSkipTLS: r.cfg.Runner.Insecure,
|
||||
}
|
||||
|
||||
rr, err := runner.New(runnerConfig)
|
||||
@ -190,10 +233,23 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
}
|
||||
executor := rr.NewPlanExecutor(plan)
|
||||
|
||||
reporter.Logf("workflow prepared")
|
||||
reporter.Logf("工作流程已准备就绪")
|
||||
|
||||
// add logger recorders
|
||||
// 添加日志记录器
|
||||
ctx = common.WithLoggerHook(ctx, reporter)
|
||||
|
||||
return executor(ctx)
|
||||
if !log.IsLevelEnabled(log.DebugLevel) {
|
||||
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
||||
}
|
||||
|
||||
execErr := executor(ctx)
|
||||
reporter.SetOutputs(job.Outputs)
|
||||
return execErr
|
||||
}
|
||||
|
||||
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
|
||||
Version: ver.Version(),
|
||||
Labels: labels,
|
||||
}))
|
||||
}
|
||||
|
54
internal/app/run/workflow.go
Normal file
54
internal/app/run/workflow.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
jobIDs := workflow.GetJobIDs()
|
||||
if len(jobIDs) != 1 {
|
||||
return nil, "", fmt.Errorf("找到多个工作: %v", jobIDs)
|
||||
}
|
||||
jobID := jobIDs[0]
|
||||
|
||||
needJobIDs := make([]string, 0, len(task.Needs))
|
||||
for id, need := range task.Needs {
|
||||
needJobIDs = append(needJobIDs, id)
|
||||
needJob := &model.Job{
|
||||
Outputs: need.Outputs,
|
||||
Result: strings.ToLower(strings.TrimPrefix(need.Result.String(), "RESULT_")),
|
||||
}
|
||||
workflow.Jobs[id] = needJob
|
||||
}
|
||||
sort.Strings(needJobIDs)
|
||||
|
||||
rawNeeds := yaml.Node{
|
||||
Kind: yaml.SequenceNode,
|
||||
Content: make([]*yaml.Node, 0, len(needJobIDs)),
|
||||
}
|
||||
for _, id := range needJobIDs {
|
||||
rawNeeds.Content = append(rawNeeds.Content, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: id,
|
||||
})
|
||||
}
|
||||
|
||||
workflow.Jobs[jobID].RawNeeds = rawNeeds
|
||||
|
||||
return workflow, jobID, nil
|
||||
}
|
74
internal/app/run/workflow_test.go
Normal file
74
internal/app/run/workflow_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func Test_生成工作流(t *testing.T) {
|
||||
type args struct {
|
||||
task *runnerv1.Task
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
assert func(t *testing.T, wf *model.Workflow)
|
||||
want1 string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "有需求",
|
||||
args: args{
|
||||
task: &runnerv1.Task{
|
||||
WorkflowPayload: []byte(`
|
||||
name: 构建部署测试
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
job9:
|
||||
needs: build
|
||||
runs-on: linux-loong64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./deploy --build ${{ needs.job1.outputs.output1 }}
|
||||
- run: ./deploy --build ${{ needs.job2.outputs.output2 }}
|
||||
`),
|
||||
Needs: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Outputs: map[string]string{
|
||||
"output1": "输出1值",
|
||||
},
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
"job2": {
|
||||
Outputs: map[string]string{
|
||||
"output2": "输出2值",
|
||||
},
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, wf *model.Workflow) {
|
||||
assert.DeepEqual(t, wf.GetJob("job9").Needs(), []string{"job1", "job2"})
|
||||
},
|
||||
want1: "job9",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := generateWorkflow(tt.args.task)
|
||||
require.NoError(t, err)
|
||||
tt.assert(t, got)
|
||||
assert.Equal(t, got1, tt.want1)
|
||||
})
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
// A Client manages communication with the runner.
|
||||
//
|
||||
//go:generate mockery --name Client
|
||||
type Client interface {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
||||
|
@ -4,7 +4,8 @@
|
||||
package client
|
||||
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
// 已弃用: 可以在Gitea 1.20发布后删除
|
||||
VersionHeader = "x-runner-version"
|
||||
)
|
||||
|
@ -11,10 +11,10 @@ import (
|
||||
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
)
|
||||
|
||||
func getHttpClient(endpoint string, insecure bool) *http.Client {
|
||||
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||
if strings.HasPrefix(endpoint, "https://") && insecure {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
@ -27,7 +27,7 @@ func getHttpClient(endpoint string, insecure bool) *http.Client {
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
// New returns a new runner client.
|
||||
// New返回一个新的runner客户端。
|
||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||
|
||||
@ -39,6 +39,7 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
||||
if token != "" {
|
||||
req.Header().Set(TokenHeader, token)
|
||||
}
|
||||
// TODO:version将在Gitea 1.20发布后从请求标头中删除。
|
||||
if version != "" {
|
||||
req.Header().Set(VersionHeader, version)
|
||||
}
|
||||
@ -48,12 +49,12 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
||||
|
||||
return &HTTPClient{
|
||||
PingServiceClient: pingv1connect.NewPingServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
getHTTPClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
getHTTPClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
@ -72,7 +73,7 @@ func (c *HTTPClient) Insecure() bool {
|
||||
|
||||
var _ Client = (*HTTPClient)(nil)
|
||||
|
||||
// An HTTPClient manages communication with the runner API.
|
||||
// HTTPClient管理与runner API的通信。
|
||||
type HTTPClient struct {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
||||
|
251
internal/pkg/client/mocks/Client.go
Normal file
251
internal/pkg/client/mocks/Client.go
Normal file
@ -0,0 +1,251 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
connect "connectrpc.com/connect"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
)
|
||||
|
||||
// Client is an autogenerated mock type for the Client type
|
||||
type Client struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Address provides a mock function with given fields:
|
||||
func (_m *Client) Address() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Address")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Declare provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) Declare(_a0 context.Context, _a1 *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Declare")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[runnerv1.DeclareResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) *connect.Response[runnerv1.DeclareResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[runnerv1.DeclareResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FetchTask provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) FetchTask(_a0 context.Context, _a1 *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FetchTask")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[runnerv1.FetchTaskResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) *connect.Response[runnerv1.FetchTaskResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[runnerv1.FetchTaskResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Insecure provides a mock function with given fields:
|
||||
func (_m *Client) Insecure() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Insecure")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Ping provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) Ping(_a0 context.Context, _a1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ping")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[pingv1.PingResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) *connect.Response[pingv1.PingResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[pingv1.PingResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[pingv1.PingRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Register provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) Register(_a0 context.Context, _a1 *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Register")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[runnerv1.RegisterResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) *connect.Response[runnerv1.RegisterResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[runnerv1.RegisterResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateLog provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) UpdateLog(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateLog")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[runnerv1.UpdateLogResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) *connect.Response[runnerv1.UpdateLogResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateLogResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateTask provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Client) UpdateTask(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateTask")
|
||||
}
|
||||
|
||||
var r0 *connect.Response[runnerv1.UpdateTaskResponse]
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) *connect.Response[runnerv1.UpdateTaskResponse]); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateTaskResponse])
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
},
|
||||
) *Client {
|
||||
mock := &Client{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,42 +1,101 @@
|
||||
# Example configuration file, it's safe to copy this as the default config file without any modification.
|
||||
# 示例配置文件,可以安全地将其复制为默认配置文件而无需任何修改。
|
||||
|
||||
# 您不必将此文件复制到您的实例,
|
||||
# 只需运行 `./act_runner generate-config > config.yaml` 来生成配置文件。
|
||||
|
||||
log:
|
||||
# The level of logging, can be trace, debug, info, warn, error, fatal
|
||||
# 日志级别,可以是 trace、debug、info、warn、error、fatal
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# Where to store the registration result.
|
||||
# 存储注册结果的路径。
|
||||
file: .runner
|
||||
# Execute how many tasks concurrently at the same time.
|
||||
# 同时执行多少个任务。
|
||||
capacity: 1
|
||||
# Extra environment variables to run jobs.
|
||||
# 运行作业的额外环境变量。
|
||||
envs:
|
||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
# Extra environment variables to run jobs from a file.
|
||||
# It will be ignored if it's empty or the file doesn't exist.
|
||||
# 从文件中运行作业的额外环境变量。
|
||||
# 如果为空或文件不存在,则会被忽略。
|
||||
env_file: .env
|
||||
# The timeout for a job to be finished.
|
||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
|
||||
# 作业完成的超时时间。
|
||||
# 请注意,Gitea 实例也有一个作业超时时间(默认为 3 小时)。
|
||||
# 如果这个超时时间比 Gitea 实例的超时时间短,作业可能会被 Gitea 实例停止。
|
||||
timeout: 3h
|
||||
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||
# 运行器在关闭时等待运行作业完成的超时时间。
|
||||
# 在这个超时时间之后仍未完成的任何运行作业将被取消。
|
||||
shutdown_timeout: 0s
|
||||
# 是否跳过验证 Gitea 实例的 TLS 证书。
|
||||
insecure: false
|
||||
# 从 Gitea 实例获取作业的超时时间。
|
||||
fetch_timeout: 5s
|
||||
# 从 Gitea 实例获取作业的时间间隔。
|
||||
fetch_interval: 2s
|
||||
# 运行器的标签用于确定运行器可以运行哪些作业以及如何运行它们。
|
||||
# 例如:"linux-loong64.abi2:host" 或 "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
|
||||
# 在 https://gitea.com/docker.gitea.com/runner-images 查找 Gitea 提供的更多镜像。
|
||||
# 如果在注册时为空,它会要求输入标签。
|
||||
# 如果在执行 `daemon` 时为空,将使用 `.runner` 文件中的标签。
|
||||
labels:
|
||||
- "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
|
||||
- "anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest"
|
||||
- "anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2"
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
# 启用缓存服务器以使用 actions/cache。
|
||||
enabled: true
|
||||
# The directory to store the cache data.
|
||||
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
|
||||
# 存储缓存数据的目录。
|
||||
# 如果为空,缓存数据将存储在 $HOME/.cache/actcache。
|
||||
dir: ""
|
||||
# The host of the cache server.
|
||||
# It's not for the address to listen, but the address to connect from job containers.
|
||||
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
|
||||
# 缓存服务器的主机。
|
||||
# 它不是用于监听的地址,而是用于作业容器连接的地址。
|
||||
# 所以 0.0.0.0 是一个糟糕的选择,留空以自动检测。
|
||||
host: ""
|
||||
# The port of the cache server.
|
||||
# 0 means to use a random available port.
|
||||
# 缓存服务器的端口。
|
||||
# 0 表示使用随机可用端口。
|
||||
port: 0
|
||||
# 外部缓存服务器 URL。仅在启用时有效。
|
||||
# 如果指定了它,act_runner 将使用此 URL 作为 ACTIONS_CACHE_URL 而不是自己启动一个服务器。
|
||||
# URL 通常应该以 "/" 结尾。
|
||||
external_server: ""
|
||||
|
||||
container:
|
||||
# Which network to use for the job containers. Could be bridge, host, none, or the name of a custom network.
|
||||
network_mode: bridge
|
||||
# 指定容器将连接的网络。
|
||||
# 可以是 host、bridge 或自定义网络的名称。
|
||||
# 如果为空,act_runner 将自动创建一个网络。
|
||||
network: ""
|
||||
# 启动任务容器时是否使用特权模式(特权模式对于 Docker-in-Docker 是必需的)。
|
||||
privileged: false
|
||||
# 容器启动时使用的其他选项(例如,--add-host=my.gitea.url:host-gateway)。
|
||||
options:
|
||||
# 作业工作目录的父目录。
|
||||
# 注意:不需要在路径前添加第一个 '/',因为 act_runner 会自动添加。
|
||||
# 如果路径以 '/' 开头,'/' 将被修剪。
|
||||
# 例如,如果父目录是 /path/to/my/dir,workdir_parent 应该是 path/to/my/dir
|
||||
# 如果为空,将使用 /workspace。
|
||||
workdir_parent:
|
||||
# 可以挂载到容器的卷(包括绑定挂载)。支持 glob 语法,参见 https://github.com/gobwas/glob
|
||||
# 您可以指定多个卷。如果序列为空,则不能挂载任何卷。
|
||||
# 例如,如果您只允许容器挂载 `data` 卷和 `/src` 中的所有 json 文件,您应该将配置更改为:
|
||||
# valid_volumes:
|
||||
# - data
|
||||
# - /src/*.json
|
||||
# 如果您想允许任何卷,请使用以下配置:
|
||||
# valid_volumes:
|
||||
# - '**'
|
||||
valid_volumes: []
|
||||
# 用指定的主机覆盖 docker 客户端主机。
|
||||
# 如果为空,act_runner 将自动查找可用的 docker 主机。
|
||||
# 如果是 "-",act_runner 将自动查找可用的 docker 主机,但 docker 主机不会挂载到作业容器和服务容器。
|
||||
# 如果不为空或 "-",将使用指定的 docker 主机。如果不起作用,将返回错误。
|
||||
docker_host: ""
|
||||
# 即使已经存在也拉取 Docker 镜像
|
||||
force_pull: true
|
||||
# 即使已经存在也重建 Docker 镜像
|
||||
force_rebuild: false
|
||||
|
||||
host:
|
||||
# 作业工作目录的父目录。
|
||||
# 如果为空,将使用 $HOME/.cache/act/。
|
||||
workdir_parent:
|
@ -10,45 +10,76 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Log struct {
|
||||
Level string `yaml:"level"`
|
||||
} `yaml:"log"`
|
||||
Runner struct {
|
||||
File string `yaml:"file"`
|
||||
Capacity int `yaml:"capacity"`
|
||||
Envs map[string]string `yaml:"envs"`
|
||||
EnvFile string `yaml:"env_file"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
} `yaml:"runner"`
|
||||
Cache struct {
|
||||
Enabled *bool `yaml:"enabled"` // pointer to distinguish between false and not set, and it will be true if not set
|
||||
Dir string `yaml:"dir"`
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
} `yaml:"cache"`
|
||||
Container struct {
|
||||
NetworkMode string `yaml:"network_mode"`
|
||||
}
|
||||
// Log 代表日志的配置。
|
||||
type Log struct {
|
||||
Level string `yaml:"level"` // Level 表示日志级别。
|
||||
}
|
||||
|
||||
// LoadDefault returns the default configuration.
|
||||
// If file is not empty, it will be used to load the configuration.
|
||||
// Runner 代表运行器的配置。
|
||||
type Runner struct {
|
||||
File string `yaml:"file"` // File 指定运行器的文件路径。
|
||||
Capacity int `yaml:"capacity"` // Capacity 指定运行器的容量。
|
||||
Envs map[string]string `yaml:"envs"` // Envs 存储运行器的环境变量。
|
||||
EnvFile string `yaml:"env_file"` // EnvFile 指定包含运行器环境变量的文件路径。
|
||||
Timeout time.Duration `yaml:"timeout"` // Timeout 指定运行器超时的持续时间。
|
||||
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout 指定在运行器关闭期间等待运行作业完成的持续时间。
|
||||
Insecure bool `yaml:"insecure"` // Insecure 表示运行器是否在不安全模式下运行。
|
||||
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout 指定获取资源的超时持续时间。
|
||||
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval 指定获取资源的间隔持续时间。
|
||||
Labels []string `yaml:"labels"` // Labels 指定运行器的标签。每个启动时声明标签。
|
||||
}
|
||||
|
||||
// Cache 代表缓存的配置。
|
||||
type Cache struct {
|
||||
Enabled *bool `yaml:"enabled"` // Enabled 表示是否启用缓存。它是指针,用于区分 false 和未设置。如果未设置,它将为 true。
|
||||
Dir string `yaml:"dir"` // Dir 指定缓存的目录路径。
|
||||
Host string `yaml:"host"` // Host 指定缓存主机。
|
||||
Port uint16 `yaml:"port"` // Port 指定缓存端口。
|
||||
ExternalServer string `yaml:"external_server"` // ExternalServer 指定外部缓存服务器的 URL。
|
||||
}
|
||||
|
||||
// Container 代表容器的配置。
|
||||
type Container struct {
|
||||
Network string `yaml:"network"` // Network 指定容器的网络。
|
||||
NetworkMode string `yaml:"network_mode"` // 已弃用:使用 Network 替代。可能在 Gitea 1.20 之后被移除。
|
||||
Privileged bool `yaml:"privileged"` // Privileged 表示容器是否以特权模式运行。
|
||||
Options string `yaml:"options"` // Options 指定容器的其他选项。
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定容器工作目录的父目录。
|
||||
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes 指定可以挂载到容器的卷(包括绑定挂载)。
|
||||
DockerHost string `yaml:"docker_host"` // DockerHost 指定 Docker 主机。它覆盖环境变量 DOCKER_HOST 中指定的值。
|
||||
ForcePull bool `yaml:"force_pull"` // 即使已经存在也拉取 Docker 镜像。
|
||||
ForceRebuild bool `yaml:"force_rebuild"` // 即使已经存在也重建 Docker 镜像。
|
||||
}
|
||||
|
||||
// Host 代表主机的配置。
|
||||
type Host struct {
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定主机工作目录的父目录。
|
||||
}
|
||||
|
||||
// Config 代表整体配置。
|
||||
type Config struct {
|
||||
Log Log `yaml:"log"` // Log 代表日志的配置。
|
||||
Runner Runner `yaml:"runner"` // Runner 代表运行器的配置。
|
||||
Cache Cache `yaml:"cache"` // Cache 代表缓存的配置。
|
||||
Container Container `yaml:"container"` // Container 代表容器的配置。
|
||||
Host Host `yaml:"host"` // Host 代表主机的配置。
|
||||
}
|
||||
|
||||
// LoadDefault 返回默认配置。
|
||||
// 如果文件不为空,它将被用来加载配置。
|
||||
func LoadDefault(file string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if file != "" {
|
||||
f, err := os.Open(file)
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("打开配置文件 %q: %w", file, err)
|
||||
}
|
||||
defer f.Close()
|
||||
decoder := yaml.NewDecoder(f)
|
||||
if err := decoder.Decode(&cfg); err != nil {
|
||||
return nil, err
|
||||
if err := yaml.Unmarshal(content, cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件 %q: %w", file, err)
|
||||
}
|
||||
}
|
||||
compatibleWithOldEnvs(file != "", cfg)
|
||||
@ -57,7 +88,10 @@ func LoadDefault(file string) (*Config, error) {
|
||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||
envs, err := godotenv.Read(cfg.Runner.EnvFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read env file %q: %w", cfg.Runner.EnvFile, err)
|
||||
return nil, fmt.Errorf("读取环境文件 %q: %w", cfg.Runner.EnvFile, err)
|
||||
}
|
||||
if cfg.Runner.Envs == nil {
|
||||
cfg.Runner.Envs = map[string]string{}
|
||||
}
|
||||
for k, v := range envs {
|
||||
cfg.Runner.Envs[k] = v
|
||||
@ -87,8 +121,30 @@ func LoadDefault(file string) (*Config, error) {
|
||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
}
|
||||
if cfg.Container.NetworkMode == "" {
|
||||
cfg.Container.NetworkMode = "bridge"
|
||||
if cfg.Container.WorkdirParent == "" {
|
||||
cfg.Container.WorkdirParent = "workspace"
|
||||
}
|
||||
if cfg.Host.WorkdirParent == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
||||
}
|
||||
if cfg.Runner.FetchTimeout <= 0 {
|
||||
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||
}
|
||||
if cfg.Runner.FetchInterval <= 0 {
|
||||
cfg.Runner.FetchInterval = 2 * time.Second
|
||||
}
|
||||
|
||||
// 虽然 `container.network_mode` 将被弃用,但我们现在必须与它兼容。
|
||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||
log.Warn("您正在尝试使用已弃用的配置项 `container.network_mode`,请使用 `container.network` 替代。")
|
||||
if cfg.Container.NetworkMode == "bridge" {
|
||||
// 以前,如果 `container.network_mode` 的值是 `bridge`,我们会为作业创建一个新的网络。
|
||||
// 但是,“bridge”很容易与 Docker 默认创建的桥接网络混淆。
|
||||
// 所以我们将 `container.network` 的值设置为空字符串,以使 `act_runner` 为作业自动创建一个新的网络。
|
||||
} else {
|
||||
cfg.Container.Network = cfg.Container.NetworkMode
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
@ -11,16 +11,16 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
||||
// Be compatible with old envs.
|
||||
// 已弃用:未来可能会移除。TODO: 在 Gitea 1.20.0 发布时将其移除。
|
||||
// 兼容旧环境。
|
||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
||||
handleEnv := func(key string) (string, bool) {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if fileUsed {
|
||||
log.Warnf("env %s has been ignored because config file is used", key)
|
||||
log.Warnf("环境 %s 已被忽略,因为使用了配置文件", key)
|
||||
return "", false
|
||||
}
|
||||
log.Warnf("env %s will be deprecated, please use config file instead", key)
|
||||
log.Warnf("环境变量 %s 将被弃用,请改用配置文件。", key)
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
|
@ -8,18 +8,19 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const registrationWarning = "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner."
|
||||
const registrationWarning = "此文件由`Loong Runner`自动生成。除非你知道自己在做什么, 否则不要手动编辑它。删除此文件将导致`Loong Runner`重新注册为新的运行器。"
|
||||
|
||||
// Registration is the registration information for a runner
|
||||
// Registration 表示运行器的注册信息
|
||||
type Registration struct {
|
||||
Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
|
||||
Warning string `json:"WARNING"` // 警告信息,始终为 registrationWarning 常量
|
||||
|
||||
ID int64 `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Labels []string `json:"labels"`
|
||||
ID int64 `json:"id"` // 唯一标识符(整数类型)
|
||||
UUID string `json:"uuid"` // 全局唯一标识符(字符串类型)
|
||||
Name string `json:"name"` // 运行器名称
|
||||
Token string `json:"token"` // 认证令牌,用于与调度器通信
|
||||
Address string `json:"address"` // 运行器的网络地址(如IP或域名)
|
||||
Labels []string `json:"labels"` // 运行器关联的标签列表(用于任务匹配)
|
||||
Ephemeral bool `json:"ephemeral"` // 是否为临时实例(完成任务后自动销毁)
|
||||
}
|
||||
|
||||
func LoadRegistration(file string) (*Registration, error) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package envcheck provides a simple way to check if the environment is ready to run jobs.
|
||||
// envcheck包中提供了一种简单的方法, 用来检查环境是否准备好执行工作。
|
||||
package envcheck
|
||||
|
@ -10,9 +10,16 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func CheckIfDockerRunning(ctx context.Context) error {
|
||||
// TODO: if runner support configures to use docker, we need config.Config to pass in
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
opts := []client.Opt{
|
||||
client.FromEnv,
|
||||
}
|
||||
|
||||
if configDockerHost != "" {
|
||||
opts = append(opts, client.WithHost(configDockerHost))
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -20,7 +27,7 @@ func CheckIfDockerRunning(ctx context.Context) error {
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ping the docker daemon, does it running? %w", err)
|
||||
return fmt.Errorf("无法ping通docker守护进程, 它是否在运行? %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SchemeHost = "host"
|
||||
// SchemeHost 表示主机模式
|
||||
SchemeHost = "host"
|
||||
// SchemeDocker 表示 Docker 模式
|
||||
SchemeDocker = "docker"
|
||||
)
|
||||
|
||||
@ -19,6 +21,7 @@ type Label struct {
|
||||
Arg string
|
||||
}
|
||||
|
||||
// Parse 解析标签字符串并返回 Label 结构体。
|
||||
func Parse(str string) (*Label, error) {
|
||||
splits := strings.SplitN(str, ":", 3)
|
||||
label := &Label{
|
||||
@ -33,13 +36,14 @@ func Parse(str string) (*Label, error) {
|
||||
label.Arg = splits[2]
|
||||
}
|
||||
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
|
||||
return nil, fmt.Errorf("unsupported schema: %s", label.Schema)
|
||||
return nil, fmt.Errorf("不支持的标签: %s", label.Schema)
|
||||
}
|
||||
return label, nil
|
||||
}
|
||||
|
||||
type Labels []*Label
|
||||
|
||||
// RequireDocker 检查 Labels 是否需要 Docker。
|
||||
func (l Labels) RequireDocker() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeDocker {
|
||||
@ -49,18 +53,18 @@ func (l Labels) RequireDocker() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PickPlatform 根据 runsOn 列表选择一个平台。
|
||||
func (l Labels) PickPlatform(runsOn []string) string {
|
||||
platforms := make(map[string]string, len(l))
|
||||
for _, label := range l {
|
||||
switch label.Schema {
|
||||
case SchemeDocker:
|
||||
// "//" will be ignored
|
||||
// TODO maybe we should use 'ubuntu-18.04:docker:node:16-buster' instead
|
||||
// 忽略 "//"
|
||||
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||
case SchemeHost:
|
||||
platforms[label.Name] = "-self-hosted"
|
||||
default:
|
||||
// It should not happen, because Parse has checked it.
|
||||
// 这不应该发生,因为 Parse 已经检查过了。
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -70,15 +74,40 @@ func (l Labels) PickPlatform(runsOn []string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support multiple labels
|
||||
// like:
|
||||
// ["ubuntu-22.04"] => "ubuntu:22.04"
|
||||
// TODO: 支持多个标签
|
||||
// 例如:
|
||||
// ["debian-12"] => "debian:12"
|
||||
// ["with-gpu"] => "linux:with-gpu"
|
||||
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
|
||||
// ["debian-12", "with-gpu"] => "debian:12_with-gpu"
|
||||
|
||||
// return default.
|
||||
// So the runner receives a task with a label that the runner doesn't have,
|
||||
// it happens when the user have edited the label of the runner in the web UI.
|
||||
// TODO: it may be not correct, what if the runner is used as host mode only?
|
||||
return "node:16-bullseye"
|
||||
// 返回默认值。
|
||||
// 因此,当运行器收到一个它没有的标签的任务时,
|
||||
// 这发生在用户在 Web UI 中编辑了运行器的标签时。
|
||||
// TODO: 这可能不正确,如果运行器仅作为主机模式使用呢?
|
||||
return "lcr.loongnix.cn/library/debian:latest"
|
||||
}
|
||||
|
||||
// Names 返回所有标签的名称。
|
||||
func (l Labels) Names() []string {
|
||||
names := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
names = append(names, label.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ToStrings 将 Labels 转换为字符串数组。
|
||||
func (l Labels) ToStrings() []string {
|
||||
ls := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
lbl := label.Name
|
||||
if label.Schema != "" {
|
||||
lbl += ":" + label.Schema
|
||||
if label.Arg != "" {
|
||||
lbl += ":" + label.Arg
|
||||
}
|
||||
}
|
||||
ls = append(ls, lbl)
|
||||
}
|
||||
return ls
|
||||
}
|
||||
|
@ -10,41 +10,41 @@ import (
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
func Test解析(t *testing.T) {
|
||||
tests := []struct {
|
||||
args string
|
||||
want *Label
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
args: "ubuntu:docker://node:18",
|
||||
args: "debian:docker://node:18",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Name: "debian",
|
||||
Schema: "docker",
|
||||
Arg: "//node:18",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu:host",
|
||||
args: "aosc:host",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Name: "aosc",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu",
|
||||
args: "aosc",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Name: "aosc",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu:vm:ubuntu-18.04",
|
||||
args: "debian:vm:debian12",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
@ -55,9 +55,8 @@ func TestParse(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.DeepEqual(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
|
@ -1,63 +1,82 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Reporter 组件是负责任务执行状态报告和日志记录的核心模块
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
retry "github.com/avast/retry-go/v4"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
ctx context.Context // 上下文,用于控制生命周期
|
||||
cancel context.CancelFunc // 取消函数
|
||||
closed bool // 是否已关闭
|
||||
client client.Client // Gitea 客户端
|
||||
clientM sync.Mutex // 客户端访问互斥锁
|
||||
|
||||
closed bool
|
||||
client client.Client
|
||||
clientM sync.Mutex
|
||||
logOffset int // 日志偏移量
|
||||
logRows []*runnerv1.LogRow // 日志行缓存
|
||||
logReplacer *strings.Replacer // 日志内容替换器
|
||||
oldnew []string // 需要替换的敏感信息
|
||||
|
||||
logOffset int
|
||||
logRows []*runnerv1.LogRow
|
||||
logReplacer *strings.Replacer
|
||||
state *runnerv1.TaskState
|
||||
stateM sync.RWMutex
|
||||
state *runnerv1.TaskState // 任务状态
|
||||
stateMu sync.RWMutex // 状态访问读写锁
|
||||
outputs sync.Map // 输出参数存储
|
||||
|
||||
debugOutputEnabled bool // 是否启用调试输出
|
||||
stopCommandEndToken string // 停止命令结束标记
|
||||
}
|
||||
|
||||
// NewReporter 构造函数 初始化日志脱敏规则、状态等信息
|
||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
if v := task.Context.Fields["gitea_runtime_token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
for _, v := range task.Secrets {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
|
||||
return &Reporter{
|
||||
rv := &Reporter{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
client: client,
|
||||
oldnew: oldnew,
|
||||
logReplacer: strings.NewReplacer(oldnew...),
|
||||
state: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
},
|
||||
}
|
||||
|
||||
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||
rv.debugOutputEnabled = true
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
// ResetSteps 重置任务状态中的步骤状态数组,为即将开始的新任务做准备
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
for i := 0; i < l; i++ {
|
||||
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
||||
Id: int64(i),
|
||||
@ -65,13 +84,23 @@ func (r *Reporter) ResetSteps(l int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Levels 返回所有支持的日志级别(通常是为了兼容 logger 接口)
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
|
||||
// appendIfNotNil 通用函数,如果传入指针不为空,则将其添加到切片中。
|
||||
func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||
if v != nil {
|
||||
return append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 接收日志条目,处理日志内容,并更新任务状态。
|
||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
log.WithFields(entry.Data).Trace(entry.Message)
|
||||
|
||||
@ -90,12 +119,15 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
for _, s := range r.state.Steps {
|
||||
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
s.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
if jobResult == runnerv1.Result_RESULT_SKIPPED {
|
||||
s.Result = runnerv1.Result_RESULT_SKIPPED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -108,7 +140,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
}
|
||||
if step == nil {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -118,14 +150,16 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
}
|
||||
if v, ok := entry.Data["raw_output"]; ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
if row := r.parseLogRow(entry); row != nil {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.logRows = append(r.logRows, row)
|
||||
}
|
||||
step.LogLength++
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
} else if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
if v, ok := entry.Data["stepResult"]; ok {
|
||||
if stepResult, ok := r.parseResult(v); ok {
|
||||
@ -140,6 +174,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunDaemon 定时运行后台任务,定期将缓存中的日志和状态上报给 Gitea
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
@ -154,10 +189,16 @@ func (r *Reporter) RunDaemon() {
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
}
|
||||
|
||||
// 自定义日志记录方法,格式化输出并保存到 logRows 中
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
r.logf(format, a...)
|
||||
}
|
||||
|
||||
// 内部使用的日志记录方法,仅在任务执行期间记录日志
|
||||
func (r *Reporter) logf(format string, a ...interface{}) {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
@ -166,10 +207,32 @@ func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetOutputs 设置输出参数,过滤无效或超限的键值对
|
||||
func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
for k, v := range outputs {
|
||||
if len(k) > 255 {
|
||||
r.logf("忽略超长键值对的键: %q", k)
|
||||
continue
|
||||
}
|
||||
if l := len(v); l > 1024*1024 {
|
||||
log.Println("忽略超长键值对的值:", k, l)
|
||||
r.logf("忽略超长键值对的值 %q: %d 字节", k, l)
|
||||
}
|
||||
if _, ok := r.outputs.Load(k); ok {
|
||||
continue
|
||||
}
|
||||
r.outputs.Store(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭 reporter,确保最后一批日志和最终状态被上报
|
||||
func (r *Reporter) Close(lastWords string) error {
|
||||
r.closed = true
|
||||
|
||||
r.stateM.Lock()
|
||||
r.stateMu.Lock()
|
||||
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
if lastWords == "" {
|
||||
lastWords = "Early termination"
|
||||
@ -184,14 +247,14 @@ func (r *Reporter) Close(lastWords string) error {
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
return nil
|
||||
r.state.StoppedAt = timestamppb.Now()
|
||||
} else if lastWords != "" {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
}
|
||||
r.stateM.Unlock()
|
||||
r.stateMu.Unlock()
|
||||
|
||||
return retry.Do(func() error {
|
||||
if err := r.ReportLog(true); err != nil {
|
||||
@ -201,13 +264,14 @@ func (r *Reporter) Close(lastWords string) error {
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
// ReportLog 将当前缓存的日志批量上报到 Gitea
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateM.RLock()
|
||||
r.stateMu.RLock()
|
||||
rows := r.logRows
|
||||
r.stateM.RUnlock()
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: r.state.Id,
|
||||
@ -221,43 +285,69 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
||||
|
||||
ack := int(resp.Msg.AckIndex)
|
||||
if ack < r.logOffset {
|
||||
return fmt.Errorf("submitted logs are lost")
|
||||
return fmt.Errorf("已提交的日志丢失了")
|
||||
}
|
||||
|
||||
r.stateM.Lock()
|
||||
r.stateMu.Lock()
|
||||
r.logRows = r.logRows[ack-r.logOffset:]
|
||||
r.logOffset = ack
|
||||
r.stateM.Unlock()
|
||||
r.stateMu.Unlock()
|
||||
|
||||
if noMore && ack < r.logOffset+len(rows) {
|
||||
return fmt.Errorf("not all logs are submitted")
|
||||
return fmt.Errorf("并非所有日志都已提交")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportState 将当前任务状态上报到 Gitea
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateM.RLock()
|
||||
r.stateMu.RLock()
|
||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||
r.stateM.RUnlock()
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
outputs := make(map[string]string)
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if val, ok := v.(string); ok {
|
||||
outputs[k.(string)] = val
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: state,
|
||||
State: state,
|
||||
Outputs: outputs,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range resp.Msg.SentOutputs {
|
||||
r.outputs.Store(k, struct{}{})
|
||||
}
|
||||
|
||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
var noSent []string
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if _, ok := v.(string); ok {
|
||||
noSent = append(noSent, k.(string))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(noSent) > 0 {
|
||||
return fmt.Errorf("仍有一些输出尚未发送: %v", noSent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// duringSteps 判断当前是否正在执行某个步骤
|
||||
func (r *Reporter) duringSteps() bool {
|
||||
if steps := r.state.Steps; len(steps) == 0 {
|
||||
return false
|
||||
@ -276,11 +366,12 @@ var stringToResult = map[string]runnerv1.Result{
|
||||
"cancelled": runnerv1.Result_RESULT_CANCELLED,
|
||||
}
|
||||
|
||||
// parseResult 将字符串或 Stringer 类型的结果解析为 runnerv1.Result 类型
|
||||
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
str := ""
|
||||
if v, ok := result.(string); ok { // for jobResult
|
||||
if v, ok := result.(string); ok { // 对于作业结果
|
||||
str = v
|
||||
} else if v, ok := result.(fmt.Stringer); ok { // for stepResult
|
||||
} else if v, ok := result.(fmt.Stringer); ok { // 对于步骤结果
|
||||
str = v.String()
|
||||
}
|
||||
|
||||
@ -288,11 +379,73 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
// 匹配 GitHub Actions/Gitea Actions 特殊命令,如 ::set-output::, ::add-mask:: 等
|
||||
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||
|
||||
// handleCommand 处理日志中的特殊指令命令(如 ::add-mask::),根据命令执行对应操作
|
||||
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "add-mask":
|
||||
r.addMask(value)
|
||||
return nil
|
||||
case "debug":
|
||||
if r.debugOutputEnabled {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
|
||||
case "notice":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "warning":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "error":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "group":
|
||||
// 返回原始内容,因为我认为前端在渲染输出时会使用它。
|
||||
return &originalContent
|
||||
case "endgroup":
|
||||
// 同上
|
||||
return &originalContent
|
||||
case "stop-commands":
|
||||
r.stopCommandEndToken = value
|
||||
return nil
|
||||
case r.stopCommandEndToken:
|
||||
r.stopCommandEndToken = ""
|
||||
return nil
|
||||
}
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
// parseLogRow 将日志条目转换为 LogRow 结构,用于后续上报
|
||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
|
||||
matches := cmdRegex.FindStringSubmatch(content)
|
||||
if matches != nil {
|
||||
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
|
||||
content = *output
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
content = r.logReplacer.Replace(content)
|
||||
|
||||
return &runnerv1.LogRow{
|
||||
Time: timestamppb.New(entry.Time),
|
||||
Content: content,
|
||||
Content: strings.ToValidUTF8(content, "?"),
|
||||
}
|
||||
}
|
||||
|
||||
// addMask 将指定字符串加入脱敏列表,之后所有日志中出现该字符串都会被替换成 ***
|
||||
func (r *Reporter) addMask(msg string) {
|
||||
r.oldnew = append(r.oldnew, msg, "***")
|
||||
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
||||
}
|
||||
|
223
internal/pkg/report/reporter_test.go
Normal file
223
internal/pkg/report/reporter_test.go
Normal file
@ -0,0 +1,223 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
connect_go "connectrpc.com/connect"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks"
|
||||
)
|
||||
|
||||
func Test记录器_解析输出日志(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string // 测试用例名称
|
||||
debugOutputEnabled bool // 是否启用调试输出
|
||||
args []string // 输入的日志行
|
||||
want []string // 期望的输出结果
|
||||
}{
|
||||
{
|
||||
name: "无命令",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{"你好,世界!"},
|
||||
want: []string{"你好,世界!"},
|
||||
},
|
||||
{
|
||||
name: "添加掩码",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"foo 我的密钥 bar", // 输入日志:普通日志行
|
||||
"::add-mask::我的密钥", // 输入命令:添加掩码
|
||||
"foo 我的密钥 bar", // 输入日志:再次普通日志行
|
||||
},
|
||||
want: []string{
|
||||
"foo 我的密钥 bar", // 原始日志直接输出
|
||||
"<nil>", // 添加掩码命令处理结果(无输出内容)
|
||||
"foo *** bar", // 掩码替换后的日志行
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "启用调试",
|
||||
debugOutputEnabled: true,
|
||||
args: []string{
|
||||
"::debug::GitHub Actions 运行时令牌访问控制",
|
||||
},
|
||||
want: []string{
|
||||
"GitHub Actions 运行时令牌访问控制", // 调试信息直接输出
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "禁用调试",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::debug::GitHub Actions 运行时令牌访问控制",
|
||||
},
|
||||
want: []string{
|
||||
"<nil>", // 调试信息被忽略
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "通知",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
|
||||
},
|
||||
want: []string{
|
||||
"::notice file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 通知日志原样输出
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "警告",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
|
||||
},
|
||||
want: []string{
|
||||
"::warning file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 警告日志原样输出
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "错误",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通",
|
||||
},
|
||||
want: []string{
|
||||
"::error file=文件.name,line=42,endLine=48,title=酷标题::天啊,这行不通", // 错误日志原样输出
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "分组",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::group::", // 开始分组
|
||||
"::endgroup::", // 结束分组
|
||||
},
|
||||
want: []string{
|
||||
"::group::", // 分组开始标记原样输出
|
||||
"::endgroup::", // 分组结束标记原样输出
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "停止命令",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::add-mask::foo", // 添加掩码命令
|
||||
"::stop-commands::我的停止令牌", // 停止命令标记
|
||||
"::add-mask::bar", // 被忽略的添加掩码命令
|
||||
"::debug::调试信息", // 被忽略的调试信息
|
||||
"我的停止令牌", // 停止命令标记结束
|
||||
"::add-mask::baz", // 恢复处理的添加掩码命令
|
||||
"::我的停止令牌::", // 另一种停止命令标记
|
||||
"::add-mask::wibble", // 被忽略的添加掩码命令
|
||||
"foo bar baz wibble", // 普通日志行
|
||||
},
|
||||
want: []string{
|
||||
"<nil>", // 第一个添加掩码命令处理结果
|
||||
"<nil>", // 停止命令标记处理结果
|
||||
"::add-mask::bar", // 被忽略的命令原样输出
|
||||
"::debug::调试信息", // 被忽略的调试信息原样输出
|
||||
"我的停止令牌", // 停止标记结束原样输出
|
||||
"::add-mask::baz", // 恢复处理的添加掩码命令
|
||||
"<nil>", // 无效停止命令标记处理结果
|
||||
"<nil>", // 被忽略的添加掩码命令处理结果
|
||||
"*** bar baz ***", // 掩码替换后的日志行
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "未知命令",
|
||||
debugOutputEnabled: false,
|
||||
args: []string{
|
||||
"::set-mask::foo", // 未知命令
|
||||
},
|
||||
want: []string{
|
||||
"::set-mask::foo", // 未知命令原样输出
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Reporter{
|
||||
logReplacer: strings.NewReplacer(),
|
||||
debugOutputEnabled: tt.debugOutputEnabled,
|
||||
}
|
||||
for idx, arg := range tt.args {
|
||||
rv := r.parseLogRow(&log.Entry{Message: arg})
|
||||
got := "<nil>"
|
||||
|
||||
if rv != nil {
|
||||
got = rv.Content
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want[idx], got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 Reporter 的 Fire 方法(验证命令行忽略逻辑)
|
||||
func Test记录器_触发(t *testing.T) {
|
||||
t.Run("忽略命令行", func(t *testing.T) { // 测试场景:验证是否正确处理需要忽略的命令行日志
|
||||
// 创建模拟客户端
|
||||
client := mocks.NewClient(t)
|
||||
|
||||
// 模拟 UpdateLog 接口调用,记录请求内容并返回响应
|
||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||
t.Logf("收到 UpdateLog 请求:%s", req.Msg.String()) // 记录日志请求内容
|
||||
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)), // 计算确认索引
|
||||
}), nil
|
||||
})
|
||||
|
||||
// 模拟 UpdateTask 接口调用
|
||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
t.Logf("收到 UpdateTask 请求:%s", req.Msg.String()) // 记录任务更新请求
|
||||
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||
})
|
||||
|
||||
// 初始化上下文和任务
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
taskCtx, err := structpb.NewStruct(map[string]interface{}{}) // 创建空任务上下文
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建 Reporter 实例
|
||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||
Context: taskCtx, // 注入任务上下文
|
||||
})
|
||||
defer func() {
|
||||
assert.NoError(t, reporter.Close("")) // 测试结束关闭 Reporter
|
||||
}()
|
||||
|
||||
reporter.ResetSteps(5) // 初始化5个步骤的日志存储
|
||||
|
||||
// 定义步骤0的日志元数据
|
||||
dataStep0 := map[string]interface{}{
|
||||
"stage": "Main", // 阶段名称
|
||||
"stepNumber": 0, // 步骤编号
|
||||
"raw_output": true, // 启用原始输出模式
|
||||
}
|
||||
|
||||
// 发送混合类型的日志条目 ---------------------------------------------------
|
||||
// 预期:普通日志被记录,调试日志被忽略
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::调试日志行", Data: dataStep0})) // 应被忽略
|
||||
assert.NoError(t, reporter.Fire(&log.Entry{Message: "普通日志行", Data: dataStep0}))
|
||||
|
||||
// 验证结果:步骤0应只有3条普通日志(调试日志被过滤)
|
||||
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength, "普通日志数量不符预期")
|
||||
})
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
package ver
|
||||
|
||||
// go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
|
||||
// go build -ldflags "-X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
func Version() string {
|
||||
|
4
main.go
4
main.go
@ -8,12 +8,12 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/cmd"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// run the command
|
||||
// 运行命令
|
||||
cmd.Execute(ctx)
|
||||
}
|
||||
|
6
renovate.json5
Normal file
6
renovate.json5
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>gitea/renovate-config"
|
||||
]
|
||||
}
|
70
scripts/run.sh
Executable file
70
scripts/run.sh
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 如果 /data 目录不存在,则创建它
|
||||
if [[ ! -d /data ]]; then
|
||||
mkdir -p /data
|
||||
fi
|
||||
|
||||
cd /data
|
||||
|
||||
# 运行器状态文件,默认为 '.runner'
|
||||
RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-'.runner'}
|
||||
|
||||
CONFIG_ARG=""
|
||||
# 如果设置了 CONFIG_FILE,则添加配置文件参数
|
||||
if [[ ! -z "${CONFIG_FILE}" ]]; then
|
||||
CONFIG_ARG="--config ${CONFIG_FILE}"
|
||||
fi
|
||||
EXTRA_ARGS=""
|
||||
# 如果设置了 GITEA_RUNNER_LABELS,则添加标签参数
|
||||
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
|
||||
EXTRA_ARGS="${EXTRA_ARGS} --labels ${GITEA_RUNNER_LABELS}"
|
||||
fi
|
||||
if [[ ! -z "${GITEA_RUNNER_EPHEMERAL}" ]]; then
|
||||
EXTRA_ARGS="${EXTRA_ARGS} --ephemeral"
|
||||
fi
|
||||
RUN_ARGS=""
|
||||
if [[ ! -z "${GITEA_RUNNER_ONCE}" ]]; then
|
||||
RUN_ARGS="${RUN_ARGS} --once"
|
||||
fi
|
||||
|
||||
# 如果没有设置令牌,可以从文件中读取令牌,例如从 Docker Secret
|
||||
if [[ -z "${GITEA_RUNNER_REGISTRATION_TOKEN}" ]] && [[ -f "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}" ]]; then
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=$(cat "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}")
|
||||
fi
|
||||
|
||||
# 使用与 https://github.com/vegardit/docker-gitea-act-runner 相同的 ENV 变量名称
|
||||
test -f "$RUNNER_STATE_FILE" || echo "$RUNNER_STATE_FILE 丢失或不是常规文件"
|
||||
|
||||
# 如果运行器状态文件不存在或为空
|
||||
if [[ ! -s "$RUNNER_STATE_FILE" ]]; then
|
||||
try=$((try + 1))
|
||||
success=0
|
||||
|
||||
# 此循环的目的是使其简单,当在 docker 中同时运行 act_runner 和 gitea 时,
|
||||
# 使 act_runner 在出错之前等待一段时间以等待 gitea 变为可用。在单个 docker-compose 的上下文中,
|
||||
# 可以通过健康检查做类似的事情,但这更灵活。
|
||||
while [[ $success -eq 0 ]] && [[ $try -lt ${GITEA_MAX_REG_ATTEMPTS:-10} ]]; do
|
||||
act_runner register \
|
||||
--instance "${GITEA_INSTANCE_URL}" \
|
||||
--token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \
|
||||
--name "${GITEA_RUNNER_NAME:-`hostname`}" \
|
||||
${CONFIG_ARG} ${EXTRA_ARGS} --no-interactive 2>&1 | tee /tmp/reg.log
|
||||
|
||||
# 检查输出中是否有成功注册的消息
|
||||
cat /tmp/reg.log | grep 'Runner registered successfully' > /dev/null
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo "成功"
|
||||
success=1
|
||||
else
|
||||
echo "等待重试..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# 防止从 act_runner 进程中读取令牌
|
||||
unset GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE
|
||||
|
||||
# 启动 act_runner 守护进程
|
||||
exec act_runner daemon ${CONFIG_ARG} ${RUN_ARGS}
|
3
scripts/s6/act_runner/finish
Executable file
3
scripts/s6/act_runner/finish
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-svscanctl -t /etc/s6
|
5
scripts/s6/act_runner/run
Executable file
5
scripts/s6/act_runner/run
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
s6-svwait -U /etc/s6/docker
|
||||
|
||||
exec run.sh
|
6
scripts/s6/docker/data/check
Executable file
6
scripts/s6/docker/data/check
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
echo "等待Docker守护进程启动……"
|
||||
exit 1
|
||||
fi
|
4
scripts/s6/docker/finish
Executable file
4
scripts/s6/docker/finish
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-svscanctl -t /etc/s6
|
||||
|
1
scripts/s6/docker/notification-fd
Normal file
1
scripts/s6/docker/notification-fd
Normal file
@ -0,0 +1 @@
|
||||
3
|
3
scripts/s6/docker/run
Executable file
3
scripts/s6/docker/run
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-notifyoncheck dockerd-entrypoint.sh
|
Reference in New Issue
Block a user