mirror of
https://gitea.com/gitea/act_runner.git
synced 2025-12-17 03:24:53 +00:00
Compare commits
37 Commits
v0.1.2
...
69c55ee003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69c55ee003 | ||
|
|
01ef57c667 | ||
|
|
a384adbbc6 | ||
|
|
e3271d8469 | ||
|
|
84386c1b16 | ||
|
|
fd7c8580af | ||
|
|
35596a182b | ||
|
|
c9d3f67264 | ||
|
|
94031fc198 | ||
|
|
d5e4baed54 | ||
|
|
d4caa7e065 | ||
|
|
de4160b023 | ||
|
|
609c0a0773 | ||
|
|
0c029f7e79 | ||
|
|
eef3c32eb2 | ||
|
|
c40b651873 | ||
|
|
b498341857 | ||
|
|
0d727eb262 | ||
|
|
7c71c94366 | ||
|
|
49d2cb0cb5 | ||
|
|
85626b6bbd | ||
|
|
35400f76fa | ||
|
|
0cf31b2d22 | ||
|
|
c8cc7b2448 | ||
|
|
3be962cdb3 | ||
|
|
a5edbc9ac4 | ||
|
|
66bab3d805 | ||
|
|
293926f5d5 | ||
|
|
43c5ba923f | ||
|
|
acc5afc428 | ||
|
|
27a1a90d25 | ||
|
|
83ec0ba909 | ||
|
|
ed86e2f15a | ||
|
|
d4bebccc12 | ||
|
|
c75b67e892 | ||
|
|
bc6031eff7 | ||
|
|
c69c353d93 |
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{go}]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -1,20 +1,37 @@
|
|||||||
name: goreleaser
|
name: release-nightly
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
GOPATH: /go_path
|
||||||
|
GOCACHE: /go_cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
- run: git fetch --force --tags
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.20.1'
|
go-version: '>=1.20.1'
|
||||||
|
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||||
|
id: hash-go
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
go.mod
|
||||||
|
go.sum
|
||||||
|
- name: cache go
|
||||||
|
id: cache-go
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/go_path
|
||||||
|
/go_cache
|
||||||
|
key: go_path-${{ steps.hash-go.outputs.hash }}
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: https://github.com/goreleaser/goreleaser-action@v4
|
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -28,3 +45,53 @@ jobs:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: dockerfile lint check
|
||||||
|
uses: https://github.com/hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,38 @@
|
|||||||
name: goreleaser
|
name: release-tag
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
GOPATH: /go_path
|
||||||
|
GOCACHE: /go_cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
- run: git fetch --force --tags
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.20.1'
|
go-version: '>=1.20.1'
|
||||||
|
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||||
|
id: hash-go
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
go.mod
|
||||||
|
go.sum
|
||||||
|
- name: cache go
|
||||||
|
id: cache-go
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/go_path
|
||||||
|
/go_cache
|
||||||
|
key: go_path-${{ steps.hash-go.outputs.hash }}
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: https://github.com/crazy-max/ghaction-import-gpg@v5
|
uses: https://github.com/crazy-max/ghaction-import-gpg@v5
|
||||||
@@ -39,3 +56,53 @@ jobs:
|
|||||||
GORELEASER_FORCE_TOKEN: 'gitea'
|
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: dockerfile lint check
|
||||||
|
uses: https://github.com/hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
name: checks
|
name: checks
|
||||||
on:
|
on:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GOPROXY: https://goproxy.io,direct
|
|
||||||
GOPATH: /go_path
|
GOPATH: /go_path
|
||||||
GOCACHE: /go_cache
|
GOCACHE: /go_cache
|
||||||
|
|
||||||
@@ -13,31 +12,31 @@ jobs:
|
|||||||
name: check and test
|
name: check and test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: cache go path
|
- uses: actions/checkout@v3
|
||||||
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.1'
|
||||||
- uses: actions/checkout@v3
|
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||||
|
id: hash-go
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
go.mod
|
||||||
|
go.sum
|
||||||
|
- name: cache go
|
||||||
|
id: cache-go
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/go_path
|
||||||
|
/go_cache
|
||||||
|
key: go_path-${{ steps.hash-go.outputs.hash }}
|
||||||
- name: vet checks
|
- name: vet checks
|
||||||
run: make vet
|
run: make vet
|
||||||
- name: build
|
- name: build
|
||||||
run: make build
|
run: make build
|
||||||
- name: test
|
- name: test
|
||||||
run: make test
|
run: make test
|
||||||
|
- name: dockerfile lint check
|
||||||
|
uses: https://github.com/hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ coverage.txt
|
|||||||
# MS VSCode
|
# MS VSCode
|
||||||
.vscode
|
.vscode
|
||||||
__debug_bin
|
__debug_bin
|
||||||
|
# gorelease binary folder
|
||||||
|
dist
|
||||||
|
|||||||
@@ -71,9 +71,8 @@ builds:
|
|||||||
no_unique_dist_dir: true
|
no_unique_dist_dir: true
|
||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- cmd: tar -cJf {{ .Path }}.xz {{ .Path }}
|
- cmd: xz -k -9 {{ .Path }}
|
||||||
env:
|
dir: ./dist/
|
||||||
- XZ_OPT=-9
|
|
||||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||||
|
|
||||||
@@ -101,11 +100,16 @@ snapshot:
|
|||||||
name_template: "{{ .Branch }}-devel"
|
name_template: "{{ .Branch }}-devel"
|
||||||
|
|
||||||
nightly:
|
nightly:
|
||||||
name_template: "{{ .Branch }}"
|
name_template: "nightly"
|
||||||
|
|
||||||
gitea_urls:
|
gitea_urls:
|
||||||
api: https://gitea.com/api/v1
|
api: https://gitea.com/api/v1
|
||||||
download: https://gitea.com
|
download: https://gitea.com
|
||||||
|
|
||||||
|
release:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
- glob: ./**.xz.sha256
|
||||||
|
|
||||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,17 +1,18 @@
|
|||||||
FROM golang:alpine as builder
|
FROM golang:1.20-alpine3.17 as builder
|
||||||
RUN apk add --update-cache make git
|
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||||
|
RUN apk add --no-cache make=4.3-r1 git=2.38.5-r0
|
||||||
|
|
||||||
COPY . /opt/src/act_runner
|
COPY . /opt/src/act_runner
|
||||||
WORKDIR /opt/src/act_runner
|
WORKDIR /opt/src/act_runner
|
||||||
|
|
||||||
RUN make clean && make build
|
RUN make clean && make build
|
||||||
|
|
||||||
FROM alpine as runner
|
FROM alpine:3.17
|
||||||
RUN apk add --update-cache \
|
RUN apk add --no-cache \
|
||||||
git bash \
|
git=2.38.5-r0 bash=5.2.15-r0 tini=0.19.0-r1 \
|
||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||||
COPY run.sh /opt/act/run.sh
|
COPY run.sh /opt/act/run.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/opt/act/run.sh"]
|
ENTRYPOINT ["/sbin/tini","--","/opt/act/run.sh"]
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -65,6 +65,9 @@ else
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||||
|
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
|
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ test: fmt-check
|
|||||||
vet:
|
vet:
|
||||||
@echo "Running go vet..."
|
@echo "Running go vet..."
|
||||||
@$(GO) build code.gitea.io/gitea-vet
|
@$(GO) build code.gitea.io/gitea-vet
|
||||||
@$(GO) vet -vettool=gitea-vet ./...
|
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_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)'
|
||||||
@@ -162,7 +165,10 @@ release-compress: | $(DIST_DIRS)
|
|||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker:
|
docker:
|
||||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
if ! docker buildx version >/dev/null 2>&1; then \
|
||||||
|
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||||
|
fi; \
|
||||||
|
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(GO) clean -x -i ./...
|
$(GO) clean -x -i ./...
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -10,7 +10,7 @@ Docker Engine Community version is required for docker mode. To install Docker C
|
|||||||
|
|
||||||
### Download pre-built binary
|
### Download pre-built binary
|
||||||
|
|
||||||
Visit https://dl.gitea.com/act_runner/ and download the right version for your platform.
|
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
@@ -85,11 +85,9 @@ You can specify the configuration file path with `-c`/`--config` argument.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner -c config.yaml register # register with config file
|
./act_runner -c config.yaml register # register with config file
|
||||||
./act_runner -c config.yaml deamon # run with config file
|
./act_runner -c config.yaml daemon # run with config file
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run a docker container
|
### Example Deployments
|
||||||
|
|
||||||
```sh
|
Check out the [examples](examples) directory for sample deployment types.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|||||||
16
examples/README.md
Normal file
16
examples/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
## Usage Examples for `act_runner`
|
||||||
|
|
||||||
|
Here you will find usage and deployment examples that can be directly used in a Gitea setup. Please feel free to contribute!
|
||||||
|
|
||||||
|
|
||||||
|
- [`docker`](docker)
|
||||||
|
Contains scripts and instructions for running containers on a workstation or server with Docker installed.
|
||||||
|
|
||||||
|
- [`docker-compose`](docker-compose)
|
||||||
|
Contains examples of using `docker-compose` to manage deployments.
|
||||||
|
|
||||||
|
- [`kubernetes`](kubernetes)
|
||||||
|
Contains examples of setting up deployments in Kubernetes clusters.
|
||||||
|
|
||||||
|
- [`vm`](vm)
|
||||||
|
Contains examples for setting up virtual or physical servers.
|
||||||
20
examples/docker-compose/README.md
Normal file
20
examples/docker-compose/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
### Running `act_runner` using `docker-compose`
|
||||||
|
|
||||||
|
```yml
|
||||||
|
...
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea
|
||||||
|
...
|
||||||
|
|
||||||
|
runner:
|
||||||
|
image: gitea/act_runner
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- gitea
|
||||||
|
volumes:
|
||||||
|
- ./data/act_runner:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- GITEA_INSTANCE_URL=<instance url>
|
||||||
|
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
||||||
|
```
|
||||||
8
examples/docker/README.md
Normal file
8
examples/docker/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
### Run `act_runner` in 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 -v $PWD/data:/data --name my_runner gitea/act_runner:nightly
|
||||||
|
```
|
||||||
|
|
||||||
|
The `/data` directory inside the docker container contains the runner API keys after registration.
|
||||||
|
It must be persisted, otherwise the runner would try to register again, using the same, now defunct registration token.
|
||||||
8
examples/kubernetes/README.md
Normal file
8
examples/kubernetes/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
## Kubernetes Docker in Docker Deployment with `act_runner`
|
||||||
|
|
||||||
|
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
|
||||||
|
|
||||||
|
Files in this directory:
|
||||||
|
|
||||||
|
- [`dind-docker.yaml`](dind-docker.yaml)
|
||||||
|
How to create a Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.
|
||||||
78
examples/kubernetes/dind-docker.yaml
Normal file
78
examples/kubernetes/dind-docker.yaml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: act-runner-vol
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
storageClassName: standard
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
token: << base64 encoded registration token >>
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: runner-secret
|
||||||
|
type: Opaque
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: act-runner
|
||||||
|
name: act-runner
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: act-runner
|
||||||
|
strategy: {}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
labels:
|
||||||
|
app: act-runner
|
||||||
|
spec:
|
||||||
|
restartPolicy: Always
|
||||||
|
volumes:
|
||||||
|
- name: docker-certs
|
||||||
|
emptyDir: {}
|
||||||
|
- name: runner-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: act-runner-vol
|
||||||
|
containers:
|
||||||
|
- name: runner
|
||||||
|
image: gitea/act_runner:nightly
|
||||||
|
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
|
||||||
|
env:
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: tcp://localhost:2376
|
||||||
|
- name: DOCKER_CERT_PATH
|
||||||
|
value: /certs/client
|
||||||
|
- name: DOCKER_TLS_VERIFY
|
||||||
|
value: "1"
|
||||||
|
- name: GITEA_INSTANCE_URL
|
||||||
|
value: http://gitea-http.gitea.svc.cluster.local:3000
|
||||||
|
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: runner-secret
|
||||||
|
key: token
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-certs
|
||||||
|
mountPath: /certs
|
||||||
|
- name: runner-data
|
||||||
|
mountPath: /data
|
||||||
|
- name: daemon
|
||||||
|
image: docker:23.0.6-dind
|
||||||
|
env:
|
||||||
|
- name: DOCKER_TLS_CERTDIR
|
||||||
|
value: /certs
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-certs
|
||||||
|
mountPath: /certs
|
||||||
6
examples/vm/README.md
Normal file
6
examples/vm/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
## `act_runner` on Virtual or Physical Servers
|
||||||
|
|
||||||
|
Files in this directory:
|
||||||
|
|
||||||
|
- [`rootless-docker.md`](rootless-docker.md)
|
||||||
|
How to set up a rootless docker implementation of the runner.
|
||||||
87
examples/vm/rootless-docker.md
Normal file
87
examples/vm/rootless-docker.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
## Using Rootless Docker with`act_runner`
|
||||||
|
|
||||||
|
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
|
||||||
|
|
||||||
|
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
|
||||||
|
|
||||||
|
As `root`:
|
||||||
|
|
||||||
|
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
useradd -m rootless
|
||||||
|
passwd rootless
|
||||||
|
```
|
||||||
|
|
||||||
|
- Install [`docker-ce`](https://docs.docker.com/engine/install/)
|
||||||
|
- (Recommended) Disable the system-wide Docker daemon
|
||||||
|
|
||||||
|
``systemctl disable --now docker.service docker.socket``
|
||||||
|
|
||||||
|
As the `rootless` user:
|
||||||
|
|
||||||
|
- Follow the instructions for [enabling rootless mode](https://docs.docker.com/engine/security/rootless/)
|
||||||
|
- Add the following lines to the `/home/rootless/.bashrc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
|
||||||
|
export PATH=/home/rootless/bin:$PATH
|
||||||
|
export DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
- Reboot. Ensure that the Docker process is working.
|
||||||
|
- Create a directory for saving `act_runner` data between restarts
|
||||||
|
|
||||||
|
`mkdir /home/rootless/act_runner`
|
||||||
|
|
||||||
|
- Register the runner from the data directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/rootless/act_runner
|
||||||
|
act_runner register
|
||||||
|
```
|
||||||
|
|
||||||
|
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
act_runner generate-config >/home/rootless/act_runner/config
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Description=Gitea Actions runner
|
||||||
|
Documentation=https://gitea.com/gitea/act_runner
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
|
||||||
|
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||||
|
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config
|
||||||
|
ExecReload=/bin/kill -s HUP $MAINPID
|
||||||
|
WorkingDirectory=/home/rootless/act_runner
|
||||||
|
TimeoutSec=0
|
||||||
|
RestartSec=2
|
||||||
|
Restart=always
|
||||||
|
StartLimitBurst=3
|
||||||
|
StartLimitInterval=60s
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
LimitNPROC=infinity
|
||||||
|
LimitCORE=infinity
|
||||||
|
TasksMax=infinity
|
||||||
|
Delegate=yes
|
||||||
|
Type=notify
|
||||||
|
NotifyAccess=all
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
- Reboot
|
||||||
|
|
||||||
|
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea.
|
||||||
|
|
||||||
|
````bash
|
||||||
|
systemctl --user status act_runner
|
||||||
|
journalctl --user -xeu act_runner
|
||||||
89
go.mod
89
go.mod
@@ -3,109 +3,88 @@ module gitea.com/gitea/act_runner
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.2.0
|
code.gitea.io/actions-proto-go v0.2.1
|
||||||
code.gitea.io/gitea-vet v0.2.3-0.20230113022436-2b1561217fa5
|
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 v23.0.1+incompatible
|
github.com/docker/docker v23.0.4+incompatible
|
||||||
github.com/go-chi/chi/v5 v5.0.8
|
|
||||||
github.com/go-chi/render v1.0.2
|
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-isatty v0.0.17
|
github.com/mattn/go-isatty v0.0.18
|
||||||
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.7.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.2
|
||||||
golang.org/x/term v0.6.0
|
golang.org/x/term v0.7.0
|
||||||
golang.org/x/time v0.1.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
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gotest.tools/v3 v3.4.0
|
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/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/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/cloudflare/circl v1.1.0 // indirect
|
||||||
github.com/containerd/containerd v1.6.18 // indirect
|
github.com/containerd/containerd v1.6.20 // indirect
|
||||||
github.com/creack/pty v1.1.18 // indirect
|
github.com/creack/pty v1.1.18 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
github.com/docker/cli v23.0.4+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.7.0 // 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.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emirpasic/gods v1.12.0 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.15.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.4.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.6.2-0.20230411180853-ce62f3e9ff86 // 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/snappy v0.0.4 // indirect
|
|
||||||
github.com/google/go-cmp v0.5.9 // 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.15 // indirect
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // 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/klauspost/compress v1.15.12 // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // 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.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
github.com/moby/buildkit v0.11.6 // indirect
|
||||||
github.com/moby/buildkit v0.11.4 // indirect
|
|
||||||
github.com/moby/patternmatcher v0.5.0 // indirect
|
github.com/moby/patternmatcher v0.5.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.5.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.1.0-rc2 // indirect
|
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
|
||||||
github.com/opencontainers/runc v1.1.3 // indirect
|
github.com/opencontainers/runc v1.1.5 // indirect
|
||||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/rhysd/actionlint v1.6.24 // indirect
|
||||||
github.com/rhysd/actionlint v1.6.23 // indirect
|
github.com/rivo/uniseg v0.4.4 // 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/skeema/knownhosts v1.1.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/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // 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 v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
golang.org/x/crypto v0.2.0 // indirect
|
go.etcd.io/bbolt v1.3.7 // indirect
|
||||||
golang.org/x/mod v0.4.2 // indirect
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
golang.org/x/net v0.7.0 // indirect
|
golang.org/x/net v0.9.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
golang.org/x/tools v0.1.5 // indirect
|
golang.org/x/tools v0.8.0 // 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
|
||||||
lukechampine.com/uint128 v1.1.1 // indirect
|
|
||||||
modernc.org/cc/v3 v3.35.18 // indirect
|
|
||||||
modernc.org/ccgo/v3 v3.12.82 // indirect
|
|
||||||
modernc.org/libc v1.11.87 // indirect
|
|
||||||
modernc.org/mathutil v1.4.1 // indirect
|
|
||||||
modernc.org/memory v1.0.5 // indirect
|
|
||||||
modernc.org/opt v0.1.1 // indirect
|
|
||||||
modernc.org/strutil v1.1.1 // indirect
|
|
||||||
modernc.org/token v1.0.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/nektos/act => gitea.com/gitea/act v0.243.2-0.20230323041428-929ea6df751b
|
replace github.com/nektos/act => gitea.com/gitea/act v0.245.2-0.20230516060355-9283cfc9b166
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
// Package artifactcache provides a cache handler for the runner.
|
|
||||||
//
|
|
||||||
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
|
|
||||||
//
|
|
||||||
// TODO: Authorization
|
|
||||||
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
|
|
||||||
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
|
||||||
|
|
||||||
package artifactcache
|
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package artifactcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
"xorm.io/builder"
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
urlBase = "/_apis/artifactcache"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = log.StandardLogger().WithField("module", "cache_request")
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
engine engine
|
|
||||||
storage *Storage
|
|
||||||
router *chi.Mux
|
|
||||||
listener net.Listener
|
|
||||||
|
|
||||||
gc atomic.Bool
|
|
||||||
gcAt time.Time
|
|
||||||
|
|
||||||
outboundIP string
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartHandler(dir, outboundIP string, port uint16) (*Handler, error) {
|
|
||||||
h := &Handler{}
|
|
||||||
|
|
||||||
if dir == "" {
|
|
||||||
if home, err := os.UserHomeDir(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
dir = filepath.Join(home, ".cache", "actcache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e, err := xorm.NewEngine("sqlite", filepath.Join(dir, "sqlite.db"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := e.Sync(&Cache{}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.engine = engine{e: e}
|
|
||||||
|
|
||||||
storage, err := NewStorage(filepath.Join(dir, "cache"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.storage = storage
|
|
||||||
|
|
||||||
if outboundIP != "" {
|
|
||||||
h.outboundIP = outboundIP
|
|
||||||
} else if ip, err := getOutboundIP(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
h.outboundIP = ip.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
router := chi.NewRouter()
|
|
||||||
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger}))
|
|
||||||
router.Use(func(handler http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
go h.gcCache()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
router.Use(middleware.Logger)
|
|
||||||
router.Route(urlBase, func(r chi.Router) {
|
|
||||||
r.Get("/cache", h.find)
|
|
||||||
r.Route("/caches", func(r chi.Router) {
|
|
||||||
r.Post("/", h.reserve)
|
|
||||||
r.Route("/{id}", func(r chi.Router) {
|
|
||||||
r.Patch("/", h.upload)
|
|
||||||
r.Post("/", h.commit)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
r.Get("/artifacts/{id}", h.get)
|
|
||||||
r.Post("/clean", h.clean)
|
|
||||||
})
|
|
||||||
|
|
||||||
h.router = router
|
|
||||||
|
|
||||||
h.gcCache()
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
if err := http.Serve(listener, h.router); err != nil {
|
|
||||||
logger.Errorf("http serve: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
h.listener = listener
|
|
||||||
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ExternalURL() string {
|
|
||||||
// TODO: make the external url configurable if necessary
|
|
||||||
return fmt.Sprintf("http://%s:%d",
|
|
||||||
h.outboundIP,
|
|
||||||
h.listener.Addr().(*net.TCPAddr).Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /_apis/artifactcache/cache
|
|
||||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request) {
|
|
||||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
|
||||||
version := r.URL.Query().Get("version")
|
|
||||||
|
|
||||||
cache, err := h.findCache(r.Context(), keys, version)
|
|
||||||
if err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cache == nil {
|
|
||||||
responseJson(w, r, 204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := h.storage.Exist(cache.ID); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
} else if !ok {
|
|
||||||
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Delete(cache)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
responseJson(w, r, 204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responseJson(w, r, 200, map[string]any{
|
|
||||||
"result": "hit",
|
|
||||||
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
|
||||||
"cacheKey": cache.Key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /_apis/artifactcache/caches
|
|
||||||
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cache := &Cache{}
|
|
||||||
if err := render.Bind(r, cache); err != nil {
|
|
||||||
responseJson(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
|
||||||
return sess.Where(builder.Eq{"key": cache.Key, "version": cache.Version}).Get(&Cache{})
|
|
||||||
}); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
} else if ok {
|
|
||||||
responseJson(w, r, 400, fmt.Errorf("already exist"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Insert(cache)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responseJson(w, r, 200, map[string]any{
|
|
||||||
"cacheId": cache.ID,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// PATCH /_apis/artifactcache/caches/:id
|
|
||||||
func (h *Handler) upload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
responseJson(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := &Cache{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
|
||||||
return sess.Get(cache)
|
|
||||||
}); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
} else if !ok {
|
|
||||||
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cache.Complete {
|
|
||||||
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
|
||||||
if err != nil {
|
|
||||||
responseJson(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
}
|
|
||||||
h.useCache(r.Context(), id)
|
|
||||||
responseJson(w, r, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /_apis/artifactcache/caches/:id
|
|
||||||
func (h *Handler) commit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
responseJson(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := &Cache{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
|
||||||
return sess.Get(cache)
|
|
||||||
}); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
} else if !ok {
|
|
||||||
responseJson(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cache.Complete {
|
|
||||||
responseJson(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.storage.Commit(cache.ID, cache.Size); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.Complete = true
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.ID(cache.ID).Cols("complete").Update(cache)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
responseJson(w, r, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseJson(w, r, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /_apis/artifactcache/artifacts/:id
|
|
||||||
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
responseJson(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.useCache(r.Context(), id)
|
|
||||||
h.storage.Serve(w, r, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /_apis/artifactcache/clean
|
|
||||||
func (h *Handler) clean(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO: don't support force deleting cache entries
|
|
||||||
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
|
||||||
|
|
||||||
responseJson(w, r, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not found, return (nil, nil) instead of an error.
|
|
||||||
func (h *Handler) findCache(ctx context.Context, keys []string, version string) (*Cache, error) {
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
key := keys[0] // the first key is for exact match.
|
|
||||||
|
|
||||||
cache := &Cache{}
|
|
||||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
|
||||||
return sess.Where(builder.Eq{"key": key, "version": version, "complete": true}).Get(cache)
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if ok {
|
|
||||||
return cache, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prefix := range keys[1:] {
|
|
||||||
if ok, err := h.engine.ExecBool(func(sess *xorm.Session) (bool, error) {
|
|
||||||
return sess.Where(builder.And(
|
|
||||||
builder.Like{"key", prefix + "%"},
|
|
||||||
builder.Eq{"version": version, "complete": true},
|
|
||||||
)).OrderBy("id DESC").Get(cache)
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if ok {
|
|
||||||
return cache, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) useCache(ctx context.Context, id int64) {
|
|
||||||
// keep quiet
|
|
||||||
_ = h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Context(ctx).Cols("used_at").Update(&Cache{
|
|
||||||
ID: id,
|
|
||||||
UsedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) gcCache() {
|
|
||||||
if h.gc.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !h.gc.CompareAndSwap(false, true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer h.gc.Store(false)
|
|
||||||
|
|
||||||
if time.Since(h.gcAt) < time.Hour {
|
|
||||||
logger.Infof("skip gc: %v", h.gcAt.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.gcAt = time.Now()
|
|
||||||
logger.Infof("gc: %v", h.gcAt.String())
|
|
||||||
|
|
||||||
const (
|
|
||||||
keepUsed = 30 * 24 * time.Hour
|
|
||||||
keepUnused = 7 * 24 * time.Hour
|
|
||||||
keepTemp = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
var caches []*Cache
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
return sess.Where(builder.And(builder.Lt{"used_at": time.Now().Add(-keepTemp).Unix()}, builder.Eq{"complete": false})).
|
|
||||||
Find(&caches)
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("find caches: %v", err)
|
|
||||||
} else {
|
|
||||||
for _, cache := range caches {
|
|
||||||
h.storage.Remove(cache.ID)
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Delete(cache)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("delete cache: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("deleted cache: %+v", cache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caches = caches[:0]
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
return sess.Where(builder.Lt{"used_at": time.Now().Add(-keepUnused).Unix()}).
|
|
||||||
Find(&caches)
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("find caches: %v", err)
|
|
||||||
} else {
|
|
||||||
for _, cache := range caches {
|
|
||||||
h.storage.Remove(cache.ID)
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Delete(cache)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("delete cache: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("deleted cache: %+v", cache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caches = caches[:0]
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
return sess.Where(builder.Lt{"created_at": time.Now().Add(-keepUsed).Unix()}).
|
|
||||||
Find(&caches)
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("find caches: %v", err)
|
|
||||||
} else {
|
|
||||||
for _, cache := range caches {
|
|
||||||
h.storage.Remove(cache.ID)
|
|
||||||
if err := h.engine.Exec(func(sess *xorm.Session) error {
|
|
||||||
_, err := sess.Delete(cache)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
logger.Warnf("delete cache: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("deleted cache: %+v", cache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package artifactcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cache struct {
|
|
||||||
ID int64 `xorm:"id pk autoincr" json:"-"`
|
|
||||||
Key string `xorm:"TEXT index unique(key_version)" json:"key"`
|
|
||||||
Version string `xorm:"TEXT unique(key_version)" json:"version"`
|
|
||||||
Size int64 `json:"cacheSize"`
|
|
||||||
Complete bool `xorm:"index(complete_used_at)" json:"-"`
|
|
||||||
UsedAt int64 `xorm:"index(complete_used_at) updated" json:"-"`
|
|
||||||
CreatedAt int64 `xorm:"index created" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind implements render.Binder
|
|
||||||
func (c *Cache) Bind(_ *http.Request) error {
|
|
||||||
if c.Key == "" {
|
|
||||||
return fmt.Errorf("missing key")
|
|
||||||
}
|
|
||||||
if c.Version == "" {
|
|
||||||
return fmt.Errorf("missing version")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package artifactcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
rootDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStorage(rootDir string) (*Storage, error) {
|
|
||||||
if err := os.MkdirAll(rootDir, 0o755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Storage{
|
|
||||||
rootDir: rootDir,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Exist(id int64) (bool, error) {
|
|
||||||
name := s.filename(id)
|
|
||||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Write(id int64, offset int64, reader io.Reader) error {
|
|
||||||
name := s.tempName(id, offset)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := os.Create(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(file, reader)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Commit(id int64, size int64) error {
|
|
||||||
defer func() {
|
|
||||||
_ = os.RemoveAll(s.tempDir(id))
|
|
||||||
}()
|
|
||||||
|
|
||||||
name := s.filename(id)
|
|
||||||
tempNames, err := s.tempNames(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := os.Create(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var written int64
|
|
||||||
for _, v := range tempNames {
|
|
||||||
f, err := os.Open(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := io.Copy(file, f)
|
|
||||||
_ = f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
written += n
|
|
||||||
}
|
|
||||||
|
|
||||||
if written != size {
|
|
||||||
_ = file.Close()
|
|
||||||
_ = os.Remove(name)
|
|
||||||
return fmt.Errorf("broken file: %v != %v", written, size)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id int64) {
|
|
||||||
name := s.filename(id)
|
|
||||||
http.ServeFile(w, r, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Remove(id int64) {
|
|
||||||
_ = os.Remove(s.filename(id))
|
|
||||||
_ = os.RemoveAll(s.tempDir(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) filename(id int64) string {
|
|
||||||
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempDir(id int64) string {
|
|
||||||
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempName(id, offset int64) string {
|
|
||||||
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempNames(id int64) ([]string, error) {
|
|
||||||
dir := s.tempDir(id)
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for _, v := range files {
|
|
||||||
if !v.IsDir() {
|
|
||||||
names = append(names, filepath.Join(dir, v.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names, nil
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package artifactcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func responseJson(w http.ResponseWriter, r *http.Request, code int, v ...any) {
|
|
||||||
render.Status(r, code)
|
|
||||||
if len(v) == 0 || v[0] == nil {
|
|
||||||
render.JSON(w, r, struct{}{})
|
|
||||||
} else if err, ok := v[0].(error); ok {
|
|
||||||
logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
|
||||||
render.JSON(w, r, map[string]any{
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
render.JSON(w, r, v[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContentRange(s string) (int64, int64, error) {
|
|
||||||
// support the format like "bytes 11-22/*" only
|
|
||||||
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
|
|
||||||
s1, s2, _ := strings.Cut(s, "-")
|
|
||||||
|
|
||||||
start, err := strconv.ParseInt(s1, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
|
||||||
}
|
|
||||||
stop, err := strconv.ParseInt(s2, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
|
||||||
}
|
|
||||||
return start, stop, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOutboundIP() (net.IP, error) {
|
|
||||||
// FIXME: It makes more sense to use the gateway IP address of container network
|
|
||||||
if conn, err := net.Dial("udp", "8.8.8.8:80"); err == nil {
|
|
||||||
defer conn.Close()
|
|
||||||
return conn.LocalAddr().(*net.UDPAddr).IP, nil
|
|
||||||
}
|
|
||||||
if ifaces, err := net.Interfaces(); err == nil {
|
|
||||||
for _, i := range ifaces {
|
|
||||||
if addrs, err := i.Addrs(); err == nil {
|
|
||||||
for _, addr := range addrs {
|
|
||||||
var ip net.IP
|
|
||||||
switch v := addr.(type) {
|
|
||||||
case *net.IPNet:
|
|
||||||
ip = v.IP
|
|
||||||
case *net.IPAddr:
|
|
||||||
ip = v.IP
|
|
||||||
}
|
|
||||||
if ip.IsGlobalUnicast() {
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("no outbound IP address found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// engine is a wrapper of *xorm.Engine, with a lock.
|
|
||||||
// To avoid racing of sqlite, we don't care performance here.
|
|
||||||
type engine struct {
|
|
||||||
e *xorm.Engine
|
|
||||||
m sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engine) Exec(f func(*xorm.Session) error) error {
|
|
||||||
e.m.Lock()
|
|
||||||
defer e.m.Unlock()
|
|
||||||
|
|
||||||
sess := e.e.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
|
|
||||||
return f(sess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engine) ExecBool(f func(*xorm.Session) (bool, error)) (bool, error) {
|
|
||||||
e.m.Lock()
|
|
||||||
defer e.m.Unlock()
|
|
||||||
|
|
||||||
sess := e.e.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
|
|
||||||
return f(sess)
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -23,14 +27,13 @@ import (
|
|||||||
|
|
||||||
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
log.Infoln("Starting runner daemon")
|
|
||||||
|
|
||||||
cfg, err := config.LoadDefault(*configFile)
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid configuration: %w", err)
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
initLogging(cfg)
|
initLogging(cfg)
|
||||||
|
log.Infoln("Starting runner daemon")
|
||||||
|
|
||||||
reg, err := config.LoadRegistration(cfg.Runner.File)
|
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -79,10 +82,11 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
|||||||
// initLogging setup the global logrus logger.
|
// initLogging setup the global logrus logger.
|
||||||
func initLogging(cfg *config.Config) {
|
func initLogging(cfg *config.Config) {
|
||||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
log.SetFormatter(&log.TextFormatter{
|
format := &log.TextFormatter{
|
||||||
DisableColors: !isTerm,
|
DisableColors: !isTerm,
|
||||||
FullTimestamp: true,
|
FullTimestamp: true,
|
||||||
})
|
}
|
||||||
|
log.SetFormatter(format)
|
||||||
|
|
||||||
if l := cfg.Log.Level; l != "" {
|
if l := cfg.Log.Level; l != "" {
|
||||||
level, err := log.ParseLevel(l)
|
level, err := log.ParseLevel(l)
|
||||||
@@ -90,6 +94,22 @@ func initLogging(cfg *config.Config) {
|
|||||||
log.WithError(err).
|
log.WithError(err).
|
||||||
Errorf("invalid log level: %q", l)
|
Errorf("invalid log level: %q", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debug level
|
||||||
|
if level == log.DebugLevel {
|
||||||
|
log.SetReportCaller(true)
|
||||||
|
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||||
|
// get function name
|
||||||
|
s := strings.Split(f.Function, ".")
|
||||||
|
funcname := "[" + s[len(s)-1] + "]"
|
||||||
|
// get file name and line number
|
||||||
|
_, filename := path.Split(f.File)
|
||||||
|
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
|
||||||
|
return funcname, filename
|
||||||
|
}
|
||||||
|
log.SetFormatter(format)
|
||||||
|
}
|
||||||
|
|
||||||
if log.GetLevel() != level {
|
if log.GetLevel() != level {
|
||||||
log.Infof("log level changed to %v", level)
|
log.Infof("log level changed to %v", level)
|
||||||
log.SetLevel(level)
|
log.SetLevel(level)
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/nektos/act/pkg/artifactcache"
|
||||||
"github.com/nektos/act/pkg/artifacts"
|
"github.com/nektos/act/pkg/artifacts"
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
@@ -21,8 +23,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type executeArgs struct {
|
type executeArgs struct {
|
||||||
@@ -57,6 +57,7 @@ type executeArgs struct {
|
|||||||
dryrun bool
|
dryrun bool
|
||||||
image string
|
image string
|
||||||
cacheHandler *artifactcache.Handler
|
cacheHandler *artifactcache.Handler
|
||||||
|
network string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkflowsPath returns path to workflow file(s)
|
// WorkflowsPath returns path to workflow file(s)
|
||||||
@@ -314,7 +315,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
|
|
||||||
if len(execArgs.event) > 0 {
|
if len(execArgs.event) > 0 {
|
||||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||||
eventName = args[0]
|
eventName = execArgs.event
|
||||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||||
eventName = events[0]
|
eventName = events[0]
|
||||||
@@ -349,13 +350,31 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
// init a cache server
|
// init a cache server
|
||||||
handler, err := artifactcache.StartHandler("", "", 0)
|
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||||
execArgs.cacheHandler = handler
|
execArgs.cacheHandler = handler
|
||||||
|
|
||||||
|
if len(execArgs.artifactServerAddr) == 0 {
|
||||||
|
if ip := common.GetOutboundIP(); ip == nil {
|
||||||
|
return fmt.Errorf("unable to determine outbound IP address")
|
||||||
|
} else {
|
||||||
|
execArgs.artifactServerAddr = ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(execArgs.artifactServerPath) == 0 {
|
||||||
|
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
execArgs.artifactServerPath = tempDir
|
||||||
|
}
|
||||||
|
|
||||||
// run the plan
|
// run the plan
|
||||||
config := &runner.Config{
|
config := &runner.Config{
|
||||||
Workdir: execArgs.Workdir(),
|
Workdir: execArgs.Workdir(),
|
||||||
@@ -380,40 +399,29 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
ArtifactServerPath: execArgs.artifactServerPath,
|
ArtifactServerPath: execArgs.artifactServerPath,
|
||||||
ArtifactServerPort: execArgs.artifactServerPort,
|
ArtifactServerPort: execArgs.artifactServerPort,
|
||||||
|
ArtifactServerAddr: execArgs.artifactServerAddr,
|
||||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||||
// PresetGitHubContext: preset,
|
// PresetGitHubContext: preset,
|
||||||
// EventJSON: string(eventJSON),
|
// EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
||||||
ContainerMaxLifetime: maxLifetime,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
ContainerNetworkMode: "bridge",
|
ContainerNetworkMode: container.NetworkMode(execArgs.network),
|
||||||
DefaultActionInstance: execArgs.defaultActionsUrl,
|
DefaultActionInstance: execArgs.defaultActionsUrl,
|
||||||
PlatformPicker: func(_ []string) string {
|
PlatformPicker: func(_ []string) string {
|
||||||
return execArgs.image
|
return execArgs.image
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle log level config
|
if !execArgs.debug {
|
||||||
// waiting https://gitea.com/gitea/act/pulls/19
|
logLevel := log.Level(log.InfoLevel)
|
||||||
// if !execArgs.debug {
|
config.JobLoggerLevel = &logLevel
|
||||||
// logLevel := log.Level(log.InfoLevel)
|
}
|
||||||
// config.JobLoggerLevel = &logLevel
|
|
||||||
// }
|
|
||||||
|
|
||||||
r, err := runner.New(config)
|
r, err := runner.New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||||
|
|
||||||
@@ -460,12 +468,14 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
|||||||
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().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.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.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "Defines the address where the artifact server listens")
|
||||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
|
execCmd.PersistentFlags().StringVarP(&execArg.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().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.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.debug, "debug", "d", false, "enable debug log")
|
||||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
|
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
|
||||||
|
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
|
||||||
|
|
||||||
return execCmd
|
return execCmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,12 +13,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/nektos/act/pkg/artifactcache"
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/nektos/act/pkg/runner"
|
"github.com/nektos/act/pkg/runner"
|
||||||
log "github.com/sirupsen/logrus"
|
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/client"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
@@ -52,7 +52,12 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
|||||||
envs[k] = v
|
envs[k] = v
|
||||||
}
|
}
|
||||||
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
||||||
cacheHandler, err := artifactcache.StartHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port)
|
cacheHandler, err := artifactcache.StartHandler(
|
||||||
|
cfg.Cache.Dir,
|
||||||
|
cfg.Cache.Host,
|
||||||
|
cfg.Cache.Port,
|
||||||
|
log.StandardLogger().WithField("module", "cache_request"),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||||
// go on
|
// go on
|
||||||
@@ -112,16 +117,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
|
|
||||||
reporter.Logf("%s(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
|
reporter.Logf("%s(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))
|
workflow, jobID, err := generateWorkflow(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -172,9 +172,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
}
|
}
|
||||||
|
|
||||||
runnerConfig := &runner.Config{
|
runnerConfig := &runner.Config{
|
||||||
// On Linux, Workdir will be like "/<owner>/<repo>"
|
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||||
// On Windows, Workdir will be like "\<owner>\<repo>"
|
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||||
Workdir: filepath.FromSlash(string(filepath.Separator) + preset.Repository),
|
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", r.cfg.Container.WorkdirParent, preset.Repository)),
|
||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
@@ -184,18 +184,19 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
JSONLogger: false,
|
JSONLogger: false,
|
||||||
Env: r.envs,
|
Env: r.envs,
|
||||||
Secrets: task.Secrets,
|
Secrets: task.Secrets,
|
||||||
GitHubInstance: r.client.Address(),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
NoSkipCheckout: true,
|
NoSkipCheckout: true,
|
||||||
PresetGitHubContext: preset,
|
PresetGitHubContext: preset,
|
||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
ContainerMaxLifetime: maxLifetime,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
ContainerNetworkMode: r.cfg.Container.NetworkMode,
|
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||||
ContainerOptions: r.cfg.Container.Options,
|
ContainerOptions: r.cfg.Container.Options,
|
||||||
Privileged: r.cfg.Container.Privileged,
|
Privileged: r.cfg.Container.Privileged,
|
||||||
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||||
PlatformPicker: r.labels.PickPlatform,
|
PlatformPicker: r.labels.PickPlatform,
|
||||||
|
Vars: task.Vars,
|
||||||
}
|
}
|
||||||
|
|
||||||
rr, err := runner.New(runnerConfig)
|
rr, err := runner.New(runnerConfig)
|
||||||
@@ -209,5 +210,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
// add logger recorders
|
// add logger recorders
|
||||||
ctx = common.WithLoggerHook(ctx, reporter)
|
ctx = common.WithLoggerHook(ctx, reporter)
|
||||||
|
|
||||||
return executor(ctx)
|
execErr := executor(ctx)
|
||||||
|
reporter.SetOutputs(job.Outputs)
|
||||||
|
return execErr
|
||||||
}
|
}
|
||||||
|
|||||||
54
internal/app/run/workflow.go
Normal file
54
internal/app/run/workflow.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
|
||||||
|
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jobIDs := workflow.GetJobIDs()
|
||||||
|
if len(jobIDs) != 1 {
|
||||||
|
return nil, "", fmt.Errorf("multiple jobs found: %v", jobIDs)
|
||||||
|
}
|
||||||
|
jobID := jobIDs[0]
|
||||||
|
|
||||||
|
needJobIDs := make([]string, 0, len(task.Needs))
|
||||||
|
for id, need := range task.Needs {
|
||||||
|
needJobIDs = append(needJobIDs, id)
|
||||||
|
needJob := &model.Job{
|
||||||
|
Outputs: need.Outputs,
|
||||||
|
Result: strings.ToLower(strings.TrimPrefix(need.Result.String(), "RESULT_")),
|
||||||
|
}
|
||||||
|
workflow.Jobs[id] = needJob
|
||||||
|
}
|
||||||
|
sort.Strings(needJobIDs)
|
||||||
|
|
||||||
|
rawNeeds := yaml.Node{
|
||||||
|
Kind: yaml.SequenceNode,
|
||||||
|
Content: make([]*yaml.Node, 0, len(needJobIDs)),
|
||||||
|
}
|
||||||
|
for _, id := range needJobIDs {
|
||||||
|
rawNeeds.Content = append(rawNeeds.Content, &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Value: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow.Jobs[jobID].RawNeeds = rawNeeds
|
||||||
|
|
||||||
|
return workflow, jobID, nil
|
||||||
|
}
|
||||||
74
internal/app/run/workflow_test.go
Normal file
74
internal/app/run/workflow_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_generateWorkflow(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
task *runnerv1.Task
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
assert func(t *testing.T, wf *model.Workflow)
|
||||||
|
want1 string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "has needs",
|
||||||
|
args: args{
|
||||||
|
task: &runnerv1.Task{
|
||||||
|
WorkflowPayload: []byte(`
|
||||||
|
name: Build and deploy
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
job9:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: ./deploy --build ${{ needs.job1.outputs.output1 }}
|
||||||
|
- run: ./deploy --build ${{ needs.job2.outputs.output2 }}
|
||||||
|
`),
|
||||||
|
Needs: map[string]*runnerv1.TaskNeed{
|
||||||
|
"job1": {
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output1": "output1 value",
|
||||||
|
},
|
||||||
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
},
|
||||||
|
"job2": {
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output2": "output2 value",
|
||||||
|
},
|
||||||
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
|
assert.DeepEqual(t, wf.GetJob("job9").Needs(), []string{"job1", "job2"})
|
||||||
|
},
|
||||||
|
want1: "job9",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1, err := generateWorkflow(tt.args.task)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tt.assert(t, got)
|
||||||
|
assert.Equal(t, got1, tt.want1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// A Client manages communication with the runner.
|
// A Client manages communication with the runner.
|
||||||
|
//
|
||||||
|
//go:generate mockery --name Client
|
||||||
type Client interface {
|
type Client interface {
|
||||||
pingv1connect.PingServiceClient
|
pingv1connect.PingServiceClient
|
||||||
runnerv1connect.RunnerServiceClient
|
runnerv1connect.RunnerServiceClient
|
||||||
|
|||||||
193
internal/pkg/client/mocks/Client.go
Normal file
193
internal/pkg/client/mocks/Client.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
// Code generated by mockery v2.26.1. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
connect "github.com/bufbuild/connect-go"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is an autogenerated mock type for the Client type
|
||||||
|
type Client struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address provides a mock function with given fields:
|
||||||
|
func (_m *Client) Address() string {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
if rf, ok := ret.Get(0).(func() string); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchTask provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) FetchTask(_a0 context.Context, _a1 *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *connect.Response[runnerv1.FetchTaskResponse]
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) *connect.Response[runnerv1.FetchTaskResponse]); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*connect.Response[runnerv1.FetchTaskResponse])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insecure provides a mock function with given fields:
|
||||||
|
func (_m *Client) Insecure() bool {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) Ping(_a0 context.Context, _a1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *connect.Response[pingv1.PingResponse]
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) *connect.Response[pingv1.PingResponse]); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*connect.Response[pingv1.PingResponse])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[pingv1.PingRequest]) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) Register(_a0 context.Context, _a1 *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *connect.Response[runnerv1.RegisterResponse]
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) *connect.Response[runnerv1.RegisterResponse]); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*connect.Response[runnerv1.RegisterResponse])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLog provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) UpdateLog(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *connect.Response[runnerv1.UpdateLogResponse]
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) *connect.Response[runnerv1.UpdateLogResponse]); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateLogResponse])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTask provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) UpdateTask(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *connect.Response[runnerv1.UpdateTaskResponse]
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) *connect.Response[runnerv1.UpdateTaskResponse]); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateTaskResponse])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewClient interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewClient(t mockConstructorTestingTNewClient) *Client {
|
||||||
|
mock := &Client{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -42,9 +42,14 @@ cache:
|
|||||||
port: 0
|
port: 0
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# Which network to use for the job containers. Could be bridge, host, none, or the name of a custom network.
|
# Specifies the network to which the container will connect.
|
||||||
network_mode: bridge
|
# Could be host, bridge or the name of a custom network.
|
||||||
|
# If it's empty, act_runner will create a network automatically.
|
||||||
|
network: ""
|
||||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||||
privileged: false
|
privileged: false
|
||||||
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||||
options:
|
options:
|
||||||
|
# The parent directory of a job's working directory.
|
||||||
|
# If it's empty, /workspace will be used.
|
||||||
|
workdir_parent:
|
||||||
|
|||||||
@@ -10,34 +10,50 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Log represents the configuration for logging.
|
||||||
|
type Log struct {
|
||||||
|
Level string `yaml:"level"` // Level indicates the logging level.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runner represents the configuration for the runner.
|
||||||
|
type Runner struct {
|
||||||
|
File string `yaml:"file"` // File specifies the file path for the runner.
|
||||||
|
Capacity int `yaml:"capacity"` // Capacity specifies the capacity of the runner.
|
||||||
|
Envs map[string]string `yaml:"envs"` // Envs stores environment variables for the runner.
|
||||||
|
EnvFile string `yaml:"env_file"` // EnvFile specifies the path to the file containing environment variables for the runner.
|
||||||
|
Timeout time.Duration `yaml:"timeout"` // Timeout specifies the duration for runner timeout.
|
||||||
|
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
|
||||||
|
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
|
||||||
|
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache represents the configuration for caching.
|
||||||
|
type Cache struct {
|
||||||
|
Enabled *bool `yaml:"enabled"` // Enabled indicates whether caching is enabled. It is a pointer to distinguish between false and not set. If not set, it will be true.
|
||||||
|
Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
|
||||||
|
Host string `yaml:"host"` // Host specifies the caching host.
|
||||||
|
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container represents the configuration for the container.
|
||||||
|
type Container struct {
|
||||||
|
Network string `yaml:"network"` // Network specifies the network for the container.
|
||||||
|
NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
|
||||||
|
Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
|
||||||
|
Options string `yaml:"options"` // Options specifies additional options for the container.
|
||||||
|
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Log struct {
|
Log Log `yaml:"log"` // Log represents the configuration for logging.
|
||||||
Level string `yaml:"level"`
|
Runner Runner `yaml:"runner"` // Runner represents the configuration for the runner.
|
||||||
} `yaml:"log"`
|
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
|
||||||
Runner struct {
|
Container Container `yaml:"container"` // Container represents the configuration for the container.
|
||||||
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.
|
// LoadDefault returns the default configuration.
|
||||||
@@ -91,8 +107,8 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.Container.NetworkMode == "" {
|
if cfg.Container.WorkdirParent == "" {
|
||||||
cfg.Container.NetworkMode = "bridge"
|
cfg.Container.WorkdirParent = "workspace"
|
||||||
}
|
}
|
||||||
if cfg.Runner.FetchTimeout <= 0 {
|
if cfg.Runner.FetchTimeout <= 0 {
|
||||||
cfg.Runner.FetchTimeout = 5 * time.Second
|
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||||
@@ -101,5 +117,18 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
cfg.Runner.FetchInterval = 2 * time.Second
|
cfg.Runner.FetchInterval = 2 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
||||||
|
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||||
|
log.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
|
||||||
|
if cfg.Container.NetworkMode == "bridge" {
|
||||||
|
// Previously, if the value of `container.network_mode` is `bridge`, we will create a new network for job.
|
||||||
|
// But “bridge” is easily confused with the bridge network created by Docker by default.
|
||||||
|
// So we set the value of `container.network` to empty string to make `act_runner` automatically create a new network for job.
|
||||||
|
cfg.Container.Network = ""
|
||||||
|
} else {
|
||||||
|
cfg.Container.Network = cfg.Container.NetworkMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package report
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,8 +32,14 @@ type Reporter struct {
|
|||||||
logOffset int
|
logOffset int
|
||||||
logRows []*runnerv1.LogRow
|
logRows []*runnerv1.LogRow
|
||||||
logReplacer *strings.Replacer
|
logReplacer *strings.Replacer
|
||||||
state *runnerv1.TaskState
|
oldnew []string
|
||||||
stateM sync.RWMutex
|
|
||||||
|
state *runnerv1.TaskState
|
||||||
|
stateMu sync.RWMutex
|
||||||
|
outputs sync.Map
|
||||||
|
|
||||||
|
debugOutputEnabled bool
|
||||||
|
stopCommandEndToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||||
@@ -44,20 +51,27 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
|||||||
oldnew = append(oldnew, v, "***")
|
oldnew = append(oldnew, v, "***")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Reporter{
|
rv := &Reporter{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
client: client,
|
client: client,
|
||||||
|
oldnew: oldnew,
|
||||||
logReplacer: strings.NewReplacer(oldnew...),
|
logReplacer: strings.NewReplacer(oldnew...),
|
||||||
state: &runnerv1.TaskState{
|
state: &runnerv1.TaskState{
|
||||||
Id: task.Id,
|
Id: task.Id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||||
|
rv.debugOutputEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) ResetSteps(l int) {
|
func (r *Reporter) ResetSteps(l int) {
|
||||||
r.stateM.Lock()
|
r.stateMu.Lock()
|
||||||
defer r.stateM.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
for i := 0; i < l; i++ {
|
for i := 0; i < l; i++ {
|
||||||
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
||||||
Id: int64(i),
|
Id: int64(i),
|
||||||
@@ -69,9 +83,16 @@ func (r *Reporter) Levels() []log.Level {
|
|||||||
return log.AllLevels
|
return log.AllLevels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||||
|
if v != nil {
|
||||||
|
return append(s, v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
r.stateM.Lock()
|
r.stateMu.Lock()
|
||||||
defer r.stateM.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
|
|
||||||
log.WithFields(entry.Data).Trace(entry.Message)
|
log.WithFields(entry.Data).Trace(entry.Message)
|
||||||
|
|
||||||
@@ -95,7 +116,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -108,7 +129,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
if step == nil {
|
if step == nil {
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -118,14 +139,16 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
if v, ok := entry.Data["raw_output"]; ok {
|
if v, ok := entry.Data["raw_output"]; ok {
|
||||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||||
if step.LogLength == 0 {
|
if row := r.parseLogRow(entry); row != nil {
|
||||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
if step.LogLength == 0 {
|
||||||
|
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||||
|
}
|
||||||
|
step.LogLength++
|
||||||
|
r.logRows = append(r.logRows, row)
|
||||||
}
|
}
|
||||||
step.LogLength++
|
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
|
||||||
}
|
}
|
||||||
} else if !r.duringSteps() {
|
} else if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
if v, ok := entry.Data["stepResult"]; ok {
|
if v, ok := entry.Data["stepResult"]; ok {
|
||||||
if stepResult, ok := r.parseResult(v); ok {
|
if stepResult, ok := r.parseResult(v); ok {
|
||||||
@@ -155,9 +178,13 @@ func (r *Reporter) RunDaemon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||||
r.stateM.Lock()
|
r.stateMu.Lock()
|
||||||
defer r.stateM.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
|
|
||||||
|
r.logf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) logf(format string, a ...interface{}) {
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||||
Time: timestamppb.Now(),
|
Time: timestamppb.Now(),
|
||||||
@@ -166,10 +193,30 @@ func (r *Reporter) Logf(format string, a ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||||
|
r.stateMu.Lock()
|
||||||
|
defer r.stateMu.Unlock()
|
||||||
|
|
||||||
|
for k, v := range outputs {
|
||||||
|
if len(k) > 255 {
|
||||||
|
r.logf("ignore output because the key is too long: %q", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if l := len(v); l > 1024*1024 {
|
||||||
|
log.Println("ignore output because the value is too long:", k, l)
|
||||||
|
r.logf("ignore output because the value %q is too long: %d", k, l)
|
||||||
|
}
|
||||||
|
if _, ok := r.outputs.Load(k); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.outputs.Store(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) Close(lastWords string) error {
|
func (r *Reporter) Close(lastWords string) error {
|
||||||
r.closed = true
|
r.closed = true
|
||||||
|
|
||||||
r.stateM.Lock()
|
r.stateMu.Lock()
|
||||||
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||||
if lastWords == "" {
|
if lastWords == "" {
|
||||||
lastWords = "Early termination"
|
lastWords = "Early termination"
|
||||||
@@ -184,14 +231,14 @@ func (r *Reporter) Close(lastWords string) error {
|
|||||||
Time: timestamppb.Now(),
|
Time: timestamppb.Now(),
|
||||||
Content: lastWords,
|
Content: lastWords,
|
||||||
})
|
})
|
||||||
return nil
|
r.state.StoppedAt = timestamppb.Now()
|
||||||
} else if lastWords != "" {
|
} else if lastWords != "" {
|
||||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||||
Time: timestamppb.Now(),
|
Time: timestamppb.Now(),
|
||||||
Content: lastWords,
|
Content: lastWords,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
r.stateM.Unlock()
|
r.stateMu.Unlock()
|
||||||
|
|
||||||
return retry.Do(func() error {
|
return retry.Do(func() error {
|
||||||
if err := r.ReportLog(true); err != nil {
|
if err := r.ReportLog(true); err != nil {
|
||||||
@@ -205,9 +252,9 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
|||||||
r.clientM.Lock()
|
r.clientM.Lock()
|
||||||
defer r.clientM.Unlock()
|
defer r.clientM.Unlock()
|
||||||
|
|
||||||
r.stateM.RLock()
|
r.stateMu.RLock()
|
||||||
rows := r.logRows
|
rows := r.logRows
|
||||||
r.stateM.RUnlock()
|
r.stateMu.RUnlock()
|
||||||
|
|
||||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||||
TaskId: r.state.Id,
|
TaskId: r.state.Id,
|
||||||
@@ -224,10 +271,10 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
|||||||
return fmt.Errorf("submitted logs are lost")
|
return fmt.Errorf("submitted logs are lost")
|
||||||
}
|
}
|
||||||
|
|
||||||
r.stateM.Lock()
|
r.stateMu.Lock()
|
||||||
r.logRows = r.logRows[ack-r.logOffset:]
|
r.logRows = r.logRows[ack-r.logOffset:]
|
||||||
r.logOffset = ack
|
r.logOffset = ack
|
||||||
r.stateM.Unlock()
|
r.stateMu.Unlock()
|
||||||
|
|
||||||
if noMore && ack < r.logOffset+len(rows) {
|
if noMore && ack < r.logOffset+len(rows) {
|
||||||
return fmt.Errorf("not all logs are submitted")
|
return fmt.Errorf("not all logs are submitted")
|
||||||
@@ -240,21 +287,45 @@ func (r *Reporter) ReportState() error {
|
|||||||
r.clientM.Lock()
|
r.clientM.Lock()
|
||||||
defer r.clientM.Unlock()
|
defer r.clientM.Unlock()
|
||||||
|
|
||||||
r.stateM.RLock()
|
r.stateMu.RLock()
|
||||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||||
r.stateM.RUnlock()
|
r.stateMu.RUnlock()
|
||||||
|
|
||||||
|
outputs := make(map[string]string)
|
||||||
|
r.outputs.Range(func(k, v interface{}) bool {
|
||||||
|
if val, ok := v.(string); ok {
|
||||||
|
outputs[k.(string)] = val
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||||
State: state,
|
State: state,
|
||||||
|
Outputs: outputs,
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, k := range resp.Msg.SentOutputs {
|
||||||
|
r.outputs.Store(k, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||||
r.cancel()
|
r.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var noSent []string
|
||||||
|
r.outputs.Range(func(k, v interface{}) bool {
|
||||||
|
if _, ok := v.(string); ok {
|
||||||
|
noSent = append(noSent, k.(string))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if len(noSent) > 0 {
|
||||||
|
return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,11 +359,70 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
|||||||
return ret, ok
|
return ret, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||||
|
|
||||||
|
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||||
|
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||||
|
return &originalContent
|
||||||
|
}
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "add-mask":
|
||||||
|
r.addMask(value)
|
||||||
|
return nil
|
||||||
|
case "debug":
|
||||||
|
if r.debugOutputEnabled {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case "notice":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "warning":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "error":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "group":
|
||||||
|
// Returning the original content, because I think the frontend
|
||||||
|
// will use it when rendering the output.
|
||||||
|
return &originalContent
|
||||||
|
case "endgroup":
|
||||||
|
// Ditto
|
||||||
|
return &originalContent
|
||||||
|
case "stop-commands":
|
||||||
|
r.stopCommandEndToken = value
|
||||||
|
return nil
|
||||||
|
case r.stopCommandEndToken:
|
||||||
|
r.stopCommandEndToken = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &originalContent
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||||
|
|
||||||
|
matches := cmdRegex.FindStringSubmatch(content)
|
||||||
|
if matches != nil {
|
||||||
|
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
|
||||||
|
content = *output
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
content = r.logReplacer.Replace(content)
|
content = r.logReplacer.Replace(content)
|
||||||
|
|
||||||
return &runnerv1.LogRow{
|
return &runnerv1.LogRow{
|
||||||
Time: timestamppb.New(entry.Time),
|
Time: timestamppb.New(entry.Time),
|
||||||
Content: content,
|
Content: content,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) addMask(msg string) {
|
||||||
|
r.oldnew = append(r.oldnew, msg, "***")
|
||||||
|
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
||||||
|
}
|
||||||
|
|||||||
197
internal/pkg/report/reporter_test.go
Normal file
197
internal/pkg/report/reporter_test.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
connect_go "github.com/bufbuild/connect-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReporter_parseLogRow(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
debugOutputEnabled bool
|
||||||
|
args []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"No command", false,
|
||||||
|
[]string{"Hello, world!"},
|
||||||
|
[]string{"Hello, world!"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Add-mask", false,
|
||||||
|
[]string{
|
||||||
|
"foo mysecret bar",
|
||||||
|
"::add-mask::mysecret",
|
||||||
|
"foo mysecret bar",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"foo mysecret bar",
|
||||||
|
"<nil>",
|
||||||
|
"foo *** bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Debug enabled", true,
|
||||||
|
[]string{
|
||||||
|
"::debug::GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Debug not enabled", false,
|
||||||
|
[]string{
|
||||||
|
"::debug::GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"<nil>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"notice", false,
|
||||||
|
[]string{
|
||||||
|
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warning", false,
|
||||||
|
[]string{
|
||||||
|
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error", false,
|
||||||
|
[]string{
|
||||||
|
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group", false,
|
||||||
|
[]string{
|
||||||
|
"::group::",
|
||||||
|
"::endgroup::",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::group::",
|
||||||
|
"::endgroup::",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stop-commands", false,
|
||||||
|
[]string{
|
||||||
|
"::add-mask::foo",
|
||||||
|
"::stop-commands::myverycoolstoptoken",
|
||||||
|
"::add-mask::bar",
|
||||||
|
"::debug::Stuff",
|
||||||
|
"myverycoolstoptoken",
|
||||||
|
"::add-mask::baz",
|
||||||
|
"::myverycoolstoptoken::",
|
||||||
|
"::add-mask::wibble",
|
||||||
|
"foo bar baz wibble",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"<nil>",
|
||||||
|
"<nil>",
|
||||||
|
"::add-mask::bar",
|
||||||
|
"::debug::Stuff",
|
||||||
|
"myverycoolstoptoken",
|
||||||
|
"::add-mask::baz",
|
||||||
|
"<nil>",
|
||||||
|
"<nil>",
|
||||||
|
"*** bar baz ***",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unknown command", false,
|
||||||
|
[]string{
|
||||||
|
"::set-mask::foo",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::set-mask::foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &Reporter{
|
||||||
|
logReplacer: strings.NewReplacer(),
|
||||||
|
debugOutputEnabled: tt.debugOutputEnabled,
|
||||||
|
}
|
||||||
|
for idx, arg := range tt.args {
|
||||||
|
rv := r.parseLogRow(&log.Entry{Message: arg})
|
||||||
|
got := "<nil>"
|
||||||
|
|
||||||
|
if rv != nil {
|
||||||
|
got = rv.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want[idx], got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReporter_Fire(t *testing.T) {
|
||||||
|
t.Run("ignore command lines", func(t *testing.T) {
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
t.Logf("Received UpdateLog: %s", req.Msg.String())
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
})
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
t.Logf("Received UpdateTask: %s", req.Msg.String())
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
})
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]interface{}{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||||
|
Context: taskCtx,
|
||||||
|
})
|
||||||
|
defer func() {
|
||||||
|
assert.NoError(t, reporter.Close(""))
|
||||||
|
}()
|
||||||
|
reporter.ResetSteps(5)
|
||||||
|
|
||||||
|
dataStep0 := map[string]interface{}{
|
||||||
|
"stage": "Main",
|
||||||
|
"stepNumber": 0,
|
||||||
|
"raw_output": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
|
assert.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(3), reporter.state.Steps[0].LogLength)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user