mirror of
https://gitea.com/gitea/act_runner.git
synced 2025-09-05 05:42:03 +08:00
Compare commits
208 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 | |||
220efa69c0 | |||
df3cb60978 | |||
7e7096e60b | |||
8eea12dd78 | |||
c8fad20f49 | |||
1596e4b1fd | |||
c9e076db68 | |||
bc1842d649 | |||
90b8cc6a7a | |||
4d5a35ac65 | |||
8f81f40d62 | |||
9f90cba993 | |||
48b05a0ca8 | |||
9eb8b08a69 | |||
4d868b7f3c | |||
63a57edaa3 | |||
5180cd56e1 | |||
370989b2d0 | |||
71f470d670 | |||
c0c363bf59 | |||
0d71463662 | |||
ebcf341de7 | |||
14334f76ed | |||
f24e0721dc | |||
e36300ce28 | |||
09ddbe166f | |||
da0713e629 | |||
bbd055ac3b | |||
462b2660de | |||
ebdbfeb54a | |||
436b441cad | |||
552dbcdda9 | |||
a50b094c1a | |||
6cc53f16d8 | |||
8fcd56dc7b | |||
c9318f08e2 | |||
c7f8919470 | |||
14dfa5cc15 | |||
99a53a1f4c | |||
df2219eeb8 | |||
216f3d1740 | |||
8aa186897f | |||
3fa7707bc1 | |||
9038442191 |
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,22 +1,101 @@
|
||||
name: goreleaser
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --nightly --clean
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
args: release --nightly
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
S3_BUCKET: ${{ secrets.S3_BUCKET }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: "gitea"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
DOCKER_ORG: gitea
|
||||
DOCKER_LATEST: nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: basic
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
|
||||
- name: Build and push dind
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
|
||||
|
||||
- name: Build and push dind-rootless
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: dind-rootless
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless
|
||||
|
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,23 +1,20 @@
|
||||
name: checks
|
||||
on:
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
env:
|
||||
GOPROXY: https://goproxy.io,direct
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: check and test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.20
|
||||
- uses: actions/checkout@v3
|
||||
go-version-file: 'go.mod'
|
||||
- name: vet checks
|
||||
run: make vet
|
||||
- name: build
|
||||
run: make build
|
||||
- name: test
|
||||
run: make test
|
||||
run: make test
|
||||
|
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,4 +1,17 @@
|
||||
act_runner
|
||||
/act_runner
|
||||
/loong_runner
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
coverage.txt
|
||||
/gitea-vet
|
||||
/config.yaml
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
# MS VSCode
|
||||
.vscode
|
||||
__debug_bin
|
||||
# gorelease binary folder
|
||||
dist
|
||||
# vim
|
||||
*.sw*
|
||||
|
@ -1,14 +1,11 @@
|
||||
linters:
|
||||
enable:
|
||||
- gosimple
|
||||
- deadcode
|
||||
- typecheck
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- structcheck
|
||||
- varcheck
|
||||
- dupl
|
||||
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
|
||||
- gofmt
|
||||
@ -112,7 +109,6 @@ issues:
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
- deadcode
|
||||
text: "swagger"
|
||||
- path: contrib/pr/checkout.go
|
||||
linters:
|
||||
@ -154,9 +150,6 @@ issues:
|
||||
- path: cmd/dump.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: services/webhook/webhook.go
|
||||
linters:
|
||||
- structcheck
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
|
12
.goreleaser.checksum.sh
Normal file
12
.goreleaser.checksum.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "usage: $0 <path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUM=$(shasum -a 256 "$1" | cut -d' ' -f1)
|
||||
BASENAME=$(basename "$1")
|
||||
echo -n "${SUM} ${BASENAME}" > "$1".sha256
|
@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@ -15,7 +17,7 @@ builds:
|
||||
- arm
|
||||
- arm64
|
||||
- s390x
|
||||
- ppc64le
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
@ -40,6 +42,8 @@ builds:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: ppc64le
|
||||
- goos: freebsd
|
||||
@ -53,14 +57,15 @@ builds:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -s -w -X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- if .IsSnapshot }}{{ .Branch }}-
|
||||
{{- else }}{{- .Version }}-{{ end }}
|
||||
{{- .Version }}-
|
||||
{{- .Os }}-
|
||||
{{- if eq .Arch "amd64" }}amd64
|
||||
{{- else if eq .Arch "amd64_v1" }}amd64
|
||||
@ -68,13 +73,22 @@ builds:
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}-{{ .Arm }}{{ end }}
|
||||
no_unique_dist_dir: true
|
||||
hooks:
|
||||
post:
|
||||
- cmd: xz -k -9 {{ .Path }}
|
||||
dir: ./dist/
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||
|
||||
blobs:
|
||||
-
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
folder: "act_runner/{{.Version}}"
|
||||
directory: "act_runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
|
||||
archives:
|
||||
- format: binary
|
||||
@ -83,10 +97,23 @@ archives:
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}"
|
||||
version_template: "{{ .Branch }}-devel"
|
||||
|
||||
nightly:
|
||||
publish_release: false
|
||||
name_template: "{{ .Branch }}"
|
||||
version_template: "nightly"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
download: https://gitea.com
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.xz.sha256
|
||||
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
54
Dockerfile
Normal file
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"]
|
53
Makefile
53
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,13 +7,19 @@ GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
XGO_VERSION := go-1.18.x
|
||||
XGO_VERSION := go-1.24.x
|
||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
||||
WINDOWS_ARCHS ?= windows/amd64
|
||||
GOFILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||
|
||||
DOCKER_IMAGE ?= gitea/loong_runner
|
||||
DOCKER_TAG ?= nightly
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||
|
||||
ifneq ($(shell uname), Darwin)
|
||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||
@ -49,7 +55,7 @@ else
|
||||
ifneq ($(DRONE_BRANCH),)
|
||||
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
||||
else
|
||||
VERSION ?= master
|
||||
VERSION ?= main
|
||||
endif
|
||||
|
||||
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
||||
@ -60,8 +66,11 @@ else
|
||||
endif
|
||||
endif
|
||||
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out git.whlug.cn/LAA/loong_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
|
||||
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X 'main.Version=$(VERSION)'
|
||||
LDFLAGS ?= -X "git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
|
||||
all: build
|
||||
|
||||
@ -69,30 +78,43 @@ fmt:
|
||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||
fi
|
||||
$(GOFMT) -w $(GOFILES)
|
||||
$(GOFMT) -w $(GO_FMT_FILES)
|
||||
|
||||
vet:
|
||||
$(GO) vet ./...
|
||||
.PHONY: go-check
|
||||
go-check:
|
||||
$(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
|
||||
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||
echo "Act Runner 需要 Go $(MIN_GO_VERSION_STR) 或更高版本才能构建。您可以从 https://go.dev/dl/ 获取。"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||
fi
|
||||
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make fmt' and commit the result:"; \
|
||||
echo "请运行'make fmt'并提交结果"; \
|
||||
echo "$${diff}"; \
|
||||
exit 1; \
|
||||
fi;
|
||||
|
||||
test: fmt-check
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo -e "\n===> \e[32mOk\e[m\n" || exit 1
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@echo "运行 go vet..."
|
||||
@$(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
|
||||
|
||||
install: $(GOFILES)
|
||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||
|
||||
build: $(EXECUTABLE)
|
||||
build: go-check $(EXECUTABLE)
|
||||
|
||||
$(EXECUTABLE): $(GOFILES)
|
||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
||||
@ -142,6 +164,13 @@ release-check: | $(DIST_DIRS)
|
||||
release-compress: | $(DIST_DIRS)
|
||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
if ! docker buildx version >/dev/null 2>&1; then \
|
||||
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||
fi; \
|
||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||
|
||||
clean:
|
||||
$(GO) clean -x -i ./...
|
||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
||||
|
110
README.md
110
README.md
@ -1,61 +1,111 @@
|
||||
# act runner
|
||||
# Loong Runner
|
||||
|
||||
Act runner is a runner for Gitea based on [act](https://gitea.com/gitea/act).
|
||||
Loong Runner 是基于 [Gitea派生(fock)的act](https://gitea.com/gitea/act) 的 [二次派生](https://git.whlug.cn/LoongArchActions/loong_runner) 主要适用于当前龙架构的运行时。
|
||||
|
||||
## Prerequisites
|
||||
> 上游的Gitea派生是可以直接在龙架构上编译使用的, 此派生仓库主要是解决上游的Docker是基于乌班图系统制作的, 目前龙架构暂无乌班图系统
|
||||
> * 当前计划使用`Debian`来代替乌班图用于构建容器镜像
|
||||
|
||||
Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
||||
|
||||
## Quickstart
|
||||
## 安装
|
||||
|
||||
### Build
|
||||
### 前提条件
|
||||
|
||||
在 Docker 模式下需要 `Docker Engine Community` 版本。要安装 `Docker CE`,请遵循官方 [安装说明](https://docs.docker.com/engine/install/)。
|
||||
|
||||
### 下载预构建的二进制文件
|
||||
|
||||
访问 [这里](https://git.whlug.cn/LoongArchActions/loong_runner) 仅提供龙架构版本。
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Register
|
||||
### 构建 Docker 容器镜像
|
||||
|
||||
```bash
|
||||
./act_runner register
|
||||
make docker
|
||||
```
|
||||
|
||||
And you will be asked to input:
|
||||
## 快速入门
|
||||
|
||||
1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
|
||||
and you should not use `localhost` or `127.0.0.1` as instance IP;
|
||||
2. Runner token, you can get it from `http://192.168.8.8:3000/admin/runners`;
|
||||
3. Runner name, you can just leave it blank;
|
||||
4. Runner labels, you can just leave it blank.
|
||||
默认情况下,操作是禁用的,因此您需要在 Gitea 实例的配置文件中添加以下内容以启用它:
|
||||
|
||||
The process looks like:
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED=true
|
||||
```
|
||||
|
||||
### 注册
|
||||
|
||||
```bash
|
||||
./loong_runner register
|
||||
```
|
||||
|
||||
系统将提示您输入:
|
||||
|
||||
1. Gitea 实例 URL,例如 `http://192.168.8.8:3000/`。您应该使用您的 Gitea 实例的 ROOT_URL 作为实例参数,并且不应使用 `localhost` 或 `127.0.0.1` 作为实例 IP;
|
||||
2. 运行器令牌,您可以从 `http://192.168.8.8:3000/admin/actions/runners` 获取;
|
||||
3. 运行器名称,您可以留空;
|
||||
4. 运行器标签,您可以留空。
|
||||
|
||||
过程如下:
|
||||
|
||||
```text
|
||||
INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
|
||||
WARN Runner in user-mode.
|
||||
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
|
||||
INFO 注册运行器,架构=loong64,操作系统=linux,版本=0.1.5。
|
||||
WARN 运行器处于用户模式。
|
||||
INFO 输入 Gitea 实例 URL(例如,https://gitea.com/):
|
||||
http://192.168.8.8:3000/
|
||||
INFO Enter the runner token:
|
||||
INFO 输入运行器令牌:
|
||||
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
||||
INFO Enter the runner name (if set empty, use hostname:Test.local ):
|
||||
INFO 输入运行器名称(如果设置为空,则使用主机名:Test.local):
|
||||
|
||||
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, self-hosted,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster):
|
||||
INFO 输入运行器标签,留空以使用默认标签(逗号分隔,例如,debian-latest:docker://docker.gitea.com/runner-images:latest)
|
||||
|
||||
INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://node:16-bullseye ubuntu-22.04:docker://node:16-bullseye ubuntu-20.04:docker://node:16-bullseye ubuntu-18.04:docker://node:16-buster].
|
||||
DEBU Successfully pinged the Gitea instance server
|
||||
INFO Runner registered successfully.
|
||||
INFO 注册运行器,名称=Test.local,实例=http://192.168.8.8:3000/,标签=[debian-latest:docker://docker.gitea.com/runner-images:latest debian-sid:docker://docker.gitea.com/runner-images:debian-sid anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest]。
|
||||
DEBU 成功 ping 到 Gitea 实例服务器
|
||||
INFO 运行器注册成功。
|
||||
```
|
||||
|
||||
You can also register with command line arguments.
|
||||
您也可以使用命令行参数进行注册。
|
||||
|
||||
```bash
|
||||
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
./loong_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
```
|
||||
|
||||
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
||||
如果注册成功,它将立即运行。下次,您可以直接运行运行器。
|
||||
|
||||
### Run
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
./act_runner daemon
|
||||
```
|
||||
./loong_runner daemon
|
||||
```
|
||||
|
||||
### 使用 Docker 运行
|
||||
|
||||
```bash
|
||||
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/loong_runner:nightly
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
您还可以使用配置文件配置运行器。
|
||||
配置文件是一个 YAML 文件,您可以使用 `./loong_runner generate-config` 生成一个示例配置文件。
|
||||
|
||||
```bash
|
||||
./loong_runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
您可以使用 `-c`/`--config` 参数指定配置文件路径。
|
||||
|
||||
```bash
|
||||
./loong_runner -c config.yaml register # 使用配置文件注册
|
||||
./loong_runner -c config.yaml daemon # 使用配置文件运行
|
||||
```
|
||||
|
||||
您可以在 [config.example.yaml](internal/pkg/config/config.example.yaml) 上在线查看配置文件的最新版本。
|
||||
|
||||
### 示例部署
|
||||
|
||||
查看 [examples](examples) 目录中的示例部署类型。
|
11
build.go
Normal file
11
build.go
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build vendor
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
// for vet
|
||||
_ "code.gitea.io/gitea-vet"
|
||||
)
|
63
cmd/cmd.go
63
cmd/cmd.go
@ -1,63 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const version = "0.1.5"
|
||||
|
||||
type globalArgs struct {
|
||||
EnvFile string
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context) {
|
||||
// task := runtime.NewTask("gitea", 0, nil, nil)
|
||||
|
||||
var gArgs globalArgs
|
||||
|
||||
// ./act_runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
rootCmd.PersistentFlags().StringVarP(&gArgs.EnvFile, "env-file", "", ".env", "Read in a file of environment variables.")
|
||||
|
||||
// ./act_runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a runner to the server",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runRegister(ctx, ®Args, gArgs.EnvFile), // must use a pointer to regArgs
|
||||
}
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||
registerCmd.Flags().BoolVar(®Args.Insecure, "insecure", false, "If check server's certificate if it's https protocol")
|
||||
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")
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
// ./act_runner daemon
|
||||
daemonCmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Run as a runner daemon",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runDaemon(ctx, gArgs.EnvFile),
|
||||
}
|
||||
// add all command
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// hide completion command
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
112
cmd/daemon.go
112
cmd/daemon.go
@ -1,112 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
"gitea.com/gitea/act_runner/config"
|
||||
"gitea.com/gitea/act_runner/engine"
|
||||
"gitea.com/gitea/act_runner/poller"
|
||||
"gitea.com/gitea/act_runner/runtime"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.Infoln("Starting runner daemon")
|
||||
|
||||
_ = godotenv.Load(envFile)
|
||||
cfg, err := config.FromEnviron()
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Fatalln("invalid configuration")
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
|
||||
// require docker if a runner label uses a docker backend
|
||||
needsDocker := false
|
||||
for _, l := range cfg.Runner.Labels {
|
||||
splits := strings.SplitN(l, ":", 2)
|
||||
if len(splits) == 2 && strings.HasPrefix(splits[1], "docker://") {
|
||||
needsDocker = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if needsDocker {
|
||||
// try to connect to docker daemon
|
||||
// if failed, exit with error
|
||||
if err := engine.Start(ctx); err != nil {
|
||||
log.WithError(err).Fatalln("failed to connect docker daemon engine")
|
||||
}
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
|
||||
cli := client.New(
|
||||
cfg.Client.Address,
|
||||
cfg.Client.Insecure,
|
||||
cfg.Runner.UUID,
|
||||
cfg.Runner.Token,
|
||||
)
|
||||
|
||||
runner := &runtime.Runner{
|
||||
Client: cli,
|
||||
Machine: cfg.Runner.Name,
|
||||
ForgeInstance: cfg.Client.Address,
|
||||
Environ: cfg.Runner.Environ,
|
||||
Labels: cfg.Runner.Labels,
|
||||
}
|
||||
|
||||
poller := poller.New(
|
||||
cli,
|
||||
runner.Run,
|
||||
cfg.Runner.Capacity,
|
||||
)
|
||||
|
||||
g.Go(func() error {
|
||||
l := log.WithField("capacity", cfg.Runner.Capacity).
|
||||
WithField("endpoint", cfg.Client.Address).
|
||||
WithField("os", cfg.Platform.OS).
|
||||
WithField("arch", cfg.Platform.Arch)
|
||||
l.Infoln("polling the remote server")
|
||||
|
||||
if err := poller.Poll(ctx); err != nil {
|
||||
l.Errorf("poller error: %v", err)
|
||||
}
|
||||
poller.Wait()
|
||||
return nil
|
||||
})
|
||||
|
||||
err = g.Wait()
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("shutting down the server")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// initLogging setup the global logrus logger.
|
||||
func initLogging(cfg config.Config) {
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
if cfg.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
if cfg.Trace {
|
||||
log.SetLevel(log.TraceLevel)
|
||||
}
|
||||
}
|
305
cmd/register.go
305
cmd/register.go
@ -1,305 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
"gitea.com/gitea/act_runner/config"
|
||||
"gitea.com/gitea/act_runner/register"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||
runtime.GOARCH, runtime.GOOS, version)
|
||||
|
||||
// runner always needs root permission
|
||||
if os.Getuid() != 0 {
|
||||
// TODO: use a better way to check root permission
|
||||
log.Warnf("Runner in user-mode.")
|
||||
}
|
||||
|
||||
if regArgs.NoInteractive {
|
||||
if err := registerNoInteractive(envFile, regArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(envFile); err != nil {
|
||||
// log.Errorln(err)
|
||||
os.Exit(2)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// registerArgs represents the arguments for register command
|
||||
type registerArgs struct {
|
||||
NoInteractive bool
|
||||
InstanceAddr string
|
||||
Insecure bool
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels string
|
||||
}
|
||||
|
||||
type registerStage int8
|
||||
|
||||
const (
|
||||
StageUnknown registerStage = -1
|
||||
StageOverwriteLocalConfig registerStage = iota + 1
|
||||
StageInputInstance
|
||||
StageInputToken
|
||||
StageInputRunnerName
|
||||
StageInputCustomLabels
|
||||
StageWaitingForRegistration
|
||||
StageExit
|
||||
)
|
||||
|
||||
var defaultLabels = []string{
|
||||
"ubuntu-latest:docker://node:16-bullseye",
|
||||
"ubuntu-22.04:docker://node:16-bullseye", // There's no node:16-bookworm yet
|
||||
"ubuntu-20.04:docker://node:16-bullseye",
|
||||
"ubuntu-18.04:docker://node:16-buster",
|
||||
}
|
||||
|
||||
type registerInputs struct {
|
||||
InstanceAddr string
|
||||
Insecure bool
|
||||
Token string
|
||||
RunnerName string
|
||||
CustomLabels []string
|
||||
}
|
||||
|
||||
func (r *registerInputs) validate() error {
|
||||
if r.InstanceAddr == "" {
|
||||
return fmt.Errorf("instance address is empty")
|
||||
}
|
||||
if r.Token == "" {
|
||||
return fmt.Errorf("token is empty")
|
||||
}
|
||||
if len(r.CustomLabels) > 0 {
|
||||
return validateLabels(r.CustomLabels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabels(labels []string) error {
|
||||
for _, label := range labels {
|
||||
values := strings.SplitN(label, ":", 2)
|
||||
if len(values) > 2 {
|
||||
return fmt.Errorf("Invalid label: %s", label)
|
||||
}
|
||||
// len(values) == 1, label for non docker execution environment
|
||||
// TODO: validate value format, like docker://node:16-buster
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string) registerStage {
|
||||
// must set instance address and token.
|
||||
// if empty, keep current stage.
|
||||
if stage == StageInputInstance || stage == StageInputToken {
|
||||
if value == "" {
|
||||
return stage
|
||||
}
|
||||
}
|
||||
|
||||
// set hostname for runner name if empty
|
||||
if stage == StageInputRunnerName && value == "" {
|
||||
value, _ = os.Hostname()
|
||||
}
|
||||
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
if value == "Y" || value == "y" {
|
||||
return StageInputInstance
|
||||
}
|
||||
return StageExit
|
||||
case StageInputInstance:
|
||||
r.InstanceAddr = value
|
||||
return StageInputToken
|
||||
case StageInputToken:
|
||||
r.Token = value
|
||||
return StageInputRunnerName
|
||||
case StageInputRunnerName:
|
||||
r.RunnerName = value
|
||||
return StageInputCustomLabels
|
||||
case StageInputCustomLabels:
|
||||
r.CustomLabels = defaultLabels
|
||||
if value != "" {
|
||||
r.CustomLabels = strings.Split(value, ",")
|
||||
}
|
||||
|
||||
if validateLabels(r.CustomLabels) != nil {
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster)")
|
||||
return StageInputCustomLabels
|
||||
}
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageUnknown
|
||||
}
|
||||
|
||||
func registerInteractive(envFile string) error {
|
||||
var (
|
||||
reader = bufio.NewReader(os.Stdin)
|
||||
stage = StageInputInstance
|
||||
inputs = new(registerInputs)
|
||||
)
|
||||
|
||||
// check if overwrite local config
|
||||
_ = godotenv.Load(envFile)
|
||||
cfg, _ := config.FromEnviron()
|
||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||
stage = StageOverwriteLocalConfig
|
||||
}
|
||||
|
||||
for {
|
||||
printStageHelp(stage)
|
||||
|
||||
cmdString, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString))
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
|
||||
if err := doRegister(&cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
} else {
|
||||
log.Infof("Runner registered successfully.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage == StageExit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage <= StageUnknown {
|
||||
log.Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printStageHelp(stage registerStage) {
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("Enter the runner name (if set empty, use hostname:%s ):\n", hostname)
|
||||
case StageInputCustomLabels:
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, self-hosted,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("Waiting for registration...")
|
||||
}
|
||||
}
|
||||
|
||||
func registerNoInteractive(envFile string, regArgs *registerArgs) error {
|
||||
_ = godotenv.Load(envFile)
|
||||
cfg, _ := config.FromEnviron()
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Insecure: regArgs.Insecure,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
CustomLabels: defaultLabels,
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
if regArgs.Labels != "" {
|
||||
inputs.CustomLabels = strings.Split(regArgs.Labels, ",")
|
||||
}
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
}
|
||||
if err := doRegister(&cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// initial http client
|
||||
cli := client.New(
|
||||
inputs.InstanceAddr,
|
||||
inputs.Insecure,
|
||||
"", "",
|
||||
)
|
||||
|
||||
for {
|
||||
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
|
||||
Data: inputs.RunnerName,
|
||||
}))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("Cannot ping the Gitea instance server")
|
||||
// TODO: if ping failed, retry or exit
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
log.Debugln("Successfully pinged the Gitea instance server")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Runner.Name = inputs.RunnerName
|
||||
cfg.Runner.Token = inputs.Token
|
||||
cfg.Runner.Labels = inputs.CustomLabels
|
||||
_, err := register.New(cli).Register(ctx, cfg.Runner)
|
||||
return err
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateLabels(t *testing.T) {
|
||||
labels := []string{"ubuntu-latest:docker://node:16-buster", "self-hosted"}
|
||||
if err := validateLabels(labels); err != nil {
|
||||
t.Errorf("validateLabels() error = %v", err)
|
||||
}
|
||||
}
|
114
config/config.go
114
config/config.go
@ -1,114 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"gitea.com/gitea/act_runner/core"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config provides the system configuration.
|
||||
Config struct {
|
||||
Debug bool `envconfig:"GITEA_DEBUG"`
|
||||
Trace bool `envconfig:"GITEA_TRACE"`
|
||||
Client Client
|
||||
Runner Runner
|
||||
Platform Platform
|
||||
}
|
||||
|
||||
Client struct {
|
||||
Address string `ignored:"true"`
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
Runner struct {
|
||||
UUID string `ignored:"true"`
|
||||
Name string `envconfig:"GITEA_RUNNER_NAME"`
|
||||
Token string `ignored:"true"`
|
||||
Capacity int `envconfig:"GITEA_RUNNER_CAPACITY" default:"1"`
|
||||
File string `envconfig:"GITEA_RUNNER_FILE" default:".runner"`
|
||||
Environ map[string]string `envconfig:"GITEA_RUNNER_ENVIRON"`
|
||||
EnvFile string `envconfig:"GITEA_RUNNER_ENV_FILE"`
|
||||
Labels []string `envconfig:"GITEA_RUNNER_LABELS"`
|
||||
}
|
||||
|
||||
Platform struct {
|
||||
OS string `envconfig:"GITEA_PLATFORM_OS"`
|
||||
Arch string `envconfig:"GITEA_PLATFORM_ARCH"`
|
||||
}
|
||||
)
|
||||
|
||||
// FromEnviron returns the settings from the environment.
|
||||
func FromEnviron() (Config, error) {
|
||||
cfg := Config{}
|
||||
if err := envconfig.Process("", &cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// check runner config exist
|
||||
f, err := os.Stat(cfg.Runner.File)
|
||||
if err == nil && !f.IsDir() {
|
||||
jsonFile, _ := os.Open(cfg.Runner.File)
|
||||
defer jsonFile.Close()
|
||||
byteValue, _ := io.ReadAll(jsonFile)
|
||||
var runner core.Runner
|
||||
if err := json.Unmarshal(byteValue, &runner); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if runner.UUID != "" {
|
||||
cfg.Runner.UUID = runner.UUID
|
||||
}
|
||||
if runner.Token != "" {
|
||||
cfg.Runner.Token = runner.Token
|
||||
}
|
||||
if len(runner.Labels) != 0 {
|
||||
cfg.Runner.Labels = runner.Labels
|
||||
}
|
||||
if runner.Address != "" {
|
||||
cfg.Client.Address = runner.Address
|
||||
}
|
||||
if runner.Insecure != "" {
|
||||
cfg.Client.Insecure, _ = strconv.ParseBool(runner.Insecure)
|
||||
}
|
||||
} else if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// runner config
|
||||
if cfg.Runner.Environ == nil {
|
||||
cfg.Runner.Environ = map[string]string{
|
||||
"GITHUB_API_URL": cfg.Client.Address + "/api/v1",
|
||||
"GITHUB_SERVER_URL": cfg.Client.Address,
|
||||
}
|
||||
}
|
||||
if cfg.Runner.Name == "" {
|
||||
cfg.Runner.Name, _ = os.Hostname()
|
||||
}
|
||||
|
||||
// platform config
|
||||
if cfg.Platform.OS == "" {
|
||||
cfg.Platform.OS = runtime.GOOS
|
||||
}
|
||||
if cfg.Platform.Arch == "" {
|
||||
cfg.Platform.Arch = runtime.GOARCH
|
||||
}
|
||||
|
||||
if file := cfg.Runner.EnvFile; file != "" {
|
||||
envs, err := godotenv.Read(file)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
for k, v := range envs {
|
||||
cfg.Runner.Environ[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package core
|
||||
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
)
|
||||
|
||||
// Runner struct
|
||||
type Runner struct {
|
||||
ID int64 `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Insecure string `json:"insecure"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type Docker struct {
|
||||
client client.APIClient
|
||||
hidePull bool
|
||||
}
|
||||
|
||||
func New(opts ...Option) (*Docker, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv := &Docker{
|
||||
client: cli,
|
||||
}
|
||||
|
||||
// Loop through each option
|
||||
for _, opt := range opts {
|
||||
// Call the option giving the instantiated
|
||||
opt.Apply(srv)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Ping pings the Docker daemon.
|
||||
func (e *Docker) Ping(ctx context.Context) error {
|
||||
_, err := e.client.Ping(ctx)
|
||||
return err
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Start start docker engine api loop
|
||||
func Start(ctx context.Context) error {
|
||||
engine, err := New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := 0
|
||||
for {
|
||||
err := engine.Ping(ctx)
|
||||
if err == context.Canceled {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("cannot ping the docker daemon")
|
||||
count++
|
||||
if count == 5 {
|
||||
return fmt.Errorf("retry connect to docker daemon failed: %d times", count)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
log.Infoln("successfully ping the docker daemon")
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package engine
|
||||
|
||||
import "github.com/docker/docker/client"
|
||||
|
||||
// An Option configures a mutex.
|
||||
type Option interface {
|
||||
Apply(*Docker)
|
||||
}
|
||||
|
||||
// OptionFunc is a function that configure a value.
|
||||
type OptionFunc func(*Docker)
|
||||
|
||||
// Apply calls f(option)
|
||||
func (f OptionFunc) Apply(docker *Docker) {
|
||||
f(docker)
|
||||
}
|
||||
|
||||
// WithClient set custom client
|
||||
func WithClient(c client.APIClient) Option {
|
||||
return OptionFunc(func(q *Docker) {
|
||||
q.client = c
|
||||
})
|
||||
}
|
||||
|
||||
// WithHidePull hide pull event.
|
||||
func WithHidePull(v bool) Option {
|
||||
return OptionFunc(func(q *Docker) {
|
||||
q.hidePull = v
|
||||
})
|
||||
}
|
12
examples/README.md
Normal file
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
|
||||
```
|
135
go.mod
135
go.mod
@ -1,80 +1,101 @@
|
||||
module gitea.com/gitea/act_runner
|
||||
module git.whlug.cn/LAA/loong_runner
|
||||
|
||||
go 1.18
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.2.0
|
||||
github.com/avast/retry-go/v4 v4.3.1
|
||||
github.com/bufbuild/connect-go v1.3.1
|
||||
github.com/docker/docker v20.10.21+incompatible
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mattn/go-isatty v0.0.16
|
||||
github.com/nektos/act v0.0.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
google.golang.org/protobuf v1.28.1
|
||||
code.gitea.io/actions-proto-go v0.4.1
|
||||
code.gitea.io/gitea-vet v0.2.3
|
||||
connectrpc.com/connect v1.16.2
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/docker/docker v25.0.5+incompatible
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/nektos/act v0.0.0 // will be replaced
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.9.3 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/containerd/cgroups v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.6.6 // indirect
|
||||
github.com/creack/pty v1.1.18 // indirect
|
||||
github.com/docker/cli v20.10.21+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/containerd/containerd v1.7.13 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/cli v25.0.3+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/buildkit v0.10.6 // indirect
|
||||
github.com/moby/sys/mount v0.3.1 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/buildkit v0.12.5 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||
github.com/opencontainers/runc v1.1.2 // indirect
|
||||
github.com/opencontainers/selinux v1.10.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rhysd/actionlint v1.6.22 // indirect
|
||||
github.com/rivo/uniseg v0.3.4 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rhysd/actionlint v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.234.3-0.20230224062034-1252e551b867
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
|
||||
|
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
|
||||
}
|
||||
}
|
88
internal/app/cmd/cmd.go
Normal file
88
internal/app/cmd/cmd.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) {
|
||||
// ./act_runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act_runner [运行事件名称]\n如果没有传递事件名称, 默认为 \"on: push\"",
|
||||
Short: "通过指定事件名称(例如 `push`)或直接指定操作名称来本地运行 GitHub Actions。",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: ver.Version(),
|
||||
SilenceUsage: true,
|
||||
}
|
||||
configFile := ""
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径")
|
||||
|
||||
// ./act_runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "将运行器注册到服务器",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // 必须使用 regArgs 的指针
|
||||
}
|
||||
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: "作为运行器守护进程运行",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runDaemon(ctx, &daemArgs, &configFile),
|
||||
}
|
||||
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "运行一个作业然后退出")
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// ./act_runner exec
|
||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||
|
||||
// ./act_runner config
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "生成示例配置文件",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("%s", config.Example)
|
||||
},
|
||||
})
|
||||
|
||||
// ./act_runner cache-server
|
||||
var cacheArgs cacheServerArgs
|
||||
cacheCmd := &cobra.Command{
|
||||
Use: "cache-server",
|
||||
Short: "启动缓存服务器用于缓存操作",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runCacheServer(ctx, &configFile, &cacheArgs),
|
||||
}
|
||||
cacheCmd.Flags().StringVarP(&cacheArgs.Dir, "dir", "d", "", "缓存目录")
|
||||
cacheCmd.Flags().StringVarP(&cacheArgs.Host, "host", "s", "", "缓存服务器主机")
|
||||
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "缓存服务器端口")
|
||||
rootCmd.AddCommand(cacheCmd)
|
||||
|
||||
// 隐藏自动补全命令
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
230
internal/app/cmd/daemon.go
Normal file
230
internal/app/cmd/daemon.go
Normal file
@ -0,0 +1,230 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/poll"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/run"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/envcheck"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的配置: %w", err)
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
log.Infoln("启动运行器守护进程")
|
||||
|
||||
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("未找到注册文件,请先注册运行器")
|
||||
return err
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("加载注册文件失败: %w", err)
|
||||
}
|
||||
|
||||
lbls := reg.Labels
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
lbls = cfg.Runner.Labels
|
||||
}
|
||||
|
||||
ls := labels.Labels{}
|
||||
for _, l := range lbls {
|
||||
label, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("忽略无效标签 %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, label)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("未配置任何标签,运行器可能无法接取作业")
|
||||
}
|
||||
|
||||
if ls.RequireDocker() {
|
||||
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := envcheck.CheckIfDockerRunning(ctx, dockerSocketPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// 如果 dockerSocketPath 通过检查,用 dockerSocketPath 覆盖 DOCKER_HOST
|
||||
os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
// 如果 cfg.Container.DockerHost 为空,意味着 act_runner 需要自动查找可用的 docker 主机
|
||||
// 并将路径分配给 cfg.Container.DockerHost
|
||||
if cfg.Container.DockerHost == "" {
|
||||
cfg.Container.DockerHost = dockerSocketPath
|
||||
}
|
||||
// 检查方案,如果方案不是 npipe 或 unix
|
||||
// 将 cfg.Container.DockerHost 设置为 "-",因为它不能挂载到作业容器
|
||||
if protoIndex := strings.Index(cfg.Container.DockerHost, "://"); protoIndex != -1 {
|
||||
scheme := cfg.Container.DockerHost[:protoIndex]
|
||||
if !strings.EqualFold(scheme, "npipe") && !strings.EqualFold(scheme, "unix") {
|
||||
cfg.Container.DockerHost = "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(reg.Labels, ls.ToStrings()) {
|
||||
reg.Labels = ls.ToStrings()
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("保存运行器配置失败: %w", err)
|
||||
}
|
||||
log.Infof("标签更新为: %v", reg.Labels)
|
||||
}
|
||||
|
||||
cli := client.New(
|
||||
reg.Address,
|
||||
cfg.Runner.Insecure,
|
||||
reg.UUID,
|
||||
reg.Token,
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
|
||||
// 在获取任务之前声明运行器的标签
|
||||
resp, err := runner.Declare(ctx, ls.Names())
|
||||
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||
log.Errorf("您的 Gitea 版本太旧,不支持运行器声明,请升级到 v1.21 或更高版本")
|
||||
return err
|
||||
} else if err != nil {
|
||||
log.WithError(err).Error("调用 Declare 失败")
|
||||
return err
|
||||
} else {
|
||||
log.Infof("运行器: %s, 版本: %s, 标签: %v, 声明成功",
|
||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||
}
|
||||
|
||||
poller := poll.New(cfg, cli, runner)
|
||||
|
||||
if daemArgs.Once || reg.Ephemeral {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
poller.PollOnce()
|
||||
}()
|
||||
|
||||
// 当完成一个作业或请求取消时关闭
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-done:
|
||||
}
|
||||
} else {
|
||||
go poller.Poll()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
log.Infof("运行器: %s 关闭开始,等待 %s 让正在运行的作业完成后再关闭", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = poller.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Warnf("运行器: %s 在关闭期间取消了正在进行的作业", resp.Msg.Runner.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type daemonArgs struct {
|
||||
Once bool // 是否只运行一次
|
||||
}
|
||||
|
||||
// initLogging 设置全局 logrus 日志记录器。
|
||||
func initLogging(cfg *config.Config) {
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
format := &log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
|
||||
if l := cfg.Log.Level; l != "" {
|
||||
level, err := log.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorf("无效的日志级别: %q", l)
|
||||
}
|
||||
|
||||
// 调试级别
|
||||
if level == log.DebugLevel {
|
||||
log.SetReportCaller(true)
|
||||
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||
// 获取函数名
|
||||
s := strings.Split(f.Function, ".")
|
||||
funcname := "[" + s[len(s)-1] + "]"
|
||||
// 获取文件名和行号
|
||||
_, filename := path.Split(f.File)
|
||||
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
|
||||
return funcname, filename
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
}
|
||||
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("日志级别更改为 %v", level)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var commonSocketPaths = []string{
|
||||
"/var/run/docker.sock",
|
||||
"/run/podman/podman.sock",
|
||||
"$HOME/.colima/docker.sock",
|
||||
"$XDG_RUNTIME_DIR/docker.sock",
|
||||
"$XDG_RUNTIME_DIR/podman/podman.sock",
|
||||
`\\.\pipe\docker_engine`,
|
||||
"$HOME/.docker/run/docker.sock",
|
||||
}
|
||||
|
||||
func getDockerSocketPath(configDockerHost string) (string, error) {
|
||||
// `-` 表示不要将 docker 套接字挂载到作业容器
|
||||
if configDockerHost != "" && configDockerHost != "-" {
|
||||
return configDockerHost, nil
|
||||
}
|
||||
|
||||
socket, found := os.LookupEnv("DOCKER_HOST")
|
||||
if found {
|
||||
return socket, nil
|
||||
}
|
||||
|
||||
for _, p := range commonSocketPaths {
|
||||
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
|
||||
if strings.HasPrefix(p, `\\.\`) {
|
||||
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
||||
}
|
||||
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("守护进程 Docker 引擎套接字未找到且 docker_host 配置无效")
|
||||
}
|
491
internal/app/cmd/exec.go
Normal file
491
internal/app/cmd/exec.go
Normal file
@ -0,0 +1,491 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2019 nektos
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
"github.com/nektos/act/pkg/artifacts"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type executeArgs struct {
|
||||
runList bool // 是否列出工作流
|
||||
job string // 特定作业 ID
|
||||
event string // 事件名称
|
||||
workdir string // 工作目录
|
||||
workflowsPath string // 工作流文件路径
|
||||
noWorkflowRecurse bool // 是否禁止递归运行子目录中的工作流
|
||||
autodetectEvent bool // 自动检测事件
|
||||
forcePull bool // 即使存在也拉取 Docker 镜像
|
||||
forceRebuild bool // 即使存在也重建本地动作 Docker 镜像
|
||||
jsonLogger bool // 以 JSON 格式输出日志
|
||||
envs []string // 环境变量
|
||||
envfile string // 环境文件
|
||||
secrets []string // 机密
|
||||
defaultActionsURL string // 默认动作 URL
|
||||
insecureSecrets bool // 不建议!打印日志时不隐藏机密
|
||||
privileged bool // 使用特权模式
|
||||
usernsMode string // 用户命名空间
|
||||
containerArchitecture string // 容器架构
|
||||
containerDaemonSocket string // 容器守护进程套接字
|
||||
useGitIgnore bool // 控制是否将 .gitignore 中指定的路径复制到容器中
|
||||
containerCapAdd []string // 添加到工作流容器的内核能力
|
||||
containerCapDrop []string // 从工作流容器中删除的内核能力
|
||||
containerOptions string // 容器选项
|
||||
artifactServerPath string // 存储上传和检索下载的路径
|
||||
artifactServerAddr string // 监听地址
|
||||
artifactServerPort string // 监听端口
|
||||
noSkipCheckout bool // 不跳过 actions/checkout
|
||||
debug bool // 启用调试日志
|
||||
dryrun bool // dryrun 模式
|
||||
image string // 使用的 Docker 镜像
|
||||
cacheHandler *artifactcache.Handler // 缓存处理器
|
||||
network string // 容器连接的网络
|
||||
githubInstance string // 使用的 Gitea 实例
|
||||
}
|
||||
|
||||
// WorkflowsPath 返回工作流文件的路径
|
||||
func (i *executeArgs) WorkflowsPath() string {
|
||||
return i.resolve(i.workflowsPath)
|
||||
}
|
||||
|
||||
// Envfile 返回 .env 文件的路径
|
||||
func (i *executeArgs) Envfile() string {
|
||||
return i.resolve(i.envfile)
|
||||
}
|
||||
|
||||
func (i *executeArgs) LoadSecrets() map[string]string {
|
||||
s := make(map[string]string)
|
||||
for _, secretPair := range i.secrets {
|
||||
secretPairParts := strings.SplitN(secretPair, "=", 2)
|
||||
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
|
||||
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
|
||||
log.Errorf("机密 %s 已经定义(机密不区分大小写)", secretPairParts[0])
|
||||
}
|
||||
if len(secretPairParts) == 2 {
|
||||
s[secretPairParts[0]] = secretPairParts[1]
|
||||
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||
s[secretPairParts[0]] = env
|
||||
} else {
|
||||
fmt.Printf("为 '%s' 提供值: ", secretPairParts[0])
|
||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Errorf("读取输入失败: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s[secretPairParts[0]] = string(val)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readEnvs(path string, envs map[string]string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
env, err := godotenv.Read(path)
|
||||
if err != nil {
|
||||
log.Fatalf("从 %s 加载失败: %v", path, err)
|
||||
}
|
||||
for k, v := range env {
|
||||
envs[k] = v
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *executeArgs) LoadEnvs() map[string]string {
|
||||
envs := make(map[string]string)
|
||||
if i.envs != nil {
|
||||
for _, envVar := range i.envs {
|
||||
e := strings.SplitN(envVar, `=`, 2)
|
||||
if len(e) == 2 {
|
||||
envs[e[0]] = e[1]
|
||||
} else {
|
||||
envs[e[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = readEnvs(i.Envfile(), envs)
|
||||
|
||||
envs["ACTIONS_CACHE_URL"] = i.cacheHandler.ExternalURL() + "/"
|
||||
|
||||
return envs
|
||||
}
|
||||
|
||||
// Workdir 返回工作目录的路径
|
||||
func (i *executeArgs) Workdir() string {
|
||||
return i.resolve(".")
|
||||
}
|
||||
|
||||
func (i *executeArgs) resolve(path string) string {
|
||||
basedir, err := filepath.Abs(i.workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if path == "" {
|
||||
return path
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(basedir, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func printList(plan *model.Plan) error {
|
||||
type lineInfoDef struct {
|
||||
jobID string // 作业 ID
|
||||
jobName string // 作业名称
|
||||
stage string // 阶段
|
||||
wfName string // 工作流名称
|
||||
wfFile string // 工作流文件
|
||||
events string // 事件
|
||||
}
|
||||
lineInfos := []lineInfoDef{}
|
||||
|
||||
header := lineInfoDef{
|
||||
jobID: "作业 ID",
|
||||
jobName: "作业名称",
|
||||
stage: "阶段",
|
||||
wfName: "工作流名称",
|
||||
wfFile: "工作流文件",
|
||||
events: "事件",
|
||||
}
|
||||
|
||||
jobs := map[string]bool{}
|
||||
duplicateJobIDs := false
|
||||
|
||||
jobIDMaxWidth := len(header.jobID)
|
||||
jobNameMaxWidth := len(header.jobName)
|
||||
stageMaxWidth := len(header.stage)
|
||||
wfNameMaxWidth := len(header.wfName)
|
||||
wfFileMaxWidth := len(header.wfFile)
|
||||
eventsMaxWidth := len(header.events)
|
||||
|
||||
for i, stage := range plan.Stages {
|
||||
for _, r := range stage.Runs {
|
||||
jobID := r.JobID
|
||||
line := lineInfoDef{
|
||||
jobID: jobID,
|
||||
jobName: r.String(),
|
||||
stage: strconv.Itoa(i),
|
||||
wfName: r.Workflow.Name,
|
||||
wfFile: r.Workflow.File,
|
||||
events: strings.Join(r.Workflow.On(), `,`),
|
||||
}
|
||||
if _, ok := jobs[jobID]; ok {
|
||||
duplicateJobIDs = true
|
||||
} else {
|
||||
jobs[jobID] = true
|
||||
}
|
||||
lineInfos = append(lineInfos, line)
|
||||
if jobIDMaxWidth < len(line.jobID) {
|
||||
jobIDMaxWidth = len(line.jobID)
|
||||
}
|
||||
if jobNameMaxWidth < len(line.jobName) {
|
||||
jobNameMaxWidth = len(line.jobName)
|
||||
}
|
||||
if stageMaxWidth < len(line.stage) {
|
||||
stageMaxWidth = len(line.stage)
|
||||
}
|
||||
if wfNameMaxWidth < len(line.wfName) {
|
||||
wfNameMaxWidth = len(line.wfName)
|
||||
}
|
||||
if wfFileMaxWidth < len(line.wfFile) {
|
||||
wfFileMaxWidth = len(line.wfFile)
|
||||
}
|
||||
if eventsMaxWidth < len(line.events) {
|
||||
eventsMaxWidth = len(line.events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobIDMaxWidth += 2
|
||||
jobNameMaxWidth += 2
|
||||
stageMaxWidth += 2
|
||||
wfNameMaxWidth += 2
|
||||
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, header.stage,
|
||||
-jobIDMaxWidth, header.jobID,
|
||||
-jobNameMaxWidth, header.jobName,
|
||||
-wfNameMaxWidth, header.wfName,
|
||||
-wfFileMaxWidth, header.wfFile,
|
||||
-eventsMaxWidth, header.events,
|
||||
)
|
||||
for _, line := range lineInfos {
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, line.stage,
|
||||
-jobIDMaxWidth, line.jobID,
|
||||
-jobNameMaxWidth, line.jobName,
|
||||
-wfNameMaxWidth, line.wfName,
|
||||
-wfFileMaxWidth, line.wfFile,
|
||||
-eventsMaxWidth, line.events,
|
||||
)
|
||||
}
|
||||
if duplicateJobIDs {
|
||||
fmt.Print("\n检测到多个作业具有相同的作业名称,请使用 `-W` 指定特定工作流的路径。\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
// 计划带有过滤的作业 - 仅用于过滤
|
||||
var filterPlan *model.Plan
|
||||
|
||||
// 确定要过滤的事件名称
|
||||
var filterEventName string
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
|
||||
filterEventName = execArgs.event
|
||||
} else if execArgs.autodetectEvent {
|
||||
// 收集所有加载的工作流中的事件
|
||||
events := planner.GetEvents()
|
||||
|
||||
// 将默认事件类型设置为第一个可用的事件
|
||||
// 这样用户就不必指定事件。
|
||||
log.Infof("使用检测到的第一个工作流事件进行过滤: %s", events[0])
|
||||
|
||||
filterEventName = events[0]
|
||||
}
|
||||
|
||||
var err error
|
||||
if execArgs.job != "" {
|
||||
log.Infof("准备带有作业的计划: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if filterEventName != "" {
|
||||
log.Infof("准备事件的计划: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("准备所有作业的计划")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = printList(filterPlan)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if execArgs.runList {
|
||||
return runExecList(ctx, planner, execArgs)
|
||||
}
|
||||
|
||||
// 计划触发作业
|
||||
var plan *model.Plan
|
||||
|
||||
// 确定要触发的事件名称
|
||||
var eventName string
|
||||
|
||||
// 收集所有加载的工作流中的事件
|
||||
events := planner.GetEvents()
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("使用选择的事件进行过滤: %s", execArgs.event)
|
||||
eventName = execArgs.event
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
log.Infof("使用唯一检测到的工作流事件: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
// 将默认事件类型设置为第一个可用的事件
|
||||
// 这样用户就不必指定事件。
|
||||
log.Infof("使用检测到的第一个工作流事件: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
log.Infof("使用默认工作流事件: push")
|
||||
eventName = "push"
|
||||
}
|
||||
|
||||
// 为此运行构建计划
|
||||
if execArgs.job != "" {
|
||||
log.Infof("规划作业: %s", execArgs.job)
|
||||
plan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("规划事件的作业: %s", eventName)
|
||||
plan, err = planner.PlanEvent(eventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
maxLifetime := 3 * time.Hour
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// 初始化缓存服务器
|
||||
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("缓存处理器监听于: %v", handler.ExternalURL())
|
||||
execArgs.cacheHandler = handler
|
||||
|
||||
if len(execArgs.artifactServerAddr) == 0 {
|
||||
ip := common.GetOutboundIP()
|
||||
if ip == nil {
|
||||
return fmt.Errorf("无法确定出站 IP 地址")
|
||||
}
|
||||
execArgs.artifactServerAddr = ip.String()
|
||||
}
|
||||
|
||||
if len(execArgs.artifactServerPath) == 0 {
|
||||
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
|
||||
// 运行计划
|
||||
config := &runner.Config{
|
||||
Workdir: execArgs.Workdir(),
|
||||
BindWorkdir: false,
|
||||
ReuseContainers: false,
|
||||
ForcePull: execArgs.forcePull,
|
||||
ForceRebuild: execArgs.forceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: execArgs.jsonLogger,
|
||||
Env: execArgs.LoadEnvs(),
|
||||
Secrets: execArgs.LoadSecrets(),
|
||||
InsecureSecrets: execArgs.insecureSecrets,
|
||||
Privileged: execArgs.privileged,
|
||||
UsernsMode: execArgs.usernsMode,
|
||||
ContainerArchitecture: execArgs.containerArchitecture,
|
||||
ContainerDaemonSocket: execArgs.containerDaemonSocket,
|
||||
UseGitIgnore: execArgs.useGitIgnore,
|
||||
GitHubInstance: execArgs.githubInstance,
|
||||
ContainerCapAdd: execArgs.containerCapAdd,
|
||||
ContainerCapDrop: execArgs.containerCapDrop,
|
||||
ContainerOptions: execArgs.containerOptions,
|
||||
AutoRemove: true,
|
||||
ArtifactServerPath: execArgs.artifactServerPath,
|
||||
ArtifactServerPort: execArgs.artifactServerPort,
|
||||
ArtifactServerAddr: execArgs.artifactServerAddr,
|
||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||
// PresetGitHubContext: preset,
|
||||
// EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: container.NetworkMode(execArgs.network),
|
||||
DefaultActionInstance: execArgs.defaultActionsURL,
|
||||
PlatformPicker: func(_ []string) string {
|
||||
return execArgs.image
|
||||
},
|
||||
ValidVolumes: []string{"**"}, // 所有挂载的卷(volumes)都允许被 exec 命令访问
|
||||
}
|
||||
|
||||
config.Env["ACT_EXEC"] = "true"
|
||||
|
||||
if t := config.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
config.Token = t
|
||||
} else if t := config.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
config.Token = t
|
||||
}
|
||||
|
||||
if !execArgs.debug {
|
||||
logLevel := log.InfoLevel
|
||||
config.JobLoggerLevel = &logLevel
|
||||
}
|
||||
|
||||
r, err := runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||
log.Debugf("artifacts 服务器启动于 %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
artifactCancel()
|
||||
return nil
|
||||
})
|
||||
|
||||
return executor(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execArg := executeArgs{}
|
||||
|
||||
execCmd := &cobra.Command{
|
||||
Use: "exec",
|
||||
Short: "本地运行工作流",
|
||||
Args: cobra.MaximumNArgs(20),
|
||||
RunE: runExec(ctx, &execArg),
|
||||
}
|
||||
|
||||
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "本地运行工作流")
|
||||
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "运行特定作业 ID")
|
||||
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "运行事件名称")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "工作流文件路径")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "工作目录")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "禁用运行指定路径的子目录中的工作流")
|
||||
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "使用工作流中的第一个事件类型作为触发工作流的事件")
|
||||
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "即使已经存在也拉取 Docker 镜像")
|
||||
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "即使已经存在也重建本地动作 Docker 镜像")
|
||||
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "以 JSON 格式输出日志")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "使环境变量对动作可用,可选值(例如 --env myenv=foo 或 --env myenv)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "读取并用作容器中的环境的环境文件")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "为 Action 提供密钥,可带可选值(例如 -s mysecret=foo 或 -s mysecret)")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "不推荐!打印日志时不会隐藏密钥信息")
|
||||
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "使用特权模式")
|
||||
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "要使用的用户命名空间")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "运行容器使用的架构(如 linux/loong64)。未指定时使用宿主机默认架构。需要 Docker 服务端 API 版本 1.41+,更低版本的 Docker 平台会忽略此参数")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "挂载到容器的 Docker 守护进程 socket 路径")
|
||||
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "控制是否将 .gitignore 中指定的路径复制到容器中")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "为工作流容器添加的内核能力(例如 --container-cap-add SYS_PTRACE)")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "从工作流容器移除的内核能力(例如 --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "容器选项")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "定义构建物服务器存储上传和下载的路径。未指定时构建物服务器不会启动")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "定义构建物服务器的监听地址")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "定义构建物服务器的监听端口(例如 --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://github.com", "定义 Action 实例的默认 URL")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "不跳过 actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "启用调试日志")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun 模式")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "lcr.loongnix.cn/library/debian:latest", "使用的 Docker 镜像。使用 \"-self-hosted\" 直接在主机上运行")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "容器连接的网络")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "使用的 Gitea 实例")
|
||||
|
||||
return execCmd
|
||||
}
|
394
internal/app/cmd/register.go
Normal file
394
internal/app/cmd/register.go
Normal file
@ -0,0 +1,394 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// runRegister 将运行器注册到服务器
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
log.Infof("注册运行器,架构=%s,操作系统=%s,版本=%s。",
|
||||
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
||||
|
||||
// 运行器始终需要 root 权限
|
||||
if os.Getuid() != 0 {
|
||||
// TODO: 使用更好的方法检查 root 权限
|
||||
log.Warnf("运行器处于用户模式。")
|
||||
}
|
||||
|
||||
if regArgs.NoInteractive {
|
||||
if err := registerNoInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// registerArgs 代表 register 命令的参数
|
||||
type registerArgs struct {
|
||||
NoInteractive bool // 是否非交互模式
|
||||
InstanceAddr string // 实例地址
|
||||
Token string // 令牌
|
||||
RunnerName string // 运行器名称
|
||||
Labels string // 标签
|
||||
Ephemeral bool // 是否临时
|
||||
}
|
||||
|
||||
type registerStage int8
|
||||
|
||||
const (
|
||||
StageUnknown registerStage = -1
|
||||
StageOverwriteLocalConfig registerStage = iota + 1
|
||||
StageInputInstance
|
||||
StageInputToken
|
||||
StageInputRunnerName
|
||||
StageInputLabels
|
||||
StageWaitingForRegistration
|
||||
StageExit
|
||||
)
|
||||
|
||||
var defaultLabels = []string{
|
||||
"debian-latest:docker://lcr.loongnix.cn/library/debian:latest",
|
||||
"anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest",
|
||||
"anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2",
|
||||
}
|
||||
|
||||
type registerInputs struct {
|
||||
InstanceAddr string
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels []string
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
func (r *registerInputs) validate() error {
|
||||
if r.InstanceAddr == "" {
|
||||
return fmt.Errorf("实例地址为空")
|
||||
}
|
||||
if r.Token == "" {
|
||||
return fmt.Errorf("令牌为空")
|
||||
}
|
||||
if len(r.Labels) > 0 {
|
||||
return validateLabels(r.Labels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabels(ls []string) error {
|
||||
for _, label := range ls {
|
||||
if _, err := labels.Parse(label); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registerInputs) stageValue(stage registerStage) string {
|
||||
switch stage {
|
||||
case StageInputInstance:
|
||||
return r.InstanceAddr
|
||||
case StageInputToken:
|
||||
return r.Token
|
||||
case StageInputRunnerName:
|
||||
return r.RunnerName
|
||||
case StageInputLabels:
|
||||
if len(r.Labels) > 0 {
|
||||
return strings.Join(r.Labels, ",")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
|
||||
// 必须设置实例地址和令牌。
|
||||
// 如果为空,保持当前阶段。
|
||||
if stage == StageInputInstance || stage == StageInputToken {
|
||||
if value == "" {
|
||||
return stage
|
||||
}
|
||||
}
|
||||
|
||||
// 如果运行器名称为空,设置为主机名
|
||||
if stage == StageInputRunnerName && value == "" {
|
||||
value, _ = os.Hostname()
|
||||
}
|
||||
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
if value == "Y" || value == "y" {
|
||||
return StageInputInstance
|
||||
}
|
||||
return StageExit
|
||||
case StageInputInstance:
|
||||
r.InstanceAddr = value
|
||||
return StageInputToken
|
||||
case StageInputToken:
|
||||
r.Token = value
|
||||
return StageInputRunnerName
|
||||
case StageInputRunnerName:
|
||||
r.RunnerName = value
|
||||
// 如果配置文件中有标签配置,跳过输入标签阶段
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
ls := make([]string, 0, len(cfg.Runner.Labels))
|
||||
for _, l := range cfg.Runner.Labels {
|
||||
_, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("忽略无效标签 %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, l)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("配置文件中没有有效的标签配置,运行器可能无法接取作业")
|
||||
}
|
||||
r.Labels = ls
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageInputLabels
|
||||
case StageInputLabels:
|
||||
r.Labels = defaultLabels
|
||||
if value != "" {
|
||||
r.Labels = strings.Split(value, ",")
|
||||
}
|
||||
|
||||
if validateLabels(r.Labels) != nil {
|
||||
log.Infoln("无效的标签, 请重新输入, 留空以使用默认标签 (例如, debian-latest:lcr.loongnix.cn/library/debian:latest) ")
|
||||
r.Labels = nil
|
||||
return StageInputLabels
|
||||
}
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageUnknown
|
||||
}
|
||||
|
||||
func initInputs(regArgs *registerArgs) *registerInputs {
|
||||
inputs := ®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
|
||||
)
|
||||
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||
stage = StageOverwriteLocalConfig
|
||||
}
|
||||
inputs := initInputs(regArgs)
|
||||
|
||||
for {
|
||||
cmdString := inputs.stageValue(stage)
|
||||
if cmdString == "" {
|
||||
printStageHelp(stage)
|
||||
var err error
|
||||
cmdString, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("注册运行器,名称=%s, 实例=%s, 标签=%v。", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("注册运行器失败: %w", err)
|
||||
}
|
||||
log.Infof("运行器注册成功。")
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage == StageExit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage <= StageUnknown {
|
||||
log.Errorf("无效输入,请重新运行命令。")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printStageHelp(stage registerStage) {
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("运行器已注册,是否覆盖本地配置?[y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("请输入 Gitea 实例 URL(例如, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("请输入运行器令牌:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("请输入运行器名称(如果留空,使用主机名:%s):\n", hostname)
|
||||
case StageInputLabels:
|
||||
log.Infoln("请输入运行器标签, 留空以使用默认标签(逗号分隔, 例如, debian-latest:docker://lcr.loongnix.cn/library/debian:latest):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("等待注册...")
|
||||
}
|
||||
}
|
||||
|
||||
func registerNoInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs := initInputs(regArgs)
|
||||
// 配置文件中指定的标签。
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
if regArgs.Labels != "" {
|
||||
log.Warn("命令行中的标签将被忽略,使用配置文件中定义的标签。")
|
||||
}
|
||||
inputs.Labels = cfg.Runner.Labels
|
||||
}
|
||||
if len(inputs.Labels) == 0 {
|
||||
inputs.Labels = defaultLabels
|
||||
}
|
||||
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
log.Infof("运行器名称为空,使用主机名 '%s'。", inputs.RunnerName)
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("无效输入,请重新运行命令。")
|
||||
return err
|
||||
}
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("注册运行器失败: %w", err)
|
||||
}
|
||||
log.Infof("运行器注册成功。")
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
|
||||
// 初始化 http 客户端
|
||||
cli := client.New(
|
||||
inputs.InstanceAddr,
|
||||
cfg.Runner.Insecure,
|
||||
"",
|
||||
"",
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
for {
|
||||
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
|
||||
Data: inputs.RunnerName,
|
||||
}))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("无法 ping 到 Gitea 实例服务器")
|
||||
// TODO: 如果 ping 失败,重试或退出
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
log.Debugln("成功 ping 到 Gitea 实例服务器")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
reg := &config.Registration{
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.Labels,
|
||||
Ephemeral: inputs.Ephemeral,
|
||||
}
|
||||
|
||||
ls := make([]string, len(reg.Labels))
|
||||
for i, v := range reg.Labels {
|
||||
l, _ := labels.Parse(v)
|
||||
ls[i] = l.Name
|
||||
}
|
||||
// 注册新的运行器。
|
||||
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: reg.Name,
|
||||
Token: reg.Token,
|
||||
Version: ver.Version(),
|
||||
AgentLabels: ls, // 在 Gitea 1.20 之后可能会被移除
|
||||
Labels: ls,
|
||||
Ephemeral: reg.Ephemeral,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: 无法注册新运行器")
|
||||
return err
|
||||
}
|
||||
|
||||
reg.ID = resp.Msg.Runner.Id
|
||||
reg.UUID = resp.Msg.Runner.Uuid
|
||||
reg.Name = resp.Msg.Runner.Name
|
||||
reg.Token = resp.Msg.Runner.Token
|
||||
|
||||
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
|
||||
// TODO 我们不能通过 runner api 移除配置,如果在这里返回错误,我们只是填充数据库
|
||||
log.Error("poller: 无法注册新的临时运行器,升级 Gitea 以获得安全性,自动使用 run-once")
|
||||
}
|
||||
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("保存运行器配置失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
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, "不支持的标签: 无效")
|
||||
}
|
208
internal/app/poll/poller.go
Normal file
208
internal/app/poll/poller.go
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package poll
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/run"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
)
|
||||
|
||||
type Poller struct {
|
||||
client client.Client // Gitea 客户端,用于与服务器通信
|
||||
runner *run.Runner // 任务执行器
|
||||
cfg *config.Config // 配置信息
|
||||
|
||||
tasksVersion atomic.Int64 // 任务版本号,用于增量同步
|
||||
|
||||
pollingCtx context.Context // 轮询上下文
|
||||
shutdownPolling context.CancelFunc // 轮询关闭函数
|
||||
|
||||
jobsCtx context.Context // 任务执行上下文
|
||||
shutdownJobs context.CancelFunc // 任务执行关闭函数
|
||||
|
||||
done chan struct{} // 完成信号通道
|
||||
}
|
||||
|
||||
// New 构造函数,创建并初始化 Poller 实例
|
||||
// 接受配置、客户端和服务运行器作为参数,并初始化必要的上下文和通道以便于后续操作。
|
||||
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
|
||||
|
||||
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
return &Poller{
|
||||
client: client,
|
||||
runner: runner,
|
||||
cfg: cfg,
|
||||
|
||||
pollingCtx: pollingCtx,
|
||||
shutdownPolling: shutdownPolling,
|
||||
|
||||
jobsCtx: jobsCtx,
|
||||
shutdownJobs: shutdownJobs,
|
||||
|
||||
done: done,
|
||||
}
|
||||
}
|
||||
|
||||
// Poll 持续轮询模式
|
||||
// 启动多个 goroutine 来并发地轮询任务。每个 goroutine 调用 poll 方法进行实际的工作。在所有工作完成之后,通过关闭 done 通道发出信号。
|
||||
func (p *Poller) Poll() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < p.cfg.Runner.Capacity; i++ {
|
||||
wg.Add(1)
|
||||
go p.poll(wg, limiter)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 发出我们正在关闭的信号
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// PollOnce 单次轮询模式
|
||||
// 类似于 Poll,但是只执行一次任务轮询。同样会在完成后关闭 done 通道。
|
||||
func (p *Poller) PollOnce() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
|
||||
p.pollOnce(limiter)
|
||||
|
||||
// 发出我们已经完成的信号
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// Shutdown 关闭
|
||||
// 关闭轮询过程。首先取消轮询上下文,然后等待所有任务完成。如果超时发生,则强制关闭所有正在运行的任务并确保状态报告给 Gitea。
|
||||
func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownPolling()
|
||||
|
||||
select {
|
||||
|
||||
// 优雅地完成关闭
|
||||
case <-p.done:
|
||||
return nil
|
||||
|
||||
// 关闭超时
|
||||
case <-ctx.Done():
|
||||
|
||||
// 当超时和优雅关闭同时发生时,
|
||||
// 这个分支可能会被触发。这里进行非阻塞检查,
|
||||
// 避免在不必要的情况下发送错误。
|
||||
_, ok := <-p.done
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 强制关闭所有运行中的任务
|
||||
p.shutdownJobs()
|
||||
|
||||
// 等待运行中的任务向Gitea报告其状态
|
||||
_, _ = <-p.done
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// poll 轮询循环
|
||||
// 这是实际执行轮询逻辑的地方。它会持续调用 pollOnce 方法来获取并处理任务,直到上下文被取消。
|
||||
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
p.pollOnce(limiter)
|
||||
|
||||
select {
|
||||
case <-p.pollingCtx.Done():
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollOnce 单次轮询执行
|
||||
// 负责单次任务轮询。它首先等待速率限制器允许继续,然后尝试获取一个新任务。一旦获取到任务,就调用 runTaskWithRecover 方法来执行任务。
|
||||
func (p *Poller) pollOnce(limiter *rate.Limiter) {
|
||||
for {
|
||||
if err := limiter.Wait(p.pollingCtx); err != nil {
|
||||
if p.pollingCtx.Err() != nil {
|
||||
log.WithError(err).Debug("速率限制等待失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
task, ok := p.fetchTask(p.pollingCtx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p.runTaskWithRecover(p.jobsCtx, task)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// runTaskWithRecover 安全执行任务
|
||||
// 执行给定的任务,并提供 panic 恢复机制以防止意外崩溃。如果任务执行过程中出现错误或 panic,都会记录相应的日志信息。
|
||||
func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
|
||||
// 建立错误恢复机制
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := fmt.Errorf("panic: %v", r)
|
||||
log.WithError(err).Error("runTaskWithRecover中发生panic")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := p.runner.Run(ctx, task); err != nil {
|
||||
log.WithError(err).Error("运行任务失败")
|
||||
}
|
||||
}
|
||||
|
||||
// fetchTask 获取任务
|
||||
// 尝试从 Gitea 获取一个新任务。如果成功获取到任务且该任务的版本号比本地缓存的大,则更新本地缓存的版本号。
|
||||
// 如果获取失败或没有可用的任务,返回 false;否则返回 true 和任务数据。
|
||||
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 加载发送请求时缓存中的版本值
|
||||
v := p.tasksVersion.Load()
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: v,
|
||||
}))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Error("获取任务失败")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if resp == nil || resp.Msg == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if resp.Msg.TasksVersion > v {
|
||||
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
||||
}
|
||||
|
||||
if resp.Msg.Task == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 收到一个任务,将`tasksVersion`设置为零,以便在下一个请求中创建查询数据库。
|
||||
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
|
||||
|
||||
return resp.Msg.Task, true
|
||||
}
|
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
|
||||
}
|
255
internal/app/run/runner.go
Normal file
255
internal/app/run/runner.go
Normal file
@ -0,0 +1,255 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/config"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/labels"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/report"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Runner 运行流水线
|
||||
type Runner struct {
|
||||
name string // Runner 名称
|
||||
|
||||
cfg *config.Config // 配置信息
|
||||
|
||||
client client.Client // Gitea 客户端
|
||||
labels labels.Labels // 标签集合
|
||||
envs map[string]string // 环境变量
|
||||
|
||||
// 正在运行的任务
|
||||
runningTasks sync.Map // 使用 sync.Map 来存储正在运行的任务 ID
|
||||
}
|
||||
|
||||
// NewRunner 使用提供的配置、注册信息和客户端创建一个新的 Runner 实例
|
||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||
ls := labels.Labels{}
|
||||
for _, v := range reg.Labels {
|
||||
if l, err := labels.Parse(v); err == nil {
|
||||
ls = append(ls, l)
|
||||
}
|
||||
}
|
||||
envs := make(map[string]string, len(cfg.Runner.Envs))
|
||||
for k, v := range cfg.Runner.Envs {
|
||||
envs[k] = v
|
||||
}
|
||||
// 初始化缓存环境变量
|
||||
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
||||
if cfg.Cache.ExternalServer != "" {
|
||||
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
|
||||
} else {
|
||||
cacheHandler, err := artifactcache.StartHandler(
|
||||
cfg.Cache.Dir,
|
||||
cfg.Cache.Host,
|
||||
cfg.Cache.Port,
|
||||
log.StandardLogger().WithField("module", "cache_request"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("无法初始化缓存服务器,将禁用它:%v", err)
|
||||
// 继续执行
|
||||
} else {
|
||||
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 artifact gitea api
|
||||
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
||||
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
||||
envs["ACTIONS_RESULTS_URL"] = strings.TrimSuffix(cli.Address(), "/")
|
||||
|
||||
// 设置特定环境变量以区分 Gitea 和 GitHub
|
||||
envs["GITEA_ACTIONS"] = "true"
|
||||
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
|
||||
|
||||
return &Runner{
|
||||
name: reg.Name,
|
||||
cfg: cfg,
|
||||
client: cli,
|
||||
labels: ls,
|
||||
envs: envs,
|
||||
}
|
||||
}
|
||||
|
||||
// Run 在给定的上下文中执行任务,确保同一时间仅运行一个任务
|
||||
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
// 检查任务是否已在运行
|
||||
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||
return fmt.Errorf("任务 %d 已经在运行", task.Id)
|
||||
}
|
||||
r.runningTasks.Store(task.Id, struct{}{})
|
||||
defer r.runningTasks.Delete(task.Id)
|
||||
|
||||
// 创建带超时的子上下文
|
||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||
defer cancel()
|
||||
reporter := report.NewReporter(ctx, cancel, r.client, task)
|
||||
var runErr error
|
||||
defer func() {
|
||||
lastWords := ""
|
||||
if runErr != nil {
|
||||
lastWords = runErr.Error()
|
||||
}
|
||||
_ = reporter.Close(lastWords)
|
||||
}()
|
||||
reporter.RunDaemon()
|
||||
runErr = r.run(ctx, task, reporter)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// run 执行具体的任务逻辑
|
||||
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// 记录任务接收日志
|
||||
reporter.Logf("%s(版本:%s) 收到任务 %v 的作业 %v,由事件触发: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
|
||||
|
||||
workflow, jobID, err := generateWorkflow(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
job := workflow.GetJob(jobID)
|
||||
reporter.ResetSteps(len(job.Steps))
|
||||
|
||||
taskContext := task.Context.Fields
|
||||
|
||||
log.Infof("任务 %v 仓库是 %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||
taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
r.client.Address())
|
||||
|
||||
// 构建预设的 GitHub 上下文环境
|
||||
preset := &model.GithubContext{
|
||||
Event: taskContext["event"].GetStructValue().AsMap(),
|
||||
RunID: taskContext["run_id"].GetStringValue(),
|
||||
RunNumber: taskContext["run_number"].GetStringValue(),
|
||||
Actor: taskContext["actor"].GetStringValue(),
|
||||
Repository: taskContext["repository"].GetStringValue(),
|
||||
EventName: taskContext["event_name"].GetStringValue(),
|
||||
Sha: taskContext["sha"].GetStringValue(),
|
||||
Ref: taskContext["ref"].GetStringValue(),
|
||||
RefName: taskContext["ref_name"].GetStringValue(),
|
||||
RefType: taskContext["ref_type"].GetStringValue(),
|
||||
HeadRef: taskContext["head_ref"].GetStringValue(),
|
||||
BaseRef: taskContext["base_ref"].GetStringValue(),
|
||||
Token: taskContext["token"].GetStringValue(),
|
||||
RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
|
||||
RetentionDays: taskContext["retention_days"].GetStringValue(),
|
||||
}
|
||||
// 优先使用 GITEA_TOKEN
|
||||
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
}
|
||||
|
||||
if actionsIdTokenRequestUrl := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIdTokenRequestUrl != "" {
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIdTokenRequestUrl
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
||||
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
||||
}
|
||||
|
||||
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
|
||||
if giteaRuntimeToken == "" {
|
||||
// 兼容旧版本 Gitea Server
|
||||
giteaRuntimeToken = preset.Token
|
||||
}
|
||||
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
||||
|
||||
eventJSON, err := json.Marshal(preset.Event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxLifetime := 3 * time.Hour
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// 创建 Runner 配置
|
||||
runnerConfig := &runner.Config{
|
||||
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
|
||||
BindWorkdir: false,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: r.cfg.Container.ForcePull,
|
||||
ForceRebuild: r.cfg.Container.ForceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: false,
|
||||
Env: r.envs,
|
||||
Secrets: task.Secrets,
|
||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||
AutoRemove: true,
|
||||
NoSkipCheckout: true,
|
||||
PresetGitHubContext: preset,
|
||||
EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||
ContainerOptions: r.cfg.Container.Options,
|
||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||
Privileged: r.cfg.Container.Privileged,
|
||||
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
PlatformPicker: r.labels.PickPlatform,
|
||||
Vars: task.Vars,
|
||||
ValidVolumes: r.cfg.Container.ValidVolumes,
|
||||
InsecureSkipTLS: r.cfg.Runner.Insecure,
|
||||
}
|
||||
|
||||
rr, err := runner.New(runnerConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executor := rr.NewPlanExecutor(plan)
|
||||
|
||||
reporter.Logf("工作流程已准备就绪")
|
||||
|
||||
// 添加日志记录器
|
||||
ctx = common.WithLoggerHook(ctx, reporter)
|
||||
|
||||
if !log.IsLevelEnabled(log.DebugLevel) {
|
||||
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
||||
}
|
||||
|
||||
execErr := executor(ctx)
|
||||
reporter.SetOutputs(job.Outputs)
|
||||
return execErr
|
||||
}
|
||||
|
||||
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
|
||||
Version: ver.Version(),
|
||||
Labels: labels,
|
||||
}))
|
||||
}
|
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)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
@ -6,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
// A Client manages communication with the runner.
|
||||
//
|
||||
//go:generate mockery --name Client
|
||||
type Client interface {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
11
internal/pkg/client/header.go
Normal file
11
internal/pkg/client/header.go
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
// 已弃用: 可以在Gitea 1.20发布后删除
|
||||
VersionHeader = "x-runner-version"
|
||||
)
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
@ -8,11 +11,10 @@ import (
|
||||
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"gitea.com/gitea/act_runner/core"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
)
|
||||
|
||||
func getHttpClient(endpoint string, insecure bool) *http.Client {
|
||||
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||
if strings.HasPrefix(endpoint, "https://") && insecure {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
@ -25,17 +27,21 @@ func getHttpClient(endpoint string, insecure bool) *http.Client {
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
// New returns a new runner client.
|
||||
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
|
||||
// New返回一个新的runner客户端。
|
||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||
|
||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
if uuid != "" {
|
||||
req.Header().Set(core.UUIDHeader, uuid)
|
||||
req.Header().Set(UUIDHeader, uuid)
|
||||
}
|
||||
if token != "" {
|
||||
req.Header().Set(core.TokenHeader, token)
|
||||
req.Header().Set(TokenHeader, token)
|
||||
}
|
||||
// TODO:version将在Gitea 1.20发布后从请求标头中删除。
|
||||
if version != "" {
|
||||
req.Header().Set(VersionHeader, version)
|
||||
}
|
||||
return next(ctx, req)
|
||||
}
|
||||
@ -43,12 +49,12 @@ func New(endpoint string, insecure bool, uuid, token string, opts ...connect.Cli
|
||||
|
||||
return &HTTPClient{
|
||||
PingServiceClient: pingv1connect.NewPingServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
getHTTPClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
getHTTPClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
@ -67,7 +73,7 @@ func (c *HTTPClient) Insecure() bool {
|
||||
|
||||
var _ Client = (*HTTPClient)(nil)
|
||||
|
||||
// An HTTPClient manages communication with the runner API.
|
||||
// HTTPClient管理与runner API的通信。
|
||||
type HTTPClient struct {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
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
|
||||
}
|
101
internal/pkg/config/config.example.yaml
Normal file
101
internal/pkg/config/config.example.yaml
Normal file
@ -0,0 +1,101 @@
|
||||
# 示例配置文件,可以安全地将其复制为默认配置文件而无需任何修改。
|
||||
|
||||
# 您不必将此文件复制到您的实例,
|
||||
# 只需运行 `./act_runner generate-config > config.yaml` 来生成配置文件。
|
||||
|
||||
log:
|
||||
# 日志级别,可以是 trace、debug、info、warn、error、fatal
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# 存储注册结果的路径。
|
||||
file: .runner
|
||||
# 同时执行多少个任务。
|
||||
capacity: 1
|
||||
# 运行作业的额外环境变量。
|
||||
envs:
|
||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
# 从文件中运行作业的额外环境变量。
|
||||
# 如果为空或文件不存在,则会被忽略。
|
||||
env_file: .env
|
||||
# 作业完成的超时时间。
|
||||
# 请注意,Gitea 实例也有一个作业超时时间(默认为 3 小时)。
|
||||
# 如果这个超时时间比 Gitea 实例的超时时间短,作业可能会被 Gitea 实例停止。
|
||||
timeout: 3h
|
||||
# 运行器在关闭时等待运行作业完成的超时时间。
|
||||
# 在这个超时时间之后仍未完成的任何运行作业将被取消。
|
||||
shutdown_timeout: 0s
|
||||
# 是否跳过验证 Gitea 实例的 TLS 证书。
|
||||
insecure: false
|
||||
# 从 Gitea 实例获取作业的超时时间。
|
||||
fetch_timeout: 5s
|
||||
# 从 Gitea 实例获取作业的时间间隔。
|
||||
fetch_interval: 2s
|
||||
# 运行器的标签用于确定运行器可以运行哪些作业以及如何运行它们。
|
||||
# 例如:"linux-loong64.abi2:host" 或 "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
|
||||
# 在 https://gitea.com/docker.gitea.com/runner-images 查找 Gitea 提供的更多镜像。
|
||||
# 如果在注册时为空,它会要求输入标签。
|
||||
# 如果在执行 `daemon` 时为空,将使用 `.runner` 文件中的标签。
|
||||
labels:
|
||||
- "debian-latest:docker://lcr.loongnix.cn/library/debian:latest"
|
||||
- "anolisos-latest:docker://lcr.loongnix.cn/library/anolisos:latest"
|
||||
- "anolisos-23.2:docker://lcr.loongnix.cn/library/anolisos:23.2"
|
||||
|
||||
cache:
|
||||
# 启用缓存服务器以使用 actions/cache。
|
||||
enabled: true
|
||||
# 存储缓存数据的目录。
|
||||
# 如果为空,缓存数据将存储在 $HOME/.cache/actcache。
|
||||
dir: ""
|
||||
# 缓存服务器的主机。
|
||||
# 它不是用于监听的地址,而是用于作业容器连接的地址。
|
||||
# 所以 0.0.0.0 是一个糟糕的选择,留空以自动检测。
|
||||
host: ""
|
||||
# 缓存服务器的端口。
|
||||
# 0 表示使用随机可用端口。
|
||||
port: 0
|
||||
# 外部缓存服务器 URL。仅在启用时有效。
|
||||
# 如果指定了它,act_runner 将使用此 URL 作为 ACTIONS_CACHE_URL 而不是自己启动一个服务器。
|
||||
# URL 通常应该以 "/" 结尾。
|
||||
external_server: ""
|
||||
|
||||
container:
|
||||
# 指定容器将连接的网络。
|
||||
# 可以是 host、bridge 或自定义网络的名称。
|
||||
# 如果为空,act_runner 将自动创建一个网络。
|
||||
network: ""
|
||||
# 启动任务容器时是否使用特权模式(特权模式对于 Docker-in-Docker 是必需的)。
|
||||
privileged: false
|
||||
# 容器启动时使用的其他选项(例如,--add-host=my.gitea.url:host-gateway)。
|
||||
options:
|
||||
# 作业工作目录的父目录。
|
||||
# 注意:不需要在路径前添加第一个 '/',因为 act_runner 会自动添加。
|
||||
# 如果路径以 '/' 开头,'/' 将被修剪。
|
||||
# 例如,如果父目录是 /path/to/my/dir,workdir_parent 应该是 path/to/my/dir
|
||||
# 如果为空,将使用 /workspace。
|
||||
workdir_parent:
|
||||
# 可以挂载到容器的卷(包括绑定挂载)。支持 glob 语法,参见 https://github.com/gobwas/glob
|
||||
# 您可以指定多个卷。如果序列为空,则不能挂载任何卷。
|
||||
# 例如,如果您只允许容器挂载 `data` 卷和 `/src` 中的所有 json 文件,您应该将配置更改为:
|
||||
# valid_volumes:
|
||||
# - data
|
||||
# - /src/*.json
|
||||
# 如果您想允许任何卷,请使用以下配置:
|
||||
# valid_volumes:
|
||||
# - '**'
|
||||
valid_volumes: []
|
||||
# 用指定的主机覆盖 docker 客户端主机。
|
||||
# 如果为空,act_runner 将自动查找可用的 docker 主机。
|
||||
# 如果是 "-",act_runner 将自动查找可用的 docker 主机,但 docker 主机不会挂载到作业容器和服务容器。
|
||||
# 如果不为空或 "-",将使用指定的 docker 主机。如果不起作用,将返回错误。
|
||||
docker_host: ""
|
||||
# 即使已经存在也拉取 Docker 镜像
|
||||
force_pull: true
|
||||
# 即使已经存在也重建 Docker 镜像
|
||||
force_rebuild: false
|
||||
|
||||
host:
|
||||
# 作业工作目录的父目录。
|
||||
# 如果为空,将使用 $HOME/.cache/act/。
|
||||
workdir_parent:
|
151
internal/pkg/config/config.go
Normal file
151
internal/pkg/config/config.go
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Log 代表日志的配置。
|
||||
type Log struct {
|
||||
Level string `yaml:"level"` // Level 表示日志级别。
|
||||
}
|
||||
|
||||
// Runner 代表运行器的配置。
|
||||
type Runner struct {
|
||||
File string `yaml:"file"` // File 指定运行器的文件路径。
|
||||
Capacity int `yaml:"capacity"` // Capacity 指定运行器的容量。
|
||||
Envs map[string]string `yaml:"envs"` // Envs 存储运行器的环境变量。
|
||||
EnvFile string `yaml:"env_file"` // EnvFile 指定包含运行器环境变量的文件路径。
|
||||
Timeout time.Duration `yaml:"timeout"` // Timeout 指定运行器超时的持续时间。
|
||||
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout 指定在运行器关闭期间等待运行作业完成的持续时间。
|
||||
Insecure bool `yaml:"insecure"` // Insecure 表示运行器是否在不安全模式下运行。
|
||||
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout 指定获取资源的超时持续时间。
|
||||
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval 指定获取资源的间隔持续时间。
|
||||
Labels []string `yaml:"labels"` // Labels 指定运行器的标签。每个启动时声明标签。
|
||||
}
|
||||
|
||||
// Cache 代表缓存的配置。
|
||||
type Cache struct {
|
||||
Enabled *bool `yaml:"enabled"` // Enabled 表示是否启用缓存。它是指针,用于区分 false 和未设置。如果未设置,它将为 true。
|
||||
Dir string `yaml:"dir"` // Dir 指定缓存的目录路径。
|
||||
Host string `yaml:"host"` // Host 指定缓存主机。
|
||||
Port uint16 `yaml:"port"` // Port 指定缓存端口。
|
||||
ExternalServer string `yaml:"external_server"` // ExternalServer 指定外部缓存服务器的 URL。
|
||||
}
|
||||
|
||||
// Container 代表容器的配置。
|
||||
type Container struct {
|
||||
Network string `yaml:"network"` // Network 指定容器的网络。
|
||||
NetworkMode string `yaml:"network_mode"` // 已弃用:使用 Network 替代。可能在 Gitea 1.20 之后被移除。
|
||||
Privileged bool `yaml:"privileged"` // Privileged 表示容器是否以特权模式运行。
|
||||
Options string `yaml:"options"` // Options 指定容器的其他选项。
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定容器工作目录的父目录。
|
||||
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes 指定可以挂载到容器的卷(包括绑定挂载)。
|
||||
DockerHost string `yaml:"docker_host"` // DockerHost 指定 Docker 主机。它覆盖环境变量 DOCKER_HOST 中指定的值。
|
||||
ForcePull bool `yaml:"force_pull"` // 即使已经存在也拉取 Docker 镜像。
|
||||
ForceRebuild bool `yaml:"force_rebuild"` // 即使已经存在也重建 Docker 镜像。
|
||||
}
|
||||
|
||||
// Host 代表主机的配置。
|
||||
type Host struct {
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent 指定主机工作目录的父目录。
|
||||
}
|
||||
|
||||
// Config 代表整体配置。
|
||||
type Config struct {
|
||||
Log Log `yaml:"log"` // Log 代表日志的配置。
|
||||
Runner Runner `yaml:"runner"` // Runner 代表运行器的配置。
|
||||
Cache Cache `yaml:"cache"` // Cache 代表缓存的配置。
|
||||
Container Container `yaml:"container"` // Container 代表容器的配置。
|
||||
Host Host `yaml:"host"` // Host 代表主机的配置。
|
||||
}
|
||||
|
||||
// LoadDefault 返回默认配置。
|
||||
// 如果文件不为空,它将被用来加载配置。
|
||||
func LoadDefault(file string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if file != "" {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开配置文件 %q: %w", file, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(content, cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件 %q: %w", file, err)
|
||||
}
|
||||
}
|
||||
compatibleWithOldEnvs(file != "", cfg)
|
||||
|
||||
if cfg.Runner.EnvFile != "" {
|
||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||
envs, err := godotenv.Read(cfg.Runner.EnvFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取环境文件 %q: %w", cfg.Runner.EnvFile, err)
|
||||
}
|
||||
if cfg.Runner.Envs == nil {
|
||||
cfg.Runner.Envs = map[string]string{}
|
||||
}
|
||||
for k, v := range envs {
|
||||
cfg.Runner.Envs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = "info"
|
||||
}
|
||||
if cfg.Runner.File == "" {
|
||||
cfg.Runner.File = ".runner"
|
||||
}
|
||||
if cfg.Runner.Capacity <= 0 {
|
||||
cfg.Runner.Capacity = 1
|
||||
}
|
||||
if cfg.Runner.Timeout <= 0 {
|
||||
cfg.Runner.Timeout = 3 * time.Hour
|
||||
}
|
||||
if cfg.Cache.Enabled == nil {
|
||||
b := true
|
||||
cfg.Cache.Enabled = &b
|
||||
}
|
||||
if *cfg.Cache.Enabled {
|
||||
if cfg.Cache.Dir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
}
|
||||
if cfg.Container.WorkdirParent == "" {
|
||||
cfg.Container.WorkdirParent = "workspace"
|
||||
}
|
||||
if cfg.Host.WorkdirParent == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
||||
}
|
||||
if cfg.Runner.FetchTimeout <= 0 {
|
||||
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||
}
|
||||
if cfg.Runner.FetchInterval <= 0 {
|
||||
cfg.Runner.FetchInterval = 2 * time.Second
|
||||
}
|
||||
|
||||
// 虽然 `container.network_mode` 将被弃用,但我们现在必须与它兼容。
|
||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||
log.Warn("您正在尝试使用已弃用的配置项 `container.network_mode`,请使用 `container.network` 替代。")
|
||||
if cfg.Container.NetworkMode == "bridge" {
|
||||
// 以前,如果 `container.network_mode` 的值是 `bridge`,我们会为作业创建一个新的网络。
|
||||
// 但是,“bridge”很容易与 Docker 默认创建的桥接网络混淆。
|
||||
// 所以我们将 `container.network` 的值设置为空字符串,以使 `act_runner` 为作业自动创建一个新的网络。
|
||||
} else {
|
||||
cfg.Container.Network = cfg.Container.NetworkMode
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
62
internal/pkg/config/deprecated.go
Normal file
62
internal/pkg/config/deprecated.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 已弃用:未来可能会移除。TODO: 在 Gitea 1.20.0 发布时将其移除。
|
||||
// 兼容旧环境。
|
||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
||||
handleEnv := func(key string) (string, bool) {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if fileUsed {
|
||||
log.Warnf("环境 %s 已被忽略,因为使用了配置文件", key)
|
||||
return "", false
|
||||
}
|
||||
log.Warnf("环境变量 %s 将被弃用,请改用配置文件。", key)
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
if v, ok := handleEnv("GITEA_DEBUG"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "debug"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_TRACE"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "trace"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
|
||||
if i, _ := strconv.Atoi(v); i > 0 {
|
||||
cfg.Runner.Capacity = i
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
|
||||
cfg.Runner.File = v
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
|
||||
splits := strings.Split(v, ",")
|
||||
if cfg.Runner.Envs == nil {
|
||||
cfg.Runner.Envs = map[string]string{}
|
||||
}
|
||||
for _, split := range splits {
|
||||
kv := strings.SplitN(split, ":", 2)
|
||||
if len(kv) == 2 && kv[0] != "" {
|
||||
cfg.Runner.Envs[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
|
||||
cfg.Runner.EnvFile = v
|
||||
}
|
||||
}
|
9
internal/pkg/config/embed.go
Normal file
9
internal/pkg/config/embed.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed config.example.yaml
|
||||
var Example []byte
|
55
internal/pkg/config/registration.go
Normal file
55
internal/pkg/config/registration.go
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
const registrationWarning = "此文件由`Loong Runner`自动生成。除非你知道自己在做什么, 否则不要手动编辑它。删除此文件将导致`Loong Runner`重新注册为新的运行器。"
|
||||
|
||||
// Registration 表示运行器的注册信息
|
||||
type Registration struct {
|
||||
Warning string `json:"WARNING"` // 警告信息,始终为 registrationWarning 常量
|
||||
|
||||
ID int64 `json:"id"` // 唯一标识符(整数类型)
|
||||
UUID string `json:"uuid"` // 全局唯一标识符(字符串类型)
|
||||
Name string `json:"name"` // 运行器名称
|
||||
Token string `json:"token"` // 认证令牌,用于与调度器通信
|
||||
Address string `json:"address"` // 运行器的网络地址(如IP或域名)
|
||||
Labels []string `json:"labels"` // 运行器关联的标签列表(用于任务匹配)
|
||||
Ephemeral bool `json:"ephemeral"` // 是否为临时实例(完成任务后自动销毁)
|
||||
}
|
||||
|
||||
func LoadRegistration(file string) (*Registration, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reg Registration
|
||||
if err := json.NewDecoder(f).Decode(®); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg.Warning = ""
|
||||
|
||||
return ®, nil
|
||||
}
|
||||
|
||||
func SaveRegistration(file string, reg *Registration) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reg.Warning = registrationWarning
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(reg)
|
||||
}
|
5
internal/pkg/envcheck/doc.go
Normal file
5
internal/pkg/envcheck/doc.go
Normal file
@ -0,0 +1,5 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// envcheck包中提供了一种简单的方法, 用来检查环境是否准备好执行工作。
|
||||
package envcheck
|
34
internal/pkg/envcheck/docker.go
Normal file
34
internal/pkg/envcheck/docker.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
opts := []client.Opt{
|
||||
client.FromEnv,
|
||||
}
|
||||
|
||||
if configDockerHost != "" {
|
||||
opts = append(opts, client.WithHost(configDockerHost))
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法ping通docker守护进程, 它是否在运行? %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
113
internal/pkg/labels/labels.go
Normal file
113
internal/pkg/labels/labels.go
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// SchemeHost 表示主机模式
|
||||
SchemeHost = "host"
|
||||
// SchemeDocker 表示 Docker 模式
|
||||
SchemeDocker = "docker"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
Name string
|
||||
Schema string
|
||||
Arg string
|
||||
}
|
||||
|
||||
// Parse 解析标签字符串并返回 Label 结构体。
|
||||
func Parse(str string) (*Label, error) {
|
||||
splits := strings.SplitN(str, ":", 3)
|
||||
label := &Label{
|
||||
Name: splits[0],
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
}
|
||||
if len(splits) >= 2 {
|
||||
label.Schema = splits[1]
|
||||
}
|
||||
if len(splits) >= 3 {
|
||||
label.Arg = splits[2]
|
||||
}
|
||||
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
|
||||
return nil, fmt.Errorf("不支持的标签: %s", label.Schema)
|
||||
}
|
||||
return label, nil
|
||||
}
|
||||
|
||||
type Labels []*Label
|
||||
|
||||
// RequireDocker 检查 Labels 是否需要 Docker。
|
||||
func (l Labels) RequireDocker() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeDocker {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PickPlatform 根据 runsOn 列表选择一个平台。
|
||||
func (l Labels) PickPlatform(runsOn []string) string {
|
||||
platforms := make(map[string]string, len(l))
|
||||
for _, label := range l {
|
||||
switch label.Schema {
|
||||
case SchemeDocker:
|
||||
// 忽略 "//"
|
||||
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||
case SchemeHost:
|
||||
platforms[label.Name] = "-self-hosted"
|
||||
default:
|
||||
// 这不应该发生,因为 Parse 已经检查过了。
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, v := range runsOn {
|
||||
if v, ok := platforms[v]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 支持多个标签
|
||||
// 例如:
|
||||
// ["debian-12"] => "debian:12"
|
||||
// ["with-gpu"] => "linux:with-gpu"
|
||||
// ["debian-12", "with-gpu"] => "debian:12_with-gpu"
|
||||
|
||||
// 返回默认值。
|
||||
// 因此,当运行器收到一个它没有的标签的任务时,
|
||||
// 这发生在用户在 Web UI 中编辑了运行器的标签时。
|
||||
// TODO: 这可能不正确,如果运行器仅作为主机模式使用呢?
|
||||
return "lcr.loongnix.cn/library/debian:latest"
|
||||
}
|
||||
|
||||
// Names 返回所有标签的名称。
|
||||
func (l Labels) Names() []string {
|
||||
names := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
names = append(names, label.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ToStrings 将 Labels 转换为字符串数组。
|
||||
func (l Labels) ToStrings() []string {
|
||||
ls := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
lbl := label.Name
|
||||
if label.Schema != "" {
|
||||
lbl += ":" + label.Schema
|
||||
if label.Arg != "" {
|
||||
lbl += ":" + label.Arg
|
||||
}
|
||||
}
|
||||
ls = append(ls, lbl)
|
||||
}
|
||||
return ls
|
||||
}
|
63
internal/pkg/labels/labels_test.go
Normal file
63
internal/pkg/labels/labels_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func Test解析(t *testing.T) {
|
||||
tests := []struct {
|
||||
args string
|
||||
want *Label
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
args: "debian:docker://node:18",
|
||||
want: &Label{
|
||||
Name: "debian",
|
||||
Schema: "docker",
|
||||
Arg: "//node:18",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "aosc:host",
|
||||
want: &Label{
|
||||
Name: "aosc",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "aosc",
|
||||
want: &Label{
|
||||
Name: "aosc",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "debian:vm:debian12",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
got, err := Parse(tt.args)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.DeepEqual(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
451
internal/pkg/report/reporter.go
Normal file
451
internal/pkg/report/reporter.go
Normal file
@ -0,0 +1,451 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Reporter 组件是负责任务执行状态报告和日志记录的核心模块
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"git.whlug.cn/LAA/loong_runner/internal/pkg/client"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
ctx context.Context // 上下文,用于控制生命周期
|
||||
cancel context.CancelFunc // 取消函数
|
||||
closed bool // 是否已关闭
|
||||
client client.Client // Gitea 客户端
|
||||
clientM sync.Mutex // 客户端访问互斥锁
|
||||
|
||||
logOffset int // 日志偏移量
|
||||
logRows []*runnerv1.LogRow // 日志行缓存
|
||||
logReplacer *strings.Replacer // 日志内容替换器
|
||||
oldnew []string // 需要替换的敏感信息
|
||||
|
||||
state *runnerv1.TaskState // 任务状态
|
||||
stateMu sync.RWMutex // 状态访问读写锁
|
||||
outputs sync.Map // 输出参数存储
|
||||
|
||||
debugOutputEnabled bool // 是否启用调试输出
|
||||
stopCommandEndToken string // 停止命令结束标记
|
||||
}
|
||||
|
||||
// NewReporter 构造函数 初始化日志脱敏规则、状态等信息
|
||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
if v := task.Context.Fields["gitea_runtime_token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
for _, v := range task.Secrets {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
|
||||
rv := &Reporter{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
client: client,
|
||||
oldnew: oldnew,
|
||||
logReplacer: strings.NewReplacer(oldnew...),
|
||||
state: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
},
|
||||
}
|
||||
|
||||
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||
rv.debugOutputEnabled = true
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
// ResetSteps 重置任务状态中的步骤状态数组,为即将开始的新任务做准备
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
for i := 0; i < l; i++ {
|
||||
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
||||
Id: int64(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Levels 返回所有支持的日志级别(通常是为了兼容 logger 接口)
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
|
||||
// appendIfNotNil 通用函数,如果传入指针不为空,则将其添加到切片中。
|
||||
func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||
if v != nil {
|
||||
return append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 接收日志条目,处理日志内容,并更新任务状态。
|
||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
log.WithFields(entry.Data).Trace(entry.Message)
|
||||
|
||||
timestamp := entry.Time
|
||||
if r.state.StartedAt == nil {
|
||||
r.state.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
|
||||
stage := entry.Data["stage"]
|
||||
|
||||
if stage != "Main" {
|
||||
if v, ok := entry.Data["jobResult"]; ok {
|
||||
if jobResult, ok := r.parseResult(v); ok {
|
||||
r.state.Result = jobResult
|
||||
r.state.StoppedAt = timestamppb.New(timestamp)
|
||||
for _, s := range r.state.Steps {
|
||||
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
s.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
if jobResult == runnerv1.Result_RESULT_SKIPPED {
|
||||
s.Result = runnerv1.Result_RESULT_SKIPPED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var step *runnerv1.StepState
|
||||
if v, ok := entry.Data["stepNumber"]; ok {
|
||||
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
||||
step = r.state.Steps[v]
|
||||
}
|
||||
}
|
||||
if step == nil {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if step.StartedAt == nil {
|
||||
step.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
if v, ok := entry.Data["raw_output"]; ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if row := r.parseLogRow(entry); row != nil {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.logRows = append(r.logRows, row)
|
||||
}
|
||||
}
|
||||
} else if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
if v, ok := entry.Data["stepResult"]; ok {
|
||||
if stepResult, ok := r.parseResult(v); ok {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.Result = stepResult
|
||||
step.StoppedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunDaemon 定时运行后台任务,定期将缓存中的日志和状态上报给 Gitea
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
if r.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ReportLog(false)
|
||||
_ = r.ReportState()
|
||||
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
}
|
||||
|
||||
// 自定义日志记录方法,格式化输出并保存到 logRows 中
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
r.logf(format, a...)
|
||||
}
|
||||
|
||||
// 内部使用的日志记录方法,仅在任务执行期间记录日志
|
||||
func (r *Reporter) logf(format string, a ...interface{}) {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: fmt.Sprintf(format, a...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SetOutputs 设置输出参数,过滤无效或超限的键值对
|
||||
func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
for k, v := range outputs {
|
||||
if len(k) > 255 {
|
||||
r.logf("忽略超长键值对的键: %q", k)
|
||||
continue
|
||||
}
|
||||
if l := len(v); l > 1024*1024 {
|
||||
log.Println("忽略超长键值对的值:", k, l)
|
||||
r.logf("忽略超长键值对的值 %q: %d 字节", k, l)
|
||||
}
|
||||
if _, ok := r.outputs.Load(k); ok {
|
||||
continue
|
||||
}
|
||||
r.outputs.Store(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭 reporter,确保最后一批日志和最终状态被上报
|
||||
func (r *Reporter) Close(lastWords string) error {
|
||||
r.closed = true
|
||||
|
||||
r.stateMu.Lock()
|
||||
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
if lastWords == "" {
|
||||
lastWords = "Early termination"
|
||||
}
|
||||
for _, v := range r.state.Steps {
|
||||
if v.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
v.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
}
|
||||
}
|
||||
r.state.Result = runnerv1.Result_RESULT_FAILURE
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
r.state.StoppedAt = timestamppb.Now()
|
||||
} else if lastWords != "" {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
}
|
||||
r.stateMu.Unlock()
|
||||
|
||||
return retry.Do(func() error {
|
||||
if err := r.ReportLog(true); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.ReportState()
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
// ReportLog 将当前缓存的日志批量上报到 Gitea
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateMu.RLock()
|
||||
rows := r.logRows
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: r.state.Id,
|
||||
Index: int64(r.logOffset),
|
||||
Rows: rows,
|
||||
NoMore: noMore,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack := int(resp.Msg.AckIndex)
|
||||
if ack < r.logOffset {
|
||||
return fmt.Errorf("已提交的日志丢失了")
|
||||
}
|
||||
|
||||
r.stateMu.Lock()
|
||||
r.logRows = r.logRows[ack-r.logOffset:]
|
||||
r.logOffset = ack
|
||||
r.stateMu.Unlock()
|
||||
|
||||
if noMore && ack < r.logOffset+len(rows) {
|
||||
return fmt.Errorf("并非所有日志都已提交")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportState 将当前任务状态上报到 Gitea
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateMu.RLock()
|
||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
outputs := make(map[string]string)
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if val, ok := v.(string); ok {
|
||||
outputs[k.(string)] = val
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: state,
|
||||
Outputs: outputs,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range resp.Msg.SentOutputs {
|
||||
r.outputs.Store(k, struct{}{})
|
||||
}
|
||||
|
||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
var noSent []string
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if _, ok := v.(string); ok {
|
||||
noSent = append(noSent, k.(string))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(noSent) > 0 {
|
||||
return fmt.Errorf("仍有一些输出尚未发送: %v", noSent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// duringSteps 判断当前是否正在执行某个步骤
|
||||
func (r *Reporter) duringSteps() bool {
|
||||
if steps := r.state.Steps; len(steps) == 0 {
|
||||
return false
|
||||
} else if first := steps[0]; first.Result == runnerv1.Result_RESULT_UNSPECIFIED && first.LogLength == 0 {
|
||||
return false
|
||||
} else if last := steps[len(steps)-1]; last.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var stringToResult = map[string]runnerv1.Result{
|
||||
"success": runnerv1.Result_RESULT_SUCCESS,
|
||||
"failure": runnerv1.Result_RESULT_FAILURE,
|
||||
"skipped": runnerv1.Result_RESULT_SKIPPED,
|
||||
"cancelled": runnerv1.Result_RESULT_CANCELLED,
|
||||
}
|
||||
|
||||
// parseResult 将字符串或 Stringer 类型的结果解析为 runnerv1.Result 类型
|
||||
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
str := ""
|
||||
if v, ok := result.(string); ok { // 对于作业结果
|
||||
str = v
|
||||
} else if v, ok := result.(fmt.Stringer); ok { // 对于步骤结果
|
||||
str = v.String()
|
||||
}
|
||||
|
||||
ret, ok := stringToResult[str]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
// 匹配 GitHub Actions/Gitea Actions 特殊命令,如 ::set-output::, ::add-mask:: 等
|
||||
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||
|
||||
// handleCommand 处理日志中的特殊指令命令(如 ::add-mask::),根据命令执行对应操作
|
||||
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "add-mask":
|
||||
r.addMask(value)
|
||||
return nil
|
||||
case "debug":
|
||||
if r.debugOutputEnabled {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
|
||||
case "notice":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "warning":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "error":
|
||||
// 尚未实现,因此仅返回原始内容。
|
||||
return &originalContent
|
||||
case "group":
|
||||
// 返回原始内容,因为我认为前端在渲染输出时会使用它。
|
||||
return &originalContent
|
||||
case "endgroup":
|
||||
// 同上
|
||||
return &originalContent
|
||||
case "stop-commands":
|
||||
r.stopCommandEndToken = value
|
||||
return nil
|
||||
case r.stopCommandEndToken:
|
||||
r.stopCommandEndToken = ""
|
||||
return nil
|
||||
}
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
// parseLogRow 将日志条目转换为 LogRow 结构,用于后续上报
|
||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
|
||||
matches := cmdRegex.FindStringSubmatch(content)
|
||||
if matches != nil {
|
||||
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
|
||||
content = *output
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
content = r.logReplacer.Replace(content)
|
||||
|
||||
return &runnerv1.LogRow{
|
||||
Time: timestamppb.New(entry.Time),
|
||||
Content: strings.ToValidUTF8(content, "?"),
|
||||
}
|
||||
}
|
||||
|
||||
// addMask 将指定字符串加入脱敏列表,之后所有日志中出现该字符串都会被替换成 ***
|
||||
func (r *Reporter) addMask(msg string) {
|
||||
r.oldnew = append(r.oldnew, msg, "***")
|
||||
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
||||
}
|
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, "普通日志数量不符预期")
|
||||
})
|
||||
}
|
11
internal/pkg/ver/version.go
Normal file
11
internal/pkg/ver/version.go
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ver
|
||||
|
||||
// go build -ldflags "-X git.whlug.cn/LAA/loong_runner/internal/pkg/ver.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
func Version() string {
|
||||
return version
|
||||
}
|
29
main.go
29
main.go
@ -1,34 +1,19 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitea.com/gitea/act_runner/cmd"
|
||||
"git.whlug.cn/LAA/loong_runner/internal/app/cmd"
|
||||
)
|
||||
|
||||
func withContextFunc(ctx context.Context, f func()) context.Context {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(c)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-c:
|
||||
cancel()
|
||||
f()
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := withContextFunc(context.Background(), func() {})
|
||||
// run the command
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// 运行命令
|
||||
cmd.Execute(ctx)
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
package poller
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
// Metric interface
|
||||
type Metric interface {
|
||||
IncBusyWorker() int64
|
||||
DecBusyWorker() int64
|
||||
BusyWorkers() int64
|
||||
}
|
||||
|
||||
var _ Metric = (*metric)(nil)
|
||||
|
||||
type metric struct {
|
||||
busyWorkers int64
|
||||
}
|
||||
|
||||
// NewMetric for default metric structure
|
||||
func NewMetric() Metric {
|
||||
return &metric{}
|
||||
}
|
||||
|
||||
func (m *metric) IncBusyWorker() int64 {
|
||||
return atomic.AddInt64(&m.busyWorkers, 1)
|
||||
}
|
||||
|
||||
func (m *metric) DecBusyWorker() int64 {
|
||||
return atomic.AddInt64(&m.busyWorkers, -1)
|
||||
}
|
||||
|
||||
func (m *metric) BusyWorkers() int64 {
|
||||
return atomic.LoadInt64(&m.busyWorkers)
|
||||
}
|
146
poller/poller.go
146
poller/poller.go
@ -1,146 +0,0 @@
|
||||
package poller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var ErrDataLock = errors.New("Data Lock Error")
|
||||
|
||||
func New(cli client.Client, dispatch func(context.Context, *runnerv1.Task) error, workerNum int) *Poller {
|
||||
return &Poller{
|
||||
Client: cli,
|
||||
Dispatch: dispatch,
|
||||
routineGroup: newRoutineGroup(),
|
||||
metric: &metric{},
|
||||
workerNum: workerNum,
|
||||
ready: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
type Poller struct {
|
||||
Client client.Client
|
||||
Dispatch func(context.Context, *runnerv1.Task) error
|
||||
|
||||
sync.Mutex
|
||||
routineGroup *routineGroup
|
||||
metric *metric
|
||||
ready chan struct{}
|
||||
workerNum int
|
||||
}
|
||||
|
||||
func (p *Poller) schedule() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
if int(p.metric.BusyWorkers()) >= p.workerNum {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case p.ready <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) Wait() {
|
||||
p.routineGroup.Wait()
|
||||
}
|
||||
|
||||
func (p *Poller) Poll(ctx context.Context) error {
|
||||
l := log.WithField("func", "Poll")
|
||||
|
||||
for {
|
||||
// check worker number
|
||||
p.schedule()
|
||||
|
||||
select {
|
||||
// wait worker ready
|
||||
case <-p.ready:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break LOOP
|
||||
default:
|
||||
task, err := p.pollTask(ctx)
|
||||
if task == nil || err != nil {
|
||||
if err != nil {
|
||||
l.Errorf("can't find the task: %v", err.Error())
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
break
|
||||
}
|
||||
|
||||
p.metric.IncBusyWorker()
|
||||
p.routineGroup.Run(func() {
|
||||
defer p.schedule()
|
||||
defer p.metric.DecBusyWorker()
|
||||
if err := p.dispatchTask(ctx, task); err != nil {
|
||||
l.Errorf("execute task: %v", err.Error())
|
||||
}
|
||||
})
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) pollTask(ctx context.Context) (*runnerv1.Task, error) {
|
||||
l := log.WithField("func", "pollTask")
|
||||
l.Info("poller: request stage from remote server")
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// request a new build stage for execution from the central
|
||||
// build server.
|
||||
resp, err := p.Client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
l.WithError(err).Trace("poller: no stage returned")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil && err == ErrDataLock {
|
||||
l.WithError(err).Info("task accepted by another runner")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot accept task")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exit if a nil or empty stage is returned from the system
|
||||
// and allow the runner to retry.
|
||||
if resp.Msg.Task == nil || resp.Msg.Task.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return resp.Msg.Task, nil
|
||||
}
|
||||
|
||||
func (p *Poller) dispatchTask(ctx context.Context, task *runnerv1.Task) error {
|
||||
l := log.WithField("func", "dispatchTask")
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
l.Errorf("panic error: %v", e)
|
||||
}
|
||||
}()
|
||||
|
||||
runCtx, cancel := context.WithTimeout(ctx, time.Hour)
|
||||
defer cancel()
|
||||
|
||||
return p.Dispatch(runCtx, task)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package poller
|
||||
|
||||
import "sync"
|
||||
|
||||
type routineGroup struct {
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func newRoutineGroup() *routineGroup {
|
||||
return new(routineGroup)
|
||||
}
|
||||
|
||||
func (g *routineGroup) Run(fn func()) {
|
||||
g.waitGroup.Add(1)
|
||||
|
||||
go func() {
|
||||
defer g.waitGroup.Done()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *routineGroup) Wait() {
|
||||
g.waitGroup.Wait()
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package register
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
"gitea.com/gitea/act_runner/config"
|
||||
"gitea.com/gitea/act_runner/core"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func New(cli client.Client) *Register {
|
||||
return &Register{
|
||||
Client: cli,
|
||||
}
|
||||
}
|
||||
|
||||
type Register struct {
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
func (p *Register) Register(ctx context.Context, cfg config.Runner) (*core.Runner, error) {
|
||||
labels := make([]string, len(cfg.Labels))
|
||||
for i, v := range cfg.Labels {
|
||||
labels[i] = strings.SplitN(v, ":", 2)[0]
|
||||
}
|
||||
// register new runner.
|
||||
resp, err := p.Client.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: cfg.Name,
|
||||
Token: cfg.Token,
|
||||
AgentLabels: labels,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &core.Runner{
|
||||
ID: resp.Msg.Runner.Id,
|
||||
UUID: resp.Msg.Runner.Uuid,
|
||||
Name: resp.Msg.Runner.Name,
|
||||
Token: resp.Msg.Runner.Token,
|
||||
Address: p.Client.Address(),
|
||||
Insecure: strconv.FormatBool(p.Client.Insecure()),
|
||||
Labels: cfg.Labels,
|
||||
}
|
||||
|
||||
file, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot marshal the json input")
|
||||
return data, err
|
||||
}
|
||||
|
||||
// store runner config in .runner file
|
||||
return data, os.WriteFile(cfg.File, file, 0o644)
|
||||
}
|
6
renovate.json5
Normal file
6
renovate.json5
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>gitea/renovate-config"
|
||||
]
|
||||
}
|
@ -1,294 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
|
||||
retry "github.com/avast/retry-go/v4"
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
closed bool
|
||||
client client.Client
|
||||
clientM sync.Mutex
|
||||
|
||||
logOffset int
|
||||
logRows []*runnerv1.LogRow
|
||||
logReplacer *strings.Replacer
|
||||
state *runnerv1.TaskState
|
||||
stateM sync.RWMutex
|
||||
}
|
||||
|
||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
for _, v := range task.Secrets {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
|
||||
return &Reporter{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
client: client,
|
||||
logReplacer: strings.NewReplacer(oldnew...),
|
||||
state: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
for i := 0; i < l; i++ {
|
||||
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
||||
Id: int64(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
|
||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
|
||||
log.WithFields(entry.Data).Trace(entry.Message)
|
||||
|
||||
timestamp := entry.Time
|
||||
if r.state.StartedAt == nil {
|
||||
r.state.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
|
||||
stage := entry.Data["stage"]
|
||||
|
||||
if stage != "Main" {
|
||||
if v, ok := entry.Data["jobResult"]; ok {
|
||||
if jobResult, ok := r.parseResult(v); ok {
|
||||
r.state.Result = jobResult
|
||||
r.state.StoppedAt = timestamppb.New(timestamp)
|
||||
for _, s := range r.state.Steps {
|
||||
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
s.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var step *runnerv1.StepState
|
||||
if v, ok := entry.Data["stepNumber"]; ok {
|
||||
if v, ok := v.(int); ok {
|
||||
step = r.state.Steps[v]
|
||||
}
|
||||
}
|
||||
if step == nil {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if step.StartedAt == nil {
|
||||
step.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
if v, ok := entry.Data["raw_output"]; ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
} else if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
if v, ok := entry.Data["stepResult"]; ok {
|
||||
if stepResult, ok := r.parseResult(v); ok {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.Result = stepResult
|
||||
step.StoppedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
if r.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ReportLog(false)
|
||||
_ = r.ReportState()
|
||||
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
}
|
||||
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateM.Lock()
|
||||
defer r.stateM.Unlock()
|
||||
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: fmt.Sprintf(format, a...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Close(lastWords string) error {
|
||||
r.closed = true
|
||||
|
||||
r.stateM.Lock()
|
||||
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
if lastWords == "" {
|
||||
lastWords = "Early termination"
|
||||
}
|
||||
for _, v := range r.state.Steps {
|
||||
if v.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
v.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
}
|
||||
}
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
return nil
|
||||
} else if lastWords != "" {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
}
|
||||
r.stateM.Unlock()
|
||||
|
||||
return retry.Do(func() error {
|
||||
if err := r.ReportLog(true); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.ReportState()
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateM.RLock()
|
||||
rows := r.logRows
|
||||
r.stateM.RUnlock()
|
||||
|
||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: r.state.Id,
|
||||
Index: int64(r.logOffset),
|
||||
Rows: rows,
|
||||
NoMore: noMore,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack := int(resp.Msg.AckIndex)
|
||||
if ack < r.logOffset {
|
||||
return fmt.Errorf("submitted logs are lost")
|
||||
}
|
||||
|
||||
r.stateM.Lock()
|
||||
r.logRows = r.logRows[ack-r.logOffset:]
|
||||
r.logOffset = ack
|
||||
r.stateM.Unlock()
|
||||
|
||||
if noMore && ack < r.logOffset+len(rows) {
|
||||
return fmt.Errorf("not all logs are submitted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateM.RLock()
|
||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||
r.stateM.RUnlock()
|
||||
|
||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: state,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) duringSteps() bool {
|
||||
if steps := r.state.Steps; len(steps) == 0 {
|
||||
return false
|
||||
} else if first := steps[0]; first.Result == runnerv1.Result_RESULT_UNSPECIFIED && first.LogLength == 0 {
|
||||
return false
|
||||
} else if last := steps[len(steps)-1]; last.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var stringToResult = map[string]runnerv1.Result{
|
||||
"success": runnerv1.Result_RESULT_SUCCESS,
|
||||
"failure": runnerv1.Result_RESULT_FAILURE,
|
||||
"skipped": runnerv1.Result_RESULT_SKIPPED,
|
||||
"cancelled": runnerv1.Result_RESULT_CANCELLED,
|
||||
}
|
||||
|
||||
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
str := ""
|
||||
if v, ok := result.(string); ok { // for jobResult
|
||||
str = v
|
||||
} else if v, ok := result.(fmt.Stringer); ok { // for stepResult
|
||||
str = v.String()
|
||||
}
|
||||
|
||||
ret, ok := stringToResult[str]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
content = r.logReplacer.Replace(content)
|
||||
return &runnerv1.LogRow{
|
||||
Time: timestamppb.New(entry.Time),
|
||||
Content: content,
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
)
|
||||
|
||||
// Runner runs the pipeline.
|
||||
type Runner struct {
|
||||
Machine string
|
||||
ForgeInstance string
|
||||
Environ map[string]string
|
||||
Client client.Client
|
||||
Labels []string
|
||||
}
|
||||
|
||||
// Run runs the pipeline stage.
|
||||
func (s *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
return NewTask(s.ForgeInstance, task.Id, s.Client, s.Environ, s.platformPicker).Run(ctx, task)
|
||||
}
|
||||
|
||||
func (s *Runner) platformPicker(labels []string) string {
|
||||
// "ubuntu-18.04:docker://node:16-buster"
|
||||
// "self-hosted"
|
||||
|
||||
platforms := make(map[string]string, len(labels))
|
||||
for _, l := range s.Labels {
|
||||
// "ubuntu-18.04:docker://node:16-buster"
|
||||
splits := strings.SplitN(l, ":", 2)
|
||||
if len(splits) == 1 {
|
||||
// identifier for non docker execution environment
|
||||
platforms[splits[0]] = "-self-hosted"
|
||||
continue
|
||||
}
|
||||
// ["ubuntu-18.04", "docker://node:16-buster"]
|
||||
k, v := splits[0], splits[1]
|
||||
|
||||
if prefix := "docker://"; !strings.HasPrefix(v, prefix) {
|
||||
continue
|
||||
} else {
|
||||
v = strings.TrimPrefix(v, prefix)
|
||||
}
|
||||
// ubuntu-18.04 => node:16-buster
|
||||
platforms[k] = v
|
||||
}
|
||||
|
||||
for _, label := range labels {
|
||||
if v, ok := platforms[label]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support multiple labels
|
||||
// like:
|
||||
// ["ubuntu-22.04"] => "ubuntu:22.04"
|
||||
// ["with-gpu"] => "linux:with-gpu"
|
||||
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
|
||||
|
||||
// return default
|
||||
return "node:16-bullseye"
|
||||
}
|
265
runtime/task.go
265
runtime/task.go
@ -1,265 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"gitea.com/gitea/act_runner/client"
|
||||
|
||||
"github.com/nektos/act/pkg/artifacts"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var globalTaskMap sync.Map
|
||||
|
||||
type TaskInput struct {
|
||||
repoDirectory string
|
||||
// actor string
|
||||
// workdir string
|
||||
// workflowsPath string
|
||||
// autodetectEvent bool
|
||||
// eventPath string
|
||||
// reuseContainers bool
|
||||
// bindWorkdir bool
|
||||
// secrets []string
|
||||
envs map[string]string
|
||||
// platforms []string
|
||||
// dryrun bool
|
||||
forcePull bool
|
||||
forceRebuild bool
|
||||
// noOutput bool
|
||||
// envfile string
|
||||
// secretfile string
|
||||
insecureSecrets bool
|
||||
// defaultBranch string
|
||||
privileged bool
|
||||
usernsMode string
|
||||
containerArchitecture string
|
||||
containerDaemonSocket string
|
||||
// noWorkflowRecurse bool
|
||||
useGitIgnore bool
|
||||
containerCapAdd []string
|
||||
containerCapDrop []string
|
||||
// autoRemove bool
|
||||
artifactServerPath string
|
||||
artifactServerPort string
|
||||
jsonLogger bool
|
||||
// noSkipCheckout bool
|
||||
// remoteName string
|
||||
|
||||
EnvFile string
|
||||
|
||||
containerNetworkMode string
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
BuildID int64
|
||||
Input *TaskInput
|
||||
|
||||
client client.Client
|
||||
log *log.Entry
|
||||
platformPicker func([]string) string
|
||||
}
|
||||
|
||||
// NewTask creates a new task
|
||||
func NewTask(forgeInstance string, buildID int64, client client.Client, runnerEnvs map[string]string, picker func([]string) string) *Task {
|
||||
task := &Task{
|
||||
Input: &TaskInput{
|
||||
envs: runnerEnvs,
|
||||
containerNetworkMode: "bridge", // TODO should be configurable
|
||||
},
|
||||
BuildID: buildID,
|
||||
|
||||
client: client,
|
||||
log: log.WithField("buildID", buildID),
|
||||
platformPicker: picker,
|
||||
}
|
||||
task.Input.repoDirectory, _ = os.Getwd()
|
||||
return task
|
||||
}
|
||||
|
||||
// getWorkflowsPath return the workflows directory, it will try .gitea first and then fallback to .github
|
||||
func getWorkflowsPath(dir string) (string, error) {
|
||||
p := filepath.Join(dir, ".gitea/workflows")
|
||||
_, err := os.Stat(p)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, ".github/workflows"), nil
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func getToken(task *runnerv1.Task) string {
|
||||
token := task.Secrets["GITHUB_TOKEN"]
|
||||
if task.Secrets["GITEA_TOKEN"] != "" {
|
||||
token = task.Secrets["GITEA_TOKEN"]
|
||||
}
|
||||
if task.Context.Fields["token"].GetStringValue() != "" {
|
||||
token = task.Context.Fields["token"].GetStringValue()
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (t *Task) Run(ctx context.Context, task *runnerv1.Task) (lastErr error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
_, exist := globalTaskMap.Load(task.Id)
|
||||
if exist {
|
||||
return fmt.Errorf("task %d already exists", task.Id)
|
||||
}
|
||||
|
||||
// set task ve to global map
|
||||
// when task is done or canceled, it will be removed from the map
|
||||
globalTaskMap.Store(task.Id, t)
|
||||
defer globalTaskMap.Delete(task.Id)
|
||||
|
||||
lastWords := ""
|
||||
reporter := NewReporter(ctx, cancel, t.client, task)
|
||||
defer func() {
|
||||
// set the job to failed on an error return value
|
||||
if lastErr != nil {
|
||||
reporter.Fire(&log.Entry{
|
||||
Data: log.Fields{
|
||||
"jobResult": "failure",
|
||||
},
|
||||
})
|
||||
}
|
||||
_ = reporter.Close(lastWords)
|
||||
}()
|
||||
reporter.RunDaemon()
|
||||
|
||||
reporter.Logf("received task %v of job %v", task.Id, task.Context.Fields["job"].GetStringValue())
|
||||
|
||||
workflowsPath, err := getWorkflowsPath(t.Input.repoDirectory)
|
||||
if err != nil {
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
t.log.Debugf("workflows path: %s", workflowsPath)
|
||||
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||
if err != nil {
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
var plan *model.Plan
|
||||
jobIDs := workflow.GetJobIDs()
|
||||
if len(jobIDs) != 1 {
|
||||
err := fmt.Errorf("multiple jobs found: %v", jobIDs)
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
jobID := jobIDs[0]
|
||||
plan = model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||
job := workflow.GetJob(jobID)
|
||||
reporter.ResetSteps(len(job.Steps))
|
||||
|
||||
log.Infof("plan: %+v", plan.Stages[0].Runs)
|
||||
|
||||
token := getToken(task)
|
||||
dataContext := task.Context.Fields
|
||||
|
||||
log.Infof("task %v repo is %v %v %v", task.Id, dataContext["repository"].GetStringValue(),
|
||||
dataContext["gitea_default_actions_url"].GetStringValue(),
|
||||
t.client.Address())
|
||||
|
||||
preset := &model.GithubContext{
|
||||
Event: dataContext["event"].GetStructValue().AsMap(),
|
||||
RunID: dataContext["run_id"].GetStringValue(),
|
||||
RunNumber: dataContext["run_number"].GetStringValue(),
|
||||
Actor: dataContext["actor"].GetStringValue(),
|
||||
Repository: dataContext["repository"].GetStringValue(),
|
||||
EventName: dataContext["event_name"].GetStringValue(),
|
||||
Sha: dataContext["sha"].GetStringValue(),
|
||||
Ref: dataContext["ref"].GetStringValue(),
|
||||
RefName: dataContext["ref_name"].GetStringValue(),
|
||||
RefType: dataContext["ref_type"].GetStringValue(),
|
||||
HeadRef: dataContext["head_ref"].GetStringValue(),
|
||||
BaseRef: dataContext["base_ref"].GetStringValue(),
|
||||
Token: token,
|
||||
RepositoryOwner: dataContext["repository_owner"].GetStringValue(),
|
||||
RetentionDays: dataContext["retention_days"].GetStringValue(),
|
||||
}
|
||||
eventJSON, err := json.Marshal(preset.Event)
|
||||
if err != nil {
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
maxLifetime := 3 * time.Hour
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
input := t.Input
|
||||
config := &runner.Config{
|
||||
Workdir: "/" + preset.Repository,
|
||||
BindWorkdir: false,
|
||||
ReuseContainers: false,
|
||||
ForcePull: input.forcePull,
|
||||
ForceRebuild: input.forceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: input.jsonLogger,
|
||||
Env: input.envs,
|
||||
Secrets: task.Secrets,
|
||||
InsecureSecrets: input.insecureSecrets,
|
||||
Privileged: input.privileged,
|
||||
UsernsMode: input.usernsMode,
|
||||
ContainerArchitecture: input.containerArchitecture,
|
||||
ContainerDaemonSocket: input.containerDaemonSocket,
|
||||
UseGitIgnore: input.useGitIgnore,
|
||||
GitHubInstance: t.client.Address(),
|
||||
ContainerCapAdd: input.containerCapAdd,
|
||||
ContainerCapDrop: input.containerCapDrop,
|
||||
AutoRemove: true,
|
||||
ArtifactServerPath: input.artifactServerPath,
|
||||
ArtifactServerPort: input.artifactServerPort,
|
||||
NoSkipCheckout: true,
|
||||
PresetGitHubContext: preset,
|
||||
EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: input.containerNetworkMode,
|
||||
DefaultActionInstance: dataContext["gitea_default_actions_url"].GetStringValue(),
|
||||
PlatformPicker: t.platformPicker,
|
||||
}
|
||||
r, err := runner.New(config)
|
||||
if err != nil {
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
artifactCancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort)
|
||||
t.log.Debugf("artifacts server started at %s:%s", input.artifactServerPath, input.artifactServerPort)
|
||||
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
artifactCancel()
|
||||
return nil
|
||||
})
|
||||
|
||||
t.log.Infof("workflow prepared")
|
||||
reporter.Logf("workflow prepared")
|
||||
|
||||
// add logger recorders
|
||||
ctx = common.WithLoggerHook(ctx, reporter)
|
||||
|
||||
if err := executor(ctx); err != nil {
|
||||
lastWords = err.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
70
scripts/run.sh
Executable file
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