mirror of
https://gitea.com/gitea/act_runner.git
synced 2025-06-15 02:57:12 +02:00
Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -9,14 +9,22 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: https://github.com/goreleaser/goreleaser-action@v4
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.20.1'
|
||||||
|
- name: goreleaser
|
||||||
|
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: latest
|
version: latest
|
||||||
args: release --nightly --clean
|
args: release --nightly
|
||||||
env:
|
env:
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
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 }}
|
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||||
AWS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
|
41
.gitea/workflows/release-tag.yml
Normal file
41
.gitea/workflows/release-tag.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.20.1'
|
||||||
|
- name: Import GPG key
|
||||||
|
id: import_gpg
|
||||||
|
uses: https://github.com/crazy-max/ghaction-import-gpg@v5
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
|
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||||
|
- name: goreleaser
|
||||||
|
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: latest
|
||||||
|
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 }}
|
@ -5,12 +5,32 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GOPROXY: https://goproxy.io,direct
|
GOPROXY: https://goproxy.io,direct
|
||||||
|
GOPATH: /go_path
|
||||||
|
GOCACHE: /go_cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: check and test
|
name: check and test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: cache go path
|
||||||
|
id: cache-go-path
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /go_path
|
||||||
|
key: go_path-${{ github.repository }}-${{ github.ref_name }}
|
||||||
|
restore-keys: |
|
||||||
|
go_path-${{ github.repository }}-
|
||||||
|
go_path-
|
||||||
|
- name: cache go cache
|
||||||
|
id: cache-go-cache
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /go_cache
|
||||||
|
key: go_cache-${{ github.repository }}-${{ github.ref_name }}
|
||||||
|
restore-keys: |
|
||||||
|
go_cache-${{ github.repository }}-
|
||||||
|
go_cache-
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.20
|
go-version: 1.20
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,4 +1,10 @@
|
|||||||
act_runner
|
act_runner
|
||||||
.env
|
.env
|
||||||
.runner
|
.runner
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
/gitea-vet
|
||||||
|
/config.yaml
|
||||||
|
|
||||||
|
# MS VSCode
|
||||||
|
.vscode
|
||||||
|
__debug_bin
|
||||||
|
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
|
@ -14,8 +14,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- s390x
|
|
||||||
- ppc64le
|
|
||||||
goarm:
|
goarm:
|
||||||
- "5"
|
- "5"
|
||||||
- "6"
|
- "6"
|
||||||
@ -40,6 +38,8 @@ builds:
|
|||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: "7"
|
goarm: "7"
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
@ -53,14 +53,15 @@ builds:
|
|||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: "7"
|
goarm: "7"
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
||||||
binary: >-
|
binary: >-
|
||||||
{{ .ProjectName }}-
|
{{ .ProjectName }}-
|
||||||
{{- if .IsSnapshot }}{{ .Branch }}-
|
{{- .Version }}-
|
||||||
{{- else }}{{- .Version }}-{{ end }}
|
|
||||||
{{- .Os }}-
|
{{- .Os }}-
|
||||||
{{- if eq .Arch "amd64" }}amd64
|
{{- if eq .Arch "amd64" }}amd64
|
||||||
{{- else if eq .Arch "amd64_v1" }}amd64
|
{{- else if eq .Arch "amd64_v1" }}amd64
|
||||||
@ -68,6 +69,13 @@ builds:
|
|||||||
{{- else }}{{ .Arch }}{{ end }}
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
{{- if .Arm }}-{{ .Arm }}{{ end }}
|
{{- if .Arm }}-{{ .Arm }}{{ end }}
|
||||||
no_unique_dist_dir: true
|
no_unique_dist_dir: true
|
||||||
|
hooks:
|
||||||
|
post:
|
||||||
|
- cmd: tar -cJf {{ .Path }}.xz {{ .Path }}
|
||||||
|
env:
|
||||||
|
- XZ_OPT=-9
|
||||||
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||||
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||||
|
|
||||||
blobs:
|
blobs:
|
||||||
-
|
-
|
||||||
@ -75,6 +83,9 @@ blobs:
|
|||||||
bucket: "{{ .Env.S3_BUCKET }}"
|
bucket: "{{ .Env.S3_BUCKET }}"
|
||||||
region: "{{ .Env.S3_REGION }}"
|
region: "{{ .Env.S3_REGION }}"
|
||||||
folder: "act_runner/{{.Version}}"
|
folder: "act_runner/{{.Version}}"
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
- glob: ./**.sha256
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: binary
|
- format: binary
|
||||||
@ -83,10 +94,18 @@ archives:
|
|||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ incpatch .Version }}"
|
name_template: "{{ .Branch }}-devel"
|
||||||
|
|
||||||
nightly:
|
nightly:
|
||||||
publish_release: false
|
name_template: "{{ .Branch }}"
|
||||||
name_template: "{{ .Branch }}"
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://gitea.com/api/v1
|
||||||
|
download: https://gitea.com
|
||||||
|
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:alpine as builder
|
||||||
|
RUN apk add --update-cache make git
|
||||||
|
|
||||||
|
COPY . /opt/src/act_runner
|
||||||
|
WORKDIR /opt/src/act_runner
|
||||||
|
|
||||||
|
RUN make clean && make build
|
||||||
|
|
||||||
|
FROM alpine as runner
|
||||||
|
RUN apk add --update-cache \
|
||||||
|
git bash \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||||
|
COPY run.sh /opt/act/run.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/opt/act/run.sh"]
|
38
Makefile
38
Makefile
@ -13,7 +13,12 @@ GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
|||||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||||
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
||||||
WINDOWS_ARCHS ?= windows/amd64
|
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/act_runner
|
||||||
|
DOCKER_TAG ?= nightly
|
||||||
|
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||||
|
|
||||||
ifneq ($(shell uname), Darwin)
|
ifneq ($(shell uname), Darwin)
|
||||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||||
@ -49,7 +54,7 @@ else
|
|||||||
ifneq ($(DRONE_BRANCH),)
|
ifneq ($(DRONE_BRANCH),)
|
||||||
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
||||||
else
|
else
|
||||||
VERSION ?= master
|
VERSION ?= main
|
||||||
endif
|
endif
|
||||||
|
|
||||||
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
||||||
@ -61,7 +66,7 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
LDFLAGS ?= -X 'main.Version=$(VERSION)'
|
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@ -69,17 +74,24 @@ fmt:
|
|||||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||||
fi
|
fi
|
||||||
$(GOFMT) -w $(GOFILES)
|
$(GOFMT) -w $(GO_FMT_FILES)
|
||||||
|
|
||||||
vet:
|
.PHONY: go-check
|
||||||
$(GO) vet ./...
|
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 requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
.PHONY: fmt-check
|
.PHONY: fmt-check
|
||||||
fmt-check:
|
fmt-check:
|
||||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||||
fi
|
fi
|
||||||
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
|
||||||
if [ -n "$$diff" ]; then \
|
if [ -n "$$diff" ]; then \
|
||||||
echo "Please run 'make fmt' and commit the result:"; \
|
echo "Please run 'make fmt' and commit the result:"; \
|
||||||
echo "$${diff}"; \
|
echo "$${diff}"; \
|
||||||
@ -89,10 +101,16 @@ fmt-check:
|
|||||||
test: fmt-check
|
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 "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
@echo "Running go vet..."
|
||||||
|
@$(GO) build code.gitea.io/gitea-vet
|
||||||
|
@$(GO) vet -vettool=gitea-vet ./...
|
||||||
|
|
||||||
install: $(GOFILES)
|
install: $(GOFILES)
|
||||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||||
|
|
||||||
build: $(EXECUTABLE)
|
build: go-check $(EXECUTABLE)
|
||||||
|
|
||||||
$(EXECUTABLE): $(GOFILES)
|
$(EXECUTABLE): $(GOFILES)
|
||||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
||||||
@ -142,6 +160,10 @@ release-check: | $(DIST_DIRS)
|
|||||||
release-compress: | $(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;
|
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:
|
||||||
|
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(GO) clean -x -i ./...
|
$(GO) clean -x -i ./...
|
||||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
||||||
|
50
README.md
50
README.md
@ -1,19 +1,31 @@
|
|||||||
# act runner
|
# act runner
|
||||||
|
|
||||||
Act runner is a runner for Gitea based on [act](https://gitea.com/gitea/act).
|
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act).
|
||||||
|
|
||||||
## Prerequisites
|
## Installation
|
||||||
|
|
||||||
Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
### Prerequisites
|
||||||
|
|
||||||
## Quickstart
|
Docker Engine Community version is required for docker mode. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
||||||
|
|
||||||
### Build
|
### Download pre-built binary
|
||||||
|
|
||||||
|
Visit https://dl.gitea.com/act_runner/ and download the right version for your platform.
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Build a docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
### Register
|
### Register
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -37,9 +49,9 @@ INFO Enter the Gitea instance URL (for example, https://gitea.com/):
|
|||||||
http://192.168.8.8:3000/
|
http://192.168.8.8:3000/
|
||||||
INFO Enter the runner token:
|
INFO Enter the runner token:
|
||||||
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
||||||
INFO Enter the runner name (if set empty, use hostname:Test.local ):
|
INFO Enter the runner name (if set empty, use hostname: 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 Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):
|
||||||
|
|
||||||
INFO 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].
|
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
|
DEBU Successfully pinged the Gitea instance server
|
||||||
@ -58,4 +70,26 @@ If the registry succeed, it will run immediately. Next time, you could run the r
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner daemon
|
./act_runner daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
You can also configure the runner with a configuration file.
|
||||||
|
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./act_runner generate-config > config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
You can specify the configuration file path with `-c`/`--config` argument.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./act_runner -c config.yaml register # register with config file
|
||||||
|
./act_runner -c config.yaml deamon # run with config file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run a docker container
|
||||||
|
|
||||||
|
```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 --name my_runner gitea/act_runner:nightly
|
||||||
|
```
|
||||||
|
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"
|
||||||
|
)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
95
go.mod
95
go.mod
@ -1,80 +1,111 @@
|
|||||||
module gitea.com/gitea/act_runner
|
module gitea.com/gitea/act_runner
|
||||||
|
|
||||||
go 1.18
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.2.0
|
code.gitea.io/actions-proto-go v0.2.0
|
||||||
|
code.gitea.io/gitea-vet v0.2.3-0.20230113022436-2b1561217fa5
|
||||||
github.com/avast/retry-go/v4 v4.3.1
|
github.com/avast/retry-go/v4 v4.3.1
|
||||||
github.com/bufbuild/connect-go v1.3.1
|
github.com/bufbuild/connect-go v1.3.1
|
||||||
github.com/docker/docker v20.10.21+incompatible
|
github.com/docker/docker v23.0.1+incompatible
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/go-chi/chi/v5 v5.0.8
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/go-chi/render v1.0.2
|
||||||
github.com/mattn/go-isatty v0.0.16
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/mattn/go-isatty v0.0.17
|
||||||
github.com/nektos/act v0.0.0
|
github.com/nektos/act v0.0.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
github.com/stretchr/testify v1.8.1
|
||||||
|
golang.org/x/term v0.6.0
|
||||||
|
golang.org/x/time v0.1.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gotest.tools/v3 v3.4.0
|
||||||
|
modernc.org/sqlite v1.14.2
|
||||||
|
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978
|
||||||
|
xorm.io/xorm v1.3.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Masterminds/semver v1.5.0 // indirect
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // 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/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||||
github.com/containerd/cgroups v1.0.3 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/containerd/containerd v1.6.6 // indirect
|
github.com/containerd/containerd v1.6.18 // indirect
|
||||||
github.com/creack/pty v1.1.18 // indirect
|
github.com/creack/pty v1.1.18 // indirect
|
||||||
github.com/docker/cli v20.10.21+incompatible // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emirpasic/gods v1.12.0 // indirect
|
github.com/emirpasic/gods v1.12.0 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.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-billy/v5 v5.4.1 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/goccy/go-json v0.8.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.15.12 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||||
github.com/moby/buildkit v0.10.6 // indirect
|
github.com/moby/buildkit v0.11.4 // indirect
|
||||||
github.com/moby/sys/mount v0.3.1 // indirect
|
github.com/moby/patternmatcher v0.5.0 // indirect
|
||||||
github.com/moby/sys/mountinfo v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.12.1 // indirect
|
||||||
|
github.com/onsi/gomega v1.10.3 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.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/image-spec v1.1.0-rc2 // indirect
|
||||||
github.com/opencontainers/runc v1.1.2 // indirect
|
github.com/opencontainers/runc v1.1.3 // indirect
|
||||||
github.com/opencontainers/selinux v1.10.2 // indirect
|
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rhysd/actionlint v1.6.22 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.3.4 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
github.com/rhysd/actionlint v1.6.23 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.3 // indirect
|
||||||
github.com/robfig/cron v1.2.0 // indirect
|
github.com/robfig/cron v1.2.0 // indirect
|
||||||
github.com/sergi/go-diff v1.2.0 // indirect
|
github.com/sergi/go-diff v1.2.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
go.opencensus.io v0.23.0 // indirect
|
golang.org/x/crypto v0.2.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
golang.org/x/mod v0.4.2 // indirect
|
||||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
golang.org/x/net v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
golang.org/x/tools v0.1.5 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
lukechampine.com/uint128 v1.1.1 // indirect
|
||||||
|
modernc.org/cc/v3 v3.35.18 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.12.82 // indirect
|
||||||
|
modernc.org/libc v1.11.87 // indirect
|
||||||
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
|
modernc.org/memory v1.0.5 // indirect
|
||||||
|
modernc.org/opt v0.1.1 // indirect
|
||||||
|
modernc.org/strutil v1.1.1 // indirect
|
||||||
|
modernc.org/token v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/nektos/act => gitea.com/gitea/act v0.234.3-0.20230224062034-1252e551b867
|
replace github.com/nektos/act => gitea.com/gitea/act v0.243.2-0.20230323041428-929ea6df751b
|
||||||
|
12
internal/app/artifactcache/doc.go
Normal file
12
internal/app/artifactcache/doc.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package artifactcache provides a cache handler for the runner.
|
||||||
|
//
|
||||||
|
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
|
||||||
|
//
|
||||||
|
// TODO: Authorization
|
||||||
|
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
|
||||||
|
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||||
|
|
||||||
|
package artifactcache
|
416
internal/app/artifactcache/handler.go
Normal file
416
internal/app/artifactcache/handler.go
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
urlBase = "/_apis/artifactcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = log.StandardLogger().WithField("module", "cache_request")
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
engine engine
|
||||||
|
storage *Storage
|
||||||
|
router *chi.Mux
|
||||||
|
listener net.Listener
|
||||||
|
|
||||||
|
gc atomic.Bool
|
||||||
|
gcAt time.Time
|
||||||
|
|
||||||
|
outboundIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartHandler(dir, outboundIP string, port uint16) (*Handler, error) {
|
||||||
|
h := &Handler{}
|
||||||
|
|
||||||
|
if dir == "" {
|
||||||
|
if home, err := os.UserHomeDir(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
dir = filepath.Join(home, ".cache", "actcache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := xorm.NewEngine("sqlite", filepath.Join(dir, "sqlite.db"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := e.Sync(&Cache{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.engine = engine{e: e}
|
||||||
|
|
||||||
|
storage, err := NewStorage(filepath.Join(dir, "cache"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.storage = storage
|
||||||
|
|
||||||
|
if outboundIP != "" {
|
||||||
|
h.outboundIP = outboundIP
|
||||||
|
} else if ip, err := getOutboundIP(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
h.outboundIP = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}))
|
||||||
|
router.Use(func(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
go h.gcCache()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.Use(middleware.Logger)
|
||||||
|
router.Route(urlBase, func(r chi.Router) {
|
||||||
|
r.Get("/cache", h.find)
|
||||||
|
r.Route("/caches", func(r chi.Router) {
|
||||||
|
r.Post("/", h.reserve)
|
||||||
|
r.Route("/{id}", func(r chi.Router) {
|
||||||
|
r.Patch("/", h.upload)
|
||||||
|
r.Post("/", h.commit)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
r.Get("/artifacts/{id}", h.get)
|
||||||
|
r.Post("/clean", h.clean)
|
||||||
|
})
|
||||||
|
|
||||||
|
h.router = router
|
||||||
|
|
||||||
|
h.gcCache()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := http.Serve(listener, h.router); err != nil {
|
||||||
|
logger.Errorf("http serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
h.listener = listener
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ExternalURL() string {
|
||||||
|
// TODO: make the external url configurable if necessary
|
||||||
|
return fmt.Sprintf("http://%s:%d",
|
||||||
|
h.outboundIP,
|
||||||
|
h.listener.Addr().(*net.TCPAddr).Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /_apis/artifactcache/cache
|
||||||
|
func (h *Handler) find(w http.ResponseWriter, r *http.Request) {
|
||||||
|
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||||
|
version := r.URL.Query().Get("version")
|
||||||
|
|
||||||
|
cache, err := h.findCache(r.Context(), keys, version)
|
||||||
|
if err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cache == nil {
|
||||||
|
responseJson(w, r, 204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := h.storage.Exist(cache.ID); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
} else if !ok {
|
||||||
|
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Delete(cache)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
responseJson(w, r, 204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responseJson(w, r, 200, map[string]any{
|
||||||
|
"result": "hit",
|
||||||
|
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
||||||
|
"cacheKey": cache.Key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /_apis/artifactcache/caches
|
||||||
|
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cache := &Cache{}
|
||||||
|
if err := render.Bind(r, cache); err != nil {
|
||||||
|
responseJson(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||||
|
return sess.Where(builder.Eq{"key": cache.Key, "version": cache.Version}).Get(&Cache{})
|
||||||
|
}); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
responseJson(w, r, 400, fmt.Errorf("already exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Insert(cache)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responseJson(w, r, 200, map[string]any{
|
||||||
|
"cacheId": cache.ID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /_apis/artifactcache/caches/:id
|
||||||
|
func (h *Handler) upload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
responseJson(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := &Cache{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||||
|
return sess.Get(cache)
|
||||||
|
}); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
} else if !ok {
|
||||||
|
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache.Complete {
|
||||||
|
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
||||||
|
if err != nil {
|
||||||
|
responseJson(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
}
|
||||||
|
h.useCache(r.Context(), id)
|
||||||
|
responseJson(w, r, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /_apis/artifactcache/caches/:id
|
||||||
|
func (h *Handler) commit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
responseJson(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := &Cache{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||||
|
return sess.Get(cache)
|
||||||
|
}); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
} else if !ok {
|
||||||
|
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache.Complete {
|
||||||
|
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.storage.Commit(cache.ID, cache.Size); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Complete = true
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.ID(cache.ID).Cols("complete").Update(cache)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
responseJson(w, r, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson(w, r, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /_apis/artifactcache/artifacts/:id
|
||||||
|
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
responseJson(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.useCache(r.Context(), id)
|
||||||
|
h.storage.Serve(w, r, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /_apis/artifactcache/clean
|
||||||
|
func (h *Handler) clean(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: don't support force deleting cache entries
|
||||||
|
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||||
|
|
||||||
|
responseJson(w, r, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not found, return (nil, nil) instead of an error.
|
||||||
|
func (h *Handler) findCache(ctx context.Context, keys []string, version string) (*Cache, error) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
key := keys[0] // the first key is for exact match.
|
||||||
|
|
||||||
|
cache := &Cache{}
|
||||||
|
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||||
|
return sess.Where(builder.Eq{"key": key, "version": version, "complete": true}).Get(cache)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range keys[1:] {
|
||||||
|
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
||||||
|
return sess.Where(builder.And(
|
||||||
|
builder.Like{"key", prefix + "%"},
|
||||||
|
builder.Eq{"version": version, "complete": true},
|
||||||
|
)).OrderBy("id DESC").Get(cache)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) useCache(ctx context.Context, id int64) {
|
||||||
|
// keep quiet
|
||||||
|
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Context(ctx).Cols("used_at").Update(&Cache{
|
||||||
|
ID: id,
|
||||||
|
UsedAt: time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) gcCache() {
|
||||||
|
if h.gc.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !h.gc.CompareAndSwap(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer h.gc.Store(false)
|
||||||
|
|
||||||
|
if time.Since(h.gcAt) < time.Hour {
|
||||||
|
logger.Infof("skip gc: %v", h.gcAt.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.gcAt = time.Now()
|
||||||
|
logger.Infof("gc: %v", h.gcAt.String())
|
||||||
|
|
||||||
|
const (
|
||||||
|
keepUsed = 30 * 24 * time.Hour
|
||||||
|
keepUnused = 7 * 24 * time.Hour
|
||||||
|
keepTemp = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var caches []*Cache
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
return sess.Where(builder.And(builder.Lt{"used_at": time.Now().Add(-keepTemp).Unix()}, builder.Eq{"complete": false})).
|
||||||
|
Find(&caches)
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("find caches: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, cache := range caches {
|
||||||
|
h.storage.Remove(cache.ID)
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Delete(cache)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("delete cache: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("deleted cache: %+v", cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caches = caches[:0]
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
return sess.Where(builder.Lt{"used_at": time.Now().Add(-keepUnused).Unix()}).
|
||||||
|
Find(&caches)
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("find caches: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, cache := range caches {
|
||||||
|
h.storage.Remove(cache.ID)
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Delete(cache)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("delete cache: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("deleted cache: %+v", cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caches = caches[:0]
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
return sess.Where(builder.Lt{"created_at": time.Now().Add(-keepUsed).Unix()}).
|
||||||
|
Find(&caches)
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("find caches: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, cache := range caches {
|
||||||
|
h.storage.Remove(cache.ID)
|
||||||
|
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Delete(cache)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
logger.Warnf("delete cache: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("deleted cache: %+v", cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
internal/app/artifactcache/model.go
Normal file
30
internal/app/artifactcache/model.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
ID int64 `xorm:"id pk autoincr" json:"-"`
|
||||||
|
Key string `xorm:"TEXT index unique(key_version)" json:"key"`
|
||||||
|
Version string `xorm:"TEXT unique(key_version)" json:"version"`
|
||||||
|
Size int64 `json:"cacheSize"`
|
||||||
|
Complete bool `xorm:"index(complete_used_at)" json:"-"`
|
||||||
|
UsedAt int64 `xorm:"index(complete_used_at) updated" json:"-"`
|
||||||
|
CreatedAt int64 `xorm:"index created" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind implements render.Binder
|
||||||
|
func (c *Cache) Bind(_ *http.Request) error {
|
||||||
|
if c.Key == "" {
|
||||||
|
return fmt.Errorf("missing key")
|
||||||
|
}
|
||||||
|
if c.Version == "" {
|
||||||
|
return fmt.Errorf("missing version")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
129
internal/app/artifactcache/storage.go
Normal file
129
internal/app/artifactcache/storage.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
rootDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(rootDir string) (*Storage, error) {
|
||||||
|
if err := os.MkdirAll(rootDir, 0o755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Storage{
|
||||||
|
rootDir: rootDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Exist(id int64) (bool, error) {
|
||||||
|
name := s.filename(id)
|
||||||
|
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Write(id int64, offset int64, reader io.Reader) error {
|
||||||
|
name := s.tempName(id, offset)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, reader)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Commit(id int64, size int64) error {
|
||||||
|
defer func() {
|
||||||
|
_ = os.RemoveAll(s.tempDir(id))
|
||||||
|
}()
|
||||||
|
|
||||||
|
name := s.filename(id)
|
||||||
|
tempNames, err := s.tempNames(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var written int64
|
||||||
|
for _, v := range tempNames {
|
||||||
|
f, err := os.Open(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, err := io.Copy(file, f)
|
||||||
|
_ = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
written += n
|
||||||
|
}
|
||||||
|
|
||||||
|
if written != size {
|
||||||
|
_ = file.Close()
|
||||||
|
_ = os.Remove(name)
|
||||||
|
return fmt.Errorf("broken file: %v != %v", written, size)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
name := s.filename(id)
|
||||||
|
http.ServeFile(w, r, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Remove(id int64) {
|
||||||
|
_ = os.Remove(s.filename(id))
|
||||||
|
_ = os.RemoveAll(s.tempDir(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) filename(id int64) string {
|
||||||
|
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) tempDir(id int64) string {
|
||||||
|
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) tempName(id, offset int64) string {
|
||||||
|
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) tempNames(id int64) ([]string, error) {
|
||||||
|
dir := s.tempDir(id)
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, v := range files {
|
||||||
|
if !v.IsDir() {
|
||||||
|
names = append(names, filepath.Join(dir, v.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
100
internal/app/artifactcache/util.go
Normal file
100
internal/app/artifactcache/util.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func responseJson(w http.ResponseWriter, r *http.Request, code int, v ...any) {
|
||||||
|
render.Status(r, code)
|
||||||
|
if len(v) == 0 || v[0] == nil {
|
||||||
|
render.JSON(w, r, struct{}{})
|
||||||
|
} else if err, ok := v[0].(error); ok {
|
||||||
|
logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
||||||
|
render.JSON(w, r, map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
render.JSON(w, r, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContentRange(s string) (int64, int64, error) {
|
||||||
|
// support the format like "bytes 11-22/*" only
|
||||||
|
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
|
||||||
|
s1, s2, _ := strings.Cut(s, "-")
|
||||||
|
|
||||||
|
start, err := strconv.ParseInt(s1, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||||
|
}
|
||||||
|
stop, err := strconv.ParseInt(s2, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return start, stop, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOutboundIP() (net.IP, error) {
|
||||||
|
// FIXME: It makes more sense to use the gateway IP address of container network
|
||||||
|
if conn, err := net.Dial("udp", "8.8.8.8:80"); err == nil {
|
||||||
|
defer conn.Close()
|
||||||
|
return conn.LocalAddr().(*net.UDPAddr).IP, nil
|
||||||
|
}
|
||||||
|
if ifaces, err := net.Interfaces(); err == nil {
|
||||||
|
for _, i := range ifaces {
|
||||||
|
if addrs, err := i.Addrs(); err == nil {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
}
|
||||||
|
if ip.IsGlobalUnicast() {
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no outbound IP address found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// engine is a wrapper of *xorm.Engine, with a lock.
|
||||||
|
// To avoid racing of sqlite, we don't care performance here.
|
||||||
|
type engine struct {
|
||||||
|
e *xorm.Engine
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *engine) Exec(f func(*xorm.Session) error) error {
|
||||||
|
e.m.Lock()
|
||||||
|
defer e.m.Unlock()
|
||||||
|
|
||||||
|
sess := e.e.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
return f(sess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *engine) ExecBool(f func(*xorm.Session) (bool, error)) (bool, error) {
|
||||||
|
e.m.Lock()
|
||||||
|
defer e.m.Unlock()
|
||||||
|
|
||||||
|
sess := e.e.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
return f(sess)
|
||||||
|
}
|
@ -1,32 +1,30 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.1.5"
|
|
||||||
|
|
||||||
type globalArgs struct {
|
|
||||||
EnvFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute(ctx context.Context) {
|
func Execute(ctx context.Context) {
|
||||||
// task := runtime.NewTask("gitea", 0, nil, nil)
|
|
||||||
|
|
||||||
var gArgs globalArgs
|
|
||||||
|
|
||||||
// ./act_runner
|
// ./act_runner
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "act [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Version: version,
|
Version: ver.Version(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
rootCmd.PersistentFlags().StringVarP(&gArgs.EnvFile, "env-file", "", ".env", "Read in a file of environment variables.")
|
configFile := ""
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||||
|
|
||||||
// ./act_runner register
|
// ./act_runner register
|
||||||
var regArgs registerArgs
|
var regArgs registerArgs
|
||||||
@ -34,11 +32,10 @@ func Execute(ctx context.Context) {
|
|||||||
Use: "register",
|
Use: "register",
|
||||||
Short: "Register a runner to the server",
|
Short: "Register a runner to the server",
|
||||||
Args: cobra.MaximumNArgs(0),
|
Args: cobra.MaximumNArgs(0),
|
||||||
RunE: runRegister(ctx, ®Args, gArgs.EnvFile), // must use a pointer to regArgs
|
RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs
|
||||||
}
|
}
|
||||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
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.Token, "token", "", "Runner token")
|
||||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||||
@ -49,11 +46,23 @@ func Execute(ctx context.Context) {
|
|||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "Run as a runner daemon",
|
Short: "Run as a runner daemon",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: runDaemon(ctx, gArgs.EnvFile),
|
RunE: runDaemon(ctx, &configFile),
|
||||||
}
|
}
|
||||||
// add all command
|
|
||||||
rootCmd.AddCommand(daemonCmd)
|
rootCmd.AddCommand(daemonCmd)
|
||||||
|
|
||||||
|
// ./act_runner exec
|
||||||
|
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||||
|
|
||||||
|
// ./act_runner config
|
||||||
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: "generate-config",
|
||||||
|
Short: "Generate an example config file",
|
||||||
|
Args: cobra.MaximumNArgs(0),
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
fmt.Printf("%s", config.Example)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// hide completion command
|
// hide completion command
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
|
98
internal/app/cmd/daemon.go
Normal file
98
internal/app/cmd/daemon.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/poll"
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/run"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
log.Infoln("Starting runner daemon")
|
||||||
|
|
||||||
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initLogging(cfg)
|
||||||
|
|
||||||
|
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Error("registration file not found, please register the runner first")
|
||||||
|
return err
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registration file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := labels.Labels{}
|
||||||
|
for _, l := range reg.Labels {
|
||||||
|
label, err := labels.Parse(l)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warnf("ignored invalid label %q", l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ls = append(ls, label)
|
||||||
|
}
|
||||||
|
if len(ls) == 0 {
|
||||||
|
log.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.RequireDocker() {
|
||||||
|
if err := envcheck.CheckIfDockerRunning(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := client.New(
|
||||||
|
reg.Address,
|
||||||
|
cfg.Runner.Insecure,
|
||||||
|
reg.UUID,
|
||||||
|
reg.Token,
|
||||||
|
ver.Version(),
|
||||||
|
)
|
||||||
|
|
||||||
|
runner := run.NewRunner(cfg, reg, cli)
|
||||||
|
poller := poll.New(cfg, cli, runner)
|
||||||
|
|
||||||
|
poller.Poll(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 l := cfg.Log.Level; l != "" {
|
||||||
|
level, err := log.ParseLevel(l)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).
|
||||||
|
Errorf("invalid log level: %q", l)
|
||||||
|
}
|
||||||
|
if log.GetLevel() != level {
|
||||||
|
log.Infof("log level changed to %v", level)
|
||||||
|
log.SetLevel(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
471
internal/app/cmd/exec.go
Normal file
471
internal/app/cmd/exec.go
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
// 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/joho/godotenv"
|
||||||
|
"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"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type executeArgs struct {
|
||||||
|
runList bool
|
||||||
|
job string
|
||||||
|
event string
|
||||||
|
workdir string
|
||||||
|
workflowsPath string
|
||||||
|
noWorkflowRecurse bool
|
||||||
|
autodetectEvent bool
|
||||||
|
forcePull bool
|
||||||
|
forceRebuild bool
|
||||||
|
jsonLogger bool
|
||||||
|
envs []string
|
||||||
|
envfile string
|
||||||
|
secrets []string
|
||||||
|
defaultActionsUrl string
|
||||||
|
insecureSecrets bool
|
||||||
|
privileged bool
|
||||||
|
usernsMode string
|
||||||
|
containerArchitecture string
|
||||||
|
containerDaemonSocket string
|
||||||
|
useGitIgnore bool
|
||||||
|
containerCapAdd []string
|
||||||
|
containerCapDrop []string
|
||||||
|
containerOptions string
|
||||||
|
artifactServerPath string
|
||||||
|
artifactServerAddr string
|
||||||
|
artifactServerPort string
|
||||||
|
noSkipCheckout bool
|
||||||
|
debug bool
|
||||||
|
dryrun bool
|
||||||
|
image string
|
||||||
|
cacheHandler *artifactcache.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowsPath returns path to workflow file(s)
|
||||||
|
func (i *executeArgs) WorkflowsPath() string {
|
||||||
|
return i.resolve(i.workflowsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envfile returns path to .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("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||||
|
}
|
||||||
|
if len(secretPairParts) == 2 {
|
||||||
|
s[secretPairParts[0]] = secretPairParts[1]
|
||||||
|
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||||
|
s[secretPairParts[0]] = env
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Provide value for '%s': ", secretPairParts[0])
|
||||||
|
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to read input: %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("Error loading from %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 returns path to 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
|
||||||
|
jobName string
|
||||||
|
stage string
|
||||||
|
wfName string
|
||||||
|
wfFile string
|
||||||
|
events string
|
||||||
|
}
|
||||||
|
lineInfos := []lineInfoDef{}
|
||||||
|
|
||||||
|
header := lineInfoDef{
|
||||||
|
jobID: "Job ID",
|
||||||
|
jobName: "Job name",
|
||||||
|
stage: "Stage",
|
||||||
|
wfName: "Workflow name",
|
||||||
|
wfFile: "Workflow file",
|
||||||
|
events: "Events",
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
wfFileMaxWidth += 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("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||||
|
// plan with filtered jobs - to be used for filtering only
|
||||||
|
var filterPlan *model.Plan
|
||||||
|
|
||||||
|
// Determine the event name to be filtered
|
||||||
|
var filterEventName string = ""
|
||||||
|
|
||||||
|
if len(execArgs.event) > 0 {
|
||||||
|
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||||
|
filterEventName = execArgs.event
|
||||||
|
} else if execArgs.autodetectEvent {
|
||||||
|
// collect all events from loaded workflows
|
||||||
|
events := planner.GetEvents()
|
||||||
|
|
||||||
|
// set default event type to first event from many available
|
||||||
|
// this way user dont have to specify the event.
|
||||||
|
log.Infof("Using first detected workflow event for filtering: %s", events[0])
|
||||||
|
|
||||||
|
filterEventName = events[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if execArgs.job != "" {
|
||||||
|
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||||
|
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if filterEventName != "" {
|
||||||
|
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||||
|
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Preparing plan with all jobs")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// plan with triggered jobs
|
||||||
|
var plan *model.Plan
|
||||||
|
|
||||||
|
// Determine the event name to be triggered
|
||||||
|
var eventName string
|
||||||
|
|
||||||
|
// collect all events from loaded workflows
|
||||||
|
events := planner.GetEvents()
|
||||||
|
|
||||||
|
if len(execArgs.event) > 0 {
|
||||||
|
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||||
|
eventName = args[0]
|
||||||
|
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||||
|
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||||
|
eventName = events[0]
|
||||||
|
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||||
|
// set default event type to first event from many available
|
||||||
|
// this way user dont have to specify the event.
|
||||||
|
log.Infof("Using first detected workflow event: %s", events[0])
|
||||||
|
eventName = events[0]
|
||||||
|
} else {
|
||||||
|
log.Infof("Using default workflow event: push")
|
||||||
|
eventName = "push"
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the plan for this run
|
||||||
|
if execArgs.job != "" {
|
||||||
|
log.Infof("Planning job: %s", execArgs.job)
|
||||||
|
plan, err = planner.PlanJob(execArgs.job)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Planning jobs for event: %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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init a cache server
|
||||||
|
handler, err := artifactcache.StartHandler("", "", 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||||
|
execArgs.cacheHandler = handler
|
||||||
|
|
||||||
|
// run the plan
|
||||||
|
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: t.client.Address(),
|
||||||
|
ContainerCapAdd: execArgs.containerCapAdd,
|
||||||
|
ContainerCapDrop: execArgs.containerCapDrop,
|
||||||
|
ContainerOptions: execArgs.containerOptions,
|
||||||
|
AutoRemove: true,
|
||||||
|
ArtifactServerPath: execArgs.artifactServerPath,
|
||||||
|
ArtifactServerPort: execArgs.artifactServerPort,
|
||||||
|
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||||
|
// PresetGitHubContext: preset,
|
||||||
|
// EventJSON: string(eventJSON),
|
||||||
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
||||||
|
ContainerMaxLifetime: maxLifetime,
|
||||||
|
ContainerNetworkMode: "bridge",
|
||||||
|
DefaultActionInstance: execArgs.defaultActionsUrl,
|
||||||
|
PlatformPicker: func(_ []string) string {
|
||||||
|
return execArgs.image
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle log level config
|
||||||
|
// waiting https://gitea.com/gitea/act/pulls/19
|
||||||
|
// if !execArgs.debug {
|
||||||
|
// logLevel := log.Level(log.InfoLevel)
|
||||||
|
// config.JobLoggerLevel = &logLevel
|
||||||
|
// }
|
||||||
|
|
||||||
|
r, err := runner.New(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(execArgs.artifactServerPath) == 0 {
|
||||||
|
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
execArgs.artifactServerPath = tempDir
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||||
|
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||||
|
|
||||||
|
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: "Run workflow locally.",
|
||||||
|
Args: cobra.MaximumNArgs(20),
|
||||||
|
RunE: runExec(ctx, &execArg),
|
||||||
|
}
|
||||||
|
|
||||||
|
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
|
||||||
|
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
|
||||||
|
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
||||||
|
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||||
|
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
|
||||||
|
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
|
||||||
|
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
|
||||||
|
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")
|
||||||
|
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
|
||||||
|
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
|
||||||
|
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
|
||||||
|
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
|
||||||
|
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
|
||||||
|
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
|
||||||
|
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
|
||||||
|
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
|
||||||
|
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsUrl, "default-actions-url", "", "https://gitea.com", "Defines the default url of action instance.")
|
||||||
|
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||||
|
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
|
||||||
|
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
|
||||||
|
|
||||||
|
return execCmd
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,24 +9,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
goruntime "runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||||
"gitea.com/gitea/act_runner/client"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"gitea.com/gitea/act_runner/config"
|
|
||||||
"gitea.com/gitea/act_runner/register"
|
|
||||||
|
|
||||||
"github.com/bufbuild/connect-go"
|
"github.com/bufbuild/connect-go"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runRegister registers a runner to the server
|
// runRegister registers a runner to the server
|
||||||
func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) func(*cobra.Command, []string) error {
|
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
log.SetReportCaller(false)
|
log.SetReportCaller(false)
|
||||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
@ -34,7 +38,7 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun
|
|||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||||
runtime.GOARCH, runtime.GOOS, version)
|
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
||||||
|
|
||||||
// runner always needs root permission
|
// runner always needs root permission
|
||||||
if os.Getuid() != 0 {
|
if os.Getuid() != 0 {
|
||||||
@ -43,14 +47,13 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun
|
|||||||
}
|
}
|
||||||
|
|
||||||
if regArgs.NoInteractive {
|
if regArgs.NoInteractive {
|
||||||
if err := registerNoInteractive(envFile, regArgs); err != nil {
|
if err := registerNoInteractive(*configFile, regArgs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
go func() {
|
go func() {
|
||||||
if err := registerInteractive(envFile); err != nil {
|
if err := registerInteractive(*configFile); err != nil {
|
||||||
// log.Errorln(err)
|
log.Fatal(err)
|
||||||
os.Exit(2)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -69,7 +72,6 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun
|
|||||||
type registerArgs struct {
|
type registerArgs struct {
|
||||||
NoInteractive bool
|
NoInteractive bool
|
||||||
InstanceAddr string
|
InstanceAddr string
|
||||||
Insecure bool
|
|
||||||
Token string
|
Token string
|
||||||
RunnerName string
|
RunnerName string
|
||||||
Labels string
|
Labels string
|
||||||
@ -97,7 +99,6 @@ var defaultLabels = []string{
|
|||||||
|
|
||||||
type registerInputs struct {
|
type registerInputs struct {
|
||||||
InstanceAddr string
|
InstanceAddr string
|
||||||
Insecure bool
|
|
||||||
Token string
|
Token string
|
||||||
RunnerName string
|
RunnerName string
|
||||||
CustomLabels []string
|
CustomLabels []string
|
||||||
@ -116,14 +117,11 @@ func (r *registerInputs) validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateLabels(labels []string) error {
|
func validateLabels(ls []string) error {
|
||||||
for _, label := range labels {
|
for _, label := range ls {
|
||||||
values := strings.SplitN(label, ":", 2)
|
if _, err := labels.Parse(label); err != nil {
|
||||||
if len(values) > 2 {
|
return err
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -164,7 +162,7 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if validateLabels(r.CustomLabels) != nil {
|
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)")
|
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host)")
|
||||||
return StageInputCustomLabels
|
return StageInputCustomLabels
|
||||||
}
|
}
|
||||||
return StageWaitingForRegistration
|
return StageWaitingForRegistration
|
||||||
@ -172,16 +170,17 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe
|
|||||||
return StageUnknown
|
return StageUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerInteractive(envFile string) error {
|
func registerInteractive(configFile string) error {
|
||||||
var (
|
var (
|
||||||
reader = bufio.NewReader(os.Stdin)
|
reader = bufio.NewReader(os.Stdin)
|
||||||
stage = StageInputInstance
|
stage = StageInputInstance
|
||||||
inputs = new(registerInputs)
|
inputs = new(registerInputs)
|
||||||
)
|
)
|
||||||
|
|
||||||
// check if overwrite local config
|
cfg, err := config.LoadDefault(configFile)
|
||||||
_ = godotenv.Load(envFile)
|
if err != nil {
|
||||||
cfg, _ := config.FromEnviron()
|
return fmt.Errorf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||||
stage = StageOverwriteLocalConfig
|
stage = StageOverwriteLocalConfig
|
||||||
}
|
}
|
||||||
@ -197,7 +196,7 @@ func registerInteractive(envFile string) error {
|
|||||||
|
|
||||||
if stage == StageWaitingForRegistration {
|
if stage == StageWaitingForRegistration {
|
||||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
|
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
|
||||||
if err := doRegister(&cfg, inputs); err != nil {
|
if err := doRegister(cfg, inputs); err != nil {
|
||||||
log.Errorf("Failed to register runner: %v", err)
|
log.Errorf("Failed to register runner: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Runner registered successfully.")
|
log.Infof("Runner registered successfully.")
|
||||||
@ -226,20 +225,21 @@ func printStageHelp(stage registerStage) {
|
|||||||
log.Infoln("Enter the runner token:")
|
log.Infoln("Enter the runner token:")
|
||||||
case StageInputRunnerName:
|
case StageInputRunnerName:
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
log.Infof("Enter the runner name (if set empty, use hostname:%s ):\n", hostname)
|
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||||
case StageInputCustomLabels:
|
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):")
|
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):")
|
||||||
case StageWaitingForRegistration:
|
case StageWaitingForRegistration:
|
||||||
log.Infoln("Waiting for registration...")
|
log.Infoln("Waiting for registration...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerNoInteractive(envFile string, regArgs *registerArgs) error {
|
func registerNoInteractive(configFile string, regArgs *registerArgs) error {
|
||||||
_ = godotenv.Load(envFile)
|
cfg, err := config.LoadDefault(configFile)
|
||||||
cfg, _ := config.FromEnviron()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
inputs := ®isterInputs{
|
inputs := ®isterInputs{
|
||||||
InstanceAddr: regArgs.InstanceAddr,
|
InstanceAddr: regArgs.InstanceAddr,
|
||||||
Insecure: regArgs.Insecure,
|
|
||||||
Token: regArgs.Token,
|
Token: regArgs.Token,
|
||||||
RunnerName: regArgs.RunnerName,
|
RunnerName: regArgs.RunnerName,
|
||||||
CustomLabels: defaultLabels,
|
CustomLabels: defaultLabels,
|
||||||
@ -256,7 +256,7 @@ func registerNoInteractive(envFile string, regArgs *registerArgs) error {
|
|||||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := doRegister(&cfg, inputs); err != nil {
|
if err := doRegister(cfg, inputs); err != nil {
|
||||||
log.Errorf("Failed to register runner: %v", err)
|
log.Errorf("Failed to register runner: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -270,8 +270,10 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
|||||||
// initial http client
|
// initial http client
|
||||||
cli := client.New(
|
cli := client.New(
|
||||||
inputs.InstanceAddr,
|
inputs.InstanceAddr,
|
||||||
inputs.Insecure,
|
cfg.Runner.Insecure,
|
||||||
"", "",
|
"",
|
||||||
|
"",
|
||||||
|
ver.Version(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -297,9 +299,36 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Runner.Name = inputs.RunnerName
|
reg := &config.Registration{
|
||||||
cfg.Runner.Token = inputs.Token
|
Name: inputs.RunnerName,
|
||||||
cfg.Runner.Labels = inputs.CustomLabels
|
Token: inputs.Token,
|
||||||
_, err := register.New(cli).Register(ctx, cfg.Runner)
|
Address: inputs.InstanceAddr,
|
||||||
return err
|
Labels: inputs.CustomLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := make([]string, len(reg.Labels))
|
||||||
|
for i, v := range reg.Labels {
|
||||||
|
l, _ := labels.Parse(v)
|
||||||
|
ls[i] = l.Name
|
||||||
|
}
|
||||||
|
// register new runner.
|
||||||
|
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||||
|
Name: reg.Name,
|
||||||
|
Token: reg.Token,
|
||||||
|
AgentLabels: ls,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("poller: cannot register new runner")
|
||||||
|
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 err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||||
|
return fmt.Errorf("failed to save runner config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
81
internal/app/poll/poller.go
Normal file
81
internal/app/poll/poller.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package poll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/bufbuild/connect-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/run"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Poller struct {
|
||||||
|
client client.Client
|
||||||
|
runner *run.Runner
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||||
|
return &Poller{
|
||||||
|
client: client,
|
||||||
|
runner: runner,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) Poll(ctx context.Context) {
|
||||||
|
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(ctx, wg, limiter)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) poll(ctx context.Context, wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||||
|
defer wg.Done()
|
||||||
|
for {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
log.WithError(err).Debug("limiter wait failed")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, ok := p.fetchTask(ctx)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := p.runner.Run(ctx, task); err != nil {
|
||||||
|
log.WithError(err).Error("failed to run task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||||
|
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("failed to fetch task")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil || resp.Msg == nil || resp.Msg.Task == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return resp.Msg.Task, true
|
||||||
|
}
|
213
internal/app/run/runner.go
Normal file
213
internal/app/run/runner.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/nektos/act/pkg/runner"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Runner runs the pipeline.
|
||||||
|
type Runner struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
client client.Client
|
||||||
|
labels labels.Labels
|
||||||
|
envs map[string]string
|
||||||
|
|
||||||
|
runningTasks sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
cacheHandler, err := artifactcache.StartHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||||
|
// go on
|
||||||
|
} else {
|
||||||
|
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set artifact gitea api
|
||||||
|
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
||||||
|
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
||||||
|
|
||||||
|
// Set specific environments to distinguish between Gitea and 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||||
|
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||||
|
return fmt.Errorf("task %d is already running", task.Id)
|
||||||
|
} else {
|
||||||
|
r.runningTasks.Store(task.Id, struct{}{})
|
||||||
|
defer r.runningTasks.Delete(task.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
|
||||||
|
|
||||||
|
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jobIDs := workflow.GetJobIDs()
|
||||||
|
if len(jobIDs) != 1 {
|
||||||
|
return fmt.Errorf("multiple jobs found: %v", jobIDs)
|
||||||
|
}
|
||||||
|
jobID := jobIDs[0]
|
||||||
|
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
job := workflow.GetJob(jobID)
|
||||||
|
reporter.ResetSteps(len(job.Steps))
|
||||||
|
|
||||||
|
taskContext := task.Context.Fields
|
||||||
|
|
||||||
|
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||||
|
taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||||
|
r.client.Address())
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||||
|
preset.Token = t
|
||||||
|
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||||
|
preset.Token = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// use task token to action api token
|
||||||
|
r.envs["ACTIONS_RUNTIME_TOKEN"] = preset.Token
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerConfig := &runner.Config{
|
||||||
|
// On Linux, Workdir will be like "/<owner>/<repo>"
|
||||||
|
// On Windows, Workdir will be like "\<owner>\<repo>"
|
||||||
|
Workdir: filepath.FromSlash(string(filepath.Separator) + preset.Repository),
|
||||||
|
BindWorkdir: false,
|
||||||
|
|
||||||
|
ReuseContainers: false,
|
||||||
|
ForcePull: false,
|
||||||
|
ForceRebuild: false,
|
||||||
|
LogOutput: true,
|
||||||
|
JSONLogger: false,
|
||||||
|
Env: r.envs,
|
||||||
|
Secrets: task.Secrets,
|
||||||
|
GitHubInstance: r.client.Address(),
|
||||||
|
AutoRemove: true,
|
||||||
|
NoSkipCheckout: true,
|
||||||
|
PresetGitHubContext: preset,
|
||||||
|
EventJSON: string(eventJSON),
|
||||||
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
|
ContainerMaxLifetime: maxLifetime,
|
||||||
|
ContainerNetworkMode: r.cfg.Container.NetworkMode,
|
||||||
|
ContainerOptions: r.cfg.Container.Options,
|
||||||
|
Privileged: r.cfg.Container.Privileged,
|
||||||
|
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||||
|
PlatformPicker: r.labels.PickPlatform,
|
||||||
|
}
|
||||||
|
|
||||||
|
rr, err := runner.New(runnerConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
executor := rr.NewPlanExecutor(plan)
|
||||||
|
|
||||||
|
reporter.Logf("workflow prepared")
|
||||||
|
|
||||||
|
// add logger recorders
|
||||||
|
ctx = common.WithLoggerHook(ctx, reporter)
|
||||||
|
|
||||||
|
return executor(ctx)
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
10
internal/pkg/client/header.go
Normal file
10
internal/pkg/client/header.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
const (
|
||||||
|
UUIDHeader = "x-runner-uuid"
|
||||||
|
TokenHeader = "x-runner-token"
|
||||||
|
VersionHeader = "x-runner-version"
|
||||||
|
)
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||||
"gitea.com/gitea/act_runner/core"
|
|
||||||
"github.com/bufbuild/connect-go"
|
"github.com/bufbuild/connect-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,16 +28,19 @@ func getHttpClient(endpoint string, insecure bool) *http.Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new runner client.
|
// New returns a new runner client.
|
||||||
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
|
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
||||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||||
|
|
||||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
if uuid != "" {
|
if uuid != "" {
|
||||||
req.Header().Set(core.UUIDHeader, uuid)
|
req.Header().Set(UUIDHeader, uuid)
|
||||||
}
|
}
|
||||||
if token != "" {
|
if token != "" {
|
||||||
req.Header().Set(core.TokenHeader, token)
|
req.Header().Set(TokenHeader, token)
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
req.Header().Set(VersionHeader, version)
|
||||||
}
|
}
|
||||||
return next(ctx, req)
|
return next(ctx, req)
|
||||||
}
|
}
|
50
internal/pkg/config/config.example.yaml
Normal file
50
internal/pkg/config/config.example.yaml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Example configuration file, it's safe to copy this as the default config file without any modification.
|
||||||
|
|
||||||
|
log:
|
||||||
|
# The level of logging, can be trace, debug, info, warn, error, fatal
|
||||||
|
level: info
|
||||||
|
|
||||||
|
runner:
|
||||||
|
# Where to store the registration result.
|
||||||
|
file: .runner
|
||||||
|
# Execute how many tasks concurrently at the same time.
|
||||||
|
capacity: 1
|
||||||
|
# Extra environment variables to run jobs.
|
||||||
|
envs:
|
||||||
|
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||||
|
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||||
|
# Extra environment variables to run jobs from a file.
|
||||||
|
# It will be ignored if it's empty or the file doesn't exist.
|
||||||
|
env_file: .env
|
||||||
|
# The timeout for a job to be finished.
|
||||||
|
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||||
|
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
|
||||||
|
timeout: 3h
|
||||||
|
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||||
|
insecure: false
|
||||||
|
# The timeout for fetching the job from the Gitea instance.
|
||||||
|
fetch_timeout: 5s
|
||||||
|
# The interval for fetching the job from the Gitea instance.
|
||||||
|
fetch_interval: 2s
|
||||||
|
|
||||||
|
cache:
|
||||||
|
# Enable cache server to use actions/cache.
|
||||||
|
enabled: true
|
||||||
|
# The directory to store the cache data.
|
||||||
|
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
|
||||||
|
dir: ""
|
||||||
|
# The host of the cache server.
|
||||||
|
# It's not for the address to listen, but the address to connect from job containers.
|
||||||
|
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
|
||||||
|
host: ""
|
||||||
|
# The port of the cache server.
|
||||||
|
# 0 means to use a random available port.
|
||||||
|
port: 0
|
||||||
|
|
||||||
|
container:
|
||||||
|
# Which network to use for the job containers. Could be bridge, host, none, or the name of a custom network.
|
||||||
|
network_mode: bridge
|
||||||
|
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||||
|
privileged: false
|
||||||
|
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||||
|
options:
|
105
internal/pkg/config/config.go
Normal file
105
internal/pkg/config/config.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Log struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
} `yaml:"log"`
|
||||||
|
Runner struct {
|
||||||
|
File string `yaml:"file"`
|
||||||
|
Capacity int `yaml:"capacity"`
|
||||||
|
Envs map[string]string `yaml:"envs"`
|
||||||
|
EnvFile string `yaml:"env_file"`
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
FetchTimeout time.Duration `yaml:"fetch_timeout"`
|
||||||
|
FetchInterval time.Duration `yaml:"fetch_interval"`
|
||||||
|
} `yaml:"runner"`
|
||||||
|
Cache struct {
|
||||||
|
Enabled *bool `yaml:"enabled"` // pointer to distinguish between false and not set, and it will be true if not set
|
||||||
|
Dir string `yaml:"dir"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port uint16 `yaml:"port"`
|
||||||
|
} `yaml:"cache"`
|
||||||
|
Container struct {
|
||||||
|
NetworkMode string `yaml:"network_mode"`
|
||||||
|
Privileged bool `yaml:"privileged"`
|
||||||
|
Options string `yaml:"options"`
|
||||||
|
} `yaml:"container"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadDefault returns the default configuration.
|
||||||
|
// If file is not empty, it will be used to load the configuration.
|
||||||
|
func LoadDefault(file string) (*Config, error) {
|
||||||
|
cfg := &Config{}
|
||||||
|
if file != "" {
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
decoder := yaml.NewDecoder(f)
|
||||||
|
if err := decoder.Decode(&cfg); err != nil {
|
||||||
|
return nil, 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("read env file %q: %w", cfg.Runner.EnvFile, err)
|
||||||
|
}
|
||||||
|
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.NetworkMode == "" {
|
||||||
|
cfg.Container.NetworkMode = "bridge"
|
||||||
|
}
|
||||||
|
if cfg.Runner.FetchTimeout <= 0 {
|
||||||
|
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.Runner.FetchInterval <= 0 {
|
||||||
|
cfg.Runner.FetchInterval = 2 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
||||||
|
// Be compatible with old envs.
|
||||||
|
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
||||||
|
handleEnv := func(key string) (string, bool) {
|
||||||
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
|
if fileUsed {
|
||||||
|
log.Warnf("env %s has been ignored because config file is used", key)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
log.Warnf("env %s will be deprecated, please use config file instead", 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
|
54
internal/pkg/config/registration.go
Normal file
54
internal/pkg/config/registration.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationWarning = "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner."
|
||||||
|
|
||||||
|
// Registration is the registration information for a runner
|
||||||
|
type Registration struct {
|
||||||
|
Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
|
||||||
|
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Package envcheck provides a simple way to check if the environment is ready to run jobs.
|
||||||
|
package envcheck
|
27
internal/pkg/envcheck/docker.go
Normal file
27
internal/pkg/envcheck/docker.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 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) error {
|
||||||
|
// TODO: if runner support configures to use docker, we need config.Config to pass in
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
_, err = cli.Ping(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot ping the docker daemon, does it running? %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
84
internal/pkg/labels/labels.go
Normal file
84
internal/pkg/labels/labels.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemeHost = "host"
|
||||||
|
SchemeDocker = "docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
Name string
|
||||||
|
Schema string
|
||||||
|
Arg string
|
||||||
|
}
|
||||||
|
|
||||||
|
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("unsupported schema: %s", label.Schema)
|
||||||
|
}
|
||||||
|
return label, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Labels []*Label
|
||||||
|
|
||||||
|
func (l Labels) RequireDocker() bool {
|
||||||
|
for _, label := range l {
|
||||||
|
if label.Schema == SchemeDocker {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Labels) PickPlatform(runsOn []string) string {
|
||||||
|
platforms := make(map[string]string, len(l))
|
||||||
|
for _, label := range l {
|
||||||
|
switch label.Schema {
|
||||||
|
case SchemeDocker:
|
||||||
|
// "//" will be ignored
|
||||||
|
// TODO maybe we should use 'ubuntu-18.04:docker:node:16-buster' instead
|
||||||
|
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||||
|
case SchemeHost:
|
||||||
|
platforms[label.Name] = "-self-hosted"
|
||||||
|
default:
|
||||||
|
// It should not happen, because Parse has checked it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range runsOn {
|
||||||
|
if v, ok := platforms[v]; 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.
|
||||||
|
// So the runner receives a task with a label that the runner doesn't have,
|
||||||
|
// it happens when the user have edited the label of the runner in the web UI.
|
||||||
|
// TODO: it may be not correct, what if the runner is used as host mode only?
|
||||||
|
return "node:16-bullseye"
|
||||||
|
}
|
64
internal/pkg/labels/labels_test.go
Normal file
64
internal/pkg/labels/labels_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// 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 TestParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args string
|
||||||
|
want *Label
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: "ubuntu:docker://node:18",
|
||||||
|
want: &Label{
|
||||||
|
Name: "ubuntu",
|
||||||
|
Schema: "docker",
|
||||||
|
Arg: "//node:18",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: "ubuntu:host",
|
||||||
|
want: &Label{
|
||||||
|
Name: "ubuntu",
|
||||||
|
Schema: "host",
|
||||||
|
Arg: "",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: "ubuntu",
|
||||||
|
want: &Label{
|
||||||
|
Name: "ubuntu",
|
||||||
|
Schema: "host",
|
||||||
|
Arg: "",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: "ubuntu:vm:ubuntu-18.04",
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
package runtime
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package report
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -8,13 +11,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"gitea.com/gitea/act_runner/client"
|
|
||||||
|
|
||||||
retry "github.com/avast/retry-go/v4"
|
retry "github.com/avast/retry-go/v4"
|
||||||
"github.com/bufbuild/connect-go"
|
"github.com/bufbuild/connect-go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reporter struct {
|
type Reporter struct {
|
||||||
@ -99,7 +102,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
|
|
||||||
var step *runnerv1.StepState
|
var step *runnerv1.StepState
|
||||||
if v, ok := entry.Data["stepNumber"]; ok {
|
if v, ok := entry.Data["stepNumber"]; ok {
|
||||||
if v, ok := v.(int); ok {
|
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
||||||
step = r.state.Steps[v]
|
step = r.state.Steps[v]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,6 +179,7 @@ func (r *Reporter) Close(lastWords string) error {
|
|||||||
v.Result = runnerv1.Result_RESULT_CANCELLED
|
v.Result = runnerv1.Result_RESULT_CANCELLED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.state.Result = runnerv1.Result_RESULT_FAILURE
|
||||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||||
Time: timestamppb.Now(),
|
Time: timestamppb.Now(),
|
||||||
Content: lastWords,
|
Content: lastWords,
|
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 gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func Version() string {
|
||||||
|
return version
|
||||||
|
}
|
27
main.go
27
main.go
@ -1,34 +1,19 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/cmd"
|
"gitea.com/gitea/act_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() {
|
func main() {
|
||||||
ctx := withContextFunc(context.Background(), func() {})
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
// run the command
|
// run the command
|
||||||
cmd.Execute(ctx)
|
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)
|
|
||||||
}
|
|
45
run.sh
Executable file
45
run.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [[ ! -d /data ]]; then
|
||||||
|
mkdir -p /data
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /data
|
||||||
|
|
||||||
|
CONFIG_ARG=""
|
||||||
|
if [[ ! -z "${CONFIG_FILE}" ]]; then
|
||||||
|
CONFIG_ARG="--config ${CONFIG_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the same ENV variable names as https://github.com/vegardit/docker-gitea-act-runner
|
||||||
|
|
||||||
|
if [[ ! -s .runner ]]; then
|
||||||
|
try=$((try + 1))
|
||||||
|
success=0
|
||||||
|
|
||||||
|
# The point of this loop is to make it simple, when running both act_runner and gitea in docker,
|
||||||
|
# for the act_runner to wait a moment for gitea to become available before erroring out. Within
|
||||||
|
# the context of a single docker-compose, something similar could be done via healthchecks, but
|
||||||
|
# this is more flexible.
|
||||||
|
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`}" \
|
||||||
|
--labels "${GITEA_RUNNER_LABELS}" \
|
||||||
|
${CONFIG_ARG} --no-interactive > /tmp/reg.log 2>&1
|
||||||
|
|
||||||
|
cat /tmp/reg.log
|
||||||
|
|
||||||
|
cat /tmp/reg.log | grep 'Runner registered successfully' > /dev/null
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo "SUCCESS"
|
||||||
|
success=1
|
||||||
|
else
|
||||||
|
echo "Waiting to retry ..."
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
act_runner daemon ${CONFIG_ARG}
|
@ -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
|
|
||||||
}
|
|
Reference in New Issue
Block a user