mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 14:55:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf31b2d22 | ||
|
|
c8cc7b2448 | ||
|
|
3be962cdb3 | ||
|
|
a5edbc9ac4 | ||
|
|
66bab3d805 | ||
|
|
293926f5d5 | ||
|
|
43c5ba923f | ||
|
|
acc5afc428 | ||
|
|
27a1a90d25 | ||
|
|
83ec0ba909 | ||
|
|
ed86e2f15a | ||
|
|
d4bebccc12 | ||
|
|
c75b67e892 | ||
|
|
bc6031eff7 | ||
|
|
c69c353d93 | ||
|
|
fcc016e9b3 | ||
|
|
d5caee38f2 | ||
|
|
9e26208e13 | ||
|
|
a05c5ba3ad | ||
|
|
c248520a66 | ||
|
|
10d639cc6b | ||
|
|
5a8134410d | ||
|
|
b79c3aa1a3 | ||
|
|
9c6499ec08 | ||
|
|
d139faa40c | ||
|
|
220efa69c0 | ||
|
|
df3cb60978 | ||
|
|
7e7096e60b | ||
|
|
8eea12dd78 | ||
|
|
c8fad20f49 | ||
|
|
1596e4b1fd | ||
|
|
c9e076db68 | ||
|
|
bc1842d649 | ||
|
|
90b8cc6a7a | ||
|
|
4d5a35ac65 | ||
|
|
8f81f40d62 | ||
|
|
9f90cba993 | ||
|
|
48b05a0ca8 | ||
|
|
9eb8b08a69 | ||
|
|
4d868b7f3c | ||
|
|
63a57edaa3 | ||
|
|
5180cd56e1 | ||
|
|
370989b2d0 | ||
|
|
71f470d670 | ||
|
|
c0c363bf59 | ||
|
|
0d71463662 | ||
|
|
ebcf341de7 | ||
|
|
14334f76ed | ||
|
|
f24e0721dc | ||
|
|
e36300ce28 | ||
|
|
09ddbe166f | ||
|
|
da0713e629 | ||
|
|
bbd055ac3b | ||
|
|
462b2660de | ||
|
|
ebdbfeb54a | ||
|
|
436b441cad | ||
|
|
552dbcdda9 | ||
|
|
a50b094c1a | ||
|
|
6cc53f16d8 | ||
|
|
8fcd56dc7b | ||
|
|
c9318f08e2 | ||
|
|
c7f8919470 | ||
|
|
14dfa5cc15 | ||
|
|
99a53a1f4c | ||
|
|
df2219eeb8 | ||
|
|
216f3d1740 | ||
|
|
8aa186897f | ||
|
|
3fa7707bc1 | ||
|
|
9038442191 | ||
|
|
c133be12d8 | ||
|
|
1072e3c383 | ||
|
|
a4513405b5 | ||
|
|
410765b516 | ||
|
|
06b9c3962e | ||
|
|
0b64655a40 | ||
|
|
75820a6a3e | ||
|
|
f1869f70b9 | ||
|
|
990cf93c71 | ||
|
|
f7a52789d9 | ||
|
|
cf27d3f300 | ||
|
|
7abe039c31 | ||
|
|
9ba9604256 | ||
|
|
6d2200b3d6 | ||
|
|
0cbdbd36b6 | ||
|
|
0f97dd873a | ||
|
|
8996b9b0e4 | ||
|
|
715d0e85ce | ||
|
|
1ab84ac8e8 | ||
|
|
1d50f0f5fd | ||
|
|
d1017d752f | ||
|
|
474683c0e8 | ||
|
|
03c4c4e116 | ||
|
|
b3e7d6a62d | ||
|
|
2354f5bb18 | ||
|
|
5781e233c1 | ||
|
|
f05e08a767 | ||
|
|
86f66eaa45 | ||
|
|
64c9856ba7 | ||
|
|
66ec50e427 | ||
|
|
44988db9f8 | ||
|
|
519726b46b | ||
|
|
3f676a742b | ||
|
|
e9e42e850b | ||
|
|
8f950ccec0 | ||
|
|
45de6199d1 | ||
|
|
ab4e06f977 | ||
|
|
cab56996b7 | ||
|
|
a13ed0c52e | ||
|
|
5fc35faf17 | ||
|
|
bfcc6f56f3 | ||
|
|
8f9d7506dc | ||
|
|
561bfad7c5 | ||
|
|
88ae188699 | ||
|
|
9830f34d36 | ||
|
|
8cffac65d9 | ||
|
|
2e1326b725 | ||
|
|
abdb547b1b | ||
|
|
d1114da299 | ||
|
|
cbd21b8ec5 | ||
|
|
d650bae0b6 | ||
|
|
222b5100b6 | ||
|
|
93e42d1465 | ||
|
|
93c8de3ec4 | ||
|
|
2442cdd8ad | ||
|
|
08c94bb564 | ||
|
|
d178051832 | ||
|
|
2f879c41c4 | ||
|
|
41b5fa6b55 | ||
|
|
378966e45f | ||
|
|
9c7d2be7c6 | ||
|
|
e08495af09 | ||
|
|
bf5e3dc302 | ||
|
|
a503f7429f | ||
|
|
cce60d466b | ||
|
|
7bebd2bbad | ||
|
|
d4c1515f4e | ||
|
|
45b0429b21 | ||
|
|
dada0730b0 | ||
|
|
c161a48a0a | ||
|
|
3a1503138b | ||
|
|
7486d2ab91 | ||
|
|
82431d8e11 | ||
|
|
20c3d85ba9 | ||
|
|
5051e4aebd | ||
|
|
d3d56ed0ef | ||
|
|
631f801aa6 | ||
|
|
6030610c04 | ||
|
|
9be39b8cd4 | ||
|
|
f2fb8798fa | ||
|
|
449388f3ab | ||
|
|
a3e9bbed25 | ||
|
|
3281c6637d | ||
|
|
c5edbbd004 | ||
|
|
aa765ea4d0 | ||
|
|
307c2c63de | ||
|
|
7d55fd57c9 | ||
|
|
bca586ffd0 | ||
|
|
d359276fe1 | ||
|
|
0b885c5e5f | ||
|
|
4d7ef95d40 | ||
|
|
14a345504f | ||
|
|
4eb5213294 | ||
|
|
9225a3a856 | ||
|
|
b83f36674b | ||
|
|
dfcb104af0 | ||
|
|
3b7ac17410 | ||
|
|
906d52ae0b | ||
|
|
aa9ed05903 | ||
|
|
5213612033 | ||
|
|
518aa1b2a7 | ||
|
|
827bd953e7 | ||
|
|
e54de37242 | ||
|
|
08282a519f | ||
|
|
db7ee2eaf4 | ||
|
|
c7cb750616 | ||
|
|
5903c08c14 | ||
|
|
2babadbc94 | ||
|
|
e397fcc259 | ||
|
|
eee792a019 |
No files matched your search
@@ -1,21 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[{*.md,*_test.go}]
|
||||
indent_style = unset
|
||||
indent_size = unset
|
||||
|
||||
[**/Dockerfile]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml,js,json,sh,nuspec}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,92 @@
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
GOPATH: /go_path
|
||||
GOCACHE: /go_cache
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '>=1.20.1'
|
||||
- uses: https://gitea.com/actions/[email protected]
|
||||
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
|
||||
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --nightly
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
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: 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 }}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
name: release-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
GOPATH: /go_path
|
||||
GOCACHE: /go_cache
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '>=1.20.1'
|
||||
- uses: https://gitea.com/actions/[email protected]
|
||||
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
|
||||
id: import_gpg
|
||||
uses: https://github.com/crazy-max/ghaction-import-gpg@v5
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||
- name: goreleaser
|
||||
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
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: 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,21 +1,38 @@
|
||||
name: checks
|
||||
on:
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
env:
|
||||
GOPATH: /go_path
|
||||
GOCACHE: /go_cache
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: check and test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version: '>=1.20.1'
|
||||
- uses: https://gitea.com/actions/[email protected]
|
||||
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
|
||||
run: go vet -v ./...
|
||||
run: make vet
|
||||
- name: build
|
||||
run: go build -v ./...
|
||||
run: make build
|
||||
- name: test
|
||||
run: | # Test only the new packages in this fork. Add more packages as needed.
|
||||
go test -v ./pkg/jobparser
|
||||
run: make test
|
||||
@@ -1 +0,0 @@
|
||||
github: cplee
|
||||
@@ -1,88 +0,0 @@
|
||||
name: Bug report
|
||||
description: Use this template for reporting bugs/issues.
|
||||
labels:
|
||||
- 'kind/bug'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: act-debug
|
||||
attributes:
|
||||
label: Bug report info
|
||||
render: plain text
|
||||
description: |
|
||||
Output of `act --bug-report`
|
||||
placeholder: |
|
||||
act --bug-report
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: act-command
|
||||
attributes:
|
||||
label: Command used with act
|
||||
description: |
|
||||
Please paste your whole command
|
||||
placeholder: |
|
||||
act -P ubuntu-latest=node:12 -v -d ...
|
||||
render: sh
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe issue
|
||||
description: |
|
||||
Also tell us what did you expect to happen?
|
||||
placeholder: |
|
||||
Describe issue
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: repo
|
||||
attributes:
|
||||
label: Link to GitHub repository
|
||||
description: |
|
||||
Provide link to GitHub repository, you can skip it if the repository is private or you don't have it on GitHub, otherwise please provide it as it might help us troubleshoot problem
|
||||
placeholder: |
|
||||
https://github.com/nektos/act
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: workflow
|
||||
attributes:
|
||||
label: Workflow content
|
||||
description: |
|
||||
Please paste your **whole** workflow here
|
||||
placeholder: |
|
||||
name: My workflow
|
||||
on: ['push', 'schedule']
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KEY: VAL
|
||||
[...]
|
||||
render: yml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Please verify that the log output doesn't contain any sensitive data.
|
||||
render: sh
|
||||
placeholder: |
|
||||
Use `act -v` for verbose output
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional information
|
||||
placeholder: |
|
||||
Additional information that doesn't fit elsewhere
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,14 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Start a discussion
|
||||
url: https://github.com/nektos/act/discussions/new
|
||||
about: You can ask for help here!
|
||||
- name: Ask on Gitter
|
||||
url: https://gitter.im/nektos/act
|
||||
about: You can ask for help here!
|
||||
- name: Ask on Gitter (via Matrix)
|
||||
url: https://matrix.to/#/#nektos_act:gitter.im?via=gitter.im&via=matrix.org
|
||||
about: If you prefer Matrix over Gitter
|
||||
- name: Want to contribute to act?
|
||||
url: https://github.com/nektos/act/blob/master/CONTRIBUTING.md
|
||||
about: Be sure to read contributing guidelines!
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Feature request
|
||||
description: Use this template for requesting a feature/enhancement.
|
||||
labels:
|
||||
- 'kind/feature-request'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please note that incompatibility with GitHub Actions should be opened as a bug report, not a new feature.
|
||||
- type: input
|
||||
id: act-version
|
||||
attributes:
|
||||
label: Act version
|
||||
description: |
|
||||
What version of `act` are you using? Version can be obtained via `act --version`
|
||||
If you've built it from source, please provide commit hash
|
||||
placeholder: |
|
||||
act --version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Describe feature that you would like to see
|
||||
placeholder: ...
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,18 +0,0 @@
|
||||
name: Wiki issue
|
||||
description: Report issue/improvement ragarding documentation (wiki)
|
||||
labels:
|
||||
- 'kind/discussion'
|
||||
- 'area/docs'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this issue!
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: Details
|
||||
description: |
|
||||
Describe issue
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM alpine:3.17
|
||||
|
||||
ARG CHOCOVERSION=1.1.0
|
||||
|
||||
RUN apk add --no-cache bash ca-certificates git \
|
||||
&& apk --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing add mono mono-dev \
|
||||
&& cert-sync /etc/ssl/certs/ca-certificates.crt \
|
||||
&& wget "https://github.com/chocolatey/choco/archive/${CHOCOVERSION}.tar.gz" -O- | tar -xzf - \
|
||||
&& cd choco-"${CHOCOVERSION}" \
|
||||
&& chmod +x build.sh zip.sh \
|
||||
&& ./build.sh -v \
|
||||
&& mv ./code_drop/chocolatey/console /opt/chocolatey \
|
||||
&& mkdir -p /opt/chocolatey/lib \
|
||||
&& rm -rf /choco-"${CHOCOVERSION}" \
|
||||
&& apk del mono-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
ENV ChocolateyInstall=/opt/chocolatey
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,16 +0,0 @@
|
||||
name: 'Chocolatey Packager'
|
||||
description: 'Create the choco package and push it'
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version of package'
|
||||
required: false
|
||||
apiKey:
|
||||
description: 'API Key for chocolately'
|
||||
required: false
|
||||
push:
|
||||
description: 'Option for if package is going to be pushed'
|
||||
required: false
|
||||
default: 'false'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
function choco {
|
||||
mono /opt/chocolatey/choco.exe "$@" --allow-unofficial --nocolor
|
||||
}
|
||||
|
||||
function get_version {
|
||||
local version=${INPUT_VERSION:-$(git describe --tags)}
|
||||
version=(${version//[!0-9.-]/})
|
||||
local version_parts=(${version//-/ })
|
||||
version=${version_parts[0]}
|
||||
if [ ${#version_parts[@]} -gt 1 ]; then
|
||||
version=${version_parts}.${version_parts[1]}
|
||||
fi
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
## Determine the version to pack
|
||||
VERSION=$(get_version)
|
||||
echo "Packing version ${VERSION} of act"
|
||||
rm -f act-cli.*.nupkg
|
||||
mkdir -p tools
|
||||
cp LICENSE tools/LICENSE.txt
|
||||
cp VERIFICATION tools/VERIFICATION.txt
|
||||
cp dist/act_windows_amd64_v1/act.exe tools/
|
||||
choco pack act-cli.nuspec --version ${VERSION}
|
||||
if [[ "$INPUT_PUSH" == "true" ]]; then
|
||||
choco push act-cli.${VERSION}.nupkg --api-key ${INPUT_APIKEY} -s https://push.chocolatey.org/ --timeout 180
|
||||
fi
|
||||
@@ -1,77 +0,0 @@
|
||||
name: 'run-tests'
|
||||
description: 'Runs go test and upload a step summary'
|
||||
inputs:
|
||||
filter:
|
||||
description: 'The go test pattern for the tests to run'
|
||||
required: false
|
||||
default: ''
|
||||
upload-logs-name:
|
||||
description: 'Choose the name of the log artifact'
|
||||
required: false
|
||||
default: logs-${{ github.job }}-${{ strategy.job-index }}
|
||||
upload-logs:
|
||||
description: 'If true uploads logs of each tests as an artifact'
|
||||
required: false
|
||||
default: 'true'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: none # No reason to grant access to the GITHUB_TOKEN
|
||||
script: |
|
||||
let myOutput = '';
|
||||
var fs = require('fs');
|
||||
var uploadLogs = process.env.UPLOAD_LOGS === 'true';
|
||||
if(uploadLogs) {
|
||||
await io.mkdirP('logs');
|
||||
}
|
||||
var filename = null;
|
||||
const options = {};
|
||||
options.ignoreReturnCode = true;
|
||||
options.env = Object.assign({}, process.env);
|
||||
delete options.env.ACTIONS_RUNTIME_URL;
|
||||
delete options.env.ACTIONS_RUNTIME_TOKEN;
|
||||
delete options.env.ACTIONS_CACHE_URL;
|
||||
options.listeners = {
|
||||
stdout: (data) => {
|
||||
for(line of data.toString().split('\n')) {
|
||||
if(/^\s*(===\s[^\s]+\s|---\s[^\s]+:\s)/.test(line)) {
|
||||
if(uploadLogs) {
|
||||
var runprefix = "=== RUN ";
|
||||
if(line.startsWith(runprefix)) {
|
||||
filename = "logs/" + line.substring(runprefix.length).replace(/[^A-Za-z0-9]/g, '-') + ".txt";
|
||||
fs.writeFileSync(filename, line + "\n");
|
||||
} else if(filename) {
|
||||
fs.appendFileSync(filename, line + "\n");
|
||||
filename = null;
|
||||
}
|
||||
}
|
||||
myOutput += line + "\n";
|
||||
} else if(filename) {
|
||||
fs.appendFileSync(filename, line + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var args = ['test', '-v', '-cover', '-coverprofile=coverage.txt', '-covermode=atomic', '-timeout', '20m'];
|
||||
var filter = process.env.FILTER;
|
||||
if(filter) {
|
||||
args.push('-run');
|
||||
args.push(filter);
|
||||
}
|
||||
args.push('./...');
|
||||
var exitcode = await exec.exec('go', args, options);
|
||||
if(process.env.GITHUB_STEP_SUMMARY) {
|
||||
core.summary.addCodeBlock(myOutput);
|
||||
await core.summary.write();
|
||||
}
|
||||
process.exit(exitcode);
|
||||
env:
|
||||
FILTER: ${{ inputs.filter }}
|
||||
UPLOAD_LOGS: ${{ inputs.upload-logs }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always() && inputs.upload-logs == 'true' && !env.ACT
|
||||
with:
|
||||
name: ${{ inputs.upload-logs-name }}
|
||||
path: logs
|
||||
@@ -1,15 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
- package-ecosystem: 'gomod'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
@@ -1 +0,0 @@
|
||||
test-*.yml
|
||||
@@ -1,181 +0,0 @@
|
||||
name: checks
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
ACT_OWNER: ${{ github.repository_owner }}
|
||||
ACT_REPOSITORY: ${{ github.repository }}
|
||||
CGO_ENABLED: 0
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: golangci/[email protected]
|
||||
with:
|
||||
version: v1.53
|
||||
only-new-issues: true
|
||||
- uses: megalinter/megalinter/flavors/[email protected]
|
||||
env:
|
||||
DEFAULT_BRANCH: master
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
GITHUB_STATUS_REPORTER: ${{ !env.ACT }}
|
||||
GITHUB_COMMENT_REPORTER: ${{ !env.ACT }}
|
||||
|
||||
test-linux:
|
||||
name: test-linux
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run Tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
upload-logs-name: logs-linux
|
||||
- name: Run act from cli
|
||||
run: go run main.go -P ubuntu-latest=node:16-buster-slim -C ./pkg/runner/testdata/ -W ./basic/push.yml
|
||||
- name: Upload Codecov report
|
||||
uses: codecov/[email protected]
|
||||
with:
|
||||
files: coverage.txt
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
|
||||
test-host:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
name: test-${{matrix.os}}
|
||||
runs-on: ${{matrix.os}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- name: Run Tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
filter: '^TestRunEventHostEnvironment$'
|
||||
upload-logs-name: logs-${{ matrix.os }}
|
||||
|
||||
snapshot:
|
||||
name: snapshot
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --snapshot --clean
|
||||
- name: Capture x86_64 (64-bit) Linux binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-linux-amd64
|
||||
path: dist/act_linux_amd64_v1/act
|
||||
- name: Capture i386 (32-bit) Linux binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-linux-i386
|
||||
path: dist/act_linux_386/act
|
||||
- name: Capture arm64 (64-bit) Linux binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-linux-arm64
|
||||
path: dist/act_linux_arm64/act
|
||||
- name: Capture armv6 (32-bit) Linux binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-linux-armv6
|
||||
path: dist/act_linux_arm_6/act
|
||||
- name: Capture armv7 (32-bit) Linux binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-linux-armv7
|
||||
path: dist/act_linux_arm_7/act
|
||||
- name: Capture x86_64 (64-bit) Windows binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-windows-amd64
|
||||
path: dist/act_windows_amd64_v1/act.exe
|
||||
- name: Capture i386 (32-bit) Windows binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-windows-i386
|
||||
path: dist/act_windows_386/act.exe
|
||||
- name: Capture arm64 (64-bit) Windows binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-windows-arm64
|
||||
path: dist/act_windows_arm64/act.exe
|
||||
- name: Capture armv7 (32-bit) Windows binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-windows-armv7
|
||||
path: dist/act_windows_arm_7/act.exe
|
||||
- name: Capture x86_64 (64-bit) MacOS binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-macos-amd64
|
||||
path: dist/act_darwin_amd64_v1/act
|
||||
- name: Capture arm64 (64-bit) MacOS binary
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: act-macos-arm64
|
||||
path: dist/act_darwin_arm64/act
|
||||
- name: Chocolatey
|
||||
uses: ./.github/actions/choco
|
||||
with:
|
||||
version: v0.0.0-pr
|
||||
@@ -1,29 +0,0 @@
|
||||
name: promote
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 1 * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: promote
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
||||
- uses: fregante/setup-git-user@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- run: make promote
|
||||
@@ -1,61 +0,0 @@
|
||||
name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/cache@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
||||
- name: Winget
|
||||
uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: nektos.act
|
||||
installers-regex: '_Windows_\w+\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
- name: Chocolatey
|
||||
uses: ./.github/actions/choco
|
||||
with:
|
||||
version: ${{ github.ref }}
|
||||
apiKey: ${{ secrets.CHOCO_APIKEY }}
|
||||
push: true
|
||||
- name: GitHub CLI extension
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
||||
script: |
|
||||
const mainRef = (await github.rest.git.getRef({
|
||||
owner: 'nektos',
|
||||
repo: 'gh-act',
|
||||
ref: 'heads/main',
|
||||
})).data;
|
||||
console.log(mainRef);
|
||||
github.rest.git.createRef({
|
||||
owner: 'nektos',
|
||||
repo: 'gh-act',
|
||||
ref: context.ref,
|
||||
sha: mainRef.object.sha,
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
name: 'Close stale issues'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: Stale
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Issue is stale and will be closed in 14 days unless there is new activity'
|
||||
stale-pr-message: 'PR is stale and will be closed in 14 days unless there is new activity'
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'stale-exempt,kind/feature-request'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'stale-exempt'
|
||||
remove-stale-when-updated: 'True'
|
||||
operations-per-run: 500
|
||||
days-before-stale: 180
|
||||
days-before-close: 14
|
||||
+8
-32
@@ -1,34 +1,10 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
/dist/
|
||||
.todo
|
||||
|
||||
*.nupkg
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Path for actions cache created when running tests
|
||||
pkg/runner/act/
|
||||
|
||||
# act binary
|
||||
dist/local/act
|
||||
|
||||
coverage.txt
|
||||
|
||||
act_runner
|
||||
.env
|
||||
.secrets
|
||||
.runner
|
||||
coverage.txt
|
||||
/gitea-vet
|
||||
/config.yaml
|
||||
|
||||
# megalinter
|
||||
report/
|
||||
act
|
||||
# MS VSCode
|
||||
.vscode
|
||||
__debug_bin
|
||||
@@ -1 +0,0 @@
|
||||
b910a42edfab7a02b08a52ecef203fd419725642:pkg/container/testdata/docker-pull-options/config.json:generic-api-key:4
|
||||
+165
-44
@@ -1,51 +1,172 @@
|
||||
# Minimum golangci-lint version required: v1.46.0
|
||||
run:
|
||||
timeout: 3m
|
||||
linters:
|
||||
enable:
|
||||
- gosimple
|
||||
- deadcode
|
||||
- typecheck
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- structcheck
|
||||
- varcheck
|
||||
- dupl
|
||||
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
|
||||
- gofmt
|
||||
- misspell
|
||||
- gocritic
|
||||
- bidichk
|
||||
- ineffassign
|
||||
- revive
|
||||
- gofumpt
|
||||
- depguard
|
||||
- nakedret
|
||||
- unconvert
|
||||
- wastedassign
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
fast: false
|
||||
|
||||
skip-dirs:
|
||||
- report # megalinter results+fixes
|
||||
run:
|
||||
go: 1.18
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- node_modules
|
||||
- public
|
||||
- web_src
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1005", "-ST1003"]
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
importas:
|
||||
aliases:
|
||||
- pkg: 'github.com/sirupsen/logrus'
|
||||
alias: log
|
||||
- pkg: 'github.com/stretchr/testify/assert'
|
||||
alias: assert
|
||||
depguard:
|
||||
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
|
||||
revive:
|
||||
ignore-generated-header: false
|
||||
severity: warning
|
||||
confidence: 0.8
|
||||
errorCode: 1
|
||||
warningCode: 1
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: github.com/pkg/errors
|
||||
desc: Please use "errors" package from standard library
|
||||
- pkg: gotest.tools/v3
|
||||
desc: Please keep tests unified using only github.com/stretchr/testify
|
||||
- pkg: log
|
||||
desc: Please keep logging unified using only github.com/sirupsen/logrus
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- govet
|
||||
- revive
|
||||
- gocyclo
|
||||
- gosec
|
||||
- unconvert
|
||||
- dupl
|
||||
- nakedret
|
||||
- prealloc
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- goimports
|
||||
- whitespace
|
||||
- misspell
|
||||
- depguard
|
||||
- importas
|
||||
- contextcheck
|
||||
- nolintlint
|
||||
- revive
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: duplicated-imports
|
||||
- name: modifies-value-receiver
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
lang-version: "1.18"
|
||||
depguard:
|
||||
# TODO: use depguard to replace import checks in gitea-vet
|
||||
list-type: denylist
|
||||
# Check the list against standard lib.
|
||||
include-go-root: true
|
||||
packages-with-error-message:
|
||||
- github.com/unknwon/com: "use gitea's util and replacements"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- unparam
|
||||
- staticcheck
|
||||
- path: models/migrations/v
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- linters:
|
||||
- dupl
|
||||
text: "webhook"
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "`ID' should not be capitalized"
|
||||
- path: modules/templates/helper.go
|
||||
linters:
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
- deadcode
|
||||
text: "swagger"
|
||||
- path: contrib/pr/checkout.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/issue.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: modules/log/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: routers/api/v1/repo/issue_subscription.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: routers/repo/view.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- unused
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "argument x is overwritten before first use"
|
||||
- path: modules/httplib/httplib.go
|
||||
linters:
|
||||
- staticcheck
|
||||
# Enabling this would require refactoring the methods and how they are called.
|
||||
- path: models/issue_comment_list.go
|
||||
linters:
|
||||
- dupl
|
||||
- linters:
|
||||
- misspell
|
||||
text: '`Unknwon` is a misspelling of `Unknown`'
|
||||
- path: models/update.go
|
||||
linters:
|
||||
- unused
|
||||
- path: cmd/dump.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: services/webhook/webhook.go
|
||||
linters:
|
||||
- structcheck
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "exitAfterDefer:"
|
||||
linters:
|
||||
- gocritic
|
||||
- path: modules/graceful/manager_windows.go
|
||||
linters:
|
||||
- staticcheck
|
||||
text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
|
||||
- path: models/user/openid.go
|
||||
linters:
|
||||
- golint
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "usage: $0 <path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUM=$(shasum -a 256 "$1" | cut -d' ' -f1)
|
||||
BASENAME=$(basename "$1")
|
||||
echo -n "${SUM} ${BASENAME}" > "$1".sha256
|
||||
@@ -0,0 +1,111 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: ppc64le
|
||||
- goos: darwin
|
||||
goarch: s390x
|
||||
- goos: windows
|
||||
goarch: ppc64le
|
||||
- goos: windows
|
||||
goarch: s390x
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
goarm: "5"
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
goarm: "6"
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: ppc64le
|
||||
- goos: freebsd
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "5"
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "6"
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- .Version }}-
|
||||
{{- .Os }}-
|
||||
{{- if eq .Arch "amd64" }}amd64
|
||||
{{- else if eq .Arch "amd64_v1" }}amd64
|
||||
{{- else if eq .Arch "386" }}386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}-{{ .Arm }}{{ end }}
|
||||
no_unique_dist_dir: true
|
||||
hooks:
|
||||
post:
|
||||
- cmd: tar -cJf {{ .Path }}.xz {{ .Path }}
|
||||
env:
|
||||
- XZ_OPT=-9
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||
|
||||
blobs:
|
||||
-
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
folder: "act_runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
|
||||
archives:
|
||||
- format: binary
|
||||
name_template: "{{ .Binary }}"
|
||||
allow_different_binary_count: true
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Branch }}-devel"
|
||||
|
||||
nightly:
|
||||
name_template: "nightly"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
download: https://gitea.com
|
||||
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
@@ -1,50 +0,0 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- '386'
|
||||
- arm64
|
||||
- arm
|
||||
goarm:
|
||||
- '6'
|
||||
- '7'
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarm: '6'
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
archives:
|
||||
- name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
changelog:
|
||||
groups:
|
||||
- title: 'New Features'
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: 'Bug fixes'
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: 'Documentation updates'
|
||||
regexp: "^.*docs[(\\w)]*:+.*$"
|
||||
order: 2
|
||||
- title: 'Other'
|
||||
order: 999
|
||||
release:
|
||||
prerelease: auto
|
||||
mode: append
|
||||
@@ -1,12 +0,0 @@
|
||||
# Default state for all rules
|
||||
default: true
|
||||
|
||||
# MD013/line-length - Line length
|
||||
MD013:
|
||||
line_length: 1024
|
||||
|
||||
# MD033/no-inline-html - Inline HTML
|
||||
MD033: false
|
||||
|
||||
# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading
|
||||
MD041: false
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
APPLY_FIXES: none
|
||||
DISABLE:
|
||||
- ACTION
|
||||
- BASH
|
||||
- COPYPASTE
|
||||
- DOCKERFILE
|
||||
- GO
|
||||
- JAVASCRIPT
|
||||
- SPELL
|
||||
DISABLE_LINTERS:
|
||||
- YAML_YAMLLINT
|
||||
- MARKDOWN_MARKDOWN_TABLE_FORMATTER
|
||||
- MARKDOWN_MARKDOWN_LINK_CHECK
|
||||
- REPOSITORY_CHECKOV
|
||||
- REPOSITORY_TRIVY
|
||||
FILTER_REGEX_EXCLUDE: (.*testdata/*|install.sh|pkg/container/docker_cli.go|pkg/container/DOCKER_LICENSE|VERSION)
|
||||
MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.yml
|
||||
PARALLEL: false
|
||||
PRINT_ALPACA: false
|
||||
@@ -1,97 +0,0 @@
|
||||
pull_request_rules:
|
||||
- name: warn on conflicts
|
||||
conditions:
|
||||
- -draft
|
||||
- -closed
|
||||
- -merged
|
||||
- conflict
|
||||
actions:
|
||||
comment:
|
||||
message: '@{{author}} this pull request is now in conflict 😩'
|
||||
label:
|
||||
add:
|
||||
- conflict
|
||||
- name: remove conflict label if not needed
|
||||
conditions:
|
||||
- -conflict
|
||||
actions:
|
||||
label:
|
||||
remove:
|
||||
- conflict
|
||||
- name: warn on needs-work
|
||||
conditions:
|
||||
- -draft
|
||||
- -closed
|
||||
- -merged
|
||||
- or:
|
||||
- check-failure=lint
|
||||
- check-failure=test-linux
|
||||
- check-failure=codecov/patch
|
||||
- check-failure=codecov/project
|
||||
- check-failure=snapshot
|
||||
actions:
|
||||
comment:
|
||||
message: '@{{author}} this pull request has failed checks 🛠'
|
||||
label:
|
||||
add:
|
||||
- needs-work
|
||||
- name: remove needs-work label if not needed
|
||||
conditions:
|
||||
- check-success=lint
|
||||
- check-success=test-linux
|
||||
- check-success=codecov/patch
|
||||
- check-success=codecov/project
|
||||
- check-success=snapshot
|
||||
actions:
|
||||
label:
|
||||
remove:
|
||||
- needs-work
|
||||
- name: Automatic maintainer assignment
|
||||
conditions:
|
||||
- '-approved-reviews-by=@nektos/act-maintainers'
|
||||
- -draft
|
||||
- -merged
|
||||
- -closed
|
||||
- -conflict
|
||||
- check-success=lint
|
||||
- check-success=test-linux
|
||||
- check-success=codecov/patch
|
||||
- check-success=codecov/project
|
||||
- check-success=snapshot
|
||||
actions:
|
||||
request_reviews:
|
||||
teams:
|
||||
- '@nektos/act-maintainers'
|
||||
- name: Automatic merge on approval
|
||||
conditions:
|
||||
- '#changes-requested-reviews-by=0'
|
||||
- or:
|
||||
- 'approved-reviews-by=@nektos/act-committers'
|
||||
- 'author~=^dependabot(|-preview)\[bot\]$'
|
||||
- and:
|
||||
- 'approved-reviews-by=@nektos/act-maintainers'
|
||||
- '#approved-reviews-by>=2'
|
||||
- and:
|
||||
- 'author=@nektos/act-maintainers'
|
||||
- 'approved-reviews-by=@nektos/act-maintainers'
|
||||
- '#approved-reviews-by>=1'
|
||||
- -draft
|
||||
- -merged
|
||||
- -closed
|
||||
- check-success=lint
|
||||
- check-success=test-linux
|
||||
- check-success=codecov/patch
|
||||
- check-success=codecov/project
|
||||
- check-success=snapshot
|
||||
actions:
|
||||
queue:
|
||||
method: squash
|
||||
name: default
|
||||
queue_rules:
|
||||
- name: default
|
||||
conditions:
|
||||
- check-success=lint
|
||||
- check-success=test-linux
|
||||
- check-success=codecov/patch
|
||||
- check-success=codecov/project
|
||||
- check-success=snapshot
|
||||
@@ -1,2 +0,0 @@
|
||||
**/testdata
|
||||
pkg/runner/res
|
||||
@@ -1,7 +0,0 @@
|
||||
overrides:
|
||||
- files: '*.yml'
|
||||
options:
|
||||
singleQuote: true
|
||||
- files: '*.json'
|
||||
options:
|
||||
singleQuote: false
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"golang.go",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"esbenp.prettier-vscode",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fix"],
|
||||
"go.testTimeout": "300s",
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
* @nektos/act-maintainers
|
||||
@@ -1,69 +0,0 @@
|
||||
# Contributing to Act
|
||||
|
||||
Help wanted! We'd love your contributions to Act. Please review the following guidelines before contributing. Also, feel free to propose changes to these guidelines by updating this file and submitting a pull request.
|
||||
|
||||
- [I have a question...](#questions)
|
||||
- [I found a bug...](#bugs)
|
||||
- [I have a feature request...](#features)
|
||||
- [I have a contribution to share...](#process)
|
||||
|
||||
## <a id="questions"></a> Have a Question?
|
||||
|
||||
Please don't open a GitHub issue for questions about how to use `act`, as the goal is to use issues for managing bugs and feature requests. Issues that are related to general support will be closed and redirected to our gitter room.
|
||||
|
||||
For all support related questions, please ask the question in our gitter room: [nektos/act](https://gitter.im/nektos/act).
|
||||
|
||||
## <a id="bugs"></a> Found a Bug?
|
||||
|
||||
If you've identified a bug in `act`, please [submit an issue](#issue) to our GitHub repo: [nektos/act](https://github.com/nektos/act/issues/new). Please also feel free to submit a [Pull Request](#pr) with a fix for the bug!
|
||||
|
||||
## <a id="features"></a> Have a Feature Request?
|
||||
|
||||
All feature requests should start with [submitting an issue](#issue) documenting the user story and acceptance criteria. Again, feel free to submit a [Pull Request](#pr) with a proposed implementation of the feature.
|
||||
|
||||
## <a id="process"></a> Ready to Contribute
|
||||
|
||||
### <a id="issue"></a> Create an issue
|
||||
|
||||
Before submitting a new issue, please search the issues to make sure there isn't a similar issue doesn't already exist.
|
||||
|
||||
Assuming no existing issues exist, please ensure you include required information when submitting the issue to ensure we can quickly reproduce your issue.
|
||||
|
||||
We may have additional questions and will communicate through the GitHub issue, so please respond back to our questions to help reproduce and resolve the issue as quickly as possible.
|
||||
|
||||
New issues can be created with in our [GitHub repo](https://github.com/nektos/act/issues/new).
|
||||
|
||||
### <a id="pr"></a>Pull Requests
|
||||
|
||||
Pull requests should target the `master` branch. Please also reference the issue from the description of the pull request using [special keyword syntax](https://help.github.com/articles/closing-issues-via-commit-messages/) to auto close the issue when the PR is merged. For example, include the phrase `fixes #14` in the PR description to have issue #14 auto close. Please send documentation updates for the [act user guide](https://nektosact.com) to [nektos/act-docs](https://github.com/nektos/act-docs).
|
||||
|
||||
### <a id="style"></a> Styleguide
|
||||
|
||||
When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Here are a few points to keep in mind:
|
||||
|
||||
- Please run `go fmt ./...` before committing to ensure code aligns with go standards.
|
||||
- We use [`golangci-lint`](https://golangci-lint.run/) for linting Go code, run `golangci-lint run --fix` before submitting PR. Editors such as Visual Studio Code or JetBrains IntelliJ; with Go support plugin will offer `golangci-lint` automatically.
|
||||
- There are additional linters and formatters for files such as Markdown documents or YAML/JSON:
|
||||
- Please refer to the [Makefile](Makefile) or [`lint` job in our workflow](.github/workflows/checks.yml) to see how to those linters/formatters work.
|
||||
- You can lint codebase by running `go run main.go -j lint --env RUN_LOCAL=true` or `act -j lint --env RUN_LOCAL=true`
|
||||
- In `Makefile`, there are tools that require `npx` which is shipped with `nodejs`.
|
||||
- Our `Makefile` exports `GITHUB_TOKEN` from `~/.config/github/token`, you have been warned.
|
||||
- You can run `make pr` to cleanup dependencies, format/lint code and run tests.
|
||||
- All dependencies must be defined in the `go.mod` file.
|
||||
- Advanced IDEs and code editors (like VSCode) will take care of that, but to be sure, run `go mod tidy` to validate dependencies.
|
||||
- For details on the approved style, check out [Effective Go](https://golang.org/doc/effective_go.html).
|
||||
- Before running tests, please be aware that they are multi-architecture so for them to not fail, you need to run `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64` before ([more info available in #765](https://github.com/nektos/act/issues/765)).
|
||||
|
||||
Also, consider the original design principles:
|
||||
|
||||
- **Polyglot** - There will be no prescribed language or framework for developing the microservices. The only requirement will be that the service will be run inside a container and exposed via an HTTP endpoint.
|
||||
- **Cloud Provider** - At this point, the tool will assume AWS for the cloud provider and will not be written in a cloud agnostic manner. However, this does not preclude refactoring to add support for other providers at a later time.
|
||||
- **Declarative** - All resource administration will be handled in a declarative vs. imperative manner. A file will be used to declared the desired state of the resources and the tool will simply assert the actual state matches the desired state. The tool will accomplish this by generating CloudFormation templates.
|
||||
- **Stateless** - The tool will not maintain its own state. Rather, it will rely on the CloudFormation stacks to determine the state of the platform.
|
||||
- **Secure** - All security will be managed by AWS IAM credentials. No additional authentication or authorization mechanisms will be introduced.
|
||||
|
||||
### License
|
||||
|
||||
By contributing your code, you agree to license your contribution under the terms of the [MIT License](LICENSE).
|
||||
|
||||
All files are released with the MIT license.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
FROM golang:alpine as builder
|
||||
RUN apk add --update-cache make git
|
||||
|
||||
COPY . /opt/src/act_runner
|
||||
WORKDIR /opt/src/act_runner
|
||||
|
||||
RUN make clean && make build
|
||||
|
||||
FROM alpine as runner
|
||||
RUN apk add --update-cache \
|
||||
git bash \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY run.sh /opt/act/run.sh
|
||||
|
||||
ENTRYPOINT ["/opt/act/run.sh"]
|
||||
@@ -1,67 +0,0 @@
|
||||
# List of Docker images for `act`
|
||||
|
||||
**Warning:** Below badges with size for each image are displaying size of **compressed image size in registry. After pulling the image, size can be drastically different due to Docker uncompressing the image layers.**
|
||||
|
||||
## Images based on [`buildpack-deps`][hub/_/buildpack-deps]
|
||||
|
||||
**Note 1: `node` images are based on Debian root filesystem, while it is extremely similar to Ubuntu, there might be some differences**
|
||||
|
||||
**Note 2: `node` `-slim` images don't have `python` installed, if you want to use actions or software that is depending on `python`, you need to specify image manually**
|
||||
|
||||
| Image | Size |
|
||||
| ------------------------------------- | ---------------------------------------------------------- |
|
||||
| [`node:16-bullseye`][hub/_/node] | ![`bullseye-size`][hub/_/node/16-bullseye/size] |
|
||||
| [`node:16-bullseye-slim`][hub/_/node] | ![`micro-bullseye-size`][hub/_/node/16-bullseye-slim/size] |
|
||||
| [`node:16-buster`][hub/_/node] | ![`buster-size`][hub/_/node/16-buster/size] |
|
||||
| [`node:16-buster-slim`][hub/_/node] | ![`micro-buster-size`][hub/_/node/16-buster-slim/size] |
|
||||
|
||||
**Note: `catthehacker/ubuntu` images are based on Ubuntu root filesystem**
|
||||
|
||||
| Image | GitHub Repository |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| [`catthehacker/ubuntu:act-latest`][ghcr/catthehacker/ubuntu] | [`catthehacker/docker-images`][gh/catthehacker/docker_images] |
|
||||
| [`catthehacker/ubuntu:act-22.04`][ghcr/catthehacker/ubuntu] | [`catthehacker/docker-images`][gh/catthehacker/docker_images] |
|
||||
| [`catthehacker/ubuntu:act-20.04`][ghcr/catthehacker/ubuntu] | [`catthehacker/docker-images`][gh/catthehacker/docker_images] |
|
||||
| [`catthehacker/ubuntu:act-18.04`][ghcr/catthehacker/ubuntu] | [`catthehacker/docker-images`][gh/catthehacker/docker_images] |
|
||||
|
||||
## Images based on [`actions/virtual-environments`][gh/actions/virtual-environments]
|
||||
|
||||
**Note: `nektos/act-environments-ubuntu` have been last updated in February, 2020. It's recommended to update the image manually after `docker pull` if you decide to use it.**
|
||||
|
||||
| Image | Size | GitHub Repository |
|
||||
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| [`nektos/act-environments-ubuntu:18.04`][hub/nektos/act-environments-ubuntu] | ![`nektos:18.04`][hub/nektos/act-environments-ubuntu/18.04/size] | [`nektos/act-environments`][gh/nektos/act-environments] |
|
||||
| [`nektos/act-environments-ubuntu:18.04-lite`][hub/nektos/act-environments-ubuntu] | ![`nektos:18.04-lite`][hub/nektos/act-environments-ubuntu/18.04-lite/size] | [`nektos/act-environments`][gh/nektos/act-environments] |
|
||||
| [`nektos/act-environments-ubuntu:18.04-full`][hub/nektos/act-environments-ubuntu] | ![`nektos:18.04-full`][hub/nektos/act-environments-ubuntu/18.04-full/size] | [`nektos/act-environments`][gh/nektos/act-environments] |
|
||||
|
||||
| Image | GitHub Repository |
|
||||
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| [`catthehacker/ubuntu:full-latest`][ghcr/catthehacker/ubuntu] | [`catthehacker/virtual-environments-fork`][gh/catthehacker/virtual-environments-fork] |
|
||||
| [`catthehacker/ubuntu:full-20.04`][ghcr/catthehacker/ubuntu] | [`catthehacker/virtual-environments-fork`][gh/catthehacker/virtual-environments-fork] |
|
||||
| [`catthehacker/ubuntu:full-18.04`][ghcr/catthehacker/ubuntu] | [`catthehacker/virtual-environments-fork`][gh/catthehacker/virtual-environments-fork] |
|
||||
|
||||
Feel free to make a pull request with your image added here
|
||||
|
||||
[hub/_/buildpack-deps]: https://hub.docker.com/_/buildpack-deps
|
||||
[hub/_/node]: https://hub.docker.com/r/_/node
|
||||
[hub/_/node/16-bullseye/size]: https://img.shields.io/docker/image-size/_/node/16-bullseye
|
||||
[hub/_/node/16-bullseye-slim/size]: https://img.shields.io/docker/image-size/_/node/16-bullseye-slim
|
||||
[hub/_/node/16-buster/size]: https://img.shields.io/docker/image-size/_/node/16-buster
|
||||
[hub/_/node/16-buster-slim/size]: https://img.shields.io/docker/image-size/_/node/16-buster-slim
|
||||
[ghcr/catthehacker/ubuntu]: https://github.com/catthehacker/docker_images/pkgs/container/ubuntu
|
||||
[hub/nektos/act-environments-ubuntu]: https://hub.docker.com/r/nektos/act-environments-ubuntu
|
||||
[hub/nektos/act-environments-ubuntu/18.04/size]: https://img.shields.io/docker/image-size/nektos/act-environments-ubuntu/18.04
|
||||
[hub/nektos/act-environments-ubuntu/18.04-lite/size]: https://img.shields.io/docker/image-size/nektos/act-environments-ubuntu/18.04-lite
|
||||
[hub/nektos/act-environments-ubuntu/18.04-full/size]: https://img.shields.io/docker/image-size/nektos/act-environments-ubuntu/18.04-full
|
||||
|
||||
<!--
|
||||
[hub/<username>/<image>]: https://hub.docker.com/r/[username]/[image]
|
||||
[hub/<username>/<image>/<tag>/size]: https://img.shields.io/docker/image-size/[username]/[image]/[tag]
|
||||
-->
|
||||
|
||||
<!-- GitHub repository links -->
|
||||
|
||||
[gh/nektos/act-environments]: https://github.com/nektos/act-environments
|
||||
[gh/actions/virtual-environments]: https://github.com/actions/virtual-environments
|
||||
[gh/catthehacker/docker_images]: https://github.com/catthehacker/docker_images
|
||||
[gh/catthehacker/virtual-environments-fork]: https://github.com/catthehacker/virtual-environments-fork
|
||||
@@ -1,7 +1,4 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 The Gitea Authors
|
||||
Copyright (c) 2019
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -10,13 +7,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,113 +1,175 @@
|
||||
PREFIX ?= /usr/local
|
||||
VERSION ?= $(shell git describe --tags --dirty --always | sed -e 's/^v//')
|
||||
IS_SNAPSHOT = $(if $(findstring -, $(VERSION)),true,false)
|
||||
MAJOR_VERSION = $(word 1, $(subst ., ,$(VERSION)))
|
||||
MINOR_VERSION = $(word 2, $(subst ., ,$(VERSION)))
|
||||
PATCH_VERSION = $(word 3, $(subst ., ,$(word 1,$(subst -, , $(VERSION)))))
|
||||
NEW_VERSION ?= $(MAJOR_VERSION).$(MINOR_VERSION).$(shell echo $$(( $(PATCH_VERSION) + 1)) )
|
||||
DIST := dist
|
||||
EXECUTABLE := act_runner
|
||||
GOFMT ?= gofumpt -l
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
XGO_VERSION := go-1.18.x
|
||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/[email protected]
|
||||
|
||||
fix = false
|
||||
ifeq (true,$(fix))
|
||||
FIX = --fix
|
||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
||||
WINDOWS_ARCHS ?= windows/amd64
|
||||
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||
|
||||
DOCKER_IMAGE ?= gitea/act_runner
|
||||
DOCKER_TAG ?= nightly
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
|
||||
ifneq ($(shell uname), Darwin)
|
||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||
else
|
||||
EXTLDFLAGS =
|
||||
endif
|
||||
|
||||
ACT ?= go run main.go
|
||||
ifeq ($(HAS_GO), GO)
|
||||
GOPATH ?= $(shell $(GO) env GOPATH)
|
||||
export PATH := $(GOPATH)/bin:$(PATH)
|
||||
|
||||
HAS_TOKEN = $(if $(test -e ~/.config/github/token),true,false)
|
||||
ifeq (true,$(HAS_TOKEN))
|
||||
export GITHUB_TOKEN := $(shell cat ~/.config/github/token)
|
||||
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
|
||||
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
|
||||
endif
|
||||
|
||||
.PHONY: pr
|
||||
pr: tidy format-all lint test
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -ldflags "-X main.version=$(VERSION)" -o dist/local/act main.go
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
go fmt ./...
|
||||
|
||||
.PHONY: format-all
|
||||
format-all:
|
||||
go fmt ./...
|
||||
npx prettier --write .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
$(ACT)
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go:
|
||||
golangci-lint run $(FIX)
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js:
|
||||
npx standard $(FIX)
|
||||
|
||||
.PHONY: lint-md
|
||||
lint-md:
|
||||
npx markdownlint . $(FIX)
|
||||
|
||||
.PHONY: lint-rest
|
||||
lint-rest:
|
||||
docker run --rm -it \
|
||||
-v $(PWD):/tmp/lint \
|
||||
-e GITHUB_STATUS_REPORTER=false \
|
||||
-e GITHUB_COMMENT_REPORTER=false \
|
||||
megalinter/megalinter-go:v5
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-go lint-rest
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: lint-md lint-go
|
||||
|
||||
.PHONY: fix
|
||||
fix:
|
||||
make lint-fix fix=true
|
||||
|
||||
.PHONY: tidy
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
.PHONY: install
|
||||
install: build
|
||||
@cp dist/local/act $(PREFIX)/bin/act
|
||||
@chmod 755 $(PREFIX)/bin/act
|
||||
@act --version
|
||||
|
||||
.PHONY: installer
|
||||
installer:
|
||||
@GO111MODULE=off go get github.com/goreleaser/godownloader
|
||||
godownloader -r nektos/act -o install.sh
|
||||
|
||||
.PHONY: promote
|
||||
promote:
|
||||
@git fetch --tags
|
||||
@echo "VERSION:$(VERSION) IS_SNAPSHOT:$(IS_SNAPSHOT) NEW_VERSION:$(NEW_VERSION)"
|
||||
ifeq (false,$(IS_SNAPSHOT))
|
||||
@echo "Unable to promote a non-snapshot"
|
||||
@exit 1
|
||||
ifeq ($(OS), Windows_NT)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= $(EXECUTABLE).exe
|
||||
else ifeq ($(OS), Windows)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= $(EXECUTABLE).exe
|
||||
else
|
||||
GOFLAGS := -v
|
||||
EXECUTABLE ?= $(EXECUTABLE)
|
||||
endif
|
||||
ifneq ($(shell git status -s),)
|
||||
@echo "Unable to promote a dirty workspace"
|
||||
@exit 1
|
||||
|
||||
STORED_VERSION_FILE := VERSION
|
||||
|
||||
ifneq ($(DRONE_TAG),)
|
||||
VERSION ?= $(subst v,,$(DRONE_TAG))
|
||||
RELASE_VERSION ?= $(VERSION)
|
||||
else
|
||||
ifneq ($(DRONE_BRANCH),)
|
||||
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
||||
else
|
||||
VERSION ?= main
|
||||
endif
|
||||
|
||||
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
||||
ifneq ($(STORED_VERSION),)
|
||||
RELASE_VERSION ?= $(STORED_VERSION)
|
||||
else
|
||||
RELASE_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
|
||||
endif
|
||||
endif
|
||||
echo -n $(NEW_VERSION) > VERSION
|
||||
git add VERSION
|
||||
git commit -m "chore: bump VERSION to $(NEW_VERSION)"
|
||||
git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION)
|
||||
git push origin master
|
||||
git push origin v$(NEW_VERSION)
|
||||
|
||||
.PHONY: snapshot
|
||||
snapshot:
|
||||
goreleaser build \
|
||||
--clean \
|
||||
--single-target \
|
||||
--snapshot
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=$(RELASE_VERSION)"
|
||||
|
||||
.PHONY: clean all
|
||||
all: build
|
||||
|
||||
fmt:
|
||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||
fi
|
||||
$(GOFMT) -w $(GO_FMT_FILES)
|
||||
|
||||
.PHONY: go-check
|
||||
go-check:
|
||||
$(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
|
||||
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||
echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) install mvdan.cc/gofumpt@latest; \
|
||||
fi
|
||||
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make fmt' and commit the result:"; \
|
||||
echo "$${diff}"; \
|
||||
exit 1; \
|
||||
fi;
|
||||
|
||||
test: fmt-check
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@$(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet ./...
|
||||
|
||||
install: $(GOFILES)
|
||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||
|
||||
build: go-check $(EXECUTABLE)
|
||||
|
||||
$(EXECUTABLE): $(GOFILES)
|
||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: deps-backend
|
||||
deps-backend:
|
||||
$(GO) mod download
|
||||
$(GO) install $(GXZ_PAGAGE)
|
||||
$(GO) install $(XGO_PACKAGE)
|
||||
|
||||
.PHONY: release
|
||||
release: release-windows release-linux release-darwin release-copy release-compress release-check
|
||||
|
||||
$(DIST_DIRS):
|
||||
mkdir -p $(DIST_DIRS)
|
||||
|
||||
.PHONY: release-windows
|
||||
release-windows: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(WINDOWS_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||
ifeq ($(CI),true)
|
||||
cp -r /build/* $(DIST)/binaries/
|
||||
endif
|
||||
|
||||
.PHONY: release-linux
|
||||
release-linux: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||
ifeq ($(CI),true)
|
||||
cp -r /build/* $(DIST)/binaries/
|
||||
endif
|
||||
|
||||
.PHONY: release-darwin
|
||||
release-darwin: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets '$(DARWIN_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||
ifeq ($(CI),true)
|
||||
cp -r /build/* $(DIST)/binaries/
|
||||
endif
|
||||
|
||||
.PHONY: release-copy
|
||||
release-copy: | $(DIST_DIRS)
|
||||
cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
|
||||
|
||||
.PHONY: release-check
|
||||
release-check: | $(DIST_DIRS)
|
||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
|
||||
|
||||
.PHONY: release-compress
|
||||
release-compress: | $(DIST_DIRS)
|
||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
if ! docker buildx version >/dev/null 2>&1; then \
|
||||
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||
fi; \
|
||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||
|
||||
clean:
|
||||
$(GO) clean -x -i ./...
|
||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
@@ -1,62 +1,95 @@
|
||||
## Forking rules
|
||||
# act runner
|
||||
|
||||
This is a custom fork of [nektos/act](https://github.com/nektos/act/), for the purpose of serving [act_runner](https://gitea.com/gitea/act_runner).
|
||||
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act).
|
||||
|
||||
It cannot be used as command line tool anymore, but only as a library.
|
||||
## Installation
|
||||
|
||||
It's a soft fork, which means that it will track the latest release of nektos/act.
|
||||
### Prerequisites
|
||||
|
||||
Branches:
|
||||
Docker Engine Community version is required for docker mode. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
||||
|
||||
- `main`: default branch, contains custom changes, based on the latest release(not the latest of the master branch) of nektos/act.
|
||||
- `nektos/master`: mirror for the master branch of nektos/act.
|
||||
### Download pre-built binary
|
||||
|
||||
Tags:
|
||||
Visit https://dl.gitea.com/act_runner/ and download the right version for your platform.
|
||||
|
||||
- `nektos/vX.Y.Z`: mirror for `vX.Y.Z` of [nektos/act](https://github.com/nektos/act/).
|
||||
- `vX.YZ.*`: based on `nektos/vX.Y.Z`, contains custom changes.
|
||||
- Examples:
|
||||
- `nektos/v0.2.23` -> `v0.223.*`
|
||||
- `nektos/v0.3.1` -> `v0.301.*`, not ~~`v0.31.*`~~
|
||||
- `nektos/v0.10.1` -> `v0.1001.*`, not ~~`v0.101.*`~~
|
||||
- `nektos/v0.3.100` -> not ~~`v0.3100.*`~~, I don't think it's really going to happen, if it does, we can find a way to handle it.
|
||||
### Build from source
|
||||
|
||||
---
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||

|
||||
### Build a docker image
|
||||
|
||||
# Overview [](https://github.com/nektos/act/actions) [](https://gitter.im/nektos/act?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://goreportcard.com/report/github.com/nektos/act) [](https://github.com/jonico/awesome-runners)
|
||||
```bash
|
||||
make docker
|
||||
```
|
||||
|
||||
> "Think globally, `act` locally"
|
||||
## Quickstart
|
||||
|
||||
Run your [GitHub Actions](https://developer.github.com/actions/) locally! Why would you want to do this? Two reasons:
|
||||
### Register
|
||||
|
||||
- **Fast Feedback** - Rather than having to commit/push every time you want to test out the changes you are making to your `.github/workflows/` files (or for any changes to embedded GitHub actions), you can use `act` to run the actions locally. The [environment variables](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables) and [filesystem](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#filesystems-on-github-hosted-runners) are all configured to match what GitHub provides.
|
||||
- **Local Task Runner** - I love [make](<https://en.wikipedia.org/wiki/Make_(software)>). However, I also hate repeating myself. With `act`, you can use the GitHub Actions defined in your `.github/workflows/` to replace your `Makefile`!
|
||||
```bash
|
||||
./act_runner register
|
||||
```
|
||||
|
||||
# How Does It Work?
|
||||
And you will be asked to input:
|
||||
|
||||
When you run `act` it reads in your GitHub Actions from `.github/workflows/` and determines the set of actions that need to be run. It uses the Docker API to either pull or build the necessary images, as defined in your workflow files and finally determines the execution path based on the dependencies that were defined. Once it has the execution path, it then uses the Docker API to run containers for each action based on the images prepared earlier. The [environment variables](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables) and [filesystem](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#file-systems) are all configured to match what GitHub provides.
|
||||
1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
|
||||
and you should not use `localhost` or `127.0.0.1` as instance IP;
|
||||
2. Runner token, you can get it from `http://192.168.8.8:3000/admin/runners`;
|
||||
3. Runner name, you can just leave it blank;
|
||||
4. Runner labels, you can just leave it blank.
|
||||
|
||||
Let's see it in action with a [sample repo](https://github.com/cplee/github-actions-demo)!
|
||||
The process looks like:
|
||||
|
||||

|
||||
```text
|
||||
INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
|
||||
WARN Runner in user-mode.
|
||||
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
|
||||
http://192.168.8.8:3000/
|
||||
INFO Enter the runner token:
|
||||
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
||||
INFO Enter the runner name (if set empty, use hostname: Test.local):
|
||||
|
||||
# Act User Guide
|
||||
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):
|
||||
|
||||
Please look at the [act user guide](https://nektosact.com) for more documentation.
|
||||
INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://node:16-bullseye ubuntu-22.04:docker://node:16-bullseye ubuntu-20.04:docker://node:16-bullseye ubuntu-18.04:docker://node:16-buster].
|
||||
DEBU Successfully pinged the Gitea instance server
|
||||
INFO Runner registered successfully.
|
||||
```
|
||||
|
||||
# Support
|
||||
You can also register with command line arguments.
|
||||
|
||||
Need help? Ask on [Gitter](https://gitter.im/nektos/act)!
|
||||
```bash
|
||||
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
```
|
||||
|
||||
# Contributing
|
||||
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
||||
|
||||
Want to contribute to act? Awesome! Check out the [contributing guidelines](CONTRIBUTING.md) to get involved.
|
||||
### Run
|
||||
|
||||
## Manually building from source
|
||||
```bash
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
- Install Go tools 1.20+ - (<https://golang.org/doc/install>)
|
||||
- Clone this repo `git clone [email protected]:nektos/act.git`
|
||||
- Run unit tests with `make test`
|
||||
- Build and install: `make install`
|
||||
### Configuration
|
||||
|
||||
You can also configure the runner with a configuration file.
|
||||
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
|
||||
|
||||
```bash
|
||||
./act_runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
You can specify the configuration file path with `-c`/`--config` argument.
|
||||
|
||||
```bash
|
||||
./act_runner -c config.yaml register # register with config file
|
||||
./act_runner -c config.yaml deamon # run with config file
|
||||
```
|
||||
|
||||
### Run a docker container
|
||||
|
||||
```sh
|
||||
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/act_runner:nightly
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
VERIFICATION
|
||||
Verification is intended to assist the Chocolatey moderators and community
|
||||
in verifying that this package's contents are trustworthy.
|
||||
|
||||
Checksums: https://github.com/nektos/act/releases, in the checksums.txt file
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>act-cli</id>
|
||||
<version>0.0.0</version>
|
||||
<packageSourceUrl>https://github.com/nektos/act</packageSourceUrl>
|
||||
<owners>nektos</owners>
|
||||
<title>act (GitHub Actions CLI)</title>
|
||||
<authors>nektos</authors>
|
||||
<projectUrl>https://github.com/nektos/act</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/wiki/nektos/act/img/logo-150.png</iconUrl>
|
||||
<copyright>Nektos</copyright>
|
||||
<licenseUrl>https://raw.githubusercontent.com/nektos/act/master/LICENSE</licenseUrl>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<projectSourceUrl>https://github.com/nektos/act</projectSourceUrl>
|
||||
<docsUrl>https://raw.githubusercontent.com/nektos/act/master/README.md</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/nektos/act/issues</bugTrackerUrl>
|
||||
<tags>act github-actions actions golang ci devops</tags>
|
||||
<summary>Run your GitHub Actions locally 🚀</summary>
|
||||
<description>Run your GitHub Actions locally 🚀</description>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools/**" target="tools" />
|
||||
</files>
|
||||
</package>
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build vendor
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
// for vet
|
||||
_ "code.gitea.io/gitea-vet"
|
||||
)
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
UserHomeDir string
|
||||
CacheHomeDir string
|
||||
)
|
||||
|
||||
func init() {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
UserHomeDir = home
|
||||
|
||||
if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
|
||||
CacheHomeDir = v
|
||||
} else {
|
||||
CacheHomeDir = filepath.Join(UserHomeDir, ".cache")
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func drawGraph(plan *model.Plan) error {
|
||||
drawings := make([]*common.Drawing, 0)
|
||||
|
||||
jobPen := common.NewPen(common.StyleSingleLine, 96)
|
||||
arrowPen := common.NewPen(common.StyleNoLine, 97)
|
||||
for i, stage := range plan.Stages {
|
||||
if i > 0 {
|
||||
drawings = append(drawings, arrowPen.DrawArrow())
|
||||
}
|
||||
|
||||
ids := make([]string, 0)
|
||||
for _, r := range stage.Runs {
|
||||
ids = append(ids, r.String())
|
||||
}
|
||||
drawings = append(drawings, jobPen.DrawBoxes(ids...))
|
||||
}
|
||||
|
||||
maxWidth := 0
|
||||
for _, d := range drawings {
|
||||
if d.GetWidth() > maxWidth {
|
||||
maxWidth = d.GetWidth()
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range drawings {
|
||||
d.Draw(os.Stdout, maxWidth)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Input contains the input for the root command
|
||||
type Input struct {
|
||||
actor string
|
||||
workdir string
|
||||
workflowsPath string
|
||||
autodetectEvent bool
|
||||
eventPath string
|
||||
reuseContainers bool
|
||||
bindWorkdir bool
|
||||
secrets []string
|
||||
vars []string
|
||||
envs []string
|
||||
inputs []string
|
||||
platforms []string
|
||||
dryrun bool
|
||||
forcePull bool
|
||||
forceRebuild bool
|
||||
noOutput bool
|
||||
envfile string
|
||||
inputfile string
|
||||
secretfile string
|
||||
varfile string
|
||||
insecureSecrets bool
|
||||
defaultBranch string
|
||||
privileged bool
|
||||
usernsMode string
|
||||
containerArchitecture string
|
||||
containerDaemonSocket string
|
||||
containerOptions string
|
||||
noWorkflowRecurse bool
|
||||
useGitIgnore bool
|
||||
githubInstance string
|
||||
containerCapAdd []string
|
||||
containerCapDrop []string
|
||||
autoRemove bool
|
||||
artifactServerPath string
|
||||
artifactServerAddr string
|
||||
artifactServerPort string
|
||||
noCacheServer bool
|
||||
cacheServerPath string
|
||||
cacheServerAddr string
|
||||
cacheServerPort uint16
|
||||
jsonLogger bool
|
||||
noSkipCheckout bool
|
||||
remoteName string
|
||||
replaceGheActionWithGithubCom []string
|
||||
replaceGheActionTokenWithGithubCom string
|
||||
matrix []string
|
||||
actionCachePath string
|
||||
actionOfflineMode bool
|
||||
logPrefixJobID bool
|
||||
networkName string
|
||||
useNewActionCache bool
|
||||
localRepository []string
|
||||
maxParallel int
|
||||
}
|
||||
|
||||
func (i *Input) resolve(path string) string {
|
||||
basedir, err := filepath.Abs(i.workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if path == "" {
|
||||
return path
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(basedir, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Envfile returns path to .env
|
||||
func (i *Input) Envfile() string {
|
||||
return i.resolve(i.envfile)
|
||||
}
|
||||
|
||||
// Secretfile returns path to secrets
|
||||
func (i *Input) Secretfile() string {
|
||||
return i.resolve(i.secretfile)
|
||||
}
|
||||
|
||||
func (i *Input) Varfile() string {
|
||||
return i.resolve(i.varfile)
|
||||
}
|
||||
|
||||
// Workdir returns path to workdir
|
||||
func (i *Input) Workdir() string {
|
||||
return i.resolve(".")
|
||||
}
|
||||
|
||||
// WorkflowsPath returns path to workflow file(s)
|
||||
func (i *Input) WorkflowsPath() string {
|
||||
return i.resolve(i.workflowsPath)
|
||||
}
|
||||
|
||||
// EventPath returns the path to events file
|
||||
func (i *Input) EventPath() string {
|
||||
return i.resolve(i.eventPath)
|
||||
}
|
||||
|
||||
// Inputfile returns the path to the input file
|
||||
func (i *Input) Inputfile() string {
|
||||
return i.resolve(i.inputfile)
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func printList(plan *model.Plan) error {
|
||||
type lineInfoDef struct {
|
||||
jobID string
|
||||
jobName string
|
||||
stage string
|
||||
wfName string
|
||||
wfFile string
|
||||
events string
|
||||
}
|
||||
lineInfos := []lineInfoDef{}
|
||||
|
||||
header := lineInfoDef{
|
||||
jobID: "Job ID",
|
||||
jobName: "Job name",
|
||||
stage: "Stage",
|
||||
wfName: "Workflow name",
|
||||
wfFile: "Workflow file",
|
||||
events: "Events",
|
||||
}
|
||||
|
||||
jobs := map[string]bool{}
|
||||
duplicateJobIDs := false
|
||||
|
||||
jobIDMaxWidth := len(header.jobID)
|
||||
jobNameMaxWidth := len(header.jobName)
|
||||
stageMaxWidth := len(header.stage)
|
||||
wfNameMaxWidth := len(header.wfName)
|
||||
wfFileMaxWidth := len(header.wfFile)
|
||||
eventsMaxWidth := len(header.events)
|
||||
|
||||
for i, stage := range plan.Stages {
|
||||
for _, r := range stage.Runs {
|
||||
jobID := r.JobID
|
||||
line := lineInfoDef{
|
||||
jobID: jobID,
|
||||
jobName: r.String(),
|
||||
stage: strconv.Itoa(i),
|
||||
wfName: r.Workflow.Name,
|
||||
wfFile: r.Workflow.File,
|
||||
events: strings.Join(r.Workflow.On(), `,`),
|
||||
}
|
||||
if _, ok := jobs[jobID]; ok {
|
||||
duplicateJobIDs = true
|
||||
} else {
|
||||
jobs[jobID] = true
|
||||
}
|
||||
lineInfos = append(lineInfos, line)
|
||||
if jobIDMaxWidth < len(line.jobID) {
|
||||
jobIDMaxWidth = len(line.jobID)
|
||||
}
|
||||
if jobNameMaxWidth < len(line.jobName) {
|
||||
jobNameMaxWidth = len(line.jobName)
|
||||
}
|
||||
if stageMaxWidth < len(line.stage) {
|
||||
stageMaxWidth = len(line.stage)
|
||||
}
|
||||
if wfNameMaxWidth < len(line.wfName) {
|
||||
wfNameMaxWidth = len(line.wfName)
|
||||
}
|
||||
if wfFileMaxWidth < len(line.wfFile) {
|
||||
wfFileMaxWidth = len(line.wfFile)
|
||||
}
|
||||
if eventsMaxWidth < len(line.events) {
|
||||
eventsMaxWidth = len(line.events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobIDMaxWidth += 2
|
||||
jobNameMaxWidth += 2
|
||||
stageMaxWidth += 2
|
||||
wfNameMaxWidth += 2
|
||||
wfFileMaxWidth += 2
|
||||
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, header.stage,
|
||||
-jobIDMaxWidth, header.jobID,
|
||||
-jobNameMaxWidth, header.jobName,
|
||||
-wfNameMaxWidth, header.wfName,
|
||||
-wfFileMaxWidth, header.wfFile,
|
||||
-eventsMaxWidth, header.events,
|
||||
)
|
||||
for _, line := range lineInfos {
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, line.stage,
|
||||
-jobIDMaxWidth, line.jobID,
|
||||
-jobNameMaxWidth, line.jobName,
|
||||
-wfNameMaxWidth, line.wfName,
|
||||
-wfFileMaxWidth, line.wfFile,
|
||||
-eventsMaxWidth, line.events,
|
||||
)
|
||||
}
|
||||
if duplicateJobIDs {
|
||||
fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Notice struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func displayNotices(input *Input) {
|
||||
select {
|
||||
case notices := <-noticesLoaded:
|
||||
if len(notices) > 0 {
|
||||
noticeLogger := log.New()
|
||||
if input.jsonLogger {
|
||||
noticeLogger.SetFormatter(&log.JSONFormatter{})
|
||||
} else {
|
||||
noticeLogger.SetFormatter(&log.TextFormatter{
|
||||
DisableQuote: true,
|
||||
DisableTimestamp: true,
|
||||
PadLevelText: true,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
for _, notice := range notices {
|
||||
level, err := log.ParseLevel(notice.Level)
|
||||
if err != nil {
|
||||
level = log.InfoLevel
|
||||
}
|
||||
noticeLogger.Log(level, notice.Message)
|
||||
}
|
||||
}
|
||||
case <-time.After(time.Second * 1):
|
||||
log.Debugf("Timeout waiting for notices")
|
||||
}
|
||||
}
|
||||
|
||||
var noticesLoaded = make(chan []Notice)
|
||||
|
||||
func loadVersionNotices(version string) {
|
||||
go func() {
|
||||
noticesLoaded <- getVersionNotices(version)
|
||||
}()
|
||||
}
|
||||
|
||||
const NoticeURL = "https://api.nektosact.com/notices"
|
||||
|
||||
func getVersionNotices(version string) []Notice {
|
||||
if os.Getenv("ACT_DISABLE_VERSION_CHECK") == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
noticeURL, err := url.Parse(NoticeURL)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil
|
||||
}
|
||||
query := noticeURL.Query()
|
||||
query.Add("os", runtime.GOOS)
|
||||
query.Add("arch", runtime.GOARCH)
|
||||
query.Add("version", version)
|
||||
|
||||
noticeURL.RawQuery = query.Encode()
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", noticeURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
etag := loadNoticesEtag()
|
||||
if etag != "" {
|
||||
log.Debugf("Conditional GET for notices etag=%s", etag)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
newEtag := resp.Header.Get("Etag")
|
||||
if newEtag != "" {
|
||||
log.Debugf("Saving notices etag=%s", newEtag)
|
||||
saveNoticesEtag(newEtag)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
notices := []Notice{}
|
||||
if resp.StatusCode == 304 {
|
||||
log.Debug("No new notices")
|
||||
return nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(¬ices); err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return notices
|
||||
}
|
||||
|
||||
func loadNoticesEtag() string {
|
||||
p := etagPath()
|
||||
content, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
log.Debugf("Unable to load etag from %s: %e", p, err)
|
||||
}
|
||||
return strings.TrimSuffix(string(content), "\n")
|
||||
}
|
||||
|
||||
func saveNoticesEtag(etag string) {
|
||||
p := etagPath()
|
||||
err := os.WriteFile(p, []byte(strings.TrimSuffix(etag, "\n")), 0o600)
|
||||
if err != nil {
|
||||
log.Debugf("Unable to save etag to %s: %e", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
func etagPath() string {
|
||||
dir := filepath.Join(CacheHomeDir, "act")
|
||||
if err := os.MkdirAll(dir, 0o777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return filepath.Join(dir, ".notices.etag")
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (i *Input) newPlatforms() map[string]string {
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": "node:16-buster-slim",
|
||||
"ubuntu-22.04": "node:16-bullseye-slim",
|
||||
"ubuntu-20.04": "node:16-buster-slim",
|
||||
"ubuntu-18.04": "node:16-buster-slim",
|
||||
}
|
||||
|
||||
for _, p := range i.platforms {
|
||||
pParts := strings.Split(p, "=")
|
||||
if len(pParts) == 2 {
|
||||
platforms[strings.ToLower(pParts[0])] = pParts[1]
|
||||
}
|
||||
}
|
||||
return platforms
|
||||
}
|
||||
-721
@@ -1,721 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/andreaskoch/go-fswatch"
|
||||
docker_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/joho/godotenv"
|
||||
gitignore "github.com/sabhiram/go-gitignore"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
"github.com/nektos/act/pkg/artifacts"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
)
|
||||
|
||||
// Execute is the entry point to running the CLI
|
||||
func Execute(ctx context.Context, version string) {
|
||||
input := new(Input)
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: newRunCommand(ctx, input),
|
||||
PersistentPreRun: setup(input),
|
||||
PersistentPostRun: cleanup(input),
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
|
||||
rootCmd.Flags().BoolP("list", "l", false, "list workflows")
|
||||
rootCmd.Flags().BoolP("graph", "g", false, "draw workflows")
|
||||
rootCmd.Flags().StringP("job", "j", "", "run a specific job ID")
|
||||
rootCmd.Flags().BoolP("bug-report", "", false, "Display system information for bug report")
|
||||
|
||||
rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
|
||||
rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
|
||||
rootCmd.Flags().StringArrayVar(&input.vars, "var", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)")
|
||||
rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
|
||||
rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
|
||||
rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
|
||||
rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
|
||||
rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
|
||||
rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present")
|
||||
rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present")
|
||||
rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
|
||||
rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
|
||||
rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch")
|
||||
rootCmd.Flags().BoolVar(&input.privileged, "privileged", false, "use privileged mode")
|
||||
rootCmd.Flags().StringVar(&input.usernsMode, "userns", "", "user namespace to use")
|
||||
rootCmd.Flags().BoolVar(&input.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
|
||||
rootCmd.Flags().StringArrayVarP(&input.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
|
||||
rootCmd.Flags().StringArrayVarP(&input.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
|
||||
rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure")
|
||||
rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com =github/super-linter)")
|
||||
rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "replace-ghe-action-token-with-github-com", "", "If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token")
|
||||
rootCmd.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
|
||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().BoolVar(&input.jsonLogger, "json", false, "Output logs in json format")
|
||||
rootCmd.PersistentFlags().BoolVar(&input.logPrefixJobID, "log-prefix-job-id", false, "Output the job id within non-json logs instead of the entire name")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", ".vars", "file with list of vars to read from (e.g. --var-file .vars)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "", "URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket)")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.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.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.noCacheServer, "no-cache-server", "", false, "Disable cache server")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.cacheServerPath, "cache-server-path", "", filepath.Join(CacheHomeDir, "actcache"), "Defines the path where the cache server stores caches.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.cacheServerAddr, "cache-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the cache server binds.")
|
||||
rootCmd.PersistentFlags().Uint16VarP(&input.cacheServerPort, "cache-server-port", "", 0, "Defines the port where the artifact server listens. 0 means a randomly available port.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.actionCachePath, "action-cache-path", "", filepath.Join(CacheHomeDir, "act"), "Defines the path where the actions get cached and host workspaces created.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.actionOfflineMode, "action-offline-mode", "", false, "If action contents exists, it will not be fetch and pull again. If turn on this,will turn off force pull")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.networkName, "network", "", "host", "Sets a docker network name. Defaults to host.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.useNewActionCache, "use-new-action-cache", "", false, "Enable using the new Action Cache for storing Actions locally")
|
||||
rootCmd.PersistentFlags().StringArrayVarP(&input.localRepository, "local-repository", "", []string{}, "Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols)")
|
||||
rootCmd.PersistentFlags().IntVarP(&input.maxParallel, "max-parallel", "", 0, "Limits the number of jobs running in parallel across all workflows (0 = no limit, uses number of CPUs)")
|
||||
rootCmd.SetArgs(args())
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Return locations where Act's config can be found in order: XDG spec, .actrc in HOME directory, .actrc in invocation directory
|
||||
func configLocations() []string {
|
||||
configFileName := ".actrc"
|
||||
|
||||
homePath := filepath.Join(UserHomeDir, configFileName)
|
||||
invocationPath := filepath.Join(".", configFileName)
|
||||
|
||||
// Though named xdg, adrg's lib support macOS and Windows config paths as well
|
||||
// It also takes cares of creating the parent folder so we don't need to bother later
|
||||
specPath, err := xdg.ConfigFile("act/actrc")
|
||||
if err != nil {
|
||||
specPath = homePath
|
||||
}
|
||||
|
||||
// This order should be enforced since the survey part relies on it
|
||||
return []string{specPath, homePath, invocationPath}
|
||||
}
|
||||
|
||||
func args() []string {
|
||||
actrc := configLocations()
|
||||
|
||||
args := make([]string, 0)
|
||||
for _, f := range actrc {
|
||||
args = append(args, readArgsFile(f, true)...)
|
||||
}
|
||||
|
||||
args = append(args, os.Args[1:]...)
|
||||
return args
|
||||
}
|
||||
|
||||
func bugReport(ctx context.Context, version string) error {
|
||||
sprintf := func(key, val string) string {
|
||||
return fmt.Sprintf("%-24s%s\n", key, val)
|
||||
}
|
||||
|
||||
report := sprintf("act version:", version)
|
||||
report += sprintf("GOOS:", runtime.GOOS)
|
||||
report += sprintf("GOARCH:", runtime.GOARCH)
|
||||
report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU()))
|
||||
|
||||
var dockerHost string
|
||||
var exists bool
|
||||
if dockerHost, exists = os.LookupEnv("DOCKER_HOST"); !exists {
|
||||
dockerHost = "DOCKER_HOST environment variable is not set"
|
||||
} else if dockerHost == "" {
|
||||
dockerHost = "DOCKER_HOST environment variable is empty."
|
||||
}
|
||||
|
||||
report += sprintf("Docker host:", dockerHost)
|
||||
report += fmt.Sprintln("Sockets found:")
|
||||
for _, p := range container.CommonSocketLocations {
|
||||
if _, err := os.Lstat(os.ExpandEnv(p)); err != nil {
|
||||
continue
|
||||
} else if _, err := os.Stat(os.ExpandEnv(p)); err != nil {
|
||||
report += fmt.Sprintf("\t%s(broken)\n", p)
|
||||
} else {
|
||||
report += fmt.Sprintf("\t%s\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
report += sprintf("Config files:", "")
|
||||
for _, c := range configLocations() {
|
||||
args := readArgsFile(c, false)
|
||||
if len(args) > 0 {
|
||||
report += fmt.Sprintf("\t%s:\n", c)
|
||||
for _, l := range args {
|
||||
report += fmt.Sprintf("\t\t%s\n", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vcs, ok := debug.ReadBuildInfo()
|
||||
if ok && vcs != nil {
|
||||
report += fmt.Sprintln("Build info:")
|
||||
vcs := *vcs
|
||||
report += sprintf("\tGo version:", vcs.GoVersion)
|
||||
report += sprintf("\tModule path:", vcs.Path)
|
||||
report += sprintf("\tMain version:", vcs.Main.Version)
|
||||
report += sprintf("\tMain path:", vcs.Main.Path)
|
||||
report += sprintf("\tMain checksum:", vcs.Main.Sum)
|
||||
|
||||
report += fmt.Sprintln("\tBuild settings:")
|
||||
for _, set := range vcs.Settings {
|
||||
report += sprintf(fmt.Sprintf("\t\t%s:", set.Key), set.Value)
|
||||
}
|
||||
}
|
||||
|
||||
info, err := container.GetHostInfo(ctx)
|
||||
if err != nil {
|
||||
fmt.Println(report)
|
||||
return err
|
||||
}
|
||||
|
||||
report += fmt.Sprintln("Docker Engine:")
|
||||
|
||||
report += sprintf("\tEngine version:", info.ServerVersion)
|
||||
report += sprintf("\tEngine runtime:", info.DefaultRuntime)
|
||||
report += sprintf("\tCgroup version:", info.CgroupVersion)
|
||||
report += sprintf("\tCgroup driver:", info.CgroupDriver)
|
||||
report += sprintf("\tStorage driver:", info.Driver)
|
||||
report += sprintf("\tRegistry URI:", info.IndexServerAddress)
|
||||
|
||||
report += sprintf("\tOS:", info.OperatingSystem)
|
||||
report += sprintf("\tOS type:", info.OSType)
|
||||
report += sprintf("\tOS version:", info.OSVersion)
|
||||
report += sprintf("\tOS arch:", info.Architecture)
|
||||
report += sprintf("\tOS kernel:", info.KernelVersion)
|
||||
report += sprintf("\tOS CPU:", fmt.Sprint(info.NCPU))
|
||||
report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024))
|
||||
|
||||
report += fmt.Sprintln("\tSecurity options:")
|
||||
for _, secopt := range info.SecurityOptions {
|
||||
report += fmt.Sprintf("\t\t%s\n", secopt)
|
||||
}
|
||||
|
||||
fmt.Println(report)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readArgsFile(file string, split bool) []string {
|
||||
args := make([]string, 0)
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return args
|
||||
}
|
||||
defer func() {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to close args file: %v", err)
|
||||
}
|
||||
}()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
arg := os.ExpandEnv(strings.TrimSpace(scanner.Text()))
|
||||
|
||||
if strings.HasPrefix(arg, "-") && split {
|
||||
args = append(args, regexp.MustCompile(`\s`).Split(arg, 2)...)
|
||||
} else if !split {
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func setup(_ *Input) func(*cobra.Command, []string) {
|
||||
return func(cmd *cobra.Command, _ []string) {
|
||||
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||
if verbose {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
loadVersionNotices(cmd.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup(inputs *Input) func(*cobra.Command, []string) {
|
||||
return func(cmd *cobra.Command, _ []string) {
|
||||
displayNotices(inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvs(env []string) map[string]string {
|
||||
envs := make(map[string]string, len(env))
|
||||
for _, envVar := range env {
|
||||
e := strings.SplitN(envVar, `=`, 2)
|
||||
if len(e) == 2 {
|
||||
envs[e[0]] = e[1]
|
||||
} else {
|
||||
envs[e[0]] = ""
|
||||
}
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
func readYamlFile(file string) (map[string]string, error) {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := map[string]string{}
|
||||
if err = yaml.Unmarshal(content, &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func readEnvs(path string, envs map[string]string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
var env map[string]string
|
||||
if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" {
|
||||
env, err = readYamlFile(path)
|
||||
} else {
|
||||
env, err = godotenv.Read(path)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading from %s: %v", path, err)
|
||||
}
|
||||
for k, v := range env {
|
||||
envs[k] = v
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseMatrix(matrix []string) map[string]map[string]bool {
|
||||
// each matrix entry should be of the form - string:string
|
||||
r := regexp.MustCompile(":")
|
||||
matrixes := make(map[string]map[string]bool)
|
||||
for _, m := range matrix {
|
||||
matrix := r.Split(m, 2)
|
||||
if len(matrix) < 2 {
|
||||
log.Fatalf("Invalid matrix format. Failed to parse %s", m)
|
||||
}
|
||||
if _, ok := matrixes[matrix[0]]; !ok {
|
||||
matrixes[matrix[0]] = make(map[string]bool)
|
||||
}
|
||||
matrixes[matrix[0]][matrix[1]] = true
|
||||
}
|
||||
return matrixes
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if input.jsonLogger {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
}
|
||||
|
||||
if ok, _ := cmd.Flags().GetBool("bug-report"); ok {
|
||||
return bugReport(ctx, cmd.Version)
|
||||
}
|
||||
if ret, err := container.GetSocketAndHost(input.containerDaemonSocket); err != nil {
|
||||
log.Warnf("Couldn't get a valid docker connection: %+v", err)
|
||||
} else {
|
||||
os.Setenv("DOCKER_HOST", ret.Host)
|
||||
input.containerDaemonSocket = ret.Socket
|
||||
log.Infof("Using docker host '%s', and daemon socket '%s'", ret.Host, ret.Socket)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
|
||||
l := log.New()
|
||||
l.SetFormatter(&log.TextFormatter{
|
||||
DisableQuote: true,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
l.Warnf(" \U000026A0 You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. \U000026A0 \n")
|
||||
}
|
||||
|
||||
log.Debugf("Loading environment from %s", input.Envfile())
|
||||
envs := parseEnvs(input.envs)
|
||||
_ = readEnvs(input.Envfile(), envs)
|
||||
|
||||
log.Debugf("Loading action inputs from %s", input.Inputfile())
|
||||
inputs := parseEnvs(input.inputs)
|
||||
_ = readEnvs(input.Inputfile(), inputs)
|
||||
|
||||
log.Debugf("Loading secrets from %s", input.Secretfile())
|
||||
secrets := newSecrets(input.secrets)
|
||||
_ = readEnvs(input.Secretfile(), secrets)
|
||||
|
||||
log.Debugf("Loading vars from %s", input.Varfile())
|
||||
vars := newSecrets(input.vars)
|
||||
_ = readEnvs(input.Varfile(), vars)
|
||||
|
||||
matrixes := parseMatrix(input.matrix)
|
||||
log.Debugf("Evaluated matrix inclusions: %v", matrixes)
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobID, err := cmd.Flags().GetString("job")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if we should just list the workflows
|
||||
list, err := cmd.Flags().GetBool("list")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if we should just draw the graph
|
||||
graph, err := cmd.Flags().GetBool("graph")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// collect all events from loaded workflows
|
||||
events := planner.GetEvents()
|
||||
|
||||
// plan with filtered jobs - to be used for filtering only
|
||||
var filterPlan *model.Plan
|
||||
|
||||
// Determine the event name to be filtered
|
||||
var filterEventName string
|
||||
|
||||
if len(args) > 0 {
|
||||
log.Debugf("Using first passed in arguments event for filtering: %s", args[0])
|
||||
filterEventName = args[0]
|
||||
} else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Debugf("Using first detected workflow event for filtering: %s", events[0])
|
||||
filterEventName = events[0]
|
||||
}
|
||||
|
||||
var plannerErr error
|
||||
if jobID != "" {
|
||||
log.Debugf("Preparing plan with a job: %s", jobID)
|
||||
filterPlan, plannerErr = planner.PlanJob(jobID)
|
||||
} else if filterEventName != "" {
|
||||
log.Debugf("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, plannerErr = planner.PlanEvent(filterEventName)
|
||||
} else {
|
||||
log.Debugf("Preparing plan with all jobs")
|
||||
filterPlan, plannerErr = planner.PlanAll()
|
||||
}
|
||||
if filterPlan == nil && plannerErr != nil {
|
||||
return plannerErr
|
||||
}
|
||||
|
||||
if list {
|
||||
err = printList(filterPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
}
|
||||
|
||||
if graph {
|
||||
err = drawGraph(filterPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
}
|
||||
|
||||
// plan with triggered jobs
|
||||
var plan *model.Plan
|
||||
|
||||
// Determine the event name to be triggered
|
||||
var eventName string
|
||||
|
||||
if len(args) > 0 {
|
||||
log.Debugf("Using first passed in arguments event: %s", args[0])
|
||||
eventName = args[0]
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
log.Debugf("Using the only detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Debugf("Using first detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
log.Debugf("Using default workflow event: push")
|
||||
eventName = "push"
|
||||
}
|
||||
|
||||
// build the plan for this run
|
||||
if jobID != "" {
|
||||
log.Debugf("Planning job: %s", jobID)
|
||||
plan, plannerErr = planner.PlanJob(jobID)
|
||||
} else {
|
||||
log.Debugf("Planning jobs for event: %s", eventName)
|
||||
plan, plannerErr = planner.PlanEvent(eventName)
|
||||
}
|
||||
if plan == nil && plannerErr != nil {
|
||||
return plannerErr
|
||||
}
|
||||
|
||||
// check to see if the main branch was defined
|
||||
defaultbranch, err := cmd.Flags().GetString("defaultbranch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if platforms flag is set, if not, run default image survey
|
||||
if len(input.platforms) == 0 {
|
||||
cfgFound := false
|
||||
cfgLocations := configLocations()
|
||||
for _, v := range cfgLocations {
|
||||
_, err := os.Stat(v)
|
||||
if os.IsExist(err) {
|
||||
cfgFound = true
|
||||
}
|
||||
}
|
||||
if !cfgFound && len(cfgLocations) > 0 {
|
||||
// The first config location refers to the global config folder one
|
||||
if err := defaultImageSurvey(cfgLocations[0]); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
input.platforms = readArgsFile(cfgLocations[0], true)
|
||||
}
|
||||
}
|
||||
deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`."
|
||||
if input.privileged {
|
||||
log.Warnf(deprecationWarning, "privileged", "--privileged")
|
||||
}
|
||||
if len(input.usernsMode) > 0 {
|
||||
log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode))
|
||||
}
|
||||
if len(input.containerCapAdd) > 0 {
|
||||
log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd))
|
||||
}
|
||||
if len(input.containerCapDrop) > 0 {
|
||||
log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop))
|
||||
}
|
||||
|
||||
// run the plan
|
||||
config := &runner.Config{
|
||||
Actor: input.actor,
|
||||
EventName: eventName,
|
||||
EventPath: input.EventPath(),
|
||||
DefaultBranch: defaultbranch,
|
||||
ForcePull: !input.actionOfflineMode && input.forcePull,
|
||||
ForceRebuild: input.forceRebuild,
|
||||
ReuseContainers: input.reuseContainers,
|
||||
Workdir: input.Workdir(),
|
||||
ActionCacheDir: input.actionCachePath,
|
||||
ActionOfflineMode: input.actionOfflineMode,
|
||||
BindWorkdir: input.bindWorkdir,
|
||||
LogOutput: !input.noOutput,
|
||||
JSONLogger: input.jsonLogger,
|
||||
LogPrefixJobID: input.logPrefixJobID,
|
||||
Env: envs,
|
||||
Secrets: secrets,
|
||||
Vars: vars,
|
||||
Inputs: inputs,
|
||||
Token: secrets["GITHUB_TOKEN"],
|
||||
InsecureSecrets: input.insecureSecrets,
|
||||
Platforms: input.newPlatforms(),
|
||||
Privileged: input.privileged,
|
||||
UsernsMode: input.usernsMode,
|
||||
ContainerArchitecture: input.containerArchitecture,
|
||||
ContainerDaemonSocket: input.containerDaemonSocket,
|
||||
ContainerOptions: input.containerOptions,
|
||||
UseGitIgnore: input.useGitIgnore,
|
||||
GitHubInstance: input.githubInstance,
|
||||
ContainerCapAdd: input.containerCapAdd,
|
||||
ContainerCapDrop: input.containerCapDrop,
|
||||
AutoRemove: input.autoRemove,
|
||||
ArtifactServerPath: input.artifactServerPath,
|
||||
ArtifactServerAddr: input.artifactServerAddr,
|
||||
ArtifactServerPort: input.artifactServerPort,
|
||||
NoSkipCheckout: input.noSkipCheckout,
|
||||
RemoteName: input.remoteName,
|
||||
ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom,
|
||||
ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom,
|
||||
Matrix: matrixes,
|
||||
ContainerNetworkMode: docker_container.NetworkMode(input.networkName),
|
||||
MaxParallel: input.maxParallel,
|
||||
}
|
||||
if input.useNewActionCache || len(input.localRepository) > 0 {
|
||||
if input.actionOfflineMode {
|
||||
config.ActionCache = &runner.GoGitActionCacheOfflineMode{
|
||||
Parent: runner.GoGitActionCache{
|
||||
Path: config.ActionCacheDir,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
config.ActionCache = &runner.GoGitActionCache{
|
||||
Path: config.ActionCacheDir,
|
||||
}
|
||||
}
|
||||
if len(input.localRepository) > 0 {
|
||||
localRepositories := map[string]string{}
|
||||
for _, l := range input.localRepository {
|
||||
k, v, _ := strings.Cut(l, "=")
|
||||
localRepositories[k] = v
|
||||
}
|
||||
config.ActionCache = &runner.LocalRepositoryCache{
|
||||
Parent: config.ActionCache,
|
||||
LocalRepositories: localRepositories,
|
||||
CacheDirCache: map[string]string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
r, err := runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort)
|
||||
|
||||
const cacheURLKey = "ACTIONS_CACHE_URL"
|
||||
var cacheHandler *artifactcache.Handler
|
||||
if !input.noCacheServer && envs[cacheURLKey] == "" {
|
||||
var err error
|
||||
cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerAddr, input.cacheServerPort, common.Logger(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envs[cacheURLKey] = cacheHandler.ExternalURL() + "/"
|
||||
}
|
||||
|
||||
ctx = common.WithDryrun(ctx, input.dryrun)
|
||||
if watch, err := cmd.Flags().GetBool("watch"); err != nil {
|
||||
return err
|
||||
} else if watch {
|
||||
err = watchAndRun(ctx, r.NewPlanExecutor(plan))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
}
|
||||
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
cancel()
|
||||
_ = cacheHandler.Close()
|
||||
return nil
|
||||
})
|
||||
err = executor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
}
|
||||
}
|
||||
|
||||
func defaultImageSurvey(actrc string) error {
|
||||
var answer string
|
||||
confirmation := &survey.Select{
|
||||
Message: "Please choose the default image you want to use with act:\n - Large size image: ca. 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images\n - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most actions\n - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions\n\nDefault image and other options can be changed manually in ~/.actrc (please refer to https://github.com/nektos/act#configuration for additional information about file structure)",
|
||||
Help: "If you want to know why act asks you that, please go to https://github.com/nektos/act/issues/107",
|
||||
Default: "Medium",
|
||||
Options: []string{"Large", "Medium", "Micro"},
|
||||
}
|
||||
|
||||
err := survey.AskOne(confirmation, &answer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var option string
|
||||
switch answer {
|
||||
case "Large":
|
||||
option = "-P ubuntu-latest=catthehacker/ubuntu:full-latest\n-P ubuntu-22.04=catthehacker/ubuntu:full-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:full-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:full-18.04\n"
|
||||
case "Medium":
|
||||
option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n"
|
||||
case "Micro":
|
||||
option = "-P ubuntu-latest=node:16-buster-slim\n-P ubuntu-22.04=node:16-bullseye-slim\n-P ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n"
|
||||
}
|
||||
|
||||
f, err := os.Create(actrc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(option)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func watchAndRun(ctx context.Context, fn common.Executor) error {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ignoreFile := filepath.Join(dir, ".gitignore")
|
||||
ignore := &gitignore.GitIgnore{}
|
||||
if info, err := os.Stat(ignoreFile); err == nil && !info.IsDir() {
|
||||
ignore, err = gitignore.CompileIgnoreFile(ignoreFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compile %q: %w", ignoreFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
folderWatcher := fswatch.NewFolderWatcher(
|
||||
dir,
|
||||
true,
|
||||
ignore.MatchesPath,
|
||||
2, // 2 seconds
|
||||
)
|
||||
|
||||
folderWatcher.Start()
|
||||
defer folderWatcher.Stop()
|
||||
|
||||
// run once before watching
|
||||
if err := fn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for folderWatcher.IsRunning() {
|
||||
log.Debugf("Watching %s for changes", dir)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case changes := <-folderWatcher.ChangeDetails():
|
||||
log.Debugf("%s", changes.String())
|
||||
if err := fn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type secrets map[string]string
|
||||
|
||||
func newSecrets(secretList []string) secrets {
|
||||
s := make(map[string]string)
|
||||
for _, secretPair := range secretList {
|
||||
secretPairParts := strings.SplitN(secretPair, "=", 2)
|
||||
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
|
||||
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
|
||||
log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||
}
|
||||
if len(secretPairParts) == 2 {
|
||||
s[secretPairParts[0]] = secretPairParts[1]
|
||||
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||
s[secretPairParts[0]] = env
|
||||
} else {
|
||||
fmt.Printf("Provide value for '%s': ", secretPairParts[0])
|
||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read input: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s[secretPairParts[0]] = string(val)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s secrets) AsMap() map[string]string {
|
||||
return s
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto # auto compares coverage to the previous base commit
|
||||
threshold: 1%
|
||||
patch:
|
||||
default:
|
||||
target: 50%
|
||||
@@ -1,92 +1,113 @@
|
||||
module github.com/nektos/act
|
||||
module gitea.com/gitea/act_runner
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/andreaskoch/go-fswatch v1.0.0
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v24.0.9+incompatible // 24.0 branch
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/imdario/mergo v0.3.16
|
||||
code.gitea.io/actions-proto-go v0.2.1
|
||||
code.gitea.io/gitea-vet v0.2.3-0.20230113022436-2b1561217fa5
|
||||
github.com/avast/retry-go/v4 v4.3.1
|
||||
github.com/bufbuild/connect-go v1.3.1
|
||||
github.com/docker/docker v23.0.1+incompatible
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-chi/render v1.0.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/moby/buildkit v0.12.5
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/opencontainers/selinux v1.11.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rhysd/actionlint v1.6.27
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2
|
||||
golang.org/x/term v0.18.0
|
||||
gotest.tools/v3 v3.5.1
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/nektos/act v0.0.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/term v0.7.0
|
||||
golang.org/x/time v0.1.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.4.0
|
||||
modernc.org/sqlite v1.22.0
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/containerd/containerd v1.7.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/containerd/containerd v1.6.18 // indirect
|
||||
github.com/creack/pty v1.1.18 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/goccy/go-json v0.8.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.17.2 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/buildkit v0.11.4 // indirect
|
||||
github.com/moby/patternmatcher v0.5.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // 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/runc v1.1.12 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||
github.com/opencontainers/runc v1.1.3 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rhysd/actionlint v1.6.23 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.243.3-0.20230420082431-e12252a43a3f
|
||||
-401
@@ -1,401 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Code generated by godownloader on 2021-12-22T16:10:52Z. DO NOT EDIT.
|
||||
#
|
||||
|
||||
usage() {
|
||||
this=$1
|
||||
cat <<EOF
|
||||
$this: download go binaries for nektos/act
|
||||
|
||||
Usage: $this [-b] bindir [-d] [tag]
|
||||
-b sets bindir or installation directory, Defaults to ./bin
|
||||
-d turns on debug logging
|
||||
[tag] is a tag from
|
||||
https://github.com/nektos/act/releases
|
||||
If tag is missing, then the latest will be used.
|
||||
|
||||
Generated by godownloader
|
||||
https://github.com/goreleaser/godownloader
|
||||
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
#BINDIR is ./bin unless set be ENV
|
||||
# over-ridden by flag below
|
||||
|
||||
BINDIR=${BINDIR:-./bin}
|
||||
while getopts "b:dh?x" arg; do
|
||||
case "$arg" in
|
||||
b) BINDIR="$OPTARG" ;;
|
||||
d) log_set_priority 10 ;;
|
||||
h | \?) usage "$0" ;;
|
||||
x) set -x ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
TAG=$1
|
||||
}
|
||||
# this function wraps all the destructive operations
|
||||
# if a curl|bash cuts off the end of the script due to
|
||||
# network, either nothing will happen or will syntax error
|
||||
# out preventing half-done work
|
||||
execute() {
|
||||
tmpdir=$(mktemp -d)
|
||||
log_debug "downloading files into ${tmpdir}"
|
||||
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
|
||||
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
|
||||
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
|
||||
srcdir="${tmpdir}"
|
||||
(cd "${tmpdir}" && untar "${TARBALL}")
|
||||
test ! -d "${BINDIR}" && install -d "${BINDIR}"
|
||||
for binexe in $BINARIES; do
|
||||
if [ "$OS" = "windows" ]; then
|
||||
binexe="${binexe}.exe"
|
||||
fi
|
||||
install "${srcdir}/${binexe}" "${BINDIR}/"
|
||||
log_info "installed ${BINDIR}/${binexe}"
|
||||
done
|
||||
rm -rf "${tmpdir}"
|
||||
}
|
||||
get_binaries() {
|
||||
case "$PLATFORM" in
|
||||
darwin/386) BINARIES="act" ;;
|
||||
darwin/amd64) BINARIES="act" ;;
|
||||
darwin/arm64) BINARIES="act" ;;
|
||||
darwin/armv6) BINARIES="act" ;;
|
||||
darwin/armv7) BINARIES="act" ;;
|
||||
linux/386) BINARIES="act" ;;
|
||||
linux/amd64) BINARIES="act" ;;
|
||||
linux/arm64) BINARIES="act" ;;
|
||||
linux/armv6) BINARIES="act" ;;
|
||||
linux/armv7) BINARIES="act" ;;
|
||||
windows/386) BINARIES="act" ;;
|
||||
windows/amd64) BINARIES="act" ;;
|
||||
windows/arm64) BINARIES="act" ;;
|
||||
windows/armv6) BINARIES="act" ;;
|
||||
windows/armv7) BINARIES="act" ;;
|
||||
*)
|
||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
tag_to_version() {
|
||||
if [ -z "${TAG}" ]; then
|
||||
log_info "checking GitHub for latest tag"
|
||||
else
|
||||
log_info "checking GitHub for tag '${TAG}'"
|
||||
fi
|
||||
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
|
||||
if test -z "$REALTAG"; then
|
||||
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
|
||||
exit 1
|
||||
fi
|
||||
# if version starts with 'v', remove it
|
||||
TAG="$REALTAG"
|
||||
VERSION=${TAG#v}
|
||||
}
|
||||
adjust_format() {
|
||||
# change format (tar.gz or zip) based on OS
|
||||
case ${OS} in
|
||||
windows) FORMAT=zip ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
adjust_os() {
|
||||
# adjust archive name based on OS
|
||||
case ${OS} in
|
||||
386) OS=i386 ;;
|
||||
amd64) OS=x86_64 ;;
|
||||
darwin) OS=Darwin ;;
|
||||
linux) OS=Linux ;;
|
||||
windows) OS=Windows ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
adjust_arch() {
|
||||
# adjust archive name based on ARCH
|
||||
case ${ARCH} in
|
||||
386) ARCH=i386 ;;
|
||||
amd64) ARCH=x86_64 ;;
|
||||
darwin) ARCH=Darwin ;;
|
||||
linux) ARCH=Linux ;;
|
||||
windows) ARCH=Windows ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
https://github.com/client9/shlib - portable posix shell functions
|
||||
Public domain - http://unlicense.org
|
||||
https://github.com/client9/shlib/blob/master/LICENSE.md
|
||||
but credit (and pull requests) appreciated.
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
is_command() {
|
||||
command -v "$1" >/dev/null
|
||||
}
|
||||
echoerr() {
|
||||
echo "$@" 1>&2
|
||||
}
|
||||
log_prefix() {
|
||||
echo "$0"
|
||||
}
|
||||
_logp=6
|
||||
log_set_priority() {
|
||||
_logp="$1"
|
||||
}
|
||||
log_priority() {
|
||||
if test -z "$1"; then
|
||||
echo "$_logp"
|
||||
return
|
||||
fi
|
||||
[ "$1" -le "$_logp" ]
|
||||
}
|
||||
log_tag() {
|
||||
case $1 in
|
||||
0) echo "emerg" ;;
|
||||
1) echo "alert" ;;
|
||||
2) echo "crit" ;;
|
||||
3) echo "err" ;;
|
||||
4) echo "warning" ;;
|
||||
5) echo "notice" ;;
|
||||
6) echo "info" ;;
|
||||
7) echo "debug" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
log_debug() {
|
||||
log_priority 7 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
|
||||
}
|
||||
log_info() {
|
||||
log_priority 6 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
|
||||
}
|
||||
log_err() {
|
||||
log_priority 3 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
|
||||
}
|
||||
log_crit() {
|
||||
log_priority 2 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
|
||||
}
|
||||
uname_os() {
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case "$os" in
|
||||
cygwin_nt*) os="windows" ;;
|
||||
mingw*) os="windows" ;;
|
||||
msys_nt*) os="windows" ;;
|
||||
esac
|
||||
echo "$os"
|
||||
}
|
||||
uname_arch() {
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64) arch="amd64" ;;
|
||||
x86) arch="386" ;;
|
||||
i686) arch="386" ;;
|
||||
i386) arch="386" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
armv5*) arch="armv5" ;;
|
||||
armv6*) arch="armv6" ;;
|
||||
armv7*) arch="armv7" ;;
|
||||
esac
|
||||
echo ${arch}
|
||||
}
|
||||
uname_os_check() {
|
||||
os=$(uname_os)
|
||||
case "$os" in
|
||||
darwin) return 0 ;;
|
||||
dragonfly) return 0 ;;
|
||||
freebsd) return 0 ;;
|
||||
linux) return 0 ;;
|
||||
android) return 0 ;;
|
||||
nacl) return 0 ;;
|
||||
netbsd) return 0 ;;
|
||||
openbsd) return 0 ;;
|
||||
plan9) return 0 ;;
|
||||
solaris) return 0 ;;
|
||||
windows) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
uname_arch_check() {
|
||||
arch=$(uname_arch)
|
||||
case "$arch" in
|
||||
386) return 0 ;;
|
||||
amd64) return 0 ;;
|
||||
arm64) return 0 ;;
|
||||
armv5) return 0 ;;
|
||||
armv6) return 0 ;;
|
||||
armv7) return 0 ;;
|
||||
ppc64) return 0 ;;
|
||||
ppc64le) return 0 ;;
|
||||
mips) return 0 ;;
|
||||
mipsle) return 0 ;;
|
||||
mips64) return 0 ;;
|
||||
mips64le) return 0 ;;
|
||||
s390x) return 0 ;;
|
||||
amd64p32) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
untar() {
|
||||
tarball=$1
|
||||
case "${tarball}" in
|
||||
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
|
||||
*.tar) tar --no-same-owner -xf "${tarball}" ;;
|
||||
*.zip) unzip "${tarball}" ;;
|
||||
*)
|
||||
log_err "untar unknown archive format for ${tarball}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
http_download_curl() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
|
||||
else
|
||||
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
log_debug "http_download_curl received HTTP status $code"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
http_download_wget() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
wget -q -O "$local_file" "$source_url"
|
||||
else
|
||||
wget -q --header "$header" -O "$local_file" "$source_url"
|
||||
fi
|
||||
}
|
||||
http_download() {
|
||||
log_debug "http_download $2"
|
||||
if is_command curl; then
|
||||
http_download_curl "$@"
|
||||
return
|
||||
elif is_command wget; then
|
||||
http_download_wget "$@"
|
||||
return
|
||||
fi
|
||||
log_crit "http_download unable to find wget or curl"
|
||||
return 1
|
||||
}
|
||||
http_copy() {
|
||||
tmp=$(mktemp)
|
||||
http_download "${tmp}" "$1" "$2" || return 1
|
||||
body=$(cat "$tmp")
|
||||
rm -f "${tmp}"
|
||||
echo "$body"
|
||||
}
|
||||
github_release() {
|
||||
owner_repo=$1
|
||||
version=$2
|
||||
test -z "$version" && version="latest"
|
||||
giturl="https://github.com/${owner_repo}/releases/${version}"
|
||||
json=$(http_copy "$giturl" "Accept:application/json")
|
||||
test -z "$json" && return 1
|
||||
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
|
||||
test -z "$version" && return 1
|
||||
echo "$version"
|
||||
}
|
||||
hash_sha256() {
|
||||
TARGET=${1:-/dev/stdin}
|
||||
if is_command gsha256sum; then
|
||||
hash=$(gsha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command sha256sum; then
|
||||
hash=$(sha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command shasum; then
|
||||
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command openssl; then
|
||||
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f a
|
||||
else
|
||||
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
hash_sha256_verify() {
|
||||
TARGET=$1
|
||||
checksums=$2
|
||||
if [ -z "$checksums" ]; then
|
||||
log_err "hash_sha256_verify checksum file not specified in arg2"
|
||||
return 1
|
||||
fi
|
||||
BASENAME=${TARGET##*/}
|
||||
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
if [ -z "$want" ]; then
|
||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||
return 1
|
||||
fi
|
||||
got=$(hash_sha256 "$TARGET")
|
||||
if [ "$want" != "$got" ]; then
|
||||
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
End of functions from https://github.com/client9/shlib
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
|
||||
PROJECT_NAME="act"
|
||||
OWNER=nektos
|
||||
REPO="act"
|
||||
BINARY=act
|
||||
FORMAT=tar.gz
|
||||
OS=$(uname_os)
|
||||
ARCH=$(uname_arch)
|
||||
PREFIX="$OWNER/$REPO"
|
||||
|
||||
# use in logging routines
|
||||
log_prefix() {
|
||||
echo "$PREFIX"
|
||||
}
|
||||
PLATFORM="${OS}/${ARCH}"
|
||||
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
|
||||
uname_os_check "$OS"
|
||||
uname_arch_check "$ARCH"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
get_binaries
|
||||
|
||||
tag_to_version
|
||||
|
||||
adjust_format
|
||||
|
||||
adjust_os
|
||||
|
||||
adjust_arch
|
||||
|
||||
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
|
||||
|
||||
NAME=${PROJECT_NAME}_${OS}_${ARCH}
|
||||
TARBALL=${NAME}.${FORMAT}
|
||||
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
|
||||
CHECKSUM=checksums.txt
|
||||
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
|
||||
|
||||
|
||||
execute
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package artifactcache
|
||||
|
||||
import _ "github.com/mattn/go-sqlite3"
|
||||
|
||||
var sqliteDriverName = "sqlite3"
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !cgo
|
||||
// +build !cgo
|
||||
|
||||
package artifactcache
|
||||
|
||||
import _ "modernc.org/sqlite"
|
||||
|
||||
var sqliteDriverName = "sqlite"
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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
|
||||
@@ -5,4 +8,5 @@
|
||||
// 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
|
||||
@@ -0,0 +1,415 @@
|
||||
// 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"
|
||||
"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(sqliteDriverName, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
ID int64 `xorm:"id pk autoincr" json:"-"`
|
||||
Key string `xorm:"TEXT index unique(key_version)" json:"key"`
|
||||
Version string `xorm:"TEXT unique(key_version)" json:"version"`
|
||||
Size int64 `json:"cacheSize"`
|
||||
Complete bool `xorm:"index(complete_used_at)" json:"-"`
|
||||
UsedAt int64 `xorm:"index(complete_used_at) updated" json:"-"`
|
||||
CreatedAt int64 `xorm:"index created" json:"-"`
|
||||
}
|
||||
|
||||
// Bind implements render.Binder
|
||||
func (c *Cache) Bind(_ *http.Request) error {
|
||||
if c.Key == "" {
|
||||
return fmt.Errorf("missing key")
|
||||
}
|
||||
if c.Version == "" {
|
||||
return fmt.Errorf("missing version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
@@ -21,7 +24,7 @@ func NewStorage(rootDir string) (*Storage, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Storage) Exist(id uint64) (bool, error) {
|
||||
func (s *Storage) Exist(id int64) (bool, error) {
|
||||
name := s.filename(id)
|
||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||
return false, nil
|
||||
@@ -31,7 +34,7 @@ func (s *Storage) Exist(id uint64) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Storage) Write(id uint64, offset int64, reader io.Reader) error {
|
||||
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
|
||||
@@ -46,7 +49,7 @@ func (s *Storage) Write(id uint64, offset int64, reader io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) Commit(id uint64, size int64) (int64, error) {
|
||||
func (s *Storage) Commit(id int64, size int64) error {
|
||||
defer func() {
|
||||
_ = os.RemoveAll(s.tempDir(id))
|
||||
}()
|
||||
@@ -54,15 +57,15 @@ func (s *Storage) Commit(id uint64, size int64) (int64, error) {
|
||||
name := s.filename(id)
|
||||
tempNames, err := s.tempNames(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -70,51 +73,47 @@ func (s *Storage) Commit(id uint64, size int64) (int64, error) {
|
||||
for _, v := range tempNames {
|
||||
f, err := os.Open(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
n, err := io.Copy(file, f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
written += n
|
||||
}
|
||||
|
||||
// If size is less than 0, it means the size is unknown.
|
||||
// We can't check the size of the file, just skip the check.
|
||||
// It happens when the request comes from old versions of actions, like `actions/cache@v2`.
|
||||
if size >= 0 && written != size {
|
||||
if written != size {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(name)
|
||||
return 0, fmt.Errorf("broken file: %v != %v", written, size)
|
||||
return fmt.Errorf("broken file: %v != %v", written, size)
|
||||
}
|
||||
|
||||
return written, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id uint64) {
|
||||
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 uint64) {
|
||||
func (s *Storage) Remove(id int64) {
|
||||
_ = os.Remove(s.filename(id))
|
||||
_ = os.RemoveAll(s.tempDir(id))
|
||||
}
|
||||
|
||||
func (s *Storage) filename(id uint64) string {
|
||||
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 uint64) string {
|
||||
func (s *Storage) tempDir(id int64) string {
|
||||
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
||||
}
|
||||
|
||||
func (s *Storage) tempName(id uint64, offset int64) string {
|
||||
func (s *Storage) tempName(id, offset int64) string {
|
||||
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
||||
}
|
||||
|
||||
func (s *Storage) tempNames(id uint64) ([]string, error) {
|
||||
func (s *Storage) tempNames(id int64) ([]string, error) {
|
||||
dir := s.tempDir(id)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func responseJson(w http.ResponseWriter, r *http.Request, code int, v ...any) {
|
||||
render.Status(r, code)
|
||||
if len(v) == 0 || v[0] == nil {
|
||||
render.JSON(w, r, struct{}{})
|
||||
} else if err, ok := v[0].(error); ok {
|
||||
logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
||||
render.JSON(w, r, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
render.JSON(w, r, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
func parseContentRange(s string) (int64, int64, error) {
|
||||
// support the format like "bytes 11-22/*" only
|
||||
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
|
||||
s1, s2, _ := strings.Cut(s, "-")
|
||||
|
||||
start, err := strconv.ParseInt(s1, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||
}
|
||||
stop, err := strconv.ParseInt(s2, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
|
||||
}
|
||||
return start, stop, nil
|
||||
}
|
||||
|
||||
func getOutboundIP() (net.IP, error) {
|
||||
// FIXME: It makes more sense to use the gateway IP address of container network
|
||||
if conn, err := net.Dial("udp", "8.8.8.8:80"); err == nil {
|
||||
defer conn.Close()
|
||||
return conn.LocalAddr().(*net.UDPAddr).IP, nil
|
||||
}
|
||||
if ifaces, err := net.Interfaces(); err == nil {
|
||||
for _, i := range ifaces {
|
||||
if addrs, err := i.Addrs(); err == nil {
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip.IsGlobalUnicast() {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no outbound IP address found")
|
||||
}
|
||||
|
||||
// engine is a wrapper of *xorm.Engine, with a lock.
|
||||
// To avoid racing of sqlite, we don't care performance here.
|
||||
type engine struct {
|
||||
e *xorm.Engine
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (e *engine) Exec(f func(*xorm.Session) error) error {
|
||||
e.m.Lock()
|
||||
defer e.m.Unlock()
|
||||
|
||||
sess := e.e.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
return f(sess)
|
||||
}
|
||||
|
||||
func (e *engine) ExecBool(f func(*xorm.Session) (bool, error)) (bool, error) {
|
||||
e.m.Lock()
|
||||
defer e.m.Unlock()
|
||||
|
||||
sess := e.e.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
return f(sess)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) {
|
||||
// ./act_runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: ver.Version(),
|
||||
SilenceUsage: true,
|
||||
}
|
||||
configFile := ""
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||
|
||||
// ./act_runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a runner to the server",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs
|
||||
}
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token")
|
||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
// ./act_runner daemon
|
||||
daemonCmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Run as a runner daemon",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runDaemon(ctx, &configFile),
|
||||
}
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// ./act_runner exec
|
||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||
|
||||
// ./act_runner config
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "Generate an example config file",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("%s", config.Example)
|
||||
},
|
||||
})
|
||||
|
||||
// hide completion command
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/poll"
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.Infoln("Starting runner daemon")
|
||||
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
|
||||
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("registration file not found, please register the runner first")
|
||||
return err
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to load registration file: %w", err)
|
||||
}
|
||||
|
||||
ls := labels.Labels{}
|
||||
for _, l := range reg.Labels {
|
||||
label, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, label)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||
}
|
||||
|
||||
if ls.RequireDocker() {
|
||||
if err := envcheck.CheckIfDockerRunning(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cli := client.New(
|
||||
reg.Address,
|
||||
cfg.Runner.Insecure,
|
||||
reg.UUID,
|
||||
reg.Token,
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
poller := poll.New(cfg, cli, runner)
|
||||
|
||||
poller.Poll(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// initLogging setup the global logrus logger.
|
||||
func initLogging(cfg *config.Config) {
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
if l := cfg.Log.Level; l != "" {
|
||||
level, err := log.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorf("invalid log level: %q", l)
|
||||
}
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("log level changed to %v", level)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2019 nektos
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/nektos/act/pkg/artifacts"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||
)
|
||||
|
||||
type executeArgs struct {
|
||||
runList bool
|
||||
job string
|
||||
event string
|
||||
workdir string
|
||||
workflowsPath string
|
||||
noWorkflowRecurse bool
|
||||
autodetectEvent bool
|
||||
forcePull bool
|
||||
forceRebuild bool
|
||||
jsonLogger bool
|
||||
envs []string
|
||||
envfile string
|
||||
secrets []string
|
||||
defaultActionsUrl string
|
||||
insecureSecrets bool
|
||||
privileged bool
|
||||
usernsMode string
|
||||
containerArchitecture string
|
||||
containerDaemonSocket string
|
||||
useGitIgnore bool
|
||||
containerCapAdd []string
|
||||
containerCapDrop []string
|
||||
containerOptions string
|
||||
artifactServerPath string
|
||||
artifactServerAddr string
|
||||
artifactServerPort string
|
||||
noSkipCheckout bool
|
||||
debug bool
|
||||
dryrun bool
|
||||
image string
|
||||
cacheHandler *artifactcache.Handler
|
||||
}
|
||||
|
||||
// WorkflowsPath returns path to workflow file(s)
|
||||
func (i *executeArgs) WorkflowsPath() string {
|
||||
return i.resolve(i.workflowsPath)
|
||||
}
|
||||
|
||||
// Envfile returns path to .env
|
||||
func (i *executeArgs) Envfile() string {
|
||||
return i.resolve(i.envfile)
|
||||
}
|
||||
|
||||
func (i *executeArgs) LoadSecrets() map[string]string {
|
||||
s := make(map[string]string)
|
||||
for _, secretPair := range i.secrets {
|
||||
secretPairParts := strings.SplitN(secretPair, "=", 2)
|
||||
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
|
||||
if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
|
||||
log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||
}
|
||||
if len(secretPairParts) == 2 {
|
||||
s[secretPairParts[0]] = secretPairParts[1]
|
||||
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||
s[secretPairParts[0]] = env
|
||||
} else {
|
||||
fmt.Printf("Provide value for '%s': ", secretPairParts[0])
|
||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read input: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s[secretPairParts[0]] = string(val)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readEnvs(path string, envs map[string]string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
env, err := godotenv.Read(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading from %s: %v", path, err)
|
||||
}
|
||||
for k, v := range env {
|
||||
envs[k] = v
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *executeArgs) LoadEnvs() map[string]string {
|
||||
envs := make(map[string]string)
|
||||
if i.envs != nil {
|
||||
for _, envVar := range i.envs {
|
||||
e := strings.SplitN(envVar, `=`, 2)
|
||||
if len(e) == 2 {
|
||||
envs[e[0]] = e[1]
|
||||
} else {
|
||||
envs[e[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = readEnvs(i.Envfile(), envs)
|
||||
|
||||
envs["ACTIONS_CACHE_URL"] = i.cacheHandler.ExternalURL() + "/"
|
||||
|
||||
return envs
|
||||
}
|
||||
|
||||
// Workdir returns path to workdir
|
||||
func (i *executeArgs) Workdir() string {
|
||||
return i.resolve(".")
|
||||
}
|
||||
|
||||
func (i *executeArgs) resolve(path string) string {
|
||||
basedir, err := filepath.Abs(i.workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if path == "" {
|
||||
return path
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(basedir, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func printList(plan *model.Plan) error {
|
||||
type lineInfoDef struct {
|
||||
jobID string
|
||||
jobName string
|
||||
stage string
|
||||
wfName string
|
||||
wfFile string
|
||||
events string
|
||||
}
|
||||
lineInfos := []lineInfoDef{}
|
||||
|
||||
header := lineInfoDef{
|
||||
jobID: "Job ID",
|
||||
jobName: "Job name",
|
||||
stage: "Stage",
|
||||
wfName: "Workflow name",
|
||||
wfFile: "Workflow file",
|
||||
events: "Events",
|
||||
}
|
||||
|
||||
jobs := map[string]bool{}
|
||||
duplicateJobIDs := false
|
||||
|
||||
jobIDMaxWidth := len(header.jobID)
|
||||
jobNameMaxWidth := len(header.jobName)
|
||||
stageMaxWidth := len(header.stage)
|
||||
wfNameMaxWidth := len(header.wfName)
|
||||
wfFileMaxWidth := len(header.wfFile)
|
||||
eventsMaxWidth := len(header.events)
|
||||
|
||||
for i, stage := range plan.Stages {
|
||||
for _, r := range stage.Runs {
|
||||
jobID := r.JobID
|
||||
line := lineInfoDef{
|
||||
jobID: jobID,
|
||||
jobName: r.String(),
|
||||
stage: strconv.Itoa(i),
|
||||
wfName: r.Workflow.Name,
|
||||
wfFile: r.Workflow.File,
|
||||
events: strings.Join(r.Workflow.On(), `,`),
|
||||
}
|
||||
if _, ok := jobs[jobID]; ok {
|
||||
duplicateJobIDs = true
|
||||
} else {
|
||||
jobs[jobID] = true
|
||||
}
|
||||
lineInfos = append(lineInfos, line)
|
||||
if jobIDMaxWidth < len(line.jobID) {
|
||||
jobIDMaxWidth = len(line.jobID)
|
||||
}
|
||||
if jobNameMaxWidth < len(line.jobName) {
|
||||
jobNameMaxWidth = len(line.jobName)
|
||||
}
|
||||
if stageMaxWidth < len(line.stage) {
|
||||
stageMaxWidth = len(line.stage)
|
||||
}
|
||||
if wfNameMaxWidth < len(line.wfName) {
|
||||
wfNameMaxWidth = len(line.wfName)
|
||||
}
|
||||
if wfFileMaxWidth < len(line.wfFile) {
|
||||
wfFileMaxWidth = len(line.wfFile)
|
||||
}
|
||||
if eventsMaxWidth < len(line.events) {
|
||||
eventsMaxWidth = len(line.events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobIDMaxWidth += 2
|
||||
jobNameMaxWidth += 2
|
||||
stageMaxWidth += 2
|
||||
wfNameMaxWidth += 2
|
||||
wfFileMaxWidth += 2
|
||||
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, header.stage,
|
||||
-jobIDMaxWidth, header.jobID,
|
||||
-jobNameMaxWidth, header.jobName,
|
||||
-wfNameMaxWidth, header.wfName,
|
||||
-wfFileMaxWidth, header.wfFile,
|
||||
-eventsMaxWidth, header.events,
|
||||
)
|
||||
for _, line := range lineInfos {
|
||||
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||
-stageMaxWidth, line.stage,
|
||||
-jobIDMaxWidth, line.jobID,
|
||||
-jobNameMaxWidth, line.jobName,
|
||||
-wfNameMaxWidth, line.wfName,
|
||||
-wfFileMaxWidth, line.wfFile,
|
||||
-eventsMaxWidth, line.events,
|
||||
)
|
||||
}
|
||||
if duplicateJobIDs {
|
||||
fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
// plan with filtered jobs - to be used for filtering only
|
||||
var filterPlan *model.Plan
|
||||
|
||||
// Determine the event name to be filtered
|
||||
var filterEventName string = ""
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
filterEventName = execArgs.event
|
||||
} else if execArgs.autodetectEvent {
|
||||
// collect all events from loaded workflows
|
||||
events := planner.GetEvents()
|
||||
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event for filtering: %s", events[0])
|
||||
|
||||
filterEventName = events[0]
|
||||
}
|
||||
|
||||
var err error
|
||||
if execArgs.job != "" {
|
||||
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if filterEventName != "" {
|
||||
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Preparing plan with all jobs")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printList(filterPlan)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if execArgs.runList {
|
||||
return runExecList(ctx, planner, execArgs)
|
||||
}
|
||||
|
||||
// plan with triggered jobs
|
||||
var plan *model.Plan
|
||||
|
||||
// Determine the event name to be triggered
|
||||
var eventName string
|
||||
|
||||
// collect all events from loaded workflows
|
||||
events := planner.GetEvents()
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
eventName = args[0]
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
log.Infof("Using default workflow event: push")
|
||||
eventName = "push"
|
||||
}
|
||||
|
||||
// build the plan for this run
|
||||
if execArgs.job != "" {
|
||||
log.Infof("Planning job: %s", execArgs.job)
|
||||
plan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Planning jobs for event: %s", eventName)
|
||||
plan, err = planner.PlanEvent(eventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
maxLifetime := 3 * time.Hour
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// init a cache server
|
||||
handler, err := artifactcache.StartHandler("", "", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||
execArgs.cacheHandler = handler
|
||||
|
||||
// run the plan
|
||||
config := &runner.Config{
|
||||
Workdir: execArgs.Workdir(),
|
||||
BindWorkdir: false,
|
||||
ReuseContainers: false,
|
||||
ForcePull: execArgs.forcePull,
|
||||
ForceRebuild: execArgs.forceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: execArgs.jsonLogger,
|
||||
Env: execArgs.LoadEnvs(),
|
||||
Secrets: execArgs.LoadSecrets(),
|
||||
InsecureSecrets: execArgs.insecureSecrets,
|
||||
Privileged: execArgs.privileged,
|
||||
UsernsMode: execArgs.usernsMode,
|
||||
ContainerArchitecture: execArgs.containerArchitecture,
|
||||
ContainerDaemonSocket: execArgs.containerDaemonSocket,
|
||||
UseGitIgnore: execArgs.useGitIgnore,
|
||||
// GitHubInstance: t.client.Address(),
|
||||
ContainerCapAdd: execArgs.containerCapAdd,
|
||||
ContainerCapDrop: execArgs.containerCapDrop,
|
||||
ContainerOptions: execArgs.containerOptions,
|
||||
AutoRemove: true,
|
||||
ArtifactServerPath: execArgs.artifactServerPath,
|
||||
ArtifactServerPort: execArgs.artifactServerPort,
|
||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||
// PresetGitHubContext: preset,
|
||||
// EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%s", eventName),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: "bridge",
|
||||
DefaultActionInstance: execArgs.defaultActionsUrl,
|
||||
PlatformPicker: func(_ []string) string {
|
||||
return execArgs.image
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: handle log level config
|
||||
// waiting https://gitea.com/gitea/act/pulls/19
|
||||
// if !execArgs.debug {
|
||||
// logLevel := log.Level(log.InfoLevel)
|
||||
// config.JobLoggerLevel = &logLevel
|
||||
// }
|
||||
|
||||
r, err := runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(execArgs.artifactServerPath) == 0 {
|
||||
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
|
||||
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
artifactCancel()
|
||||
return nil
|
||||
})
|
||||
|
||||
return executor(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execArg := executeArgs{}
|
||||
|
||||
execCmd := &cobra.Command{
|
||||
Use: "exec",
|
||||
Short: "Run workflow locally.",
|
||||
Args: cobra.MaximumNArgs(20),
|
||||
RunE: runExec(ctx, &execArg),
|
||||
}
|
||||
|
||||
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
|
||||
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
|
||||
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||
execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
|
||||
execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
|
||||
execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
|
||||
execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
|
||||
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
|
||||
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
|
||||
execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
|
||||
execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsUrl, "default-actions-url", "", "https://gitea.com", "Defines the default url of action instance.")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:16-bullseye", "docker image to use")
|
||||
|
||||
return execCmd
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
||||
|
||||
// runner always needs root permission
|
||||
if os.Getuid() != 0 {
|
||||
// TODO: use a better way to check root permission
|
||||
log.Warnf("Runner in user-mode.")
|
||||
}
|
||||
|
||||
if regArgs.NoInteractive {
|
||||
if err := registerNoInteractive(*configFile, regArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(*configFile); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// registerArgs represents the arguments for register command
|
||||
type registerArgs struct {
|
||||
NoInteractive bool
|
||||
InstanceAddr string
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels string
|
||||
}
|
||||
|
||||
type registerStage int8
|
||||
|
||||
const (
|
||||
StageUnknown registerStage = -1
|
||||
StageOverwriteLocalConfig registerStage = iota + 1
|
||||
StageInputInstance
|
||||
StageInputToken
|
||||
StageInputRunnerName
|
||||
StageInputCustomLabels
|
||||
StageWaitingForRegistration
|
||||
StageExit
|
||||
)
|
||||
|
||||
var defaultLabels = []string{
|
||||
"ubuntu-latest:docker://node:16-bullseye",
|
||||
"ubuntu-22.04:docker://node:16-bullseye", // There's no node:16-bookworm yet
|
||||
"ubuntu-20.04:docker://node:16-bullseye",
|
||||
"ubuntu-18.04:docker://node:16-buster",
|
||||
}
|
||||
|
||||
type registerInputs struct {
|
||||
InstanceAddr string
|
||||
Token string
|
||||
RunnerName string
|
||||
CustomLabels []string
|
||||
}
|
||||
|
||||
func (r *registerInputs) validate() error {
|
||||
if r.InstanceAddr == "" {
|
||||
return fmt.Errorf("instance address is empty")
|
||||
}
|
||||
if r.Token == "" {
|
||||
return fmt.Errorf("token is empty")
|
||||
}
|
||||
if len(r.CustomLabels) > 0 {
|
||||
return validateLabels(r.CustomLabels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabels(ls []string) error {
|
||||
for _, label := range ls {
|
||||
if _, err := labels.Parse(label); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string) registerStage {
|
||||
// must set instance address and token.
|
||||
// if empty, keep current stage.
|
||||
if stage == StageInputInstance || stage == StageInputToken {
|
||||
if value == "" {
|
||||
return stage
|
||||
}
|
||||
}
|
||||
|
||||
// set hostname for runner name if empty
|
||||
if stage == StageInputRunnerName && value == "" {
|
||||
value, _ = os.Hostname()
|
||||
}
|
||||
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
if value == "Y" || value == "y" {
|
||||
return StageInputInstance
|
||||
}
|
||||
return StageExit
|
||||
case StageInputInstance:
|
||||
r.InstanceAddr = value
|
||||
return StageInputToken
|
||||
case StageInputToken:
|
||||
r.Token = value
|
||||
return StageInputRunnerName
|
||||
case StageInputRunnerName:
|
||||
r.RunnerName = value
|
||||
return StageInputCustomLabels
|
||||
case StageInputCustomLabels:
|
||||
r.CustomLabels = defaultLabels
|
||||
if value != "" {
|
||||
r.CustomLabels = strings.Split(value, ",")
|
||||
}
|
||||
|
||||
if validateLabels(r.CustomLabels) != nil {
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host)")
|
||||
return StageInputCustomLabels
|
||||
}
|
||||
return StageWaitingForRegistration
|
||||
}
|
||||
return StageUnknown
|
||||
}
|
||||
|
||||
func registerInteractive(configFile string) error {
|
||||
var (
|
||||
reader = bufio.NewReader(os.Stdin)
|
||||
stage = StageInputInstance
|
||||
inputs = new(registerInputs)
|
||||
)
|
||||
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||
stage = StageOverwriteLocalConfig
|
||||
}
|
||||
|
||||
for {
|
||||
printStageHelp(stage)
|
||||
|
||||
cmdString, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString))
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.CustomLabels)
|
||||
if err := doRegister(cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
} else {
|
||||
log.Infof("Runner registered successfully.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage == StageExit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if stage <= StageUnknown {
|
||||
log.Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printStageHelp(stage registerStage) {
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||
case StageInputCustomLabels:
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("Waiting for registration...")
|
||||
}
|
||||
}
|
||||
|
||||
func registerNoInteractive(configFile string, regArgs *registerArgs) error {
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
CustomLabels: defaultLabels,
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
if regArgs.Labels != "" {
|
||||
inputs.CustomLabels = strings.Split(regArgs.Labels, ",")
|
||||
}
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
}
|
||||
if err := doRegister(cfg, inputs); err != nil {
|
||||
log.Errorf("Failed to register runner: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRegister(cfg *config.Config, inputs *registerInputs) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// initial http client
|
||||
cli := client.New(
|
||||
inputs.InstanceAddr,
|
||||
cfg.Runner.Insecure,
|
||||
"",
|
||||
"",
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
for {
|
||||
_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
|
||||
Data: inputs.RunnerName,
|
||||
}))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("Cannot ping the Gitea instance server")
|
||||
// TODO: if ping failed, retry or exit
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
log.Debugln("Successfully pinged the Gitea instance server")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
reg := &config.Registration{
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.CustomLabels,
|
||||
}
|
||||
|
||||
ls := make([]string, len(reg.Labels))
|
||||
for i, v := range reg.Labels {
|
||||
l, _ := labels.Parse(v)
|
||||
ls[i] = l.Name
|
||||
}
|
||||
// register new runner.
|
||||
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: reg.Name,
|
||||
Token: reg.Token,
|
||||
AgentLabels: ls,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
return err
|
||||
}
|
||||
|
||||
reg.ID = resp.Msg.Runner.Id
|
||||
reg.UUID = resp.Msg.Runner.Uuid
|
||||
reg.Name = resp.Msg.Runner.Name
|
||||
reg.Token = resp.Msg.Runner.Token
|
||||
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("failed to save runner config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package poll
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
)
|
||||
|
||||
type Poller struct {
|
||||
client client.Client
|
||||
runner *run.Runner
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
return &Poller{
|
||||
client: client,
|
||||
runner: runner,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) Poll(ctx context.Context) {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < p.cfg.Runner.Capacity; i++ {
|
||||
wg.Add(1)
|
||||
go p.poll(ctx, wg, limiter)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (p *Poller) poll(ctx context.Context, wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.WithError(err).Debug("limiter wait failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
task, ok := p.fetchTask(ctx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := p.runner.Run(ctx, task); err != nil {
|
||||
log.WithError(err).Error("failed to run task")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to fetch task")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if resp == nil || resp.Msg == nil || resp.Msg.Task == nil {
|
||||
return nil, false
|
||||
}
|
||||
return resp.Msg.Task, true
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/artifactcache"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Runner runs the pipeline.
|
||||
type Runner struct {
|
||||
name string
|
||||
|
||||
cfg *config.Config
|
||||
|
||||
client client.Client
|
||||
labels labels.Labels
|
||||
envs map[string]string
|
||||
|
||||
runningTasks sync.Map
|
||||
}
|
||||
|
||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||
ls := labels.Labels{}
|
||||
for _, v := range reg.Labels {
|
||||
if l, err := labels.Parse(v); err == nil {
|
||||
ls = append(ls, l)
|
||||
}
|
||||
}
|
||||
envs := make(map[string]string, len(cfg.Runner.Envs))
|
||||
for k, v := range cfg.Runner.Envs {
|
||||
envs[k] = v
|
||||
}
|
||||
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
||||
cacheHandler, err := artifactcache.StartHandler(cfg.Cache.Dir, cfg.Cache.Host, cfg.Cache.Port)
|
||||
if err != nil {
|
||||
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||
// go on
|
||||
} else {
|
||||
envs["ACTIONS_CACHE_URL"] = cacheHandler.ExternalURL() + "/"
|
||||
}
|
||||
}
|
||||
|
||||
// set artifact gitea api
|
||||
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
||||
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
||||
|
||||
// Set specific environments to distinguish between Gitea and GitHub
|
||||
envs["GITEA_ACTIONS"] = "true"
|
||||
envs["GITEA_ACTIONS_RUNNER_VERSION"] = ver.Version()
|
||||
|
||||
return &Runner{
|
||||
name: reg.Name,
|
||||
cfg: cfg,
|
||||
client: cli,
|
||||
labels: ls,
|
||||
envs: envs,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||
return fmt.Errorf("task %d is already running", task.Id)
|
||||
} else {
|
||||
r.runningTasks.Store(task.Id, struct{}{})
|
||||
defer r.runningTasks.Delete(task.Id)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||
defer cancel()
|
||||
reporter := report.NewReporter(ctx, cancel, r.client, task)
|
||||
var runErr error
|
||||
defer func() {
|
||||
lastWords := ""
|
||||
if runErr != nil {
|
||||
lastWords = runErr.Error()
|
||||
}
|
||||
_ = reporter.Close(lastWords)
|
||||
}()
|
||||
reporter.RunDaemon()
|
||||
runErr = r.run(ctx, task, reporter)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
reporter.Logf("%s(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
|
||||
|
||||
workflow, jobID, err := generateWorkflow(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
job := workflow.GetJob(jobID)
|
||||
reporter.ResetSteps(len(job.Steps))
|
||||
|
||||
taskContext := task.Context.Fields
|
||||
|
||||
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||
taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
r.client.Address())
|
||||
|
||||
preset := &model.GithubContext{
|
||||
Event: taskContext["event"].GetStructValue().AsMap(),
|
||||
RunID: taskContext["run_id"].GetStringValue(),
|
||||
RunNumber: taskContext["run_number"].GetStringValue(),
|
||||
Actor: taskContext["actor"].GetStringValue(),
|
||||
Repository: taskContext["repository"].GetStringValue(),
|
||||
EventName: taskContext["event_name"].GetStringValue(),
|
||||
Sha: taskContext["sha"].GetStringValue(),
|
||||
Ref: taskContext["ref"].GetStringValue(),
|
||||
RefName: taskContext["ref_name"].GetStringValue(),
|
||||
RefType: taskContext["ref_type"].GetStringValue(),
|
||||
HeadRef: taskContext["head_ref"].GetStringValue(),
|
||||
BaseRef: taskContext["base_ref"].GetStringValue(),
|
||||
Token: taskContext["token"].GetStringValue(),
|
||||
RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
|
||||
RetentionDays: taskContext["retention_days"].GetStringValue(),
|
||||
}
|
||||
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
}
|
||||
|
||||
// use task token to action api token
|
||||
r.envs["ACTIONS_RUNTIME_TOKEN"] = preset.Token
|
||||
|
||||
eventJSON, err := json.Marshal(preset.Event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxLifetime := 3 * time.Hour
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
runnerConfig := &runner.Config{
|
||||
// On Linux, Workdir will be like "/<owner>/<repo>"
|
||||
// On Windows, Workdir will be like "\<owner>\<repo>"
|
||||
Workdir: filepath.FromSlash(string(filepath.Separator) + preset.Repository),
|
||||
BindWorkdir: false,
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: false,
|
||||
ForceRebuild: false,
|
||||
LogOutput: true,
|
||||
JSONLogger: false,
|
||||
Env: r.envs,
|
||||
Secrets: task.Secrets,
|
||||
GitHubInstance: r.client.Address(),
|
||||
AutoRemove: true,
|
||||
NoSkipCheckout: true,
|
||||
PresetGitHubContext: preset,
|
||||
EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
ContainerNetworkMode: r.cfg.Container.NetworkMode,
|
||||
ContainerOptions: r.cfg.Container.Options,
|
||||
Privileged: r.cfg.Container.Privileged,
|
||||
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
PlatformPicker: r.labels.PickPlatform,
|
||||
}
|
||||
|
||||
rr, err := runner.New(runnerConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executor := rr.NewPlanExecutor(plan)
|
||||
|
||||
reporter.Logf("workflow prepared")
|
||||
|
||||
// add logger recorders
|
||||
ctx = common.WithLoggerHook(ctx, reporter)
|
||||
|
||||
execErr := executor(ctx)
|
||||
reporter.SetOutputs(job.Outputs)
|
||||
return execErr
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
)
|
||||
|
||||
// A Client manages communication with the runner.
|
||||
type Client interface {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
||||
Address() string
|
||||
Insecure() bool
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
VersionHeader = "x-runner-version"
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"github.com/bufbuild/connect-go"
|
||||
)
|
||||
|
||||
func getHttpClient(endpoint string, insecure bool) *http.Client {
|
||||
if strings.HasPrefix(endpoint, "https://") && insecure {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
// New returns a new runner client.
|
||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||
|
||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
if uuid != "" {
|
||||
req.Header().Set(UUIDHeader, uuid)
|
||||
}
|
||||
if token != "" {
|
||||
req.Header().Set(TokenHeader, token)
|
||||
}
|
||||
if version != "" {
|
||||
req.Header().Set(VersionHeader, version)
|
||||
}
|
||||
return next(ctx, req)
|
||||
}
|
||||
})))
|
||||
|
||||
return &HTTPClient{
|
||||
PingServiceClient: pingv1connect.NewPingServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
|
||||
getHttpClient(endpoint, insecure),
|
||||
baseURL,
|
||||
opts...,
|
||||
),
|
||||
endpoint: endpoint,
|
||||
insecure: insecure,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Address() string {
|
||||
return c.endpoint
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Insecure() bool {
|
||||
return c.insecure
|
||||
}
|
||||
|
||||
var _ Client = (*HTTPClient)(nil)
|
||||
|
||||
// An HTTPClient manages communication with the runner API.
|
||||
type HTTPClient struct {
|
||||
pingv1connect.PingServiceClient
|
||||
runnerv1connect.RunnerServiceClient
|
||||
endpoint string
|
||||
insecure bool
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Example configuration file, it's safe to copy this as the default config file without any modification.
|
||||
|
||||
log:
|
||||
# The level of logging, can be trace, debug, info, warn, error, fatal
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# Where to store the registration result.
|
||||
file: .runner
|
||||
# Execute how many tasks concurrently at the same time.
|
||||
capacity: 1
|
||||
# Extra environment variables to run jobs.
|
||||
envs:
|
||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
# Extra environment variables to run jobs from a file.
|
||||
# It will be ignored if it's empty or the file doesn't exist.
|
||||
env_file: .env
|
||||
# The timeout for a job to be finished.
|
||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
|
||||
timeout: 3h
|
||||
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||
insecure: false
|
||||
# The timeout for fetching the job from the Gitea instance.
|
||||
fetch_timeout: 5s
|
||||
# The interval for fetching the job from the Gitea instance.
|
||||
fetch_interval: 2s
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
enabled: true
|
||||
# The directory to store the cache data.
|
||||
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
|
||||
dir: ""
|
||||
# The host of the cache server.
|
||||
# It's not for the address to listen, but the address to connect from job containers.
|
||||
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
|
||||
host: ""
|
||||
# The port of the cache server.
|
||||
# 0 means to use a random available port.
|
||||
port: 0
|
||||
|
||||
container:
|
||||
# Which network to use for the job containers. Could be bridge, host, none, or the name of a custom network.
|
||||
network_mode: bridge
|
||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||
privileged: false
|
||||
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||
options:
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Log struct {
|
||||
Level string `yaml:"level"`
|
||||
} `yaml:"log"`
|
||||
Runner struct {
|
||||
File string `yaml:"file"`
|
||||
Capacity int `yaml:"capacity"`
|
||||
Envs map[string]string `yaml:"envs"`
|
||||
EnvFile string `yaml:"env_file"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
FetchTimeout time.Duration `yaml:"fetch_timeout"`
|
||||
FetchInterval time.Duration `yaml:"fetch_interval"`
|
||||
} `yaml:"runner"`
|
||||
Cache struct {
|
||||
Enabled *bool `yaml:"enabled"` // pointer to distinguish between false and not set, and it will be true if not set
|
||||
Dir string `yaml:"dir"`
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
} `yaml:"cache"`
|
||||
Container struct {
|
||||
NetworkMode string `yaml:"network_mode"`
|
||||
Privileged bool `yaml:"privileged"`
|
||||
Options string `yaml:"options"`
|
||||
} `yaml:"container"`
|
||||
}
|
||||
|
||||
// LoadDefault returns the default configuration.
|
||||
// If file is not empty, it will be used to load the configuration.
|
||||
func LoadDefault(file string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if file != "" {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
decoder := yaml.NewDecoder(f)
|
||||
if err := decoder.Decode(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
compatibleWithOldEnvs(file != "", cfg)
|
||||
|
||||
if cfg.Runner.EnvFile != "" {
|
||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||
envs, err := godotenv.Read(cfg.Runner.EnvFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read env file %q: %w", cfg.Runner.EnvFile, err)
|
||||
}
|
||||
for k, v := range envs {
|
||||
cfg.Runner.Envs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = "info"
|
||||
}
|
||||
if cfg.Runner.File == "" {
|
||||
cfg.Runner.File = ".runner"
|
||||
}
|
||||
if cfg.Runner.Capacity <= 0 {
|
||||
cfg.Runner.Capacity = 1
|
||||
}
|
||||
if cfg.Runner.Timeout <= 0 {
|
||||
cfg.Runner.Timeout = 3 * time.Hour
|
||||
}
|
||||
if cfg.Cache.Enabled == nil {
|
||||
b := true
|
||||
cfg.Cache.Enabled = &b
|
||||
}
|
||||
if *cfg.Cache.Enabled {
|
||||
if cfg.Cache.Dir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
}
|
||||
if cfg.Container.NetworkMode == "" {
|
||||
cfg.Container.NetworkMode = "bridge"
|
||||
}
|
||||
if cfg.Runner.FetchTimeout <= 0 {
|
||||
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||
}
|
||||
if cfg.Runner.FetchInterval <= 0 {
|
||||
cfg.Runner.FetchInterval = 2 * time.Second
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
||||
// Be compatible with old envs.
|
||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
||||
handleEnv := func(key string) (string, bool) {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if fileUsed {
|
||||
log.Warnf("env %s has been ignored because config file is used", key)
|
||||
return "", false
|
||||
}
|
||||
log.Warnf("env %s will be deprecated, please use config file instead", key)
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
if v, ok := handleEnv("GITEA_DEBUG"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "debug"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_TRACE"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "trace"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
|
||||
if i, _ := strconv.Atoi(v); i > 0 {
|
||||
cfg.Runner.Capacity = i
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
|
||||
cfg.Runner.File = v
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
|
||||
splits := strings.Split(v, ",")
|
||||
if cfg.Runner.Envs == nil {
|
||||
cfg.Runner.Envs = map[string]string{}
|
||||
}
|
||||
for _, split := range splits {
|
||||
kv := strings.SplitN(split, ":", 2)
|
||||
if len(kv) == 2 && kv[0] != "" {
|
||||
cfg.Runner.Envs[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
|
||||
cfg.Runner.EnvFile = v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed config.example.yaml
|
||||
var Example []byte
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
const registrationWarning = "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner."
|
||||
|
||||
// Registration is the registration information for a runner
|
||||
type Registration struct {
|
||||
Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
|
||||
|
||||
ID int64 `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
func LoadRegistration(file string) (*Registration, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reg Registration
|
||||
if err := json.NewDecoder(f).Decode(®); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg.Warning = ""
|
||||
|
||||
return ®, nil
|
||||
}
|
||||
|
||||
func SaveRegistration(file string, reg *Registration) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reg.Warning = registrationWarning
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(reg)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package envcheck provides a simple way to check if the environment is ready to run jobs.
|
||||
package envcheck
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func CheckIfDockerRunning(ctx context.Context) error {
|
||||
// TODO: if runner support configures to use docker, we need config.Config to pass in
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot ping the docker daemon, does it running? %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
SchemeHost = "host"
|
||||
SchemeDocker = "docker"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
Name string
|
||||
Schema string
|
||||
Arg string
|
||||
}
|
||||
|
||||
func Parse(str string) (*Label, error) {
|
||||
splits := strings.SplitN(str, ":", 3)
|
||||
label := &Label{
|
||||
Name: splits[0],
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
}
|
||||
if len(splits) >= 2 {
|
||||
label.Schema = splits[1]
|
||||
}
|
||||
if len(splits) >= 3 {
|
||||
label.Arg = splits[2]
|
||||
}
|
||||
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
|
||||
return nil, fmt.Errorf("unsupported schema: %s", label.Schema)
|
||||
}
|
||||
return label, nil
|
||||
}
|
||||
|
||||
type Labels []*Label
|
||||
|
||||
func (l Labels) RequireDocker() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeDocker {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l Labels) PickPlatform(runsOn []string) string {
|
||||
platforms := make(map[string]string, len(l))
|
||||
for _, label := range l {
|
||||
switch label.Schema {
|
||||
case SchemeDocker:
|
||||
// "//" will be ignored
|
||||
// TODO maybe we should use 'ubuntu-18.04:docker:node:16-buster' instead
|
||||
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||
case SchemeHost:
|
||||
platforms[label.Name] = "-self-hosted"
|
||||
default:
|
||||
// It should not happen, because Parse has checked it.
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, v := range runsOn {
|
||||
if v, ok := platforms[v]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support multiple labels
|
||||
// like:
|
||||
// ["ubuntu-22.04"] => "ubuntu:22.04"
|
||||
// ["with-gpu"] => "linux:with-gpu"
|
||||
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
|
||||
|
||||
// return default.
|
||||
// So the runner receives a task with a label that the runner doesn't have,
|
||||
// it happens when the user have edited the label of the runner in the web UI.
|
||||
// TODO: it may be not correct, what if the runner is used as host mode only?
|
||||
return "node:16-bullseye"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
args string
|
||||
want *Label
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
args: "ubuntu:docker://node:18",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Schema: "docker",
|
||||
Arg: "//node:18",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu:host",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu",
|
||||
want: &Label{
|
||||
Name: "ubuntu",
|
||||
Schema: "host",
|
||||
Arg: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu:vm:ubuntu-18.04",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
got, err := Parse(tt.args)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.DeepEqual(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
retry "github.com/avast/retry-go/v4"
|
||||
"github.com/bufbuild/connect-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
closed bool
|
||||
client client.Client
|
||||
clientM sync.Mutex
|
||||
|
||||
logOffset int
|
||||
logRows []*runnerv1.LogRow
|
||||
logReplacer *strings.Replacer
|
||||
oldnew []string
|
||||
|
||||
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 {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
for _, v := range task.Secrets {
|
||||
oldnew = append(oldnew, v, "***")
|
||||
}
|
||||
|
||||
rv := &Reporter{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
client: client,
|
||||
oldnew: oldnew,
|
||||
logReplacer: strings.NewReplacer(oldnew...),
|
||||
state: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
},
|
||||
}
|
||||
|
||||
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||
rv.debugOutputEnabled = true
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
for i := 0; i < l; i++ {
|
||||
r.state.Steps = append(r.state.Steps, &runnerv1.StepState{
|
||||
Id: int64(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
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 {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
log.WithFields(entry.Data).Trace(entry.Message)
|
||||
|
||||
timestamp := entry.Time
|
||||
if r.state.StartedAt == nil {
|
||||
r.state.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
|
||||
stage := entry.Data["stage"]
|
||||
|
||||
if stage != "Main" {
|
||||
if v, ok := entry.Data["jobResult"]; ok {
|
||||
if jobResult, ok := r.parseResult(v); ok {
|
||||
r.state.Result = jobResult
|
||||
r.state.StoppedAt = timestamppb.New(timestamp)
|
||||
for _, s := range r.state.Steps {
|
||||
if s.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
s.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var step *runnerv1.StepState
|
||||
if v, ok := entry.Data["stepNumber"]; ok {
|
||||
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
||||
step = r.state.Steps[v]
|
||||
}
|
||||
}
|
||||
if step == nil {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if step.StartedAt == nil {
|
||||
step.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
if v, ok := entry.Data["raw_output"]; ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
} else if !r.duringSteps() {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
if v, ok := entry.Data["stepResult"]; ok {
|
||||
if stepResult, ok := r.parseResult(v); ok {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.Result = stepResult
|
||||
step.StoppedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
if r.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ReportLog(false)
|
||||
_ = r.ReportState()
|
||||
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
}
|
||||
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
|
||||
r.logf(format, a...)
|
||||
}
|
||||
|
||||
func (r *Reporter) logf(format string, a ...interface{}) {
|
||||
if !r.duringSteps() {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: fmt.Sprintf(format, a...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
r.closed = true
|
||||
|
||||
r.stateMu.Lock()
|
||||
if r.state.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
if lastWords == "" {
|
||||
lastWords = "Early termination"
|
||||
}
|
||||
for _, v := range r.state.Steps {
|
||||
if v.Result == runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
v.Result = runnerv1.Result_RESULT_CANCELLED
|
||||
}
|
||||
}
|
||||
r.state.Result = runnerv1.Result_RESULT_FAILURE
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
r.state.StoppedAt = timestamppb.Now()
|
||||
} else if lastWords != "" {
|
||||
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||
Time: timestamppb.Now(),
|
||||
Content: lastWords,
|
||||
})
|
||||
}
|
||||
r.stateMu.Unlock()
|
||||
|
||||
return retry.Do(func() error {
|
||||
if err := r.ReportLog(true); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.ReportState()
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateMu.RLock()
|
||||
rows := r.logRows
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: r.state.Id,
|
||||
Index: int64(r.logOffset),
|
||||
Rows: rows,
|
||||
NoMore: noMore,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack := int(resp.Msg.AckIndex)
|
||||
if ack < r.logOffset {
|
||||
return fmt.Errorf("submitted logs are lost")
|
||||
}
|
||||
|
||||
r.stateMu.Lock()
|
||||
r.logRows = r.logRows[ack-r.logOffset:]
|
||||
r.logOffset = ack
|
||||
r.stateMu.Unlock()
|
||||
|
||||
if noMore && ack < r.logOffset+len(rows) {
|
||||
return fmt.Errorf("not all logs are submitted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
r.stateMu.RLock()
|
||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||
r.stateMu.RUnlock()
|
||||
|
||||
outputs := make(map[string]string)
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if val, ok := v.(string); ok {
|
||||
outputs[k.(string)] = val
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: state,
|
||||
Outputs: outputs,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range resp.Msg.SentOutputs {
|
||||
r.outputs.Store(k, struct{}{})
|
||||
}
|
||||
|
||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
var noSent []string
|
||||
r.outputs.Range(func(k, v interface{}) bool {
|
||||
if _, ok := v.(string); ok {
|
||||
noSent = append(noSent, k.(string))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(noSent) > 0 {
|
||||
return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) duringSteps() bool {
|
||||
if steps := r.state.Steps; len(steps) == 0 {
|
||||
return false
|
||||
} else if first := steps[0]; first.Result == runnerv1.Result_RESULT_UNSPECIFIED && first.LogLength == 0 {
|
||||
return false
|
||||
} else if last := steps[len(steps)-1]; last.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var stringToResult = map[string]runnerv1.Result{
|
||||
"success": runnerv1.Result_RESULT_SUCCESS,
|
||||
"failure": runnerv1.Result_RESULT_FAILURE,
|
||||
"skipped": runnerv1.Result_RESULT_SKIPPED,
|
||||
"cancelled": runnerv1.Result_RESULT_CANCELLED,
|
||||
}
|
||||
|
||||
func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
str := ""
|
||||
if v, ok := result.(string); ok { // for jobResult
|
||||
str = v
|
||||
} else if v, ok := result.(fmt.Stringer); ok { // for stepResult
|
||||
str = v.String()
|
||||
}
|
||||
|
||||
ret, ok := stringToResult[str]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
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 {
|
||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
|
||||
matches := cmdRegex.FindStringSubmatch(content)
|
||||
if matches != nil {
|
||||
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
|
||||
content = *output
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
content = r.logReplacer.Replace(content)
|
||||
|
||||
return &runnerv1.LogRow{
|
||||
Time: timestamppb.New(entry.Time),
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) addMask(msg string) {
|
||||
r.oldnew = append(r.oldnew, msg, "***")
|
||||
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ver
|
||||
|
||||
// go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
func Version() string {
|
||||
return version
|
||||
}
|
||||
@@ -1,37 +1,19 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/nektos/act/cmd"
|
||||
"gitea.com/gitea/act_runner/internal/app/cmd"
|
||||
)
|
||||
|
||||
//go:embed VERSION
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// trap Ctrl+C and call cancel on the context
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
defer func() {
|
||||
signal.Stop(c)
|
||||
cancel()
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-c:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// run the command
|
||||
cmd.Execute(ctx, version)
|
||||
cmd.Execute(ctx)
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/timshannon/bolthold"
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
urlBase = "/_apis/artifactcache"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
dir string
|
||||
storage *Storage
|
||||
router *httprouter.Router
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
logger logrus.FieldLogger
|
||||
|
||||
gcing atomic.Bool
|
||||
gcAt time.Time
|
||||
|
||||
outboundIP string
|
||||
}
|
||||
|
||||
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
|
||||
h := &Handler{}
|
||||
|
||||
if logger == nil {
|
||||
discard := logrus.New()
|
||||
discard.Out = io.Discard
|
||||
logger = discard
|
||||
}
|
||||
logger = logger.WithField("module", "artifactcache")
|
||||
h.logger = logger
|
||||
|
||||
if dir == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.dir = dir
|
||||
|
||||
storage, err := NewStorage(filepath.Join(dir, "cache"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.storage = storage
|
||||
|
||||
if outboundIP != "" {
|
||||
h.outboundIP = outboundIP
|
||||
} else if ip := common.GetOutboundIP(); ip == nil {
|
||||
return nil, fmt.Errorf("unable to determine outbound IP address")
|
||||
} else {
|
||||
h.outboundIP = ip.String()
|
||||
}
|
||||
|
||||
router := httprouter.New()
|
||||
router.GET(urlBase+"/cache", h.middleware(h.find))
|
||||
router.POST(urlBase+"/caches", h.middleware(h.reserve))
|
||||
router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload))
|
||||
router.POST(urlBase+"/caches/:id", h.middleware(h.commit))
|
||||
router.GET(urlBase+"/artifacts/:id", h.middleware(h.get))
|
||||
router.POST(urlBase+"/clean", h.middleware(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
|
||||
}
|
||||
server := &http.Server{
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
Handler: router,
|
||||
}
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && errors.Is(err, net.ErrClosed) {
|
||||
logger.Errorf("http serve: %v", err)
|
||||
}
|
||||
}()
|
||||
h.listener = listener
|
||||
h.server = server
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (h *Handler) Close() error {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
var retErr error
|
||||
if h.server != nil {
|
||||
err := h.server.Close()
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
h.server = nil
|
||||
}
|
||||
if h.listener != nil {
|
||||
err := h.listener.Close()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
h.listener = nil
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (h *Handler) openDB() (*bolthold.Store, error) {
|
||||
return bolthold.Open(filepath.Join(h.dir, "bolt.db"), 0o644, &bolthold.Options{
|
||||
Encoder: json.Marshal,
|
||||
Decoder: json.Unmarshal,
|
||||
Options: &bbolt.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
NoGrowSync: bbolt.DefaultOptions.NoGrowSync,
|
||||
FreelistType: bbolt.DefaultOptions.FreelistType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /_apis/artifactcache/cache
|
||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||
// cache keys are case insensitive
|
||||
for i, key := range keys {
|
||||
keys[i] = strings.ToLower(key)
|
||||
}
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cache, err := findCache(db, keys, version)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
if cache == nil {
|
||||
h.responseJSON(w, r, 204)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := h.storage.Exist(cache.ID); err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
} else if !ok {
|
||||
_ = db.Delete(cache.ID, cache)
|
||||
h.responseJSON(w, r, 204)
|
||||
return
|
||||
}
|
||||
h.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, _ httprouter.Params) {
|
||||
api := &Request{}
|
||||
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
// cache keys are case insensitive
|
||||
api.Key = strings.ToLower(api.Key)
|
||||
|
||||
cache := api.ToCache()
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
now := time.Now().Unix()
|
||||
cache.CreatedAt = now
|
||||
cache.UsedAt = now
|
||||
if err := insertCache(db, cache); err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
h.responseJSON(w, r, 200, map[string]any{
|
||||
"cacheId": cache.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH /_apis/artifactcache/caches/:id
|
||||
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
cache := &Cache{}
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
if err := db.Get(id, cache); err != nil {
|
||||
if errors.Is(err, bolthold.ErrNotFound) {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||
return
|
||||
}
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
}
|
||||
db.Close()
|
||||
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
}
|
||||
h.useCache(id)
|
||||
h.responseJSON(w, r, 200)
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/caches/:id
|
||||
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
cache := &Cache{}
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
if err := db.Get(id, cache); err != nil {
|
||||
if errors.Is(err, bolthold.ErrNotFound) {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
|
||||
return
|
||||
}
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
}
|
||||
|
||||
db.Close()
|
||||
|
||||
size, err := h.storage.Commit(cache.ID, cache.Size)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
// write real size back to cache, it may be different from the current value when the request doesn't specify it.
|
||||
cache.Size = size
|
||||
|
||||
db, err = h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cache.Complete = true
|
||||
if err := db.Update(cache.ID, cache); err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.responseJSON(w, r, 200)
|
||||
}
|
||||
|
||||
// GET /_apis/artifactcache/artifacts/:id
|
||||
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
return
|
||||
}
|
||||
h.useCache(id)
|
||||
h.storage.Serve(w, r, uint64(id))
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/clean
|
||||
func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
// 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
|
||||
|
||||
h.responseJSON(w, r, 200)
|
||||
}
|
||||
|
||||
func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
h.logger.Debugf("%s %s", r.Method, r.RequestURI)
|
||||
handler(w, r, params)
|
||||
go h.gcCache()
|
||||
}
|
||||
}
|
||||
|
||||
// if not found, return (nil, nil) instead of an error.
|
||||
func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) {
|
||||
cache := &Cache{}
|
||||
for _, prefix := range keys {
|
||||
// if a key in the list matches exactly, don't return partial matches
|
||||
if err := db.FindOne(cache,
|
||||
bolthold.Where("Key").Eq(prefix).
|
||||
And("Version").Eq(version).
|
||||
And("Complete").Eq(true).
|
||||
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find cache: %w", err)
|
||||
}
|
||||
return cache, nil
|
||||
}
|
||||
prefixPattern := fmt.Sprintf("^%s", regexp.QuoteMeta(prefix))
|
||||
re, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := db.FindOne(cache,
|
||||
bolthold.Where("Key").RegExp(re).
|
||||
And("Version").Eq(version).
|
||||
And("Complete").Eq(true).
|
||||
SortBy("CreatedAt").Reverse()); err != nil {
|
||||
if errors.Is(err, bolthold.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("find cache: %w", err)
|
||||
}
|
||||
return cache, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func insertCache(db *bolthold.Store, cache *Cache) error {
|
||||
if err := db.Insert(bolthold.NextSequence(), cache); err != nil {
|
||||
return fmt.Errorf("insert cache: %w", err)
|
||||
}
|
||||
// write back id to db
|
||||
if err := db.Update(cache.ID, cache); err != nil {
|
||||
return fmt.Errorf("write back id to db: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) useCache(id int64) {
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
cache := &Cache{}
|
||||
if err := db.Get(id, cache); err != nil {
|
||||
return
|
||||
}
|
||||
cache.UsedAt = time.Now().Unix()
|
||||
_ = db.Update(cache.ID, cache)
|
||||
}
|
||||
|
||||
const (
|
||||
keepUsed = 30 * 24 * time.Hour
|
||||
keepUnused = 7 * 24 * time.Hour
|
||||
keepTemp = 5 * time.Minute
|
||||
keepOld = 5 * time.Minute
|
||||
)
|
||||
|
||||
func (h *Handler) gcCache() {
|
||||
if h.gcing.Load() {
|
||||
return
|
||||
}
|
||||
if !h.gcing.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
defer h.gcing.Store(false)
|
||||
|
||||
if time.Since(h.gcAt) < time.Hour {
|
||||
h.logger.Debugf("skip gc: %v", h.gcAt.String())
|
||||
return
|
||||
}
|
||||
h.gcAt = time.Now()
|
||||
h.logger.Debugf("gc: %v", h.gcAt.String())
|
||||
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Remove the caches which are not completed for a while, they are most likely to be broken.
|
||||
var caches []*Cache
|
||||
if err := db.Find(&caches, bolthold.
|
||||
Where("UsedAt").Lt(time.Now().Add(-keepTemp).Unix()).
|
||||
And("Complete").Eq(false),
|
||||
); err != nil {
|
||||
h.logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := db.Delete(cache.ID, cache); err != nil {
|
||||
h.logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
h.logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old caches which have not been used recently.
|
||||
caches = caches[:0]
|
||||
if err := db.Find(&caches, bolthold.
|
||||
Where("UsedAt").Lt(time.Now().Add(-keepUnused).Unix()),
|
||||
); err != nil {
|
||||
h.logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := db.Delete(cache.ID, cache); err != nil {
|
||||
h.logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
h.logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old caches which are too old.
|
||||
caches = caches[:0]
|
||||
if err := db.Find(&caches, bolthold.
|
||||
Where("CreatedAt").Lt(time.Now().Add(-keepUsed).Unix()),
|
||||
); err != nil {
|
||||
h.logger.Warnf("find caches: %v", err)
|
||||
} else {
|
||||
for _, cache := range caches {
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := db.Delete(cache.ID, cache); err != nil {
|
||||
h.logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
h.logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old caches with the same key and version, keep the latest one.
|
||||
// Also keep the olds which have been used recently for a while in case of the cache is still in use.
|
||||
if results, err := db.FindAggregate(
|
||||
&Cache{},
|
||||
bolthold.Where("Complete").Eq(true),
|
||||
"Key", "Version",
|
||||
); err != nil {
|
||||
h.logger.Warnf("find aggregate caches: %v", err)
|
||||
} else {
|
||||
for _, result := range results {
|
||||
if result.Count() <= 1 {
|
||||
continue
|
||||
}
|
||||
result.Sort("CreatedAt")
|
||||
caches = caches[:0]
|
||||
result.Reduction(&caches)
|
||||
for _, cache := range caches[:len(caches)-1] {
|
||||
if time.Since(time.Unix(cache.UsedAt, 0)) < keepOld {
|
||||
// Keep it since it has been used recently, even if it's old.
|
||||
// Or it could break downloading in process.
|
||||
continue
|
||||
}
|
||||
h.storage.Remove(cache.ID)
|
||||
if err := db.Delete(cache.ID, cache); err != nil {
|
||||
h.logger.Warnf("delete cache: %v", err)
|
||||
continue
|
||||
}
|
||||
h.logger.Infof("deleted cache: %+v", cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int, v ...any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
var data []byte
|
||||
if len(v) == 0 || v[0] == nil {
|
||||
data, _ = json.Marshal(struct{}{})
|
||||
} else if err, ok := v[0].(error); ok {
|
||||
h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
||||
data, _ = json.Marshal(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
data, _ = json.Marshal(v[0])
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,697 +0,0 @@
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/timshannon/bolthold"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||
handler, err := StartHandler(dir, "", 0, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
||||
|
||||
defer func() {
|
||||
t.Run("inpect db", func(t *testing.T) {
|
||||
db, err := handler.openDB()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
require.NoError(t, db.Bolt().View(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket([]byte("Cache")).ForEach(func(k, v []byte) error {
|
||||
t.Logf("%s: %s", k, v)
|
||||
return nil
|
||||
})
|
||||
}))
|
||||
})
|
||||
t.Run("close", func(t *testing.T) {
|
||||
require.NoError(t, handler.Close())
|
||||
assert.Nil(t, handler.server)
|
||||
assert.Nil(t, handler.listener)
|
||||
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}()
|
||||
|
||||
t.Run("get not exist", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 204, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("reserve and upload", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
uploadCacheNormally(t, base, key, version, content)
|
||||
})
|
||||
|
||||
t.Run("clean", func(t *testing.T) {
|
||||
resp, err := http.Post(fmt.Sprintf("%s/clean", base), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("reserve with bad request", func(t *testing.T) {
|
||||
body := []byte(`invalid json`)
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("duplicate reserve", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var first, second struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
|
||||
assert.NotZero(t, first.CacheID)
|
||||
}
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
|
||||
assert.NotZero(t, second.CacheID)
|
||||
}
|
||||
|
||||
assert.NotEqual(t, first.CacheID, second.CacheID)
|
||||
})
|
||||
|
||||
t.Run("upload with bad id", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/invalid_id", base), bytes.NewReader(nil))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("upload without reserve", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, 1000), bytes.NewReader(nil))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("upload with complete", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var id uint64
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("upload with invalid range", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var id uint64
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes xx-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit with bad id", func(t *testing.T) {
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit with not exist id", func(t *testing.T) {
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate commit", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var id uint64
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit early", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var id uint64
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content[:50]))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-59/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 500, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get with bad id", func(t *testing.T) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/invalid_id", base))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("get with not exist id", func(t *testing.T) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("get with not exist id", func(t *testing.T) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("get with multiple keys", func(t *testing.T) {
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
key := strings.ToLower(t.Name())
|
||||
keys := [3]string{
|
||||
key + "_a_b_c",
|
||||
key + "_a_b",
|
||||
key + "_a",
|
||||
}
|
||||
contents := [3][]byte{
|
||||
make([]byte, 100),
|
||||
make([]byte, 200),
|
||||
make([]byte, 300),
|
||||
}
|
||||
for i := range contents {
|
||||
_, err := rand.Read(contents[i])
|
||||
require.NoError(t, err)
|
||||
uploadCacheNormally(t, base, keys[i], version, contents[i])
|
||||
time.Sleep(time.Second) // ensure CreatedAt of caches are different
|
||||
}
|
||||
|
||||
reqKeys := strings.Join([]string{
|
||||
key + "_a_b_x",
|
||||
key + "_a_b",
|
||||
key + "_a",
|
||||
}, ",")
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
Expect `key_a_b` because:
|
||||
- `key_a_b_x" doesn't match any caches.
|
||||
- `key_a_b" matches `key_a_b` and `key_a_b_c`, but `key_a_b` is newer.
|
||||
*/
|
||||
except := 1
|
||||
|
||||
got := struct {
|
||||
Result string `json:"result"`
|
||||
ArchiveLocation string `json:"archiveLocation"`
|
||||
CacheKey string `json:"cacheKey"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, "hit", got.Result)
|
||||
assert.Equal(t, keys[except], got.CacheKey)
|
||||
|
||||
contentResp, err := http.Get(got.ArchiveLocation)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, contents[except], content)
|
||||
})
|
||||
|
||||
t.Run("case insensitive", func(t *testing.T) {
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
key := strings.ToLower(t.Name())
|
||||
content := make([]byte, 100)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
uploadCacheNormally(t, base, key+"_ABC", version, content)
|
||||
|
||||
{
|
||||
reqKey := key + "_aBc"
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got := struct {
|
||||
Result string `json:"result"`
|
||||
ArchiveLocation string `json:"archiveLocation"`
|
||||
CacheKey string `json:"cacheKey"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, "hit", got.Result)
|
||||
assert.Equal(t, key+"_abc", got.CacheKey)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact keys are preferred (key 0)", func(t *testing.T) {
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
key := strings.ToLower(t.Name())
|
||||
keys := [3]string{
|
||||
key + "_a",
|
||||
key + "_a_b_c",
|
||||
key + "_a_b",
|
||||
}
|
||||
contents := [3][]byte{
|
||||
make([]byte, 100),
|
||||
make([]byte, 200),
|
||||
make([]byte, 300),
|
||||
}
|
||||
for i := range contents {
|
||||
_, err := rand.Read(contents[i])
|
||||
require.NoError(t, err)
|
||||
uploadCacheNormally(t, base, keys[i], version, contents[i])
|
||||
time.Sleep(time.Second) // ensure CreatedAt of caches are different
|
||||
}
|
||||
|
||||
reqKeys := strings.Join([]string{
|
||||
key + "_a",
|
||||
key + "_a_b",
|
||||
}, ",")
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
Expect `key_a` because:
|
||||
- `key_a` matches `key_a`, `key_a_b` and `key_a_b_c`, but `key_a` is an exact match.
|
||||
- `key_a_b` matches `key_a_b` and `key_a_b_c`, but previous key had a match
|
||||
*/
|
||||
expect := 0
|
||||
|
||||
got := struct {
|
||||
ArchiveLocation string `json:"archiveLocation"`
|
||||
CacheKey string `json:"cacheKey"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, keys[expect], got.CacheKey)
|
||||
|
||||
contentResp, err := http.Get(got.ArchiveLocation)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, contents[expect], content)
|
||||
})
|
||||
|
||||
t.Run("exact keys are preferred (key 1)", func(t *testing.T) {
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
key := strings.ToLower(t.Name())
|
||||
keys := [3]string{
|
||||
key + "_a",
|
||||
key + "_a_b_c",
|
||||
key + "_a_b",
|
||||
}
|
||||
contents := [3][]byte{
|
||||
make([]byte, 100),
|
||||
make([]byte, 200),
|
||||
make([]byte, 300),
|
||||
}
|
||||
for i := range contents {
|
||||
_, err := rand.Read(contents[i])
|
||||
require.NoError(t, err)
|
||||
uploadCacheNormally(t, base, keys[i], version, contents[i])
|
||||
time.Sleep(time.Second) // ensure CreatedAt of caches are different
|
||||
}
|
||||
|
||||
reqKeys := strings.Join([]string{
|
||||
"------------------------------------------------------",
|
||||
key + "_a",
|
||||
key + "_a_b",
|
||||
}, ",")
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
Expect `key_a` because:
|
||||
- `------------------------------------------------------` doesn't match any caches.
|
||||
- `key_a` matches `key_a`, `key_a_b` and `key_a_b_c`, but `key_a` is an exact match.
|
||||
- `key_a_b` matches `key_a_b` and `key_a_b_c`, but previous key had a match
|
||||
*/
|
||||
expect := 0
|
||||
|
||||
got := struct {
|
||||
ArchiveLocation string `json:"archiveLocation"`
|
||||
CacheKey string `json:"cacheKey"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, keys[expect], got.CacheKey)
|
||||
|
||||
contentResp, err := http.Get(got.ArchiveLocation)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, contents[expect], content)
|
||||
})
|
||||
}
|
||||
|
||||
func uploadCacheNormally(t *testing.T, base, key, version string, content []byte) {
|
||||
var id uint64
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: int64(len(content)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
var archiveLocation string
|
||||
{
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got := struct {
|
||||
Result string `json:"result"`
|
||||
ArchiveLocation string `json:"archiveLocation"`
|
||||
CacheKey string `json:"cacheKey"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, "hit", got.Result)
|
||||
assert.Equal(t, strings.ToLower(key), got.CacheKey)
|
||||
archiveLocation = got.ArchiveLocation
|
||||
}
|
||||
{
|
||||
resp, err := http.Get(archiveLocation) //nolint:gosec
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_gcCache(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||
handler, err := StartHandler(dir, "", 0, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, handler.Close())
|
||||
}()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
cases := []struct {
|
||||
Cache *Cache
|
||||
Kept bool
|
||||
}{
|
||||
{
|
||||
// should be kept, since it's used recently and not too old.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_1",
|
||||
Version: "test_version",
|
||||
Complete: true,
|
||||
UsedAt: now.Unix(),
|
||||
CreatedAt: now.Add(-time.Hour).Unix(),
|
||||
},
|
||||
Kept: true,
|
||||
},
|
||||
{
|
||||
// should be removed, since it's not complete and not used for a while.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_2",
|
||||
Version: "test_version",
|
||||
Complete: false,
|
||||
UsedAt: now.Add(-(keepTemp + time.Second)).Unix(),
|
||||
CreatedAt: now.Add(-(keepTemp + time.Hour)).Unix(),
|
||||
},
|
||||
Kept: false,
|
||||
},
|
||||
{
|
||||
// should be removed, since it's not used for a while.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_3",
|
||||
Version: "test_version",
|
||||
Complete: true,
|
||||
UsedAt: now.Add(-(keepUnused + time.Second)).Unix(),
|
||||
CreatedAt: now.Add(-(keepUnused + time.Hour)).Unix(),
|
||||
},
|
||||
Kept: false,
|
||||
},
|
||||
{
|
||||
// should be removed, since it's used but too old.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_3",
|
||||
Version: "test_version",
|
||||
Complete: true,
|
||||
UsedAt: now.Unix(),
|
||||
CreatedAt: now.Add(-(keepUsed + time.Second)).Unix(),
|
||||
},
|
||||
Kept: false,
|
||||
},
|
||||
{
|
||||
// should be kept, since it has a newer edition but be used recently.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_1",
|
||||
Version: "test_version",
|
||||
Complete: true,
|
||||
UsedAt: now.Add(-(keepOld - time.Minute)).Unix(),
|
||||
CreatedAt: now.Add(-(time.Hour + time.Second)).Unix(),
|
||||
},
|
||||
Kept: true,
|
||||
},
|
||||
{
|
||||
// should be removed, since it has a newer edition and not be used recently.
|
||||
Cache: &Cache{
|
||||
Key: "test_key_1",
|
||||
Version: "test_version",
|
||||
Complete: true,
|
||||
UsedAt: now.Add(-(keepOld + time.Second)).Unix(),
|
||||
CreatedAt: now.Add(-(time.Hour + time.Second)).Unix(),
|
||||
},
|
||||
Kept: false,
|
||||
},
|
||||
}
|
||||
|
||||
db, err := handler.openDB()
|
||||
require.NoError(t, err)
|
||||
for _, c := range cases {
|
||||
require.NoError(t, insertCache(db, c.Cache))
|
||||
}
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
handler.gcAt = time.Time{} // ensure gcCache will not skip
|
||||
handler.gcCache()
|
||||
|
||||
db, err = handler.openDB()
|
||||
require.NoError(t, err)
|
||||
for i, v := range cases {
|
||||
t.Run(fmt.Sprintf("%d_%s", i, v.Cache.Key), func(t *testing.T) {
|
||||
cache := &Cache{}
|
||||
err = db.Get(v.Cache.ID, cache)
|
||||
if v.Kept {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorIs(t, err, bolthold.ErrNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
require.NoError(t, db.Close())
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package artifactcache
|
||||
|
||||
type Request struct {
|
||||
Key string `json:"key" `
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"cacheSize"`
|
||||
}
|
||||
|
||||
func (c *Request) ToCache() *Cache {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
ret := &Cache{
|
||||
Key: c.Key,
|
||||
Version: c.Version,
|
||||
Size: c.Size,
|
||||
}
|
||||
if c.Size == 0 {
|
||||
// So the request comes from old versions of actions, like `actions/cache@v2`.
|
||||
// It doesn't send cache size. Set it to -1 to indicate that.
|
||||
ret.Size = -1
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
ID uint64 `json:"id" boltholdKey:"ID"`
|
||||
Key string `json:"key" boltholdIndex:"Key"`
|
||||
Version string `json:"version" boltholdIndex:"Version"`
|
||||
Size int64 `json:"cacheSize"`
|
||||
Complete bool `json:"complete" boltholdIndex:"Complete"`
|
||||
UsedAt int64 `json:"usedAt" boltholdIndex:"UsedAt"`
|
||||
CreatedAt int64 `json:"createdAt" boltholdIndex:"CreatedAt"`
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
# Copied from https://github.com/actions/cache#example-cache-workflow
|
||||
name: Caching Primes
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- run: env
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Primes
|
||||
id: cache-primes
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: prime-numbers
|
||||
key: ${{ runner.os }}-primes-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-primes
|
||||
${{ runner.os }}
|
||||
|
||||
- name: Generate Prime Numbers
|
||||
if: steps.cache-primes.outputs.cache-hit != 'true'
|
||||
run: cat /proc/sys/kernel/random/uuid > prime-numbers
|
||||
|
||||
- name: Use Prime Numbers
|
||||
run: cat prime-numbers
|
||||
@@ -1,318 +0,0 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
type FileContainerResourceURL struct {
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
|
||||
type NamedFileContainerResourceURL struct {
|
||||
Name string `json:"name"`
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
|
||||
type NamedFileContainerResourceURLResponse struct {
|
||||
Count int `json:"count"`
|
||||
Value []NamedFileContainerResourceURL `json:"value"`
|
||||
}
|
||||
|
||||
type ContainerItem struct {
|
||||
Path string `json:"path"`
|
||||
ItemType string `json:"itemType"`
|
||||
ContentLocation string `json:"contentLocation"`
|
||||
}
|
||||
|
||||
type ContainerItemResponse struct {
|
||||
Value []ContainerItem `json:"value"`
|
||||
}
|
||||
|
||||
type ResponseMessage struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type WritableFile interface {
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
type WriteFS interface {
|
||||
OpenWritable(name string) (WritableFile, error)
|
||||
OpenAppendable(name string) (WritableFile, error)
|
||||
}
|
||||
|
||||
type readWriteFSImpl struct {
|
||||
}
|
||||
|
||||
func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
|
||||
}
|
||||
|
||||
func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = file.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
var gzipExtension = ".gz__"
|
||||
|
||||
func safeResolve(baseDir string, relPath string) string {
|
||||
return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
|
||||
}
|
||||
|
||||
func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
|
||||
router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
runID := params.ByName("runId")
|
||||
|
||||
json, err := json.Marshal(FileContainerResourceURL{
|
||||
FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
itemPath := req.URL.Query().Get("itemPath")
|
||||
runID := params.ByName("runId")
|
||||
|
||||
if req.Header.Get("Content-Encoding") == "gzip" {
|
||||
itemPath += gzipExtension
|
||||
}
|
||||
|
||||
safeRunPath := safeResolve(baseDir, runID)
|
||||
safePath := safeResolve(safeRunPath, itemPath)
|
||||
|
||||
file, err := func() (WritableFile, error) {
|
||||
contentRange := req.Header.Get("Content-Range")
|
||||
if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") {
|
||||
return fsys.OpenAppendable(safePath)
|
||||
}
|
||||
return fsys.OpenWritable(safePath)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer, ok := file.(io.Writer)
|
||||
if !ok {
|
||||
panic(errors.New("File is not writable"))
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
panic(errors.New("No body given"))
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, req.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
json, err := json.Marshal(ResponseMessage{
|
||||
Message: "success",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
json, err := json.Marshal(ResponseMessage{
|
||||
Message: "success",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
|
||||
router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
runID := params.ByName("runId")
|
||||
|
||||
safePath := safeResolve(baseDir, runID)
|
||||
|
||||
entries, err := fs.ReadDir(fsys, safePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var list []NamedFileContainerResourceURL
|
||||
for _, entry := range entries {
|
||||
list = append(list, NamedFileContainerResourceURL{
|
||||
Name: entry.Name(),
|
||||
FileContainerResourceURL: fmt.Sprintf("http://%s/download/%s", req.Host, runID),
|
||||
})
|
||||
}
|
||||
|
||||
json, err := json.Marshal(NamedFileContainerResourceURLResponse{
|
||||
Count: len(list),
|
||||
Value: list,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
container := params.ByName("container")
|
||||
itemPath := req.URL.Query().Get("itemPath")
|
||||
safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
|
||||
|
||||
var files []ContainerItem
|
||||
err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error {
|
||||
if !entry.IsDir() {
|
||||
rel, err := filepath.Rel(safePath, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// if it was upload as gzip
|
||||
rel = strings.TrimSuffix(rel, gzipExtension)
|
||||
path := filepath.Join(itemPath, rel)
|
||||
|
||||
rel = filepath.ToSlash(rel)
|
||||
path = filepath.ToSlash(path)
|
||||
|
||||
files = append(files, ContainerItem{
|
||||
Path: path,
|
||||
ItemType: "file",
|
||||
ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
json, err := json.Marshal(ContainerItemResponse{
|
||||
Value: files,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
path := params.ByName("path")[1:]
|
||||
|
||||
safePath := safeResolve(baseDir, path)
|
||||
|
||||
file, err := fsys.Open(safePath)
|
||||
if err != nil {
|
||||
// try gzip file
|
||||
file, err = fsys.Open(safePath + gzipExtension)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.Header().Add("Content-Encoding", "gzip")
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc {
|
||||
serverContext, cancel := context.WithCancel(ctx)
|
||||
logger := common.Logger(serverContext)
|
||||
|
||||
if artifactPath == "" {
|
||||
return cancel
|
||||
}
|
||||
|
||||
router := httprouter.New()
|
||||
|
||||
logger.Debugf("Artifacts base path '%s'", artifactPath)
|
||||
fsys := readWriteFSImpl{}
|
||||
uploads(router, artifactPath, fsys)
|
||||
downloads(router, artifactPath, fsys)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", addr, port),
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
// run server
|
||||
go func() {
|
||||
logger.Infof("Start server on http://%s:%s", addr, port)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for cancel to gracefully shutdown server
|
||||
go func() {
|
||||
<-serverContext.Done()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.Errorf("Failed shutdown gracefully - force shutdown: %v", err)
|
||||
server.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return cancel
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
)
|
||||
|
||||
type writableMapFile struct {
|
||||
fstest.MapFile
|
||||
}
|
||||
|
||||
func (f *writableMapFile) Write(data []byte) (int, error) {
|
||||
f.Data = data
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (f *writableMapFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type writeMapFS struct {
|
||||
fstest.MapFS
|
||||
}
|
||||
|
||||
func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
|
||||
var file = &writableMapFile{
|
||||
MapFile: fstest.MapFile{
|
||||
Data: []byte("content2"),
|
||||
},
|
||||
}
|
||||
fsys.MapFS[name] = &file.MapFile
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
|
||||
var file = &writableMapFile{
|
||||
MapFile: fstest.MapFile{
|
||||
Data: []byte("content2"),
|
||||
},
|
||||
}
|
||||
fsys.MapFS[name] = &file.MapFile
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func TestNewArtifactUploadPrepare(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
router := httprouter.New()
|
||||
uploads(router, "artifact/server/path", writeMapFS{memfs})
|
||||
|
||||
req, _ := http.NewRequest("POST", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.Fail("Wrong status")
|
||||
}
|
||||
|
||||
response := FileContainerResourceURL{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal("http://localhost/upload/1", response.FileContainerResourceURL)
|
||||
}
|
||||
|
||||
func TestArtifactUploadBlob(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
router := httprouter.New()
|
||||
uploads(router, "artifact/server/path", writeMapFS{memfs})
|
||||
|
||||
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content"))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.Fail("Wrong status")
|
||||
}
|
||||
|
||||
response := ResponseMessage{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal("success", response.Message)
|
||||
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
|
||||
}
|
||||
|
||||
func TestFinalizeArtifactUpload(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
router := httprouter.New()
|
||||
uploads(router, "artifact/server/path", writeMapFS{memfs})
|
||||
|
||||
req, _ := http.NewRequest("PATCH", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.Fail("Wrong status")
|
||||
}
|
||||
|
||||
response := ResponseMessage{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal("success", response.Message)
|
||||
}
|
||||
|
||||
func TestListArtifacts(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/file.txt": {
|
||||
Data: []byte(""),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
|
||||
}
|
||||
|
||||
response := NamedFileContainerResourceURLResponse{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(1, response.Count)
|
||||
assert.Equal("file.txt", response.Value[0].Name)
|
||||
assert.Equal("http://localhost/download/1", response.Value[0].FileContainerResourceURL)
|
||||
}
|
||||
|
||||
func TestListArtifactContainer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/some/file": {
|
||||
Data: []byte(""),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
|
||||
}
|
||||
|
||||
response := ContainerItemResponse{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(1, len(response.Value))
|
||||
assert.Equal("some/file", response.Value[0].Path)
|
||||
assert.Equal("file", response.Value[0].ItemType)
|
||||
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
|
||||
}
|
||||
|
||||
func TestDownloadArtifactFile(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/some/file": {
|
||||
Data: []byte("content"),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
|
||||
}
|
||||
|
||||
data := rr.Body.Bytes()
|
||||
|
||||
assert.Equal("content", string(data))
|
||||
}
|
||||
|
||||
type TestJobFileInfo struct {
|
||||
workdir string
|
||||
workflowPath string
|
||||
eventName string
|
||||
errorMessage string
|
||||
platforms map[string]string
|
||||
containerArchitecture string
|
||||
}
|
||||
|
||||
var (
|
||||
artifactsPath = path.Join(os.TempDir(), "test-artifacts")
|
||||
artifactsAddr = "127.0.0.1"
|
||||
artifactsPort = "12345"
|
||||
)
|
||||
|
||||
func TestArtifactFlow(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cancel := Serve(ctx, artifactsPath, artifactsAddr, artifactsPort)
|
||||
defer cancel()
|
||||
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": "node:16-buster", // Don't use node:16-buster-slim because it doesn't have curl command, which is used in the tests
|
||||
}
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
{"testdata", "upload-and-download", "push", "", platforms, ""},
|
||||
{"testdata", "GHSL-2023-004", "push", "", platforms, ""},
|
||||
}
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
for _, table := range tables {
|
||||
runTestJobFile(ctx, t, table)
|
||||
}
|
||||
}
|
||||
|
||||
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||
t.Run(tjfi.workflowPath, func(t *testing.T) {
|
||||
fmt.Printf("::group::%s\n", tjfi.workflowPath)
|
||||
|
||||
if err := os.RemoveAll(artifactsPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
workdir, err := filepath.Abs(tjfi.workdir)
|
||||
assert.Nil(t, err, workdir)
|
||||
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
|
||||
runnerConfig := &runner.Config{
|
||||
Workdir: workdir,
|
||||
BindWorkdir: false,
|
||||
EventName: tjfi.eventName,
|
||||
Platforms: tjfi.platforms,
|
||||
ReuseContainers: false,
|
||||
ContainerArchitecture: tjfi.containerArchitecture,
|
||||
GitHubInstance: "github.com",
|
||||
ArtifactServerPath: artifactsPath,
|
||||
ArtifactServerAddr: artifactsAddr,
|
||||
ArtifactServerPort: artifactsPort,
|
||||
}
|
||||
|
||||
runner, err := runner.New(runnerConfig)
|
||||
assert.Nil(t, err, tjfi.workflowPath)
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
|
||||
plan, err := planner.PlanEvent(tjfi.eventName)
|
||||
if err == nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if tjfi.errorMessage == "" {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
} else {
|
||||
assert.Error(t, err, tjfi.errorMessage)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, plan)
|
||||
}
|
||||
|
||||
fmt.Println("::endgroup::")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMkdirFsImplSafeResolve(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
baseDir := "/foo/bar"
|
||||
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
"simple": {input: "baz", want: "/foo/bar/baz"},
|
||||
"nested": {input: "baz/blue", want: "/foo/bar/baz/blue"},
|
||||
"dots in middle": {input: "baz/../../blue", want: "/foo/bar/blue"},
|
||||
"leading dots": {input: "../../parent", want: "/foo/bar/parent"},
|
||||
"root path": {input: "/root", want: "/foo/bar/root"},
|
||||
"root": {input: "/", want: "/foo/bar"},
|
||||
"empty": {input: "", want: "/foo/bar"},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(tc.want, safeResolve(baseDir, tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadArtifactFileUnsafePath(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/some/file": {
|
||||
Data: []byte("content"),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/artifact/2/../../some/file", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
|
||||
}
|
||||
|
||||
data := rr.Body.Bytes()
|
||||
|
||||
assert.Equal("content", string(data))
|
||||
}
|
||||
|
||||
func TestArtifactUploadBlobUnsafePath(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
router := httprouter.New()
|
||||
uploads(router, "artifact/server/path", writeMapFS{memfs})
|
||||
|
||||
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=../../some/file", strings.NewReader("content"))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
assert.Fail("Wrong status")
|
||||
}
|
||||
|
||||
response := ResponseMessage{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal("success", response.Message)
|
||||
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
name: "GHSL-2023-0004"
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "hello world" > test.txt
|
||||
- name: curl upload
|
||||
run: curl --silent --show-error --fail ${ACTIONS_RUNTIME_URL}upload/1?itemPath=../../my-artifact/secret.txt --upload-file test.txt
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: my-artifact
|
||||
path: test-artifacts
|
||||
- name: 'Verify Artifact #1'
|
||||
run: |
|
||||
file="test-artifacts/secret.txt"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $file)" != "hello world" ] ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
- name: Verify download should work by clean extra dots
|
||||
run: curl --silent --show-error --fail --path-as-is -o out.txt ${ACTIONS_RUNTIME_URL}artifact/1/../../../1/my-artifact/secret.txt
|
||||
- name: 'Verify download content'
|
||||
run: |
|
||||
file="out.txt"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $file)" != "hello world" ] ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,230 +0,0 @@
|
||||
|
||||
name: "Test that artifact uploads and downloads succeed"
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: mkdir -p path/to/artifact
|
||||
- run: echo hello > path/to/artifact/world.txt
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: my-artifact
|
||||
path: path/to/artifact/world.txt
|
||||
|
||||
- run: rm -rf path
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: my-artifact
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -la
|
||||
|
||||
# Test end-to-end by uploading two artifacts and then downloading them
|
||||
- name: Create artifact files
|
||||
run: |
|
||||
mkdir -p path/to/dir-1
|
||||
mkdir -p path/to/dir-2
|
||||
mkdir -p path/to/dir-3
|
||||
mkdir -p path/to/dir-5
|
||||
mkdir -p path/to/dir-6
|
||||
mkdir -p path/to/dir-7
|
||||
echo "Lorem ipsum dolor sit amet" > path/to/dir-1/file1.txt
|
||||
echo "Hello world from file #2" > path/to/dir-2/file2.txt
|
||||
echo "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip" > path/to/dir-3/gzip.txt
|
||||
dd if=/dev/random of=path/to/dir-5/file5.rnd bs=1024 count=1024
|
||||
dd if=/dev/random of=path/to/dir-6/file6.rnd bs=1024 count=$((10*1024))
|
||||
dd if=/dev/random of=path/to/dir-7/file7.rnd bs=1024 count=$((10*1024))
|
||||
|
||||
# Upload a single file artifact
|
||||
- name: 'Upload artifact #1'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Artifact-A'
|
||||
path: path/to/dir-1/file1.txt
|
||||
|
||||
# Upload using a wildcard pattern, name should default to 'artifact' if not provided
|
||||
- name: 'Upload artifact #2'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: path/**/dir*/
|
||||
|
||||
# Upload a directory that contains a file that will be uploaded with GZip
|
||||
- name: 'Upload artifact #3'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'GZip-Artifact'
|
||||
path: path/to/dir-3/
|
||||
|
||||
# Upload a directory that contains a file that will be uploaded with GZip
|
||||
- name: 'Upload artifact #4'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Multi-Path-Artifact'
|
||||
path: |
|
||||
path/to/dir-1/*
|
||||
path/to/dir-[23]/*
|
||||
!path/to/dir-3/*.txt
|
||||
|
||||
# Upload a mid-size file artifact
|
||||
- name: 'Upload artifact #5'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Mid-Size-Artifact'
|
||||
path: path/to/dir-5/file5.rnd
|
||||
|
||||
# Upload a big file artifact
|
||||
- name: 'Upload artifact #6'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Big-Artifact'
|
||||
path: path/to/dir-6/file6.rnd
|
||||
|
||||
# Upload a big file artifact twice
|
||||
- name: 'Upload artifact #7 (First)'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Big-Uploaded-Twice'
|
||||
path: path/to/dir-7/file7.rnd
|
||||
|
||||
# Upload a big file artifact twice
|
||||
- name: 'Upload artifact #7 (Second)'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'Big-Uploaded-Twice'
|
||||
path: path/to/dir-7/file7.rnd
|
||||
|
||||
# Verify artifacts. Switch to download-artifact@v2 once it's out of preview
|
||||
|
||||
# Download Artifact #1 and verify the correctness of the content
|
||||
- name: 'Download artifact #1'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'Artifact-A'
|
||||
path: some/new/path
|
||||
|
||||
- name: 'Verify Artifact #1'
|
||||
run: |
|
||||
file="some/new/path/file1.txt"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $file)" != "Lorem ipsum dolor sit amet" ] ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Artifact #2 and verify the correctness of the content
|
||||
- name: 'Download artifact #2'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'artifact'
|
||||
path: some/other/path
|
||||
|
||||
- name: 'Verify Artifact #2'
|
||||
run: |
|
||||
file1="some/other/path/to/dir-1/file1.txt"
|
||||
file2="some/other/path/to/dir-2/file2.txt"
|
||||
if [ ! -f $file1 -o ! -f $file2 ] ; then
|
||||
echo "Expected files do not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $file1)" != "Lorem ipsum dolor sit amet" -o "$(cat $file2)" != "Hello world from file #2" ] ; then
|
||||
echo "File contents of downloaded artifacts are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Artifact #3 and verify the correctness of the content
|
||||
- name: 'Download artifact #3'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'GZip-Artifact'
|
||||
path: gzip/artifact/path
|
||||
|
||||
# Because a directory was used as input during the upload the parent directories, path/to/dir-3/, should not be included in the uploaded artifact
|
||||
- name: 'Verify Artifact #3'
|
||||
run: |
|
||||
gzipFile="gzip/artifact/path/gzip.txt"
|
||||
if [ ! -f $gzipFile ] ; then
|
||||
echo "Expected file do not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $gzipFile)" != "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip" ] ; then
|
||||
echo "File contents of downloaded artifact is incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Download artifact #4'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'Multi-Path-Artifact'
|
||||
path: multi/artifact
|
||||
|
||||
- name: 'Verify Artifact #4'
|
||||
run: |
|
||||
file1="multi/artifact/dir-1/file1.txt"
|
||||
file2="multi/artifact/dir-2/file2.txt"
|
||||
if [ ! -f $file1 -o ! -f $file2 ] ; then
|
||||
echo "Expected files do not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(cat $file1)" != "Lorem ipsum dolor sit amet" -o "$(cat $file2)" != "Hello world from file #2" ] ; then
|
||||
echo "File contents of downloaded artifacts are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Download artifact #5'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'Mid-Size-Artifact'
|
||||
path: mid-size/artifact/path
|
||||
|
||||
- name: 'Verify Artifact #5'
|
||||
run: |
|
||||
file="mid-size/artifact/path/file5.rnd"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if ! diff $file path/to/dir-5/file5.rnd ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Download artifact #6'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'Big-Artifact'
|
||||
path: big/artifact/path
|
||||
|
||||
- name: 'Verify Artifact #6'
|
||||
run: |
|
||||
file="big/artifact/path/file6.rnd"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if ! diff $file path/to/dir-6/file6.rnd ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Download artifact #7'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: 'Big-Uploaded-Twice'
|
||||
path: big-uploaded-twice/artifact/path
|
||||
|
||||
- name: 'Verify Artifact #7'
|
||||
run: |
|
||||
file="big-uploaded-twice/artifact/path/file7.rnd"
|
||||
if [ ! -f $file ] ; then
|
||||
echo "Expected file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if ! diff $file path/to/dir-7/file7.rnd ; then
|
||||
echo "File contents of downloaded artifact are incorrect"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,54 +0,0 @@
|
||||
package common
|
||||
|
||||
// CartesianProduct takes map of lists and returns list of unique tuples
|
||||
func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interface{} {
|
||||
listNames := make([]string, 0)
|
||||
lists := make([][]interface{}, 0)
|
||||
for k, v := range mapOfLists {
|
||||
listNames = append(listNames, k)
|
||||
lists = append(lists, v)
|
||||
}
|
||||
|
||||
listCart := cartN(lists...)
|
||||
|
||||
rtn := make([]map[string]interface{}, 0)
|
||||
for _, list := range listCart {
|
||||
vMap := make(map[string]interface{})
|
||||
for i, v := range list {
|
||||
vMap[listNames[i]] = v
|
||||
}
|
||||
rtn = append(rtn, vMap)
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
func cartN(a ...[]interface{}) [][]interface{} {
|
||||
c := 1
|
||||
for _, a := range a {
|
||||
c *= len(a)
|
||||
}
|
||||
if c == 0 || len(a) == 0 {
|
||||
return nil
|
||||
}
|
||||
p := make([][]interface{}, c)
|
||||
b := make([]interface{}, c*len(a))
|
||||
n := make([]int, len(a))
|
||||
s := 0
|
||||
for i := range p {
|
||||
e := s + len(a)
|
||||
pi := b[s:e]
|
||||
p[i] = pi
|
||||
s = e
|
||||
for j, n := range n {
|
||||
pi[j] = a[j][n]
|
||||
}
|
||||
for j := len(n) - 1; j >= 0; j-- {
|
||||
n[j]++
|
||||
if n[j] < len(a[j]) {
|
||||
break
|
||||
}
|
||||
n[j] = 0
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCartesianProduct(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
input := map[string][]interface{}{
|
||||
"foo": {1, 2, 3, 4},
|
||||
"bar": {"a", "b", "c"},
|
||||
"baz": {false, true},
|
||||
}
|
||||
|
||||
output := CartesianProduct(input)
|
||||
assert.Len(output, 24)
|
||||
|
||||
for _, v := range output {
|
||||
assert.Len(v, 3)
|
||||
|
||||
assert.Contains(v, "foo")
|
||||
assert.Contains(v, "bar")
|
||||
assert.Contains(v, "baz")
|
||||
}
|
||||
|
||||
input = map[string][]interface{}{
|
||||
"foo": {1, 2, 3, 4},
|
||||
"bar": {},
|
||||
"baz": {false, true},
|
||||
}
|
||||
output = CartesianProduct(input)
|
||||
assert.Len(output, 0)
|
||||
|
||||
input = map[string][]interface{}{}
|
||||
output = CartesianProduct(input)
|
||||
assert.Len(output, 0)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Style is a specific style
|
||||
type Style int
|
||||
|
||||
// Styles
|
||||
const (
|
||||
StyleDoubleLine = iota
|
||||
StyleSingleLine
|
||||
StyleDashedLine
|
||||
StyleNoLine
|
||||
)
|
||||
|
||||
// NewPen creates a new pen
|
||||
func NewPen(style Style, color int) *Pen {
|
||||
bgcolor := 49
|
||||
if os.Getenv("CLICOLOR") == "0" {
|
||||
color = 0
|
||||
bgcolor = 0
|
||||
}
|
||||
return &Pen{
|
||||
style: style,
|
||||
color: color,
|
||||
bgcolor: bgcolor,
|
||||
}
|
||||
}
|
||||
|
||||
type styleDef struct {
|
||||
cornerTL string
|
||||
cornerTR string
|
||||
cornerBL string
|
||||
cornerBR string
|
||||
lineH string
|
||||
lineV string
|
||||
}
|
||||
|
||||
var styleDefs = []styleDef{
|
||||
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
|
||||
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
|
||||
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
|
||||
{" ", " ", " ", " ", " ", " "},
|
||||
}
|
||||
|
||||
// Pen struct
|
||||
type Pen struct {
|
||||
style Style
|
||||
color int
|
||||
bgcolor int
|
||||
}
|
||||
|
||||
// Drawing struct
|
||||
type Drawing struct {
|
||||
buf *strings.Builder
|
||||
width int
|
||||
}
|
||||
|
||||
func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
|
||||
// DrawArrow between boxes
|
||||
func (p *Pen) DrawArrow() *Drawing {
|
||||
drawing := &Drawing{
|
||||
buf: new(strings.Builder),
|
||||
width: 1,
|
||||
}
|
||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
|
||||
fmt.Fprintf(drawing.buf, "\u2b07")
|
||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
|
||||
return drawing
|
||||
}
|
||||
|
||||
// DrawBoxes to draw boxes
|
||||
func (p *Pen) DrawBoxes(labels ...string) *Drawing {
|
||||
width := 0
|
||||
for _, l := range labels {
|
||||
width += len(l) + 2 + 2 + 1
|
||||
}
|
||||
drawing := &Drawing{
|
||||
buf: new(strings.Builder),
|
||||
width: width,
|
||||
}
|
||||
p.drawTopBars(drawing.buf, labels...)
|
||||
p.drawLabels(drawing.buf, labels...)
|
||||
p.drawBottomBars(drawing.buf, labels...)
|
||||
|
||||
return drawing
|
||||
}
|
||||
|
||||
// Draw to writer
|
||||
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
|
||||
padSize := (centerOnWidth - d.GetWidth()) / 2
|
||||
if padSize < 0 {
|
||||
padSize = 0
|
||||
}
|
||||
for _, l := range strings.Split(d.buf.String(), "\n") {
|
||||
if len(l) > 0 {
|
||||
padding := strings.Repeat(" ", padSize)
|
||||
fmt.Fprintf(writer, "%s%s\n", padding, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetWidth of drawing
|
||||
func (d *Drawing) GetWidth() int {
|
||||
return d.width
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type dryrunContextKey string
|
||||
|
||||
const dryrunContextKeyVal = dryrunContextKey("dryrun")
|
||||
|
||||
// Dryrun returns true if the current context is dryrun
|
||||
func Dryrun(ctx context.Context) bool {
|
||||
val := ctx.Value(dryrunContextKeyVal)
|
||||
if val != nil {
|
||||
if dryrun, ok := val.(bool); ok {
|
||||
return dryrun
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithDryrun adds a value to the context for dryrun
|
||||
func WithDryrun(ctx context.Context, dryrun bool) context.Context {
|
||||
return context.WithValue(ctx, dryrunContextKeyVal, dryrun)
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Warning that implements `error` but safe to ignore
|
||||
type Warning struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error the contract for error
|
||||
func (w Warning) Error() string {
|
||||
return w.Message
|
||||
}
|
||||
|
||||
// Warningf create a warning
|
||||
func Warningf(format string, args ...interface{}) Warning {
|
||||
w := Warning{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Executor define contract for the steps of a workflow
|
||||
type Executor func(ctx context.Context) error
|
||||
|
||||
// Conditional define contract for the conditional predicate
|
||||
type Conditional func(ctx context.Context) bool
|
||||
|
||||
// NewInfoExecutor is an executor that logs messages
|
||||
func NewInfoExecutor(format string, args ...interface{}) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := Logger(ctx)
|
||||
logger.Infof(format, args...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewDebugExecutor is an executor that logs messages
|
||||
func NewDebugExecutor(format string, args ...interface{}) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := Logger(ctx)
|
||||
logger.Debugf(format, args...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewPipelineExecutor creates a new executor from a series of other executors
|
||||
func NewPipelineExecutor(executors ...Executor) Executor {
|
||||
if len(executors) == 0 {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var rtn Executor
|
||||
for _, executor := range executors {
|
||||
if rtn == nil {
|
||||
rtn = executor
|
||||
} else {
|
||||
rtn = rtn.Then(executor)
|
||||
}
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
// NewConditionalExecutor creates a new executor based on conditions
|
||||
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if conditional(ctx) {
|
||||
if trueExecutor != nil {
|
||||
return trueExecutor(ctx)
|
||||
}
|
||||
} else {
|
||||
if falseExecutor != nil {
|
||||
return falseExecutor(ctx)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorExecutor creates a new executor that always errors out
|
||||
func NewErrorExecutor(err error) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NewParallelExecutor creates a new executor from a parallel of other executors
|
||||
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
work := make(chan Executor, len(executors))
|
||||
errs := make(chan error, len(executors))
|
||||
|
||||
if 1 > parallel {
|
||||
log.Debugf("Parallel tasks (%d) below minimum, setting to 1", parallel)
|
||||
parallel = 1
|
||||
}
|
||||
|
||||
log.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))
|
||||
|
||||
for i := 0; i < parallel; i++ {
|
||||
go func(workerID int, work <-chan Executor, errs chan<- error) {
|
||||
log.Debugf("Worker %d started", workerID)
|
||||
taskCount := 0
|
||||
for executor := range work {
|
||||
taskCount++
|
||||
log.Debugf("Worker %d executing task %d", workerID, taskCount)
|
||||
// Recover from panics in executors to avoid crashing the worker
|
||||
// goroutine which would leave the runner process hung.
|
||||
// https://gitea.com/gitea/act_runner/issues/371
|
||||
errs <- func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("panic in executor: %v\n%s", r, debug.Stack())
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
return executor(ctx)
|
||||
}()
|
||||
}
|
||||
log.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
|
||||
}(i, work, errs)
|
||||
}
|
||||
|
||||
for i := 0; i < len(executors); i++ {
|
||||
work <- executors[i]
|
||||
}
|
||||
close(work)
|
||||
|
||||
// Executor waits all executors to cleanup these resources.
|
||||
var firstErr error
|
||||
for i := 0; i < len(executors); i++ {
|
||||
err := <-errs
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
}
|
||||
|
||||
// Then runs another executor if this executor succeeds
|
||||
func (e Executor) Then(then Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
err := e(ctx)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case Warning:
|
||||
Logger(ctx).Warning(err.Error())
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return then(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// If only runs this executor if conditional is true
|
||||
func (e Executor) If(conditional Conditional) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if conditional(ctx) {
|
||||
return e(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IfNot only runs this executor if conditional is true
|
||||
func (e Executor) IfNot(conditional Conditional) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if !conditional(ctx) {
|
||||
return e(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IfBool only runs this executor if conditional is true
|
||||
func (e Executor) IfBool(conditional bool) Executor {
|
||||
return e.If(func(ctx context.Context) bool {
|
||||
return conditional
|
||||
})
|
||||
}
|
||||
|
||||
// Finally adds an executor to run after other executor
|
||||
func (e Executor) Finally(finally Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
err := e(ctx)
|
||||
err2 := finally(ctx)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Not return an inverted conditional
|
||||
func (c Conditional) Not() Conditional {
|
||||
return func(ctx context.Context) bool {
|
||||
return !c(ctx)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1911 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user