mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-08 16:23:23 +02:00
Compare commits
7 Commits
b5a66bda89
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40dcee0991 | ||
|
|
f33e5a6245 | ||
|
|
f2d545565f | ||
|
|
90c1275f0e | ||
|
|
505907eb2a | ||
|
|
9933ea0d92 | ||
|
|
5dd5436169 |
@@ -1,6 +0,0 @@
|
|||||||
[codespell]
|
|
||||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
|
||||||
skip = .git*,go.sum,package-lock.json,*.min.*,.codespellrc,testdata,./pkg/runner/hashfiles/index.js
|
|
||||||
check-hidden = true
|
|
||||||
ignore-regex = .*Te\{0\}st.*
|
|
||||||
# ignore-words-list =
|
|
||||||
@@ -1,135 +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
|
|
||||||
NO_QEMU: 1
|
|
||||||
NO_EXTERNAL_IP: 1
|
|
||||||
DOOD: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make lint-go
|
|
||||||
|
|
||||||
test-linux:
|
|
||||||
name: test-linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Set up QEMU
|
|
||||||
if: '!env.NO_QEMU'
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: ${{ !env.ACT }}
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Install gotestfmt
|
|
||||||
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
|
|
||||||
# Regressions by Gitea Actions CI Migration
|
|
||||||
# GITHUB_REPOSITORY contains the server url
|
|
||||||
# ACTIONS_RUNTIME_URL provided to every step, act does not override
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
unset ACTIONS_RUNTIME_URL
|
|
||||||
unset ACTIONS_RESULTS_URL
|
|
||||||
unset ACTIONS_RUNTIME_TOKEN
|
|
||||||
export GITHUB_REPOSITORY="${GITHUB_REPOSITORY#${SERVER_URL%/}/}"
|
|
||||||
export ACT_REPOSITORY="${GITHUB_REPOSITORY#${SERVER_URL%/}/}"
|
|
||||||
export ACT_OWNER="${ACT_OWNER#${SERVER_URL%/}/}"
|
|
||||||
env
|
|
||||||
go test -json -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m ./... | gotestfmt -hide successful-packages,empty-packages 2>&1
|
|
||||||
env:
|
|
||||||
SERVER_URL: ${{ github.server_url }}
|
|
||||||
- name: Run act from cli
|
|
||||||
run: go run ./internal/app/act-cli -P ubuntu-latest=node:16-buster-slim -C ./pkg/runner/testdata/ -W ./basic/push.yml
|
|
||||||
- name: Run act from cli without docker support
|
|
||||||
run: go run -tags WITHOUT_DOCKER ./internal/app/act-cli -P ubuntu-latest=-self-hosted -C ./pkg/runner/testdata/ -W ./local-action-js/push.yml
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
name: snapshot
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: ${{ !env.ACT }}
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: GoReleaser
|
|
||||||
id: goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
version: v2
|
|
||||||
args: release --snapshot --clean -f ./.goreleaser.act-cli.yml
|
|
||||||
- name: Setup Node
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
- name: Install @actions/artifact@2.1.0
|
|
||||||
continue-on-error: true
|
|
||||||
run: npm install @actions/artifact@2.1.0
|
|
||||||
- name: Upload All
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
// We do not use features depending on GITHUB_API_URL so we can hardcode it to avoid the GHES no support error
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "https://github.com";
|
|
||||||
const {DefaultArtifactClient} = require('@actions/artifact');
|
|
||||||
const aartifact = new DefaultArtifactClient();
|
|
||||||
var artifacts = JSON.parse(process.env.ARTIFACTS);
|
|
||||||
for(var artifact of artifacts) {
|
|
||||||
if(artifact.type === "Binary") {
|
|
||||||
const {id, size} = await aartifact.uploadArtifact(
|
|
||||||
// name of the artifact
|
|
||||||
`${artifact.name}-${artifact.target}`,
|
|
||||||
// files to include (supports absolute and relative paths)
|
|
||||||
[artifact.path],
|
|
||||||
process.cwd(),
|
|
||||||
{
|
|
||||||
// optional: how long to retain the artifact
|
|
||||||
// if unspecified, defaults to repository/org retention settings (the limit of this value)
|
|
||||||
retentionDays: 10
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(`Created artifact with id: ${id} (bytes: ${size}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
ARTIFACTS: ${{ steps.goreleaser.outputs.artifacts }}
|
|
||||||
- name: Chocolatey
|
|
||||||
uses: ./.github/actions/choco
|
|
||||||
with:
|
|
||||||
version: v0.0.0-pr
|
|
||||||
@@ -15,7 +15,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
if: (!vars.PUBLISH_ACT_CLI)
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -40,7 +39,6 @@ jobs:
|
|||||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
release-image:
|
release-image:
|
||||||
if: (!vars.PUBLISH_ACT_CLI)
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
if: (!vars.PUBLISH_ACT_CLI)
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -39,8 +38,16 @@ jobs:
|
|||||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
release-image:
|
release-image:
|
||||||
if: (!vars.PUBLISH_ACT_CLI)
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
variant:
|
||||||
|
- target: basic
|
||||||
|
tag_suffix: ""
|
||||||
|
- target: dind
|
||||||
|
tag_suffix: "-dind"
|
||||||
|
- target: dind-rootless
|
||||||
|
tag_suffix: "-dind-rootless"
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
env:
|
env:
|
||||||
@@ -64,50 +71,33 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Get Meta
|
- name: Repo Meta
|
||||||
id: meta
|
id: repo_meta
|
||||||
run: |
|
run: |
|
||||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
|
||||||
|
- name: "Docker meta"
|
||||||
|
id: docker_meta
|
||||||
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
target: basic
|
target: ${{ matrix.variant.target }}
|
||||||
platforms: |
|
platforms: |
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
|
||||||
|
|
||||||
- name: Build and push dind
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
target: dind
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind
|
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
|
|
||||||
|
|
||||||
- name: Build and push dind-rootless
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
target: dind-rootless
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind-rootless
|
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
name: release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
# TODO use environment to scope secrets
|
|
||||||
name: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
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@v6
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: release --clean -f ./.goreleaser.act-cli.yml -f ./.goreleaser.act-cli.gitea.yml
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN || github.token }}
|
|
||||||
- name: Winget
|
|
||||||
uses: vedantmgoyal2009/winget-releaser@v2
|
|
||||||
with:
|
|
||||||
identifier: nektos.act
|
|
||||||
installers-regex: '_Windows_\w+\.zip$'
|
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ secrets.WINGET_TOKEN && '1' || '' }}
|
|
||||||
- name: Chocolatey
|
|
||||||
uses: ./.github/actions/choco
|
|
||||||
with:
|
|
||||||
version: ${{ github.ref }}
|
|
||||||
apiKey: ${{ secrets.CHOCO_APIKEY }}
|
|
||||||
push: true
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ secrets.CHOCO_APIKEY && '1' || '' }}
|
|
||||||
# TODO use ssh deployment key
|
|
||||||
- name: GitHub CLI extension
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const mainRef = (await github.rest.git.getRef({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ (secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN) && '1' || '' }}
|
|
||||||
@@ -3,11 +3,6 @@ on:
|
|||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
env:
|
|
||||||
DOOD: 1
|
|
||||||
NO_QEMU: 1
|
|
||||||
NO_EXTERNAL_IP: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: check and test
|
name: check and test
|
||||||
@@ -22,13 +17,4 @@ jobs:
|
|||||||
- name: build
|
- name: build
|
||||||
run: make build
|
run: make build
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: make test
|
||||||
unset ACTIONS_RUNTIME_URL
|
|
||||||
unset ACTIONS_RESULTS_URL
|
|
||||||
unset ACTIONS_RUNTIME_TOKEN
|
|
||||||
export GITHUB_REPOSITORY="${GITHUB_REPOSITORY#${SERVER_URL%/}/}"
|
|
||||||
export ACT_REPOSITORY="${GITHUB_REPOSITORY}"
|
|
||||||
export ACT_OWNER="${ACT_REPOSITORY%%/*}"
|
|
||||||
make test
|
|
||||||
env:
|
|
||||||
SERVER_URL: ${{ github.server_url }}
|
|
||||||
|
|||||||
88
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
88
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: true
|
|
||||||
contact_links:
|
|
||||||
- name: Start a discussion
|
|
||||||
url: https://github.com/actions-oss/act-cli/discussions/new
|
|
||||||
about: You can ask for help here!
|
|
||||||
- name: Want to contribute to act?
|
|
||||||
url: https://github.com/actions-oss/act-cli/blob/main/CONTRIBUTING.md
|
|
||||||
about: Be sure to read contributing guidelines!
|
|
||||||
28
.github/ISSUE_TEMPLATE/feature_template.yml
vendored
28
.github/ISSUE_TEMPLATE/feature_template.yml
vendored
@@ -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
|
|
||||||
20
.github/actions/choco/Dockerfile
vendored
20
.github/actions/choco/Dockerfile
vendored
@@ -1,20 +0,0 @@
|
|||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
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/community 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"]
|
|
||||||
16
.github/actions/choco/action.yml
vendored
16
.github/actions/choco/action.yml
vendored
@@ -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'
|
|
||||||
31
.github/actions/choco/entrypoint.sh
vendored
31
.github/actions/choco/entrypoint.sh
vendored
@@ -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-cli_windows_amd64*/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
|
|
||||||
23
.github/dependabot.yml
vendored
23
.github/dependabot.yml
vendored
@@ -1,23 +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://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: 'github-actions'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'monthly'
|
|
||||||
groups:
|
|
||||||
dependencies:
|
|
||||||
patterns:
|
|
||||||
- '*'
|
|
||||||
- package-ecosystem: 'gomod'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'monthly'
|
|
||||||
groups:
|
|
||||||
dependencies:
|
|
||||||
patterns:
|
|
||||||
- '*'
|
|
||||||
1
.github/workflows/.gitignore
vendored
1
.github/workflows/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
test-*.yml
|
|
||||||
151
.github/workflows/checks.yml
vendored
151
.github/workflows/checks.yml
vendored
@@ -1,151 +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@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: golangci/golangci-lint-action@v8.0.0
|
|
||||||
with:
|
|
||||||
version: v2.1.6
|
|
||||||
- uses: megalinter/megalinter/flavors/go@v9.1.0
|
|
||||||
env:
|
|
||||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
||||||
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@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: ${{ !env.ACT }}
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Install gotestfmt
|
|
||||||
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
|
|
||||||
- name: Run Tests
|
|
||||||
run: go test -json -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m ./... | gotestfmt -hide successful-packages,empty-packages 2>&1
|
|
||||||
- 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: Run act from cli without docker support
|
|
||||||
run: go run -tags WITHOUT_DOCKER main.go -P ubuntu-latest=-self-hosted -C ./pkg/runner/testdata/ -W ./local-action-js/push.yml
|
|
||||||
- name: Upload Codecov report
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
files: coverage.txt
|
|
||||||
fail_ci_if_error: true # optional (default = false)
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
test-host:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- windows-latest
|
|
||||||
- macos-latest
|
|
||||||
name: test-host-${{matrix.os}}
|
|
||||||
runs-on: ${{matrix.os}}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- name: Install gotestfmt
|
|
||||||
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
|
|
||||||
- name: Run Tests
|
|
||||||
run: go test -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m -run ^TestRunEventHostEnvironment$ ./...
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
name: snapshot
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: ${{ !env.ACT }}
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: GoReleaser
|
|
||||||
id: goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
version: v2
|
|
||||||
args: release --snapshot --clean -f ./.goreleaser.act-cli.yml
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
- name: Install @actions/artifact
|
|
||||||
run: npm install @actions/artifact
|
|
||||||
- name: Upload All
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const {DefaultArtifactClient} = require('@actions/artifact');
|
|
||||||
const aartifact = new DefaultArtifactClient();
|
|
||||||
var artifacts = JSON.parse(process.env.ARTIFACTS);
|
|
||||||
for(var artifact of artifacts) {
|
|
||||||
if(artifact.type === "Binary") {
|
|
||||||
const {id, size} = await aartifact.uploadArtifact(
|
|
||||||
// name of the artifact
|
|
||||||
`${artifact.name}-${artifact.target}`,
|
|
||||||
// files to include (supports absolute and relative paths)
|
|
||||||
[artifact.path],
|
|
||||||
process.cwd(),
|
|
||||||
{
|
|
||||||
// optional: how long to retain the artifact
|
|
||||||
// if unspecified, defaults to repository/org retention settings (the limit of this value)
|
|
||||||
retentionDays: 10
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(`Created artifact with id: ${id} (bytes: ${size}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
ARTIFACTS: ${{ steps.goreleaser.outputs.artifacts }}
|
|
||||||
- name: Chocolatey
|
|
||||||
uses: ./.github/actions/choco
|
|
||||||
with:
|
|
||||||
version: v0.0.0-pr
|
|
||||||
23
.github/workflows/codespell.yml
vendored
23
.github/workflows/codespell.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
# Codespell configuration is within .codespellrc
|
|
||||||
---
|
|
||||||
name: Codespell
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codespell:
|
|
||||||
name: Check for spelling errors
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
- name: Codespell
|
|
||||||
uses: codespell-project/actions-codespell@v2
|
|
||||||
30
.github/workflows/promote.yml
vendored
30
.github/workflows/promote.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: promote
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 2 1 * *'
|
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: vars.ENABLE_PROMOTE || github.event_name != 'schedule'
|
|
||||||
name: promote
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: master
|
|
||||||
token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
|
||||||
- uses: fregante/setup-git-user@v2
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: ${{ !env.ACT }}
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- run: make promote
|
|
||||||
73
.github/workflows/release.yml
vendored
73
.github/workflows/release.yml
vendored
@@ -1,73 +0,0 @@
|
|||||||
name: release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: vars.PUBLISH_ACT_CLI
|
|
||||||
# TODO use environment to scope secrets
|
|
||||||
name: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
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@v6
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: release --clean -f ./.goreleaser.act-cli.yml
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN || github.token }}
|
|
||||||
- name: Winget
|
|
||||||
uses: vedantmgoyal2009/winget-releaser@v2
|
|
||||||
with:
|
|
||||||
identifier: nektos.act
|
|
||||||
installers-regex: '_Windows_\w+\.zip$'
|
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ secrets.WINGET_TOKEN && '1' || '' }}
|
|
||||||
- name: Chocolatey
|
|
||||||
uses: ./.github/actions/choco
|
|
||||||
with:
|
|
||||||
version: ${{ github.ref }}
|
|
||||||
apiKey: ${{ secrets.CHOCO_APIKEY }}
|
|
||||||
push: true
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ secrets.CHOCO_APIKEY && '1' || '' }}
|
|
||||||
# TODO use ssh deployment key
|
|
||||||
- name: GitHub CLI extension
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const mainRef = (await github.rest.git.getRef({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
if: env.ENABLED
|
|
||||||
env:
|
|
||||||
ENABLED: ${{ (secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN) && '1' || '' }}
|
|
||||||
23
.github/workflows/stale.yml
vendored
23
.github/workflows/stale.yml
vendored
@@ -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@v10
|
|
||||||
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
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
/act_runner
|
/act_runner
|
||||||
/act
|
|
||||||
.env
|
.env
|
||||||
.runner
|
.runner
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
b910a42edfab7a02b08a52ecef203fd419725642:pkg/container/testdata/docker-pull-options/config.json:generic-api-key:4
|
|
||||||
710a3ac94c3dc0eaf680d417c87f37f92b4887f4:pkg/container/docker_pull_test.go:generic-api-key:45
|
|
||||||
@@ -35,23 +35,56 @@ linters:
|
|||||||
rules:
|
rules:
|
||||||
main:
|
main:
|
||||||
deny:
|
deny:
|
||||||
|
- pkg: io/ioutil
|
||||||
|
desc: use os or io instead
|
||||||
|
- pkg: golang.org/x/exp
|
||||||
|
desc: it's experimental and unreliable
|
||||||
- pkg: github.com/pkg/errors
|
- pkg: github.com/pkg/errors
|
||||||
desc: Please use "errors" package from standard library
|
desc: use builtin errors package instead
|
||||||
- pkg: gotest.tools/v3
|
nolintlint:
|
||||||
desc: Please keep tests unified using only github.com/stretchr/testify
|
allow-unused: false
|
||||||
- pkg: log
|
require-explanation: true
|
||||||
desc: Please keep logging unified using only github.com/sirupsen/logrus
|
require-specific: true
|
||||||
gocritic:
|
gocritic:
|
||||||
|
enabled-checks:
|
||||||
|
- equalFold
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
- ifElseChain
|
- ifElseChain
|
||||||
gocyclo:
|
revive:
|
||||||
min-complexity: 20
|
severity: error
|
||||||
importas:
|
rules:
|
||||||
alias:
|
- name: blank-imports
|
||||||
- pkg: github.com/sirupsen/logrus
|
- name: constant-logical-expr
|
||||||
alias: log
|
- name: context-as-argument
|
||||||
- pkg: github.com/stretchr/testify/assert
|
- name: context-keys-type
|
||||||
alias: assert
|
- name: dot-imports
|
||||||
|
- name: empty-lines
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: exported
|
||||||
|
- name: identical-branches
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: modifies-value-receiver
|
||||||
|
- name: package-comments
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: time-naming
|
||||||
|
- name: unexported-return
|
||||||
|
- name: var-declaration
|
||||||
|
- name: var-naming
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -ST1005
|
||||||
|
usetesting:
|
||||||
|
os-temp-dir: true
|
||||||
|
perfsprint:
|
||||||
|
concat-loop: false
|
||||||
|
govet:
|
||||||
|
enable:
|
||||||
|
- nilness
|
||||||
|
- unusedwrite
|
||||||
exclusions:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
presets:
|
presets:
|
||||||
@@ -60,23 +93,20 @@ linters:
|
|||||||
- legacy
|
- legacy
|
||||||
- std-error-handling
|
- std-error-handling
|
||||||
rules:
|
rules:
|
||||||
- linters: [revive]
|
- linters:
|
||||||
text: avoid meaningless package names
|
- forbidigo
|
||||||
paths:
|
path: cmd
|
||||||
- report
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- goimports
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
settings:
|
||||||
|
gofumpt:
|
||||||
|
extra-rules: true
|
||||||
exclusions:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
paths:
|
run:
|
||||||
- report
|
timeout: 10m
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
gitea_urls:
|
|
||||||
api: https://gitea.com/api/v1/
|
|
||||||
download: https://gitea.com/
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
version: 2
|
|
||||||
project_name: act-cli
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- darwin
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- '386'
|
|
||||||
- arm64
|
|
||||||
- arm
|
|
||||||
- riscv64
|
|
||||||
goarm:
|
|
||||||
- '6'
|
|
||||||
- '7'
|
|
||||||
ignore:
|
|
||||||
- goos: windows
|
|
||||||
goarch: arm
|
|
||||||
binary: act
|
|
||||||
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
|
|
||||||
formats:
|
|
||||||
- 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
|
|
||||||
98
.mergify.yml
98
.mergify.yml
@@ -1,98 +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: []
|
|
||||||
actions:
|
|
||||||
queue:
|
|
||||||
queue_rules:
|
|
||||||
- name: default
|
|
||||||
queue_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
|
|
||||||
merge_conditions:
|
|
||||||
- check-success=lint
|
|
||||||
- check-success=test-linux
|
|
||||||
- check-success=codecov/patch
|
|
||||||
- check-success=codecov/project
|
|
||||||
- check-success=snapshot
|
|
||||||
merge_method: squash
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
**/testdata
|
|
||||||
pkg/runner/res
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
overrides:
|
|
||||||
- files: '*.yml'
|
|
||||||
options:
|
|
||||||
singleQuote: true
|
|
||||||
- files: '*.json'
|
|
||||||
options:
|
|
||||||
singleQuote: false
|
|
||||||
9
.vscode/extensions.json
vendored
9
.vscode/extensions.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"editorconfig.editorconfig",
|
|
||||||
"golang.go",
|
|
||||||
"davidanson.vscode-markdownlint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"redhat.vscode-yaml"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -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 discussions: [actions-oss/act-cli](https://github.com/actions-oss/act-cli/discussions).
|
|
||||||
|
|
||||||
## <a id="bugs"></a> Found a Bug?
|
|
||||||
|
|
||||||
If you've identified a bug in `act`, please [submit an issue](#issue) to our GitHub repo: [actions-oss/act-cli](https://github.com/actions-oss/act-cli/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/actions-oss/act-cli/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://actions-oss.github.io/act-docs/) to [actions-oss/act-docs](https://github.com/actions-oss/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.
|
|
||||||
12
Makefile
12
Makefile
@@ -1,6 +1,5 @@
|
|||||||
DIST := dist
|
DIST := dist
|
||||||
EXECUTABLE := act_runner
|
EXECUTABLE := act_runner
|
||||||
ACT_EXECUTABLE := act
|
|
||||||
GOFMT ?= gofumpt -l
|
GOFMT ?= gofumpt -l
|
||||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||||
GO ?= go
|
GO ?= go
|
||||||
@@ -21,7 +20,7 @@ DOCKER_TAG ?= nightly
|
|||||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||||
|
|
||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||||
|
|
||||||
ifneq ($(shell uname), Darwin)
|
ifneq ($(shell uname), Darwin)
|
||||||
@@ -69,7 +68,7 @@ else
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/cmd gitea.com/gitea/act_runner/internal/app/act-cli gitea.com/gitea/act_runner/internal/eval/functions gitea.com/gitea/act_runner/internal/eval/v2 gitea.com/gitea/act_runner/internal/expr gitea.com/gitea/act_runner/internal/model gitea.com/gitea/act_runner/internal/templateeval gitea.com/gitea/act_runner/pkg/artifactcache gitea.com/gitea/act_runner/pkg/artifacts gitea.com/gitea/act_runner/pkg/common gitea.com/gitea/act_runner/pkg/common/git gitea.com/gitea/act_runner/pkg/container gitea.com/gitea/act_runner/pkg/exprparser gitea.com/gitea/act_runner/pkg/filecollector gitea.com/gitea/act_runner/pkg/gh gitea.com/gitea/act_runner/pkg/model gitea.com/gitea/act_runner/pkg/runner gitea.com/gitea/act_runner/pkg/schema gitea.com/gitea/act_runner/pkg/tart gitea.com/gitea/act_runner/pkg/workflowpattern gitea.com/gitea/act_runner/pkg/lookpath gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||||
|
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
@@ -138,7 +137,7 @@ tidy-check: tidy
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
test: fmt-check security-check
|
test: fmt-check security-check
|
||||||
@$(GO) test -test.short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
@$(GO) test -race -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
.PHONY: vet
|
.PHONY: vet
|
||||||
vet:
|
vet:
|
||||||
@@ -149,14 +148,11 @@ vet:
|
|||||||
install: $(GOFILES)
|
install: $(GOFILES)
|
||||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||||
|
|
||||||
build: go-check $(EXECUTABLE) $(ACT_EXECUTABLE)
|
build: go-check $(EXECUTABLE)
|
||||||
|
|
||||||
$(EXECUTABLE): $(GOFILES)
|
$(EXECUTABLE): $(GOFILES)
|
||||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
||||||
|
|
||||||
$(ACT_EXECUTABLE): $(GOFILES)
|
|
||||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@ ./internal/app/act-cli
|
|
||||||
|
|
||||||
.PHONY: deps-backend
|
.PHONY: deps-backend
|
||||||
deps-backend:
|
deps-backend:
|
||||||
$(GO) mod download
|
$(GO) mod download
|
||||||
|
|||||||
@@ -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>
|
|
||||||
27
cmd/dir.go
27
cmd/dir.go
@@ -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,45 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper function to test main with different os.Args
|
|
||||||
func testMain(args []string) (exitCode int) {
|
|
||||||
// Save original os.Args and defer restoring it
|
|
||||||
origArgs := os.Args
|
|
||||||
defer func() { os.Args = origArgs }()
|
|
||||||
|
|
||||||
// Save original os.Exit and defer restoring it
|
|
||||||
defer func() { exitFunc = os.Exit }()
|
|
||||||
|
|
||||||
// Mock os.Exit
|
|
||||||
fakeExit := func(code int) {
|
|
||||||
exitCode = code
|
|
||||||
}
|
|
||||||
exitFunc = fakeExit
|
|
||||||
|
|
||||||
// Mock os.Args
|
|
||||||
os.Args = args
|
|
||||||
|
|
||||||
// Run the main function
|
|
||||||
Execute(context.Background(), "")
|
|
||||||
|
|
||||||
return exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMainHelp(t *testing.T) {
|
|
||||||
exitCode := testMain([]string{"cmd", "--help"})
|
|
||||||
if exitCode != 0 {
|
|
||||||
t.Errorf("expected exit code 0, got %d", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMainNoArgsError(t *testing.T) {
|
|
||||||
exitCode := testMain([]string{"cmd"})
|
|
||||||
if exitCode != 1 {
|
|
||||||
t.Errorf("expected exit code 1, got %d", exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
cmd/graph.go
37
cmd/graph.go
@@ -1,37 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/pkg/common"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func drawGraph(plan *model.Plan) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
118
cmd/input.go
118
cmd/input.go
@@ -1,118 +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
|
|
||||||
pullIfNeeded bool
|
|
||||||
noRebuild 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
|
|
||||||
workflowRecurse bool
|
|
||||||
useGitIgnore bool
|
|
||||||
githubInstance string
|
|
||||||
gitHubServerURL string
|
|
||||||
gitHubAPIServerURL string
|
|
||||||
gitHubGraphQlAPIServerURL 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
|
|
||||||
localRepository []string
|
|
||||||
listOptions bool
|
|
||||||
validate bool
|
|
||||||
strict bool
|
|
||||||
parallel int
|
|
||||||
gitea bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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
cmd/list.go
107
cmd/list.go
@@ -1,107 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func printList(plan *model.Plan) {
|
|
||||||
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.Fprintf(os.Stdout, "%*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.Fprintf(os.Stdout, "%*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.Fprint(os.Stdout, "\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.SplitN(p, "=", 2)
|
|
||||||
if len(pParts) == 2 {
|
|
||||||
platforms[strings.ToLower(pParts[0])] = pParts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return platforms
|
|
||||||
}
|
|
||||||
866
cmd/root.go
866
cmd/root.go
@@ -1,866 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"runtime/debug"
|
|
||||||
"strconv"
|
|
||||||
"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"
|
|
||||||
"github.com/spf13/cobra/doc"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/pkg/artifactcache"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/artifacts"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/common"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/container"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/gh"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/runner"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Flag struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Default string `json:"default"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitFunc = os.Exit
|
|
||||||
|
|
||||||
// Execute is the entry point to running the CLI
|
|
||||||
func Execute(ctx context.Context, version string) {
|
|
||||||
input := new(Input)
|
|
||||||
rootCmd := createRootCommand(ctx, input, version)
|
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
exitFunc(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRootCommand(ctx context.Context, input *Input, version string) *cobra.Command {
|
|
||||||
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\"\nSee documentation at: https://gitea.com/actions-oss/act-cli or https://github.com/actions-oss/act-cli",
|
|
||||||
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),
|
|
||||||
Version: version,
|
|
||||||
SilenceUsage: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
|
|
||||||
rootCmd.Flags().BoolVar(&input.validate, "validate", false, "validate workflows")
|
|
||||||
rootCmd.Flags().BoolVar(&input.strict, "strict", false, "use strict workflow schema")
|
|
||||||
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().BoolP("man-page", "", false, "Print a generated manual page to stdout")
|
|
||||||
|
|
||||||
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.pullIfNeeded, "pull-if-needed", "", false, "only pull docker image(s) if not present")
|
|
||||||
rootCmd.Flags().BoolVarP(&input.noRebuild, "no-rebuild", "", false, "don't rebuild local action docker action image(s) if already present for correct platform")
|
|
||||||
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.Flags().IntVarP(&input.parallel, "parallel", "", 0, "number of jobs to run in parallel")
|
|
||||||
rootCmd.Flags().IntVarP(&input.parallel, "concurrent-jobs", "", 0, "number of jobs to run in parallel")
|
|
||||||
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.workflowRecurse, "recurse", "", false, "Flag to enable running workflows from subdirectories of specified path in '--workflows'/'-W' flag, this feature doesn't exist on GitHub Actions as of 2024/11")
|
|
||||||
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, "disable container creation, validates only workflow correctness")
|
|
||||||
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. Only use this when using GitHub Enterprise Server.")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.gitHubServerURL, "github-server-url", "", "", "Fully qualified URL to the GitHub instance to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.gitHubAPIServerURL, "github-api-server-url", "", "", "Fully qualified URL to the GitHub instance api url to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.gitHubGraphQlAPIServerURL, "github-graph-ql-api-server-url", "", "", "Fully qualified URL to the GitHub instance graphql api to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.")
|
|
||||||
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().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().BoolVar(&input.listOptions, "list-options", false, "Print a json structure of compatible options")
|
|
||||||
rootCmd.PersistentFlags().BoolVar(&input.gitea, "gitea", false, "Use Gitea instead of GitHub")
|
|
||||||
rootCmd.SetArgs(args())
|
|
||||||
return rootCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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("Variant:", "https://gitea.com/actions-oss/act-cli / https://github.com/actions-oss/act-cli")
|
|
||||||
report += sprintf("GOOS:", runtime.GOOS)
|
|
||||||
report += sprintf("GOARCH:", runtime.GOARCH)
|
|
||||||
report += sprintf("NumCPU:", strconv.Itoa(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:", "")
|
|
||||||
var reportSb202 strings.Builder
|
|
||||||
var reportSb205 strings.Builder
|
|
||||||
for _, c := range configLocations() {
|
|
||||||
args := readArgsFile(c, false)
|
|
||||||
if len(args) > 0 {
|
|
||||||
fmt.Fprintf(&reportSb202, "\t%s:\n", c)
|
|
||||||
var reportSb206 strings.Builder
|
|
||||||
for _, l := range args {
|
|
||||||
fmt.Fprintf(&reportSb206, "\t\t%s\n", l)
|
|
||||||
}
|
|
||||||
reportSb205.WriteString(reportSb206.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
report += reportSb205.String()
|
|
||||||
report += reportSb202.String()
|
|
||||||
|
|
||||||
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:")
|
|
||||||
var reportSb223 strings.Builder
|
|
||||||
for _, set := range vcs.Settings {
|
|
||||||
reportSb223.WriteString(sprintf(fmt.Sprintf("\t\t%s:", set.Key), set.Value))
|
|
||||||
}
|
|
||||||
report += reportSb223.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := container.GetHostInfo(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stdout, 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:", strconv.Itoa(info.NCPU))
|
|
||||||
report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024))
|
|
||||||
|
|
||||||
report += fmt.Sprintln("\tSecurity options:")
|
|
||||||
var reportSb252 strings.Builder
|
|
||||||
for _, secopt := range info.SecurityOptions {
|
|
||||||
fmt.Fprintf(&reportSb252, "\t\t%s\n", secopt)
|
|
||||||
}
|
|
||||||
report += reportSb252.String()
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, report)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateManPage(cmd *cobra.Command) {
|
|
||||||
header := &doc.GenManHeader{
|
|
||||||
Title: "act",
|
|
||||||
Section: "1",
|
|
||||||
Source: "act " + cmd.Version,
|
|
||||||
}
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
cobra.CheckErr(doc.GenMan(cmd, header, buf))
|
|
||||||
fmt.Fprint(os.Stdout, buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func listOptions(cmd *cobra.Command) error {
|
|
||||||
flags := []Flag{}
|
|
||||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
|
||||||
flags = append(flags, Flag{Name: f.Name, Default: f.DefValue, Description: f.Usage, Type: f.Value.Type()})
|
|
||||||
})
|
|
||||||
a, err := json.Marshal(flags)
|
|
||||||
fmt.Fprintln(os.Stdout, string(a))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
scanner.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return readEnvsEx(path, envs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readEnvsEx(path string, envs map[string]string, caseInsensitive bool) 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 {
|
|
||||||
if caseInsensitive {
|
|
||||||
k = strings.ToUpper(k)
|
|
||||||
}
|
|
||||||
if _, ok := envs[k]; !ok {
|
|
||||||
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 {
|
|
||||||
ctx, cancel := common.EarlyCancelContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
return bugReport(ctx, cmd.Version)
|
|
||||||
}
|
|
||||||
if ok, _ := cmd.Flags().GetBool("man-page"); ok {
|
|
||||||
generateManPage(cmd)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if input.listOptions {
|
|
||||||
return listOptions(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
_ = readEnvsEx(input.Secretfile(), secrets, true)
|
|
||||||
|
|
||||||
if _, hasGitHubToken := secrets["GITHUB_TOKEN"]; !hasGitHubToken {
|
|
||||||
ctx, cancel := common.EarlyCancelContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
secrets["GITHUB_TOKEN"], _ = gh.GetToken(ctx, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Loading vars from %s", input.Varfile())
|
|
||||||
vars := newSecrets(input.vars)
|
|
||||||
_ = readEnvs(input.Varfile(), vars)
|
|
||||||
|
|
||||||
log.Debugf("Cleaning up %s old action cache format", input.actionCachePath)
|
|
||||||
entries, _ := os.ReadDir(input.actionCachePath)
|
|
||||||
for _, entry := range entries {
|
|
||||||
if strings.Contains(entry.Name(), "@") {
|
|
||||||
fullPath := filepath.Join(input.actionCachePath, entry.Name())
|
|
||||||
log.Debugf("Removing %s", fullPath)
|
|
||||||
_ = os.RemoveAll(fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixes := parseMatrix(input.matrix)
|
|
||||||
log.Debugf("Evaluated matrix inclusions: %v", matrixes)
|
|
||||||
|
|
||||||
// TODO switch to Gitea Schema when supported
|
|
||||||
plannerConfig := model.PlannerConfig{
|
|
||||||
Recursive: input.workflowRecurse,
|
|
||||||
Workflow: model.WorkflowConfig{
|
|
||||||
Strict: input.strict,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if input.gitea {
|
|
||||||
plannerConfig.Workflow.Schema = schema.GetGiteaWorkflowSchema()
|
|
||||||
}
|
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), plannerConfig)
|
|
||||||
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 validate the workflows
|
|
||||||
if input.validate {
|
|
||||||
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 {
|
|
||||||
printList(filterPlan)
|
|
||||||
return plannerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if graph {
|
|
||||||
drawGraph(filterPlan)
|
|
||||||
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 {
|
|
||||||
if len(plan.Stages) == 0 {
|
|
||||||
plannerErr = errors.New("could not find any stages to run. View the valid jobs with `act --list`. Use `act --help` to find how to filter by Job ID/Workflow/Event Name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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", "--userns="+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.pullIfNeeded,
|
|
||||||
ForceRebuild: !input.noRebuild,
|
|
||||||
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,
|
|
||||||
GitHubServerURL: input.gitHubServerURL,
|
|
||||||
GitHubAPIServerURL: input.gitHubAPIServerURL,
|
|
||||||
GitHubGraphQlAPIServerURL: input.gitHubGraphQlAPIServerURL,
|
|
||||||
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),
|
|
||||||
Parallel: input.parallel,
|
|
||||||
Planner: plannerConfig,
|
|
||||||
Action: model.ActionConfig{}, // TODO Gitea Action Schema
|
|
||||||
MainContextNames: []string{"github"},
|
|
||||||
}
|
|
||||||
if input.gitea {
|
|
||||||
config.Action.Schema = schema.GetGiteaActionSchema()
|
|
||||||
config.MainContextNames = append(config.MainContextNames, "gitea")
|
|
||||||
}
|
|
||||||
actionCache := runner.GoGitActionCache{
|
|
||||||
Path: config.ActionCacheDir,
|
|
||||||
}
|
|
||||||
config.ActionCache = &actionCache
|
|
||||||
if input.actionOfflineMode {
|
|
||||||
config.ActionCache = &runner.GoGitActionCacheOfflineMode{
|
|
||||||
Parent: actionCache,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r runner.Runner
|
|
||||||
if eventName == "workflow_call" {
|
|
||||||
// Do not use the totally broken code and instead craft a fake caller
|
|
||||||
convertedInputs := make(map[string]any)
|
|
||||||
for k, v := range inputs {
|
|
||||||
var raw any
|
|
||||||
if err := yaml.Unmarshal([]byte(v), &raw); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal input %s: %w", k, err)
|
|
||||||
}
|
|
||||||
convertedInputs[k] = raw
|
|
||||||
}
|
|
||||||
r, err = runner.NewReusableWorkflowRunner(&runner.RunContext{
|
|
||||||
Config: config,
|
|
||||||
Name: "_",
|
|
||||||
JobName: "_",
|
|
||||||
Run: &model.Run{
|
|
||||||
JobID: "_",
|
|
||||||
Workflow: &model.Workflow{
|
|
||||||
Jobs: map[string]*model.Job{
|
|
||||||
"_": {
|
|
||||||
Name: "_",
|
|
||||||
With: convertedInputs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
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(_ 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 " + configLocations()[0] + " (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/actions-oss/act-cli/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
|
|
||||||
}
|
|
||||||
|
|
||||||
earlyCancelCtx, cancel := common.EarlyCancelContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for folderWatcher.IsRunning() {
|
|
||||||
log.Debugf("Watching %s for changes", dir)
|
|
||||||
select {
|
|
||||||
case <-earlyCancelCtx.Done():
|
|
||||||
return nil
|
|
||||||
case changes := <-folderWatcher.ChangeDetails():
|
|
||||||
log.Debugf("%s", changes.String())
|
|
||||||
if err := fn(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
129
cmd/root_test.go
129
cmd/root_test.go
@@ -1,129 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReadSecrets(t *testing.T) {
|
|
||||||
secrets := map[string]string{}
|
|
||||||
ret := readEnvsEx(path.Join("testdata", "secrets.yml"), secrets, true)
|
|
||||||
assert.True(t, ret)
|
|
||||||
assert.Equal(t, `line1
|
|
||||||
line2
|
|
||||||
line3
|
|
||||||
`, secrets["MYSECRET"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadEnv(t *testing.T) {
|
|
||||||
secrets := map[string]string{}
|
|
||||||
ret := readEnvs(path.Join("testdata", "secrets.yml"), secrets)
|
|
||||||
assert.True(t, ret)
|
|
||||||
assert.Equal(t, `line1
|
|
||||||
line2
|
|
||||||
line3
|
|
||||||
`, secrets["mysecret"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListOptions(t *testing.T) {
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
listOptions: true,
|
|
||||||
})(rootCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: "../pkg/runner/testdata/",
|
|
||||||
workflowsPath: "./basic/push.yml",
|
|
||||||
})(rootCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunPush(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: "../pkg/runner/testdata/",
|
|
||||||
workflowsPath: "./basic/push.yml",
|
|
||||||
})(rootCmd, []string{"push"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunPushJsonLogger(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: "../pkg/runner/testdata/",
|
|
||||||
workflowsPath: "./basic/push.yml",
|
|
||||||
jsonLogger: true,
|
|
||||||
})(rootCmd, []string{"push"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlags(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
for _, f := range []string{"graph", "list", "bug-report", "man-page"} {
|
|
||||||
t.Run("TestFlag-"+f, func(t *testing.T) {
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := rootCmd.Flags().Set(f, "true")
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = newRunCommand(context.Background(), &Input{
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: "../pkg/runner/testdata/",
|
|
||||||
workflowsPath: "./basic/push.yml",
|
|
||||||
})(rootCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkflowCall(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: "../pkg/runner/testdata/",
|
|
||||||
workflowsPath: "./workflow_call_inputs/workflow_call_inputs.yml",
|
|
||||||
inputs: []string{"required=required input", "boolean=true"},
|
|
||||||
})(rootCmd, []string{"workflow_call"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalRepositories(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration test")
|
|
||||||
}
|
|
||||||
wd, _ := filepath.Abs("../pkg/runner/testdata/")
|
|
||||||
rootCmd := createRootCommand(context.Background(), &Input{}, "")
|
|
||||||
err := newRunCommand(context.Background(), &Input{
|
|
||||||
githubInstance: "github.com",
|
|
||||||
platforms: []string{"ubuntu-latest=node:16-buster-slim"},
|
|
||||||
workdir: wd,
|
|
||||||
workflowsPath: "./remote-action-composite-action-ref-partial-override/push.yml",
|
|
||||||
localRepository: []string{"needs/override@main=" + wd + "/actions-environment-and-context-tests"},
|
|
||||||
})(rootCmd, []string{"push"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -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.Fprintf(os.Stdout, "Provide value for '%s': ", secretPairParts[0])
|
|
||||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
fmt.Fprintln(os.Stdout)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
4
cmd/testdata/secrets.yml
vendored
4
cmd/testdata/secrets.yml
vendored
@@ -1,4 +0,0 @@
|
|||||||
mysecret: |
|
|
||||||
line1
|
|
||||||
line2
|
|
||||||
line3
|
|
||||||
12
codecov.yml
12
codecov.yml
@@ -1,12 +0,0 @@
|
|||||||
coverage:
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: auto # auto compares coverage to the previous base commit
|
|
||||||
threshold: 1%
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: 50%
|
|
||||||
ignore:
|
|
||||||
# Files generated by Google Protobuf do not require coverage
|
|
||||||
- '**/*.pb.go'
|
|
||||||
95
go.mod
95
go.mod
@@ -7,114 +7,117 @@ require (
|
|||||||
code.gitea.io/gitea-vet v0.2.3
|
code.gitea.io/gitea-vet v0.2.3
|
||||||
connectrpc.com/connect v1.19.1
|
connectrpc.com/connect v1.19.1
|
||||||
github.com/avast/retry-go/v4 v4.7.0
|
github.com/avast/retry-go/v4 v4.7.0
|
||||||
github.com/docker/docker v28.5.1+incompatible
|
github.com/docker/docker v25.0.13+incompatible
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
github.com/nektos/act v0.0.0 // will be replaced
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/term v0.40.0
|
golang.org/x/term v0.40.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require github.com/prometheus/client_golang v1.23.2
|
||||||
dario.cat/mergo v1.0.2
|
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
|
||||||
github.com/Masterminds/semver v1.5.0
|
|
||||||
github.com/adrg/xdg v0.5.3
|
|
||||||
github.com/andreaskoch/go-fswatch v1.0.0
|
|
||||||
github.com/avast/retry-go v3.0.0+incompatible
|
|
||||||
github.com/containerd/errdefs v1.0.0
|
|
||||||
github.com/creack/pty v1.1.24
|
|
||||||
github.com/distribution/reference v0.6.0
|
|
||||||
github.com/docker/cli v28.5.1+incompatible
|
|
||||||
github.com/docker/go-connections v0.6.0
|
|
||||||
github.com/go-git/go-billy/v5 v5.7.0
|
|
||||||
github.com/go-git/go-git/v5 v5.16.5
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
|
||||||
github.com/moby/go-archive v0.1.0
|
|
||||||
github.com/moby/patternmatcher v0.6.0
|
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
|
||||||
github.com/opencontainers/selinux v1.13.1
|
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
|
||||||
github.com/spf13/pflag v1.0.10
|
|
||||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
|
||||||
go.etcd.io/bbolt v1.4.3
|
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
|
||||||
golang.org/x/crypto v0.48.0
|
|
||||||
golang.org/x/sync v0.19.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cyphar.com/go-pathrs v0.2.3 // indirect
|
cyphar.com/go-pathrs v0.2.3 // indirect
|
||||||
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
|
||||||
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/containerd v1.7.29 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
github.com/creack/pty v1.1.24 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/cli v25.0.3+incompatible // indirect
|
||||||
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||||
|
github.com/go-git/go-git/v5 v5.16.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.16 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||||
|
github.com/moby/buildkit v0.13.2 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/user v0.4.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/opencontainers/selinux v1.13.1 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/rhysd/actionlint v1.7.11 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
|
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
|
go.etcd.io/bbolt v1.4.3 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
|
google.golang.org/grpc v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/actions-oss/act-cli => gitea.com/actions-oss/act-cli v0.4.2-0.20260220200604-40ee0f3ef6fc
|
replace github.com/nektos/act => gitea.com/gitea/act v0.261.10
|
||||||
|
|
||||||
// Remove after github.com/docker/distribution is updated to support distribution/reference v0.6.0
|
// Remove after github.com/docker/distribution is updated to support distribution/reference v0.6.0
|
||||||
// (pulled in via moby/buildkit, breaks on undefined: reference.SplitHostname)
|
// (pulled in via moby/buildkit, breaks on undefined: reference.SplitHostname)
|
||||||
|
|||||||
147
go.sum
147
go.sum
@@ -8,10 +8,10 @@ cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
|||||||
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
|
||||||
|
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
@@ -19,37 +19,33 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
|
|||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
|
||||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
|
||||||
github.com/andreaskoch/go-fswatch v1.0.0 h1:la8nP/HiaFCxP2IM6NZNUCoxgLWuyNFgH0RligBbnJU=
|
|
||||||
github.com/andreaskoch/go-fswatch v1.0.0/go.mod h1:r5/iV+4jfwoY2sYqBkg8vpF04ehOvEl4qPptVGdxmqo=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
|
||||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
|
||||||
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
|
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
|
||||||
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
|
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
@@ -59,10 +55,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
|
||||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
||||||
|
github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
@@ -73,6 +71,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
|
|||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
@@ -90,10 +90,10 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -102,10 +102,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
|
|||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
@@ -118,6 +118,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
@@ -129,23 +131,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI=
|
||||||
|
github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY=
|
||||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
@@ -156,6 +157,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -170,12 +173,21 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
|
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
|
||||||
|
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
|
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
|
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
@@ -196,7 +208,6 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
@@ -214,7 +225,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
|
|||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
@@ -225,10 +237,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
@@ -237,62 +249,59 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
@@ -300,12 +309,15 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||||
@@ -321,6 +333,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
433
install.sh
433
install.sh
@@ -1,433 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
# Code originally generated by godownloader on 2021-12-22T16:10:52Z. DO NOT EDIT.
|
|
||||||
# (godownloader is deprecated, so changes to this script are maintained in install.sh in https://github.com/nektos/act)
|
|
||||||
#
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
this=$1
|
|
||||||
cat <<EOF
|
|
||||||
$this: download go binaries for nektos/act
|
|
||||||
|
|
||||||
Usage: $this [-b bindir] [-d] [-f] [tag]
|
|
||||||
-b sets bindir or installation directory, Defaults to ./bin
|
|
||||||
-d turns on debug logging
|
|
||||||
-f forces installation, bypassing version checks
|
|
||||||
[tag] is a tag from
|
|
||||||
https://github.com/nektos/act/releases
|
|
||||||
If tag is missing, then the latest will be used.
|
|
||||||
EOF
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_args() {
|
|
||||||
#BINDIR is ./bin unless set be ENV
|
|
||||||
# over-ridden by flag below
|
|
||||||
|
|
||||||
BINDIR=${BINDIR:-./bin}
|
|
||||||
while getopts "b:dfh?x" arg; do
|
|
||||||
case "$arg" in
|
|
||||||
b) BINDIR="$OPTARG" ;;
|
|
||||||
d) log_set_priority 10 ;;
|
|
||||||
f) FORCE_INSTALL="true" ;;
|
|
||||||
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
|
|
||||||
}
|
|
||||||
check_installed_version() {
|
|
||||||
# Check if force install flag is set
|
|
||||||
if [ "${FORCE_INSTALL}" = "true" ]; then
|
|
||||||
log_info "force install enabled. Skipping version check."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the binary exists
|
|
||||||
if is_command "$BINARY"; then
|
|
||||||
# Extract installed version using cut
|
|
||||||
INSTALLED_VERSION=$($BINARY --version | cut -d' ' -f3)
|
|
||||||
|
|
||||||
if [ -z "$INSTALLED_VERSION" ]; then
|
|
||||||
log_err "failed to detect installed version. Proceeding with installation."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "found installed version: $INSTALLED_VERSION"
|
|
||||||
|
|
||||||
# Compare versions
|
|
||||||
if [ "$INSTALLED_VERSION" = "$VERSION" ]; then
|
|
||||||
log_info "$BINARY version $INSTALLED_VERSION is already installed."
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
log_debug "updating $BINARY from version $INSTALLED_VERSION to $VERSION..."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_debug "$BINARY is not installed. Proceeding with installation..."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
check_installed_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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
0.4.0
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/cmd"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed VERSION
|
|
||||||
var version string
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx, cancel := common.CreateGracefulJobCancellationContext()
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// run the command
|
|
||||||
cmd.Execute(ctx, version)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(_ *testing.T) {
|
|
||||||
os.Args = []string{"act", "--help"}
|
|
||||||
main()
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/pkg/artifactcache"
|
"github.com/nektos/act/pkg/artifactcache"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -22,7 +22,7 @@ type cacheServerArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(_ *cobra.Command, _ []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
cfg, err := config.LoadDefault(*configFile)
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid configuration: %w", err)
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func Execute(ctx context.Context) {
|
|||||||
Short: "Generate an example config file",
|
Short: "Generate an example config file",
|
||||||
Args: cobra.MaximumNArgs(0),
|
Args: cobra.MaximumNArgs(0),
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
fmt.Fprintf(os.Stdout, "%s", config.Example)
|
fmt.Printf("%s", config.Example)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ import (
|
|||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/metrics"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(_ *cobra.Command, _ []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
cfg, err := config.LoadDefault(*configFile)
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid configuration: %w", err)
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
@@ -144,9 +145,19 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.WithError(err).Error("fail to invoke Declare")
|
log.WithError(err).Error("fail to invoke Declare")
|
||||||
return err
|
return err
|
||||||
}
|
} else {
|
||||||
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
||||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Metrics.Enabled {
|
||||||
|
metrics.Init()
|
||||||
|
metrics.RunnerInfo.WithLabelValues(ver.Version(), resp.Msg.Runner.Name).Set(1)
|
||||||
|
metrics.RunnerCapacity.Set(float64(cfg.Runner.Capacity))
|
||||||
|
metrics.RegisterUptimeFunc(time.Now())
|
||||||
|
metrics.RegisterRunningJobsFunc(runner.RunningCount, cfg.Runner.Capacity)
|
||||||
|
metrics.StartServer(ctx, cfg.Metrics.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
poller := poll.New(cfg, cli, runner)
|
poller := poll.New(cfg, cli, runner)
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,15 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/app/run"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/artifactcache"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/artifacts"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/common"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/runner"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/nektos/act/pkg/artifactcache"
|
||||||
|
"github.com/nektos/act/pkg/artifacts"
|
||||||
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/nektos/act/pkg/runner"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
@@ -88,9 +87,9 @@ func (i *executeArgs) LoadSecrets() map[string]string {
|
|||||||
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
} else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
|
||||||
s[secretPairParts[0]] = env
|
s[secretPairParts[0]] = env
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stdout, "Provide value for '%s': ", secretPairParts[0])
|
fmt.Printf("Provide value for '%s': ", secretPairParts[0])
|
||||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
fmt.Fprintln(os.Stdout)
|
fmt.Println()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to read input: %v", err)
|
log.Errorf("failed to read input: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -241,7 +240,7 @@ func printList(plan *model.Plan) {
|
|||||||
wfNameMaxWidth += 2
|
wfNameMaxWidth += 2
|
||||||
wfFileMaxWidth += 2
|
wfFileMaxWidth += 2
|
||||||
|
|
||||||
fmt.Fprintf(os.Stdout, "%*s%*s%*s%*s%*s%*s\n",
|
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||||
-stageMaxWidth, header.stage,
|
-stageMaxWidth, header.stage,
|
||||||
-jobIDMaxWidth, header.jobID,
|
-jobIDMaxWidth, header.jobID,
|
||||||
-jobNameMaxWidth, header.jobName,
|
-jobNameMaxWidth, header.jobName,
|
||||||
@@ -250,7 +249,7 @@ func printList(plan *model.Plan) {
|
|||||||
-eventsMaxWidth, header.events,
|
-eventsMaxWidth, header.events,
|
||||||
)
|
)
|
||||||
for _, line := range lineInfos {
|
for _, line := range lineInfos {
|
||||||
fmt.Fprintf(os.Stdout, "%*s%*s%*s%*s%*s%*s\n",
|
fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
|
||||||
-stageMaxWidth, line.stage,
|
-stageMaxWidth, line.stage,
|
||||||
-jobIDMaxWidth, line.jobID,
|
-jobIDMaxWidth, line.jobID,
|
||||||
-jobNameMaxWidth, line.jobName,
|
-jobNameMaxWidth, line.jobName,
|
||||||
@@ -260,7 +259,7 @@ func printList(plan *model.Plan) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if duplicateJobIDs {
|
if duplicateJobIDs {
|
||||||
fmt.Fprint(os.Stdout, "\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,13 +311,8 @@ func runExecList(planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
|
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(_ *cobra.Command, _ []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), model.PlannerConfig{
|
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
|
||||||
Recursive: !execArgs.noWorkflowRecurse,
|
|
||||||
Workflow: model.WorkflowConfig{
|
|
||||||
Schema: schema.GetGiteaWorkflowSchema(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -367,11 +361,10 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO GITEA
|
maxLifetime := 3 * time.Hour
|
||||||
// maxLifetime := 3 * time.Hour
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
// if deadline, ok := ctx.Deadline(); ok {
|
maxLifetime = time.Until(deadline)
|
||||||
// maxLifetime = time.Until(deadline)
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// init a cache server
|
// init a cache server
|
||||||
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
|
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
|
||||||
@@ -392,7 +385,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
if len(execArgs.artifactServerPath) == 0 {
|
if len(execArgs.artifactServerPath) == 0 {
|
||||||
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
tempDir, err := os.MkdirTemp("", "gitea-act-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
@@ -428,16 +421,14 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||||
// PresetGitHubContext: preset,
|
// PresetGitHubContext: preset,
|
||||||
// EventJSON: string(eventJSON),
|
// EventJSON: string(eventJSON),
|
||||||
// TODO GITEA
|
ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
|
||||||
// ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
// ContainerMaxLifetime: maxLifetime,
|
|
||||||
ContainerNetworkMode: container.NetworkMode(execArgs.network),
|
ContainerNetworkMode: container.NetworkMode(execArgs.network),
|
||||||
// TODO GITEA
|
DefaultActionInstance: execArgs.defaultActionsURL,
|
||||||
// DefaultActionInstance: execArgs.defaultActionsURL,
|
PlatformPicker: func(_ []string) string {
|
||||||
// PlatformPicker: func(_ []string) string {
|
return execArgs.image
|
||||||
// return execArgs.image
|
},
|
||||||
// },
|
ValidVolumes: []string{"**"}, // All volumes are allowed for `exec` command
|
||||||
// ValidVolumes: []string{"**"}, // All volumes are allowed for `exec` command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Env["ACT_EXEC"] = "true"
|
config.Env["ACT_EXEC"] = "true"
|
||||||
@@ -448,8 +439,10 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
config.Token = t
|
config.Token = t
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO GITEA
|
if !execArgs.debug {
|
||||||
ctx = runner.WithJobLoggerFactory(ctx, &run.JobLoggerFactoryWithInfoLevel{})
|
logLevel := log.InfoLevel
|
||||||
|
config.JobLoggerLevel = &logLevel
|
||||||
|
}
|
||||||
|
|
||||||
r, err := runner.New(config)
|
r, err := runner.New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -460,7 +453,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||||
|
|
||||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||||
executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
|
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||||
artifactCancel()
|
artifactCancel()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
// runRegister registers a runner to the server
|
// runRegister registers a runner to the server
|
||||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||||
return func(_ *cobra.Command, _ []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
log.SetReportCaller(false)
|
log.SetReportCaller(false)
|
||||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
log.SetFormatter(&log.TextFormatter{
|
log.SetFormatter(&log.TextFormatter{
|
||||||
@@ -251,7 +251,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
|||||||
if stage == StageWaitingForRegistration {
|
if stage == StageWaitingForRegistration {
|
||||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||||
return fmt.Errorf("failed to register runner: %w", err)
|
return fmt.Errorf("Failed to register runner: %w", err)
|
||||||
}
|
}
|
||||||
log.Infof("Runner registered successfully.")
|
log.Infof("Runner registered successfully.")
|
||||||
return nil
|
return nil
|
||||||
@@ -312,7 +312,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||||
return fmt.Errorf("failed to register runner: %w", err)
|
return fmt.Errorf("Failed to register runner: %w", err)
|
||||||
}
|
}
|
||||||
log.Infof("Runner registered successfully.")
|
log.Infof("Runner registered successfully.")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
||||||
@@ -15,5 +15,5 @@ func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
|||||||
Token: "token",
|
Token: "token",
|
||||||
InstanceAddr: "http://localhost:3000",
|
InstanceAddr: "http://localhost:3000",
|
||||||
})
|
})
|
||||||
require.Error(t, err, "unsupported schema: invalid")
|
assert.Error(t, err, "unsupported schema: invalid")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/time/rate"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/app/run"
|
"gitea.com/gitea/act_runner/internal/app/run"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Poller struct {
|
type Poller struct {
|
||||||
@@ -35,6 +37,19 @@ type Poller struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// workerState holds per-goroutine polling state. Backoff counters are
|
||||||
|
// per-worker so that with Capacity > 1, N workers each seeing one empty
|
||||||
|
// response don't combine into a "consecutive N empty" reading on a shared
|
||||||
|
// counter and trigger an unnecessarily long backoff.
|
||||||
|
type workerState struct {
|
||||||
|
consecutiveEmpty int64
|
||||||
|
consecutiveErrors int64
|
||||||
|
// lastBackoff is the last interval reported to the PollBackoffSeconds gauge
|
||||||
|
// from this worker; used to suppress redundant no-op Set calls when the
|
||||||
|
// backoff plateaus (e.g. at FetchIntervalMax).
|
||||||
|
lastBackoff time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||||
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
|
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
|
||||||
|
|
||||||
@@ -58,11 +73,10 @@ func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller) Poll() {
|
func (p *Poller) Poll() {
|
||||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
for i := 0; i < p.cfg.Runner.Capacity; i++ {
|
for i := 0; i < p.cfg.Runner.Capacity; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go p.poll(wg, limiter)
|
go p.poll(wg)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
@@ -71,9 +85,7 @@ func (p *Poller) Poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller) PollOnce() {
|
func (p *Poller) PollOnce() {
|
||||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
p.pollOnce(&workerState{})
|
||||||
|
|
||||||
p.pollOnce(limiter)
|
|
||||||
|
|
||||||
// signal that we're done
|
// signal that we're done
|
||||||
close(p.done)
|
close(p.done)
|
||||||
@@ -108,10 +120,11 @@ func (p *Poller) Shutdown(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
func (p *Poller) poll(wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
s := &workerState{}
|
||||||
for {
|
for {
|
||||||
p.pollOnce(limiter)
|
p.pollOnce(s)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-p.pollingCtx.Done():
|
case <-p.pollingCtx.Done():
|
||||||
@@ -122,19 +135,61 @@ func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller) pollOnce(limiter *rate.Limiter) {
|
// calculateInterval returns the polling interval with exponential backoff based on
|
||||||
for {
|
// consecutive empty or error responses. The interval starts at FetchInterval and
|
||||||
if err := limiter.Wait(p.pollingCtx); err != nil {
|
// doubles with each consecutive empty/error, capped at FetchIntervalMax.
|
||||||
if p.pollingCtx.Err() != nil {
|
func (p *Poller) calculateInterval(s *workerState) time.Duration {
|
||||||
log.WithError(err).Debug("limiter wait failed")
|
base := p.cfg.Runner.FetchInterval
|
||||||
|
maxInterval := p.cfg.Runner.FetchIntervalMax
|
||||||
|
|
||||||
|
n := max(s.consecutiveEmpty, s.consecutiveErrors)
|
||||||
|
if n <= 1 {
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capped exponential backoff: base * 2^(n-1), max shift=5 so multiplier <= 32
|
||||||
|
shift := min(n-1, 5)
|
||||||
|
interval := base * time.Duration(int64(1)<<shift)
|
||||||
|
return min(interval, maxInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addJitter adds +/- 20% random jitter to the given duration to avoid thundering herd.
|
||||||
|
func addJitter(d time.Duration) time.Duration {
|
||||||
|
if d <= 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
// jitter range: [-20%, +20%] of d
|
||||||
|
jitterRange := int64(d) * 2 / 5 // 40% total range
|
||||||
|
if jitterRange <= 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
jitter := rand.Int64N(jitterRange) - jitterRange/2
|
||||||
|
return d + time.Duration(jitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) pollOnce(s *workerState) {
|
||||||
|
for {
|
||||||
|
task, ok := p.fetchTask(p.pollingCtx, s)
|
||||||
|
if !ok {
|
||||||
|
base := p.calculateInterval(s)
|
||||||
|
if base != s.lastBackoff {
|
||||||
|
metrics.PollBackoffSeconds.Set(base.Seconds())
|
||||||
|
s.lastBackoff = base
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(addJitter(base))
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
case <-p.pollingCtx.Done():
|
||||||
|
timer.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
task, ok := p.fetchTask(p.pollingCtx)
|
|
||||||
if !ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Got a task — reset backoff counters for fast subsequent polling.
|
||||||
|
s.consecutiveEmpty = 0
|
||||||
|
s.consecutiveErrors = 0
|
||||||
|
|
||||||
p.runTaskWithRecover(p.jobsCtx, task)
|
p.runTaskWithRecover(p.jobsCtx, task)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -153,24 +208,42 @@ func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task, bool) {
|
||||||
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Load the version value that was in the cache when the request was sent.
|
// Load the version value that was in the cache when the request was sent.
|
||||||
v := p.tasksVersion.Load()
|
v := p.tasksVersion.Load()
|
||||||
|
start := time.Now()
|
||||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
|
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||||
TasksVersion: v,
|
TasksVersion: v,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// DeadlineExceeded is the designed idle path for a long-poll: the server
|
||||||
|
// found no work within FetchTimeout. Treat it as an empty response and do
|
||||||
|
// not record the duration — the timeout value would swamp the histogram.
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
err = nil
|
s.consecutiveEmpty++
|
||||||
|
s.consecutiveErrors = 0 // timeout is a healthy idle response
|
||||||
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
metrics.PollFetchDuration.Observe(time.Since(start).Seconds())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("failed to fetch task")
|
log.WithError(err).Error("failed to fetch task")
|
||||||
|
s.consecutiveErrors++
|
||||||
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
||||||
|
metrics.ClientErrors.WithLabelValues(metrics.LabelMethodFetchTask).Inc()
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Successful response — reset error counter.
|
||||||
|
s.consecutiveErrors = 0
|
||||||
|
|
||||||
if resp == nil || resp.Msg == nil {
|
if resp == nil || resp.Msg == nil {
|
||||||
|
s.consecutiveEmpty++
|
||||||
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,11 +252,14 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.Msg.Task == nil {
|
if resp.Msg.Task == nil {
|
||||||
|
s.consecutiveEmpty++
|
||||||
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// got a task, set `tasksVersion` to zero to focre query db in next request.
|
// got a task, set `tasksVersion` to zero to force query db in next request.
|
||||||
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
|
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
|
||||||
|
|
||||||
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultTask).Inc()
|
||||||
return resp.Msg.Task, true
|
return resp.Msg.Task, true
|
||||||
}
|
}
|
||||||
|
|||||||
108
internal/app/poll/poller_test.go
Normal file
108
internal/app/poll/poller_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package poll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
connect_go "connectrpc.com/connect"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPoller_PerWorkerCounters verifies that each worker maintains its own
|
||||||
|
// backoff counters. With a shared counter, N workers each seeing one empty
|
||||||
|
// response would inflate the counter to N and trigger an unnecessarily long
|
||||||
|
// backoff. With per-worker state, each worker only sees its own count.
|
||||||
|
func TestPoller_PerWorkerCounters(t *testing.T) {
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("FetchTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
|
||||||
|
// Always return an empty response.
|
||||||
|
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg, err := config.LoadDefault("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
p := &Poller{client: client, cfg: cfg}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
s1 := &workerState{}
|
||||||
|
s2 := &workerState{}
|
||||||
|
|
||||||
|
// Each worker independently observes one empty response.
|
||||||
|
_, ok := p.fetchTask(ctx, s1)
|
||||||
|
require.False(t, ok)
|
||||||
|
_, ok = p.fetchTask(ctx, s2)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1), s1.consecutiveEmpty, "worker 1 should only count its own empty response")
|
||||||
|
assert.Equal(t, int64(1), s2.consecutiveEmpty, "worker 2 should only count its own empty response")
|
||||||
|
|
||||||
|
// Worker 1 sees a second empty; worker 2 stays at 1.
|
||||||
|
_, ok = p.fetchTask(ctx, s1)
|
||||||
|
require.False(t, ok)
|
||||||
|
assert.Equal(t, int64(2), s1.consecutiveEmpty)
|
||||||
|
assert.Equal(t, int64(1), s2.consecutiveEmpty, "worker 2's counter must not be affected by worker 1's empty fetches")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPoller_FetchErrorIncrementsErrorsOnly verifies that a fetch error
|
||||||
|
// increments only the per-worker error counter, not the empty counter.
|
||||||
|
func TestPoller_FetchErrorIncrementsErrorsOnly(t *testing.T) {
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("FetchTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
|
||||||
|
return nil, errors.New("network unreachable")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg, err := config.LoadDefault("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
p := &Poller{client: client, cfg: cfg}
|
||||||
|
|
||||||
|
s := &workerState{}
|
||||||
|
_, ok := p.fetchTask(context.Background(), s)
|
||||||
|
require.False(t, ok)
|
||||||
|
assert.Equal(t, int64(1), s.consecutiveErrors)
|
||||||
|
assert.Equal(t, int64(0), s.consecutiveEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPoller_CalculateInterval verifies the per-worker exponential backoff
|
||||||
|
// math is correctly driven by the worker's own counters.
|
||||||
|
func TestPoller_CalculateInterval(t *testing.T) {
|
||||||
|
cfg, err := config.LoadDefault("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg.Runner.FetchInterval = 2 * time.Second
|
||||||
|
cfg.Runner.FetchIntervalMax = 60 * time.Second
|
||||||
|
p := &Poller{cfg: cfg}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
empty, errs int64
|
||||||
|
wantInterval time.Duration
|
||||||
|
}{
|
||||||
|
{"first poll, no backoff", 0, 0, 2 * time.Second},
|
||||||
|
{"single empty, still base", 1, 0, 2 * time.Second},
|
||||||
|
{"two empties, doubled", 2, 0, 4 * time.Second},
|
||||||
|
{"five empties, capped path", 5, 0, 32 * time.Second},
|
||||||
|
{"many empties, capped at max", 20, 0, 60 * time.Second},
|
||||||
|
{"errors drive backoff too", 0, 3, 8 * time.Second},
|
||||||
|
{"max(empty, errors) wins", 2, 4, 16 * time.Second},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s := &workerState{consecutiveEmpty: tc.empty, consecutiveErrors: tc.errs}
|
||||||
|
assert.Equal(t, tc.wantInterval, p.calculateInterval(s))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,34 +5,20 @@ package run
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobLoggerFactoryWithInfoLevel struct{}
|
// NullLogger is used to create a new JobLogger to discard logs. This
|
||||||
|
// will prevent these logs from being logged to the stdout, but
|
||||||
|
// forward them to the Reporter via its hook.
|
||||||
|
type NullLogger struct{}
|
||||||
|
|
||||||
// WithJobLogger implements [runner.JobLoggerFactory].
|
// WithJobLogger creates a new logrus.Logger that will discard all logs.
|
||||||
func (j *JobLoggerFactoryWithInfoLevel) WithJobLogger() *log.Logger {
|
func (n NullLogger) WithJobLogger() *log.Logger {
|
||||||
jobLogger := log.New()
|
logger := log.New()
|
||||||
jobLogger.SetLevel(log.InfoLevel)
|
logger.SetOutput(io.Discard)
|
||||||
return jobLogger
|
logger.SetLevel(log.TraceLevel)
|
||||||
}
|
|
||||||
|
|
||||||
type JobLoggerWithReporter struct {
|
return logger
|
||||||
Reporter *report.Reporter
|
|
||||||
LogToTerminal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithJobLogger implements [runner.JobLoggerFactory].
|
|
||||||
func (j *JobLoggerWithReporter) WithJobLogger() *log.Logger {
|
|
||||||
jobLogger := log.New()
|
|
||||||
if j.LogToTerminal {
|
|
||||||
jobLogger.SetOutput(os.Stdout)
|
|
||||||
} else {
|
|
||||||
jobLogger.SetOutput(io.Discard)
|
|
||||||
}
|
|
||||||
jobLogger.AddHook(j.Reporter)
|
|
||||||
return jobLogger
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,26 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"gitea.com/gitea/act_runner/pkg/artifactcache"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/runner"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/nektos/act/pkg/artifactcache"
|
||||||
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/nektos/act/pkg/runner"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/metrics"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
)
|
)
|
||||||
@@ -40,6 +43,7 @@ type Runner struct {
|
|||||||
envs map[string]string
|
envs map[string]string
|
||||||
|
|
||||||
runningTasks sync.Map
|
runningTasks sync.Map
|
||||||
|
runningCount atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||||
@@ -95,16 +99,25 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
|||||||
r.runningTasks.Store(task.Id, struct{}{})
|
r.runningTasks.Store(task.Id, struct{}{})
|
||||||
defer r.runningTasks.Delete(task.Id)
|
defer r.runningTasks.Delete(task.Id)
|
||||||
|
|
||||||
|
r.runningCount.Add(1)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
reporter := report.NewReporter(ctx, cancel, r.client, task)
|
reporter := report.NewReporter(ctx, cancel, r.client, task, r.cfg)
|
||||||
var runErr error
|
var runErr error
|
||||||
defer func() {
|
defer func() {
|
||||||
|
r.runningCount.Add(-1)
|
||||||
|
|
||||||
lastWords := ""
|
lastWords := ""
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
lastWords = runErr.Error()
|
lastWords = runErr.Error()
|
||||||
}
|
}
|
||||||
_ = reporter.Close(lastWords)
|
_ = reporter.Close(lastWords)
|
||||||
|
|
||||||
|
metrics.JobDuration.Observe(time.Since(start).Seconds())
|
||||||
|
metrics.JobsTotal.WithLabelValues(metrics.ResultToStatusLabel(reporter.Result())).Inc()
|
||||||
}()
|
}()
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
runErr = r.run(ctx, task, reporter)
|
runErr = r.run(ctx, task, reporter)
|
||||||
@@ -137,20 +150,12 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO GITEA
|
|
||||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
job := workflow.GetJob(jobID)
|
job := workflow.GetJob(jobID)
|
||||||
var stepIDs []string
|
reporter.ResetSteps(len(job.Steps))
|
||||||
for i, v := range job.Steps {
|
|
||||||
if v.ID == "" {
|
|
||||||
v.ID = strconv.Itoa(i)
|
|
||||||
}
|
|
||||||
stepIDs = append(stepIDs, v.ID)
|
|
||||||
}
|
|
||||||
reporter.SetStepIdMapping(stepIDs...)
|
|
||||||
|
|
||||||
taskContext := task.Context.Fields
|
taskContext := task.Context.Fields
|
||||||
|
|
||||||
@@ -162,6 +167,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
Event: taskContext["event"].GetStructValue().AsMap(),
|
Event: taskContext["event"].GetStructValue().AsMap(),
|
||||||
RunID: taskContext["run_id"].GetStringValue(),
|
RunID: taskContext["run_id"].GetStringValue(),
|
||||||
RunNumber: taskContext["run_number"].GetStringValue(),
|
RunNumber: taskContext["run_number"].GetStringValue(),
|
||||||
|
RunAttempt: taskContext["run_attempt"].GetStringValue(),
|
||||||
Actor: taskContext["actor"].GetStringValue(),
|
Actor: taskContext["actor"].GetStringValue(),
|
||||||
Repository: taskContext["repository"].GetStringValue(),
|
Repository: taskContext["repository"].GetStringValue(),
|
||||||
EventName: taskContext["event_name"].GetStringValue(),
|
EventName: taskContext["event_name"].GetStringValue(),
|
||||||
@@ -194,27 +200,28 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
}
|
}
|
||||||
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
||||||
|
|
||||||
// TODO GITEA
|
|
||||||
eventJSON, err := json.Marshal(preset.Event)
|
eventJSON, err := json.Marshal(preset.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxLifetime := 3 * time.Hour
|
maxLifetime := 3 * time.Hour
|
||||||
// if deadline, ok := ctx.Deadline(); ok {
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
// maxLifetime = time.Until(deadline)
|
maxLifetime = time.Until(deadline)
|
||||||
// }
|
}
|
||||||
|
|
||||||
actCtx := map[string]any{}
|
workdirParent := strings.TrimLeft(r.cfg.Container.WorkdirParent, "/")
|
||||||
forgeCtx := task.Context.AsMap()
|
if r.cfg.Container.BindWorkdir {
|
||||||
actCtx["github"] = forgeCtx
|
// Append the task ID to isolate concurrent jobs from the same repo.
|
||||||
actCtx["gitea"] = forgeCtx
|
workdirParent = fmt.Sprintf("%s/%d", workdirParent, task.Id)
|
||||||
|
}
|
||||||
|
workdir := filepath.FromSlash(fmt.Sprintf("/%s/%s", workdirParent, preset.Repository))
|
||||||
|
|
||||||
runnerConfig := &runner.Config{
|
runnerConfig := &runner.Config{
|
||||||
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||||
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||||
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
|
Workdir: workdir,
|
||||||
BindWorkdir: false,
|
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
@@ -224,40 +231,22 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
JSONLogger: false,
|
JSONLogger: false,
|
||||||
Env: r.envs,
|
Env: r.envs,
|
||||||
Secrets: task.Secrets,
|
Secrets: task.Secrets,
|
||||||
// GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
NoSkipCheckout: true,
|
NoSkipCheckout: true,
|
||||||
// TODO GITEA
|
PresetGitHubContext: preset,
|
||||||
// PresetGitHubContext: preset,
|
|
||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
// ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
// ContainerMaxLifetime: maxLifetime,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||||
ContainerOptions: r.cfg.Container.Options,
|
ContainerOptions: r.cfg.Container.Options,
|
||||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||||
Privileged: r.cfg.Container.Privileged,
|
Privileged: r.cfg.Container.Privileged,
|
||||||
|
DefaultActionInstance: r.getDefaultActionsURL(task),
|
||||||
Platforms: map[string]string{
|
PlatformPicker: r.labels.PickPlatform,
|
||||||
"dummy": "-self-hosted",
|
|
||||||
},
|
|
||||||
// TODO GITEA
|
|
||||||
// DefaultActionInstance: r.getDefaultActionsURL(task),
|
|
||||||
// PlatformPicker: r.labels.PickPlatform,
|
|
||||||
Vars: task.Vars,
|
Vars: task.Vars,
|
||||||
// TODO GITEA
|
ValidVolumes: r.cfg.Container.ValidVolumes,
|
||||||
// ValidVolumes: r.cfg.Container.ValidVolumes,
|
InsecureSkipTLS: r.cfg.Runner.Insecure,
|
||||||
// InsecureSkipTLS: r.cfg.Runner.Insecure,
|
|
||||||
|
|
||||||
GitHubServerURL: strings.TrimSuffix(r.client.Address(), "/"),
|
|
||||||
GitHubAPIServerURL: strings.TrimSuffix(r.client.Address(), "/") + "/api/v1",
|
|
||||||
// Invalid but ok
|
|
||||||
GitHubGraphQlAPIServerURL: strings.TrimSuffix(r.client.Address(), "/api/graphql"),
|
|
||||||
MainContextNames: []string{"gitea", "github"},
|
|
||||||
|
|
||||||
Action: model.ActionConfig{
|
|
||||||
Schema: schema.GetGiteaActionSchema(),
|
|
||||||
},
|
|
||||||
ContextData: actCtx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rr, err := runner.New(runnerConfig)
|
rr, err := runner.New(runnerConfig)
|
||||||
@@ -268,14 +257,31 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
|
|
||||||
reporter.Logf("workflow prepared")
|
reporter.Logf("workflow prepared")
|
||||||
|
|
||||||
// TODO GITEA
|
// add logger recorders
|
||||||
ctx = runner.WithJobLoggerFactory(ctx, &JobLoggerWithReporter{Reporter: reporter, LogToTerminal: log.IsLevelEnabled(log.DebugLevel)})
|
ctx = common.WithLoggerHook(ctx, reporter)
|
||||||
|
|
||||||
|
if !log.IsLevelEnabled(log.DebugLevel) {
|
||||||
|
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
||||||
|
}
|
||||||
|
|
||||||
execErr := executor(ctx)
|
execErr := executor(ctx)
|
||||||
reporter.SetOutputs(job.Outputs)
|
reporter.SetOutputs(job.Outputs)
|
||||||
|
|
||||||
|
if r.cfg.Container.BindWorkdir {
|
||||||
|
// Remove the entire task-specific directory (e.g. /workspace/<task_id>).
|
||||||
|
taskDir := filepath.FromSlash("/" + workdirParent)
|
||||||
|
if err := os.RemoveAll(taskDir); err != nil {
|
||||||
|
log.Warnf("failed to clean up workspace %s: %v", taskDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return execErr
|
return execErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Runner) RunningCount() int64 {
|
||||||
|
return r.runningCount.Load()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||||
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
|
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
|
||||||
Version: ver.Version(),
|
Version: ver.Version(),
|
||||||
|
|||||||
@@ -10,24 +10,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
"go.yaml.in/yaml/v4"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
|
func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
|
||||||
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload), model.WorkflowConfig{
|
workflow, err := model.ReadWorkflow(bytes.NewReader(task.WorkflowPayload))
|
||||||
// Schema: schema.GetGiteaWorkflowSchema(),
|
|
||||||
// Allow everything
|
|
||||||
Schema: &schema.Schema{
|
|
||||||
Definitions: map[string]schema.Definition{
|
|
||||||
"workflow-root": {
|
|
||||||
Context: []string{"github", "gitea", "env", "job", "matrix", "strategy", "inputs", "vars", "runner", "steps", "needs"},
|
|
||||||
OneOf: &[]string{"any"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -60,9 +48,7 @@ func generateWorkflow(task *runnerv1.Task) (*model.Workflow, string, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO GITEA
|
|
||||||
workflow.Jobs[jobID].RawNeeds = rawNeeds
|
workflow.Jobs[jobID].RawNeeds = rawNeeds
|
||||||
_ = workflow.Jobs[jobID].RawRunsOn.Encode("dummy")
|
|
||||||
|
|
||||||
return workflow, jobID, nil
|
return workflow, jobID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"gitea.com/gitea/act_runner/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_generateWorkflow(t *testing.T) {
|
func Test_generateWorkflow(t *testing.T) {
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
assert.Equal(t, []string{"job1", "job2"}, wf.GetJob("job9").Needs())
|
assert.DeepEqual(t, wf.GetJob("job9").Needs(), []string{"job1", "job2"})
|
||||||
},
|
},
|
||||||
want1: "job9",
|
want1: "job9",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -83,8 +83,8 @@ jobs:
|
|||||||
},
|
},
|
||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
job := wf.GetJob("test")
|
job := wf.GetJob("test")
|
||||||
assert.Equal(t, []string{}, job.Needs())
|
assert.DeepEqual(t, job.Needs(), []string{})
|
||||||
assert.Len(t, job.Steps, 2)
|
assert.Equal(t, len(job.Steps), 2)
|
||||||
},
|
},
|
||||||
want1: "test",
|
want1: "test",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -125,9 +125,9 @@ jobs:
|
|||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
job := wf.GetJob("deploy")
|
job := wf.GetJob("deploy")
|
||||||
needs := job.Needs()
|
needs := job.Needs()
|
||||||
assert.Equal(t, []string{"build", "lint", "test"}, needs)
|
assert.DeepEqual(t, needs, []string{"build", "lint", "test"})
|
||||||
assert.Equal(t, "80%", wf.Jobs["test"].Outputs["coverage"])
|
assert.Equal(t, wf.Jobs["test"].Outputs["coverage"], "80%")
|
||||||
assert.Equal(t, "failure", wf.Jobs["lint"].Result)
|
assert.Equal(t, wf.Jobs["lint"].Result, "failure")
|
||||||
},
|
},
|
||||||
want1: "deploy",
|
want1: "deploy",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -165,11 +165,11 @@ jobs:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
assert.Equal(t, "Complex workflow", wf.Name)
|
assert.Equal(t, wf.Name, "Complex workflow")
|
||||||
assert.Equal(t, "production", wf.Env["NODE_ENV"])
|
assert.Equal(t, wf.Env["NODE_ENV"], "production")
|
||||||
assert.Equal(t, "true", wf.Env["CI"])
|
assert.Equal(t, wf.Env["CI"], "true")
|
||||||
job := wf.GetJob("build")
|
job := wf.GetJob("build")
|
||||||
assert.Len(t, job.Steps, 4)
|
assert.Equal(t, len(job.Steps), 4)
|
||||||
},
|
},
|
||||||
want1: "build",
|
want1: "build",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -200,8 +200,8 @@ jobs:
|
|||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
job := wf.GetJob("integration")
|
job := wf.GetJob("integration")
|
||||||
container := job.Container()
|
container := job.Container()
|
||||||
assert.Equal(t, "node:18", container.Image)
|
assert.Equal(t, container.Image, "node:18")
|
||||||
assert.Equal(t, "postgres:15", job.Services["postgres"].Image)
|
assert.Equal(t, job.Services["postgres"].Image, "postgres:15")
|
||||||
},
|
},
|
||||||
want1: "integration",
|
want1: "integration",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
job := wf.GetJob("test")
|
job := wf.GetJob("test")
|
||||||
matrixes, err := job.GetMatrixes()
|
matrixes, err := job.GetMatrixes()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, matrixes, 2)
|
assert.Equal(t, len(matrixes), 2)
|
||||||
},
|
},
|
||||||
want1: "test",
|
want1: "test",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -245,9 +245,9 @@ jobs:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
assert: func(t *testing.T, wf *model.Workflow) {
|
assert: func(t *testing.T, wf *model.Workflow) {
|
||||||
assert.Equal(t, "Special: characters & test", wf.Name)
|
assert.Equal(t, wf.Name, "Special: characters & test")
|
||||||
job := wf.GetJob("test")
|
job := wf.GetJob("test")
|
||||||
assert.Len(t, job.Steps, 3)
|
assert.Equal(t, len(job.Steps), 3)
|
||||||
},
|
},
|
||||||
want1: "test",
|
want1: "test",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -283,7 +283,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tt.assert(t, got)
|
tt.assert(t, got)
|
||||||
assert.Equal(t, tt.want1, got1)
|
assert.Equal(t, got1, tt.want1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
out, err := yaml.Marshal(&node)
|
out, err := yaml.Marshal(&node)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "- a\n- b\n- c\n", string(out))
|
assert.Equal(t, string(out), "- a\n- b\n- c\n")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarshal and re-marshal workflow", func(t *testing.T) {
|
t.Run("unmarshal and re-marshal workflow", func(t *testing.T) {
|
||||||
@@ -310,7 +310,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
|
|||||||
var wf map[string]any
|
var wf map[string]any
|
||||||
err := yaml.Unmarshal(input, &wf)
|
err := yaml.Unmarshal(input, &wf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "test", wf["name"])
|
assert.Equal(t, wf["name"], "test")
|
||||||
|
|
||||||
out, err := yaml.Marshal(wf)
|
out, err := yaml.Marshal(wf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -318,7 +318,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
|
|||||||
var wf2 map[string]any
|
var wf2 map[string]any
|
||||||
err = yaml.Unmarshal(out, &wf2)
|
err = yaml.Unmarshal(out, &wf2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "test", wf2["name"])
|
assert.Equal(t, wf2["name"], "test")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("node kind constants", func(t *testing.T) {
|
t.Run("node kind constants", func(t *testing.T) {
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
package functions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Format evaluates a format string with the supplied arguments.
|
|
||||||
// It behaves like the C# implementation in the repository –
|
|
||||||
// it supports escaped braces and numeric argument indices.
|
|
||||||
// Format specifiers (e.g. :D) are recognised but currently ignored.
|
|
||||||
func Format(formatStr string, args ...any) (string, error) {
|
|
||||||
var sb strings.Builder
|
|
||||||
i := 0
|
|
||||||
for i < len(formatStr) {
|
|
||||||
lbrace := strings.IndexByte(formatStr[i:], '{')
|
|
||||||
rbrace := strings.IndexByte(formatStr[i:], '}')
|
|
||||||
|
|
||||||
// left brace
|
|
||||||
if lbrace >= 0 && (rbrace < 0 || rbrace > lbrace) {
|
|
||||||
l := i + lbrace
|
|
||||||
|
|
||||||
sb.WriteString(formatStr[i:l])
|
|
||||||
|
|
||||||
// escaped left brace
|
|
||||||
if l+1 < len(formatStr) && formatStr[l+1] == '{' {
|
|
||||||
sb.WriteString(formatStr[l : l+1])
|
|
||||||
i = l + 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// normal placeholder
|
|
||||||
if rbrace > lbrace+1 {
|
|
||||||
// read index
|
|
||||||
idx, endIdx, ok := readArgIndex(formatStr, l+1)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid format string: %s", formatStr)
|
|
||||||
}
|
|
||||||
// read optional format specifier
|
|
||||||
spec, r, ok := readFormatSpecifiers(formatStr, endIdx+1)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid format string: %s", formatStr)
|
|
||||||
}
|
|
||||||
if idx >= len(args) {
|
|
||||||
return "", fmt.Errorf("argument index %d out of range", idx)
|
|
||||||
}
|
|
||||||
// append argument (format specifier is ignored here)
|
|
||||||
arg := args[idx]
|
|
||||||
fmt.Fprintf(&sb, "%v", arg)
|
|
||||||
if spec != "" {
|
|
||||||
// placeholder for future specifier handling
|
|
||||||
_ = spec
|
|
||||||
}
|
|
||||||
i = r + 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid format string: %s", formatStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// right brace
|
|
||||||
if rbrace >= 0 {
|
|
||||||
// escaped right brace
|
|
||||||
if i+rbrace+1 < len(formatStr) && formatStr[i+rbrace+1] == '}' {
|
|
||||||
sb.WriteString(formatStr[i : i+rbrace+1])
|
|
||||||
i += rbrace + 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid format string: %s", formatStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rest of string
|
|
||||||
sb.WriteString(formatStr[i:])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return sb.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readArgIndex parses a decimal number starting at pos.
|
|
||||||
// It returns the parsed value, the index of the last digit and true on success.
|
|
||||||
func readArgIndex(s string, pos int) (int, int, bool) {
|
|
||||||
start := pos
|
|
||||||
for pos < len(s) && s[pos] >= '0' && s[pos] <= '9' {
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
if start == pos {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
idx, err := strconv.Atoi(s[start:pos])
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
return idx, pos - 1, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// readFormatSpecifiers reads an optional format specifier block.
|
|
||||||
// It returns the specifier string, the index of the closing '}' and true on success.
|
|
||||||
func readFormatSpecifiers(s string, pos int) (string, int, bool) {
|
|
||||||
if pos >= len(s) {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
if s[pos] == '}' {
|
|
||||||
return "", pos, true
|
|
||||||
}
|
|
||||||
if s[pos] != ':' {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
pos++ // skip ':'
|
|
||||||
start := pos
|
|
||||||
for pos < len(s) {
|
|
||||||
if s[pos] == '}' {
|
|
||||||
return s[start:pos], pos, true
|
|
||||||
}
|
|
||||||
if s[pos] == '}' && pos+1 < len(s) && s[pos+1] == '}' {
|
|
||||||
// escaped '}'
|
|
||||||
pos += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package functions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFormat(t *testing.T) {
|
|
||||||
_, err := Format("Hello {0}, you have {1} new messages", "Alice", 5)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// fmt.Println(s) // Hello Alice, you have 5 new messages
|
|
||||||
}
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValueKind represents the type of a value in the evaluation engine.
|
|
||||||
// The values mirror the C# ValueKind enum.
|
|
||||||
//
|
|
||||||
// Note: The names are kept identical to the C# implementation for easier mapping.
|
|
||||||
//
|
|
||||||
// The lexer is intentionally simple – it only tokenises the subset of
|
|
||||||
// expressions that are used in GitHub Actions workflow `if:` expressions.
|
|
||||||
// It does not evaluate the expression – that is left to the parser.
|
|
||||||
|
|
||||||
type ValueKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ValueKindNull ValueKind = iota
|
|
||||||
ValueKindBoolean
|
|
||||||
ValueKindNumber
|
|
||||||
ValueKindString
|
|
||||||
ValueKindObject
|
|
||||||
ValueKindArray
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReadOnlyArray[T any] interface {
|
|
||||||
GetAt(i int64) T
|
|
||||||
GetEnumerator() []T
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReadOnlyObject[T any] interface {
|
|
||||||
Get(key string) T
|
|
||||||
GetKv(key string) (string, T) // Returns the actual key used (for case-insensitive objects)
|
|
||||||
GetEnumerator() map[string]T
|
|
||||||
}
|
|
||||||
|
|
||||||
type BasicArray[T any] []T
|
|
||||||
|
|
||||||
func (a BasicArray[T]) GetAt(i int64) T {
|
|
||||||
if int(i) >= len(a) {
|
|
||||||
var zero T
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
return a[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a BasicArray[T]) GetEnumerator() []T {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
type CaseInsensitiveObject[T any] map[string]T
|
|
||||||
|
|
||||||
func (o CaseInsensitiveObject[T]) Get(key string) T {
|
|
||||||
_, v := o.GetKv(key)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o CaseInsensitiveObject[T]) GetKv(key string) (k string, v T) {
|
|
||||||
for k, v := range o {
|
|
||||||
if strings.EqualFold(k, key) {
|
|
||||||
return k, v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var zero T
|
|
||||||
return key, zero
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o CaseInsensitiveObject[T]) GetEnumerator() map[string]T {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
type CaseSensitiveObject[T any] map[string]T
|
|
||||||
|
|
||||||
func (o CaseSensitiveObject[T]) Get(key string) T {
|
|
||||||
return o[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o CaseSensitiveObject[T]) GetKv(key string) (string, T) {
|
|
||||||
return key, o[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o CaseSensitiveObject[T]) GetEnumerator() map[string]T {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// EvaluationResult holds the result of evaluating an expression node.
|
|
||||||
// It mirrors the C# EvaluationResult class.
|
|
||||||
|
|
||||||
type EvaluationResult struct {
|
|
||||||
context *EvaluationContext
|
|
||||||
level int
|
|
||||||
value any
|
|
||||||
kind ValueKind
|
|
||||||
raw any
|
|
||||||
omitTracing bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvaluationResult creates a new EvaluationResult.
|
|
||||||
func NewEvaluationResult(context *EvaluationContext, level int, val any, kind ValueKind, raw any, omitTracing bool) *EvaluationResult {
|
|
||||||
er := &EvaluationResult{context: context, level: level, value: val, kind: kind, raw: raw, omitTracing: omitTracing}
|
|
||||||
if !omitTracing {
|
|
||||||
er.traceValue()
|
|
||||||
}
|
|
||||||
return er
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kind returns the ValueKind of the result.
|
|
||||||
func (er *EvaluationResult) Kind() ValueKind { return er.kind }
|
|
||||||
|
|
||||||
// Raw returns the raw value that was passed to the constructor.
|
|
||||||
func (er *EvaluationResult) Raw() any { return er.raw }
|
|
||||||
|
|
||||||
// Value returns the canonical value.
|
|
||||||
func (er *EvaluationResult) Value() any { return er.value }
|
|
||||||
|
|
||||||
// IsFalsy implements the logic from the C# class.
|
|
||||||
func (er *EvaluationResult) IsFalsy() bool {
|
|
||||||
switch er.kind {
|
|
||||||
case ValueKindNull:
|
|
||||||
return true
|
|
||||||
case ValueKindBoolean:
|
|
||||||
return !er.value.(bool)
|
|
||||||
case ValueKindNumber:
|
|
||||||
v := er.value.(float64)
|
|
||||||
return v == 0 || isNaN(v)
|
|
||||||
case ValueKindString:
|
|
||||||
return er.value.(string) == ""
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNaN(v float64) bool { return v != v }
|
|
||||||
|
|
||||||
// IsPrimitive returns true if the kind is a primitive type.
|
|
||||||
func (er *EvaluationResult) IsPrimitive() bool { return er.kind <= ValueKindString }
|
|
||||||
|
|
||||||
// IsTruthy is the negation of IsFalsy.
|
|
||||||
func (er *EvaluationResult) IsTruthy() bool { return !er.IsFalsy() }
|
|
||||||
|
|
||||||
// AbstractEqual compares two EvaluationResults using the abstract equality algorithm.
|
|
||||||
func (er *EvaluationResult) AbstractEqual(other *EvaluationResult) bool {
|
|
||||||
return abstractEqual(er.value, other.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractGreaterThan compares two EvaluationResults.
|
|
||||||
func (er *EvaluationResult) AbstractGreaterThan(other *EvaluationResult) bool {
|
|
||||||
return abstractGreaterThan(er.value, other.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractGreaterThanOrEqual
|
|
||||||
func (er *EvaluationResult) AbstractGreaterThanOrEqual(other *EvaluationResult) bool {
|
|
||||||
return er.AbstractEqual(other) || er.AbstractGreaterThan(other)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractLessThan
|
|
||||||
func (er *EvaluationResult) AbstractLessThan(other *EvaluationResult) bool {
|
|
||||||
return abstractLessThan(er.value, other.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractLessThanOrEqual
|
|
||||||
func (er *EvaluationResult) AbstractLessThanOrEqual(other *EvaluationResult) bool {
|
|
||||||
return er.AbstractEqual(other) || er.AbstractLessThan(other)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractNotEqual
|
|
||||||
func (er *EvaluationResult) AbstractNotEqual(other *EvaluationResult) bool {
|
|
||||||
return !er.AbstractEqual(other)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToNumber converts the value to a float64.
|
|
||||||
func (er *EvaluationResult) ConvertToNumber() float64 { return convertToNumber(er.value) }
|
|
||||||
|
|
||||||
// ConvertToString converts the value to a string.
|
|
||||||
func (er *EvaluationResult) ConvertToString() string {
|
|
||||||
switch er.kind {
|
|
||||||
case ValueKindNull:
|
|
||||||
return ""
|
|
||||||
case ValueKindBoolean:
|
|
||||||
if er.value.(bool) {
|
|
||||||
return ExpressionConstants.True
|
|
||||||
}
|
|
||||||
return ExpressionConstants.False
|
|
||||||
case ValueKindNumber:
|
|
||||||
return fmt.Sprintf(ExpressionConstants.NumberFormat, er.value.(float64))
|
|
||||||
case ValueKindString:
|
|
||||||
return er.value.(string)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", er.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TryGetCollectionInterface returns the underlying collection if the value is an array or object.
|
|
||||||
func (er *EvaluationResult) TryGetCollectionInterface() (any, bool) {
|
|
||||||
switch v := er.value.(type) {
|
|
||||||
case ReadOnlyArray[any]:
|
|
||||||
return v, true
|
|
||||||
case ReadOnlyObject[any]:
|
|
||||||
return v, true
|
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateIntermediateResult creates an EvaluationResult from an arbitrary object.
|
|
||||||
func CreateIntermediateResult(context *EvaluationContext, obj any) *EvaluationResult {
|
|
||||||
val, kind, raw := convertToCanonicalValue(obj)
|
|
||||||
return NewEvaluationResult(context, 0, val, kind, raw, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helper functions and constants ---------------------------------------
|
|
||||||
|
|
||||||
// ExpressionConstants holds string constants used in conversions.
|
|
||||||
var ExpressionConstants = struct {
|
|
||||||
True string
|
|
||||||
False string
|
|
||||||
NumberFormat string
|
|
||||||
}{
|
|
||||||
True: "true",
|
|
||||||
False: "false",
|
|
||||||
NumberFormat: "%.15g",
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertToCanonicalValue converts an arbitrary Go value to a canonical form.
|
|
||||||
func convertToCanonicalValue(obj any) (any, ValueKind, any) {
|
|
||||||
switch v := obj.(type) {
|
|
||||||
case nil:
|
|
||||||
return nil, ValueKindNull, nil
|
|
||||||
case bool:
|
|
||||||
return v, ValueKindBoolean, v
|
|
||||||
case int, int8, int16, int32, int64:
|
|
||||||
f := float64(toInt64(v))
|
|
||||||
return f, ValueKindNumber, f
|
|
||||||
case uint, uint8, uint16, uint32, uint64:
|
|
||||||
f := float64(toUint64(v))
|
|
||||||
return f, ValueKindNumber, f
|
|
||||||
case float32, float64:
|
|
||||||
f := toFloat64(v)
|
|
||||||
return f, ValueKindNumber, f
|
|
||||||
case string:
|
|
||||||
return v, ValueKindString, v
|
|
||||||
case []any:
|
|
||||||
return BasicArray[any](v), ValueKindArray, v
|
|
||||||
case ReadOnlyArray[any]:
|
|
||||||
return v, ValueKindArray, v
|
|
||||||
case map[string]any:
|
|
||||||
return CaseInsensitiveObject[any](v), ValueKindObject, v
|
|
||||||
case ReadOnlyObject[any]:
|
|
||||||
return v, ValueKindObject, v
|
|
||||||
default:
|
|
||||||
// Fallback: treat as object
|
|
||||||
return v, ValueKindObject, v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toInt64(v any) int64 {
|
|
||||||
switch i := v.(type) {
|
|
||||||
case int:
|
|
||||||
return int64(i)
|
|
||||||
case int8:
|
|
||||||
return int64(i)
|
|
||||||
case int16:
|
|
||||||
return int64(i)
|
|
||||||
case int32:
|
|
||||||
return int64(i)
|
|
||||||
case int64:
|
|
||||||
return i
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUint64(v any) uint64 {
|
|
||||||
switch i := v.(type) {
|
|
||||||
case uint:
|
|
||||||
return uint64(i)
|
|
||||||
case uint8:
|
|
||||||
return uint64(i)
|
|
||||||
case uint16:
|
|
||||||
return uint64(i)
|
|
||||||
case uint32:
|
|
||||||
return uint64(i)
|
|
||||||
case uint64:
|
|
||||||
return i
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toFloat64(v any) float64 {
|
|
||||||
switch f := v.(type) {
|
|
||||||
case float32:
|
|
||||||
return float64(f)
|
|
||||||
case float64:
|
|
||||||
return f
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coerceTypes implements the C# CoerceTypes logic.
|
|
||||||
// It converts values to compatible types before comparison.
|
|
||||||
func coerceTypes(left, right any) (any, any, ValueKind, ValueKind) {
|
|
||||||
leftKind := getKind(left)
|
|
||||||
rightKind := getKind(right)
|
|
||||||
|
|
||||||
// same kind – nothing to do
|
|
||||||
if leftKind == rightKind {
|
|
||||||
return left, right, leftKind, rightKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number <-> String
|
|
||||||
if leftKind == ValueKindNumber && rightKind == ValueKindString {
|
|
||||||
right = convertToNumber(right)
|
|
||||||
rightKind = ValueKindNumber
|
|
||||||
return left, right, leftKind, rightKind
|
|
||||||
}
|
|
||||||
if leftKind == ValueKindString && rightKind == ValueKindNumber {
|
|
||||||
left = convertToNumber(left)
|
|
||||||
leftKind = ValueKindNumber
|
|
||||||
return left, right, leftKind, rightKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boolean or Null -> Number
|
|
||||||
if leftKind == ValueKindBoolean || leftKind == ValueKindNull {
|
|
||||||
left = convertToNumber(left)
|
|
||||||
return coerceTypes(left, right)
|
|
||||||
}
|
|
||||||
if rightKind == ValueKindBoolean || rightKind == ValueKindNull {
|
|
||||||
right = convertToNumber(right)
|
|
||||||
return coerceTypes(left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise keep as is
|
|
||||||
return left, right, leftKind, rightKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// abstractEqual uses coerceTypes before comparing.
|
|
||||||
func abstractEqual(left, right any) bool {
|
|
||||||
left, right, leftKind, rightKind := coerceTypes(left, right)
|
|
||||||
if leftKind != rightKind {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch leftKind {
|
|
||||||
case ValueKindNull:
|
|
||||||
return true
|
|
||||||
case ValueKindNumber:
|
|
||||||
l := left.(float64)
|
|
||||||
r := right.(float64)
|
|
||||||
if isNaN(l) || isNaN(r) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return l == r
|
|
||||||
case ValueKindString:
|
|
||||||
return strings.EqualFold(left.(string), right.(string))
|
|
||||||
case ValueKindBoolean:
|
|
||||||
return left.(bool) == right.(bool)
|
|
||||||
// Compare object equality fails via panic
|
|
||||||
// case ValueKindObject, ValueKindArray:
|
|
||||||
// return left == right
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// abstractGreaterThan uses coerceTypes before comparing.
|
|
||||||
func abstractGreaterThan(left, right any) bool {
|
|
||||||
left, right, leftKind, rightKind := coerceTypes(left, right)
|
|
||||||
if leftKind != rightKind {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch leftKind {
|
|
||||||
case ValueKindNumber:
|
|
||||||
l := left.(float64)
|
|
||||||
r := right.(float64)
|
|
||||||
if isNaN(l) || isNaN(r) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return l > r
|
|
||||||
case ValueKindString:
|
|
||||||
return strings.Compare(left.(string), right.(string)) > 0
|
|
||||||
case ValueKindBoolean:
|
|
||||||
return left.(bool) && !right.(bool)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// abstractLessThan uses coerceTypes before comparing.
|
|
||||||
func abstractLessThan(left, right any) bool {
|
|
||||||
left, right, leftKind, rightKind := coerceTypes(left, right)
|
|
||||||
if leftKind != rightKind {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch leftKind {
|
|
||||||
case ValueKindNumber:
|
|
||||||
l := left.(float64)
|
|
||||||
r := right.(float64)
|
|
||||||
if isNaN(l) || isNaN(r) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return l < r
|
|
||||||
case ValueKindString:
|
|
||||||
return strings.Compare(left.(string), right.(string)) < 0
|
|
||||||
case ValueKindBoolean:
|
|
||||||
return !left.(bool) && right.(bool)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertToNumber converts a value to a float64 following JavaScript rules.
|
|
||||||
func convertToNumber(v any) float64 {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return 0
|
|
||||||
case bool:
|
|
||||||
if val {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
case float64:
|
|
||||||
return val
|
|
||||||
case float32:
|
|
||||||
return float64(val)
|
|
||||||
case string:
|
|
||||||
// parsenumber
|
|
||||||
if val == "" {
|
|
||||||
return float64(0)
|
|
||||||
}
|
|
||||||
if len(val) > 2 {
|
|
||||||
switch val[:2] {
|
|
||||||
case "0x", "0o":
|
|
||||||
if i, err := strconv.ParseInt(val, 0, 32); err == nil {
|
|
||||||
return float64(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
return math.NaN()
|
|
||||||
default:
|
|
||||||
return math.NaN()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getKind returns the ValueKind for a Go value.
|
|
||||||
func getKind(v any) ValueKind {
|
|
||||||
switch v.(type) {
|
|
||||||
case nil:
|
|
||||||
return ValueKindNull
|
|
||||||
case bool:
|
|
||||||
return ValueKindBoolean
|
|
||||||
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
|
||||||
return ValueKindNumber
|
|
||||||
case string:
|
|
||||||
return ValueKindString
|
|
||||||
case []any:
|
|
||||||
return ValueKindArray
|
|
||||||
case map[string]any:
|
|
||||||
return ValueKindObject
|
|
||||||
default:
|
|
||||||
return ValueKindObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// traceValue is a placeholder for tracing logic.
|
|
||||||
func (er *EvaluationResult) traceValue() {
|
|
||||||
// No-op in this simplified implementation.
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- End of file ---------------------------------------
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
exprparser "gitea.com/gitea/act_runner/internal/expr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvaluationContext holds variables that can be referenced in expressions.
|
|
||||||
type EvaluationContext struct {
|
|
||||||
Variables ReadOnlyObject[any]
|
|
||||||
Functions ReadOnlyObject[Function]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEvaluationContext() *EvaluationContext {
|
|
||||||
return &EvaluationContext{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Function interface {
|
|
||||||
Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluator evaluates workflow expressions using the lexer and parser from workflow.
|
|
||||||
type Evaluator struct {
|
|
||||||
ctx *EvaluationContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvaluator creates an Evaluator with the supplied context.
|
|
||||||
func NewEvaluator(ctx *EvaluationContext) *Evaluator {
|
|
||||||
return &Evaluator{ctx: ctx}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) Context() *EvaluationContext {
|
|
||||||
return e.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) Evaluate(root exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
result, err := e.evalNode(root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EvaluateBoolean parses and evaluates the expression, returning a boolean result.
|
|
||||||
func (e *Evaluator) EvaluateBoolean(expr string) (bool, error) {
|
|
||||||
root, err := exprparser.Parse(expr)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("parse error: %w", err)
|
|
||||||
}
|
|
||||||
result, err := e.evalNode(root)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return result.IsTruthy(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) ToRaw(result *EvaluationResult) (any, error) {
|
|
||||||
if col, ok := result.TryGetCollectionInterface(); ok {
|
|
||||||
switch node := col.(type) {
|
|
||||||
case ReadOnlyObject[any]:
|
|
||||||
rawMap := map[string]any{}
|
|
||||||
for k, v := range node.GetEnumerator() {
|
|
||||||
rawRes, err := e.ToRaw(CreateIntermediateResult(e.Context(), v))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawMap[k] = rawRes
|
|
||||||
}
|
|
||||||
return rawMap, nil
|
|
||||||
case ReadOnlyArray[any]:
|
|
||||||
rawArray := []any{}
|
|
||||||
for _, v := range node.GetEnumerator() {
|
|
||||||
rawRes, err := e.ToRaw(CreateIntermediateResult(e.Context(), v))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawArray = append(rawArray, rawRes)
|
|
||||||
}
|
|
||||||
return rawArray, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.Value(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate parses and evaluates the expression, returning a boolean result.
|
|
||||||
func (e *Evaluator) EvaluateRaw(expr string) (any, error) {
|
|
||||||
root, err := exprparser.Parse(expr)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("parse error: %w", err)
|
|
||||||
}
|
|
||||||
result, err := e.evalNode(root)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return e.ToRaw(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilteredArray []any
|
|
||||||
|
|
||||||
func (a FilteredArray) GetAt(i int64) any {
|
|
||||||
if int(i) > len(a) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a FilteredArray) GetEnumerator() []any {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalNode recursively evaluates a parser node and returns an EvaluationResult.
|
|
||||||
func (e *Evaluator) evalNode(n exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
switch node := n.(type) {
|
|
||||||
case *exprparser.ValueNode:
|
|
||||||
return e.evalValueNode(node)
|
|
||||||
case *exprparser.FunctionNode:
|
|
||||||
return e.evalFunctionNode(node)
|
|
||||||
case *exprparser.BinaryNode:
|
|
||||||
return e.evalBinaryNode(node)
|
|
||||||
case *exprparser.UnaryNode:
|
|
||||||
return e.evalUnaryNode(node)
|
|
||||||
}
|
|
||||||
return nil, errors.New("unknown node type")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalValueNode(node *exprparser.ValueNode) (*EvaluationResult, error) {
|
|
||||||
if node.Kind == exprparser.TokenKindNamedValue {
|
|
||||||
if e.ctx != nil {
|
|
||||||
val := e.ctx.Variables.Get(node.Value.(string))
|
|
||||||
if val == nil {
|
|
||||||
return nil, fmt.Errorf("undefined variable %s", node.Value)
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(e.Context(), val), nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("no evaluation context")
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(e.Context(), node.Value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalFunctionNode(node *exprparser.FunctionNode) (*EvaluationResult, error) {
|
|
||||||
fn := e.ctx.Functions.Get(node.Name)
|
|
||||||
if fn == nil {
|
|
||||||
return nil, fmt.Errorf("unknown function %v", node.Name)
|
|
||||||
}
|
|
||||||
return fn.Evaluate(e, node.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalBinaryNode(node *exprparser.BinaryNode) (*EvaluationResult, error) {
|
|
||||||
left, err := e.evalNode(node.Left)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res, err := e.evalBinaryNodeLeft(node, left); res != nil || err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
right, err := e.evalNode(node.Right)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return e.evalBinaryNodeRight(node, left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalBinaryNodeLeft(node *exprparser.BinaryNode, left *EvaluationResult) (*EvaluationResult, error) { //nolint:unparam
|
|
||||||
switch node.Op {
|
|
||||||
case "&&":
|
|
||||||
if left.IsFalsy() {
|
|
||||||
return left, nil
|
|
||||||
}
|
|
||||||
case "||":
|
|
||||||
if left.IsTruthy() {
|
|
||||||
return left, nil
|
|
||||||
}
|
|
||||||
case ".":
|
|
||||||
if v, ok := node.Right.(*exprparser.ValueNode); ok && v.Kind == exprparser.TokenKindWildcard {
|
|
||||||
var ret FilteredArray
|
|
||||||
if col, ok := left.TryGetCollectionInterface(); ok {
|
|
||||||
if farray, ok := col.(FilteredArray); ok {
|
|
||||||
for _, subcol := range farray.GetEnumerator() {
|
|
||||||
ret = processStar(CreateIntermediateResult(e.Context(), subcol).Value(), ret)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ret = processStar(col, ret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(e.Context(), ret), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil //nolint:nilnil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalBinaryNodeRight(node *exprparser.BinaryNode, left *EvaluationResult, right *EvaluationResult) (*EvaluationResult, error) {
|
|
||||||
switch node.Op {
|
|
||||||
case "&&":
|
|
||||||
return right, nil
|
|
||||||
case "||":
|
|
||||||
return right, nil
|
|
||||||
case "==":
|
|
||||||
// Use abstract equality per spec
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractEqual(right)), nil
|
|
||||||
case "!=":
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractNotEqual(right)), nil
|
|
||||||
case ">":
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractGreaterThan(right)), nil
|
|
||||||
case "<":
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractLessThan(right)), nil
|
|
||||||
case ">=":
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractGreaterThanOrEqual(right)), nil
|
|
||||||
case "<=":
|
|
||||||
return CreateIntermediateResult(e.Context(), left.AbstractLessThanOrEqual(right)), nil
|
|
||||||
case ".", "[":
|
|
||||||
if farray, ok := left.Value().(FilteredArray); ok {
|
|
||||||
var ret FilteredArray
|
|
||||||
for _, subcol := range farray.GetEnumerator() {
|
|
||||||
res := processIndex(CreateIntermediateResult(e.Context(), subcol).Value(), right)
|
|
||||||
if res != nil {
|
|
||||||
ret = append(ret, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ret == nil {
|
|
||||||
return CreateIntermediateResult(e.Context(), nil), nil
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(e.Context(), ret), nil
|
|
||||||
}
|
|
||||||
col, _ := left.TryGetCollectionInterface()
|
|
||||||
result := processIndex(col, right)
|
|
||||||
return CreateIntermediateResult(e.Context(), result), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported operator %s", node.Op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Evaluator) evalUnaryNode(node *exprparser.UnaryNode) (*EvaluationResult, error) {
|
|
||||||
operand, err := e.evalNode(node.Operand)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch node.Op {
|
|
||||||
case "!":
|
|
||||||
return CreateIntermediateResult(e.Context(), !operand.IsTruthy()), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported unary operator %s", node.Op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processIndex(col any, right *EvaluationResult) any {
|
|
||||||
if mapVal, ok := col.(ReadOnlyObject[any]); ok {
|
|
||||||
key, ok := right.Value().(string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
val := mapVal.Get(key)
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
if arrayVal, ok := col.(ReadOnlyArray[any]); ok {
|
|
||||||
key, ok := right.Value().(float64)
|
|
||||||
if !ok || key < 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
val := arrayVal.GetAt(int64(key))
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func processStar(subcol any, ret FilteredArray) FilteredArray {
|
|
||||||
if array, ok := subcol.(ReadOnlyArray[any]); ok {
|
|
||||||
ret = append(ret, array.GetEnumerator()...)
|
|
||||||
} else if obj, ok := subcol.(ReadOnlyObject[any]); ok {
|
|
||||||
for _, v := range obj.GetEnumerator() {
|
|
||||||
ret = append(ret, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test boolean and comparison operations using the evaluator.
|
|
||||||
func TestEvaluator_BooleanOps(t *testing.T) {
|
|
||||||
ctx := &EvaluationContext{Variables: CaseInsensitiveObject[any](map[string]any{"a": 5, "b": 3})}
|
|
||||||
eval := NewEvaluator(ctx)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
expr string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"1 == 1", true},
|
|
||||||
{"1 != 2", true},
|
|
||||||
{"5 > 3", true},
|
|
||||||
{"2 < 4", true},
|
|
||||||
{"5 >= 5", true},
|
|
||||||
{"3 <= 4", true},
|
|
||||||
{"true && false", false},
|
|
||||||
{"!false", true},
|
|
||||||
{"a > b", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
got, err := eval.EvaluateBoolean(tt.expr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("evaluate %s error: %v", tt.expr, err)
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("evaluate %s expected %v got %v", tt.expr, tt.want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvaluator_Raw(t *testing.T) {
|
|
||||||
ctx := &EvaluationContext{
|
|
||||||
Variables: CaseInsensitiveObject[any](map[string]any{"a": 5, "b": 3}),
|
|
||||||
Functions: GetFunctions(),
|
|
||||||
}
|
|
||||||
eval := NewEvaluator(ctx)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
expr string
|
|
||||||
want any
|
|
||||||
}{
|
|
||||||
{"a.b['x']", nil},
|
|
||||||
{"(a.b).c['x']", nil},
|
|
||||||
{"(a.b).*['x']", nil},
|
|
||||||
{"(a['x'])", nil},
|
|
||||||
{"true || false", true},
|
|
||||||
{"false || false", false},
|
|
||||||
{"false || true", true},
|
|
||||||
{"false || true || false", true},
|
|
||||||
{"contains('', '') || contains('', '') || contains('', '')", true},
|
|
||||||
{"1 == 1", true},
|
|
||||||
{"1 != 2", true},
|
|
||||||
{"5 > 3", true},
|
|
||||||
{"2 < 4", true},
|
|
||||||
{"5 >= 5", true},
|
|
||||||
{"3 <= 4", true},
|
|
||||||
{"true && false", false},
|
|
||||||
{"!false", true},
|
|
||||||
{"a > b", true},
|
|
||||||
{"!(a > b)", false},
|
|
||||||
{"!(a > b) || !0", true},
|
|
||||||
{"!(a > b) || !(1)", false},
|
|
||||||
{"'Hello World'", "Hello World"},
|
|
||||||
{"23.5", 23.5},
|
|
||||||
{"fromjson('{\"twst\":\"x\"}')['twst']", "x"},
|
|
||||||
{"fromjson('{\"Twst\":\"x\"}')['twst']", "x"},
|
|
||||||
{"fromjson('{\"TwsT\":\"x\"}')['twst']", "x"},
|
|
||||||
{"fromjson('{\"TwsT\":\"x\"}')['tWst']", "x"},
|
|
||||||
{"fromjson('{\"TwsT\":{\"a\":\"y\"}}').TwsT.a", "y"},
|
|
||||||
{"fromjson('{\"TwsT\":{\"a\":\"y\"}}')['TwsT'].a", "y"},
|
|
||||||
{"fromjson('{\"TwsT\":{\"a\":\"y\"}}')['TwsT']['a']", "y"},
|
|
||||||
{"fromjson('{\"TwsT\":{\"a\":\"y\"}}').TwsT['a']", "y"},
|
|
||||||
// {"fromjson('{\"TwsT\":\"x\"}').*[0]", "x"},
|
|
||||||
{"fromjson('{\"TwsT\":[\"x\"]}')['TwsT'][0]", "x"},
|
|
||||||
{"fromjson('[]')['tWst']", nil},
|
|
||||||
{"fromjson('[]').tWst", nil},
|
|
||||||
{"contains('a', 'a')", true},
|
|
||||||
{"contains('bab', 'a')", true},
|
|
||||||
{"contains('bab', 'ac')", false},
|
|
||||||
{"contains(fromjson('[\"ac\"]'), 'ac')", true},
|
|
||||||
{"contains(fromjson('[\"ac\"]'), 'a')", false},
|
|
||||||
// {"fromjson('{\"TwsT\":{\"a\":\"y\"}}').*['a']", "y"},
|
|
||||||
{"fromjson(tojson(fromjson('{\"TwsT\":{\"a\":\"y\"}}').*.a))[0]", "y"},
|
|
||||||
{"fromjson(tojson(fromjson('{\"TwsT\":{\"a\":\"y\"}}').*['a']))[0]", "y"},
|
|
||||||
{"fromjson('{}').x", nil},
|
|
||||||
{"format('{0}', fromjson('{}').x)", ""},
|
|
||||||
{"format('{0}', fromjson('{}')[0])", ""},
|
|
||||||
{"fromjson(tojson(fromjson('[[3,5],[5,6]]').*[1]))[1]", float64(6)},
|
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], 5)", true},
|
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], 6)", true},
|
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], 3)", false},
|
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], '6')", true},
|
|
||||||
{"case(6 == 6, 0, 1)", 0.0},
|
|
||||||
{"case(6 != 6, 0, 1)", 1.0},
|
|
||||||
{"case(6 != 6, 0, 'test')", "test"},
|
|
||||||
{"case(contains(fromjson('[\"ac\"]'), 'a'), 0, 'test')", "test"},
|
|
||||||
{"case(0 == 1, 0, 2 == 2, 1, 0)", 1.0},
|
|
||||||
{"case(0 == 1, 0, 2 != 2, 1, 0)", 0.0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
got, err := eval.EvaluateRaw(tt.expr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("evaluate %s error: %v", tt.expr, err)
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("evaluate %s expected %v got %v", tt.expr, tt.want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/eval/functions"
|
|
||||||
exprparser "gitea.com/gitea/act_runner/internal/expr"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FromJSON struct{}
|
|
||||||
|
|
||||||
func (FromJSON) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
r, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var res any
|
|
||||||
if err := json.Unmarshal([]byte(r.ConvertToString()), &res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreateIntermediateResult(eval.Context(), res), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToJSON struct{}
|
|
||||||
|
|
||||||
func (ToJSON) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
r, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
raw, err := eval.ToRaw(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, err := json.MarshalIndent(raw, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(eval.Context(), string(data)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Contains struct{}
|
|
||||||
|
|
||||||
func (Contains) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
collection, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
el, err := eval.Evaluate(args[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Array
|
|
||||||
if col, ok := collection.TryGetCollectionInterface(); ok {
|
|
||||||
if node, ok := col.(ReadOnlyArray[any]); ok {
|
|
||||||
for _, v := range node.GetEnumerator() {
|
|
||||||
canon := CreateIntermediateResult(eval.Context(), v)
|
|
||||||
if canon.AbstractEqual(el) {
|
|
||||||
return CreateIntermediateResult(eval.Context(), true), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(eval.Context(), false), nil
|
|
||||||
}
|
|
||||||
// String
|
|
||||||
return CreateIntermediateResult(eval.Context(), strings.Contains(strings.ToLower(collection.ConvertToString()), strings.ToLower(el.ConvertToString()))), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StartsWith struct{}
|
|
||||||
|
|
||||||
func (StartsWith) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
collection, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
el, err := eval.Evaluate(args[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// String
|
|
||||||
return CreateIntermediateResult(eval.Context(), strings.HasPrefix(strings.ToLower(collection.ConvertToString()), strings.ToLower(el.ConvertToString()))), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type EndsWith struct{}
|
|
||||||
|
|
||||||
func (EndsWith) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
collection, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
el, err := eval.Evaluate(args[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// String
|
|
||||||
return CreateIntermediateResult(eval.Context(), strings.HasSuffix(strings.ToLower(collection.ConvertToString()), strings.ToLower(el.ConvertToString()))), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Format struct{}
|
|
||||||
|
|
||||||
func (Format) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
collection, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sargs := []any{}
|
|
||||||
for _, arg := range args[1:] {
|
|
||||||
el, err := eval.Evaluate(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sargs = append(sargs, el.ConvertToString())
|
|
||||||
}
|
|
||||||
|
|
||||||
ret, err := functions.Format(collection.ConvertToString(), sargs...)
|
|
||||||
return CreateIntermediateResult(eval.Context(), ret), err
|
|
||||||
}
|
|
||||||
|
|
||||||
type Join struct{}
|
|
||||||
|
|
||||||
func (Join) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
collection, err := eval.Evaluate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var el *EvaluationResult
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
if el, err = eval.Evaluate(args[1]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Array
|
|
||||||
if col, ok := collection.TryGetCollectionInterface(); ok {
|
|
||||||
var elements []string
|
|
||||||
if node, ok := col.(ReadOnlyArray[any]); ok {
|
|
||||||
for _, v := range node.GetEnumerator() {
|
|
||||||
elements = append(elements, CreateIntermediateResult(eval.Context(), v).ConvertToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sep string
|
|
||||||
if el != nil {
|
|
||||||
sep = el.ConvertToString()
|
|
||||||
} else {
|
|
||||||
sep = ","
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(eval.Context(), strings.Join(elements, sep)), nil
|
|
||||||
}
|
|
||||||
// Primitive
|
|
||||||
if collection.IsPrimitive() {
|
|
||||||
return CreateIntermediateResult(eval.Context(), collection.ConvertToString()), nil
|
|
||||||
}
|
|
||||||
return CreateIntermediateResult(eval.Context(), ""), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Case struct{}
|
|
||||||
|
|
||||||
func (Case) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
|
||||||
if len(args)%2 == 0 {
|
|
||||||
return nil, errors.New("case function requires an odd number of arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(args)-1; i += 2 {
|
|
||||||
condition, err := eval.Evaluate(args[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if condition.kind != ValueKindBoolean {
|
|
||||||
return nil, errors.New("case function conditions must evaluate to boolean")
|
|
||||||
}
|
|
||||||
if condition.IsTruthy() {
|
|
||||||
return eval.Evaluate(args[i+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return eval.Evaluate(args[len(args)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFunctions() CaseInsensitiveObject[Function] {
|
|
||||||
return CaseInsensitiveObject[Function](map[string]Function{
|
|
||||||
"fromjson": &FromJSON{},
|
|
||||||
"tojson": &ToJSON{},
|
|
||||||
"contains": &Contains{},
|
|
||||||
"startswith": &StartsWith{},
|
|
||||||
"endswith": &EndsWith{},
|
|
||||||
"format": &Format{},
|
|
||||||
"join": &Join{},
|
|
||||||
"case": &Case{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestExpressionParser(t *testing.T) {
|
|
||||||
node, err := Parse("github.event_name")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Parsed expression: %+v", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpressionParserWildcard(t *testing.T) {
|
|
||||||
node, err := Parse("github.commits.*.message")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Parsed expression: %+v", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpressionParserDot(t *testing.T) {
|
|
||||||
node, err := Parse("github.head_commit.message")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Parsed expression: %+v", node)
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node represents a node in the expression tree.
|
|
||||||
// It is intentionally minimal – only the fields needed for the parser.
|
|
||||||
// Users can extend it with more information if required.
|
|
||||||
|
|
||||||
type Node interface {
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueNode represents a literal value (number, string, boolean, null) or a named value.
|
|
||||||
// The Kind field indicates the type.
|
|
||||||
// For named values the Value is nil.
|
|
||||||
|
|
||||||
type ValueNode struct {
|
|
||||||
Kind TokenKind
|
|
||||||
Value any
|
|
||||||
}
|
|
||||||
|
|
||||||
// FunctionNode represents a function call with arguments.
|
|
||||||
|
|
||||||
type FunctionNode struct {
|
|
||||||
Name string
|
|
||||||
Args []Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryNode represents a binary operator.
|
|
||||||
|
|
||||||
type BinaryNode struct {
|
|
||||||
Op string
|
|
||||||
Left Node
|
|
||||||
Right Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnaryNode represents a unary operator.
|
|
||||||
|
|
||||||
type UnaryNode struct {
|
|
||||||
Op string
|
|
||||||
Operand Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser holds the lexer and the stacks used by the shunting‑yard algorithm.
|
|
||||||
|
|
||||||
type Parser struct {
|
|
||||||
lexer *Lexer
|
|
||||||
tokens []Token
|
|
||||||
pos int
|
|
||||||
ops []OpToken
|
|
||||||
vals []Node
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpToken struct {
|
|
||||||
Token
|
|
||||||
StartPos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func precedence(tkn Token) int {
|
|
||||||
switch tkn.Kind {
|
|
||||||
case TokenKindStartGroup:
|
|
||||||
return 20
|
|
||||||
case TokenKindStartIndex, TokenKindStartParameters, TokenKindDereference:
|
|
||||||
return 19
|
|
||||||
case TokenKindLogicalOperator:
|
|
||||||
switch tkn.Raw {
|
|
||||||
case "!":
|
|
||||||
return 16
|
|
||||||
case ">", ">=", "<", "<=":
|
|
||||||
return 11
|
|
||||||
case "==", "!=":
|
|
||||||
return 10
|
|
||||||
case "&&":
|
|
||||||
return 6
|
|
||||||
case "||":
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
case TokenKindEndGroup, TokenKindEndIndex, TokenKindEndParameters, TokenKindSeparator:
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses the expression and returns the root node.
|
|
||||||
func Parse(expression string) (Node, error) {
|
|
||||||
lexer := NewLexer(expression, 0)
|
|
||||||
p := &Parser{}
|
|
||||||
// Tokenise all tokens
|
|
||||||
if err := p.initWithLexer(lexer); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p.parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parse() (Node, error) {
|
|
||||||
// Shunting‑yard algorithm
|
|
||||||
for p.pos < len(p.tokens) {
|
|
||||||
tok := p.tokens[p.pos]
|
|
||||||
p.pos++
|
|
||||||
switch tok.Kind {
|
|
||||||
case TokenKindNumber, TokenKindString, TokenKindBoolean, TokenKindNull:
|
|
||||||
p.pushValue(&ValueNode{Kind: tok.Kind, Value: tok.Value})
|
|
||||||
case TokenKindNamedValue, TokenKindPropertyName, TokenKindWildcard:
|
|
||||||
p.pushValue(&ValueNode{Kind: tok.Kind, Value: tok.Raw})
|
|
||||||
case TokenKindFunction:
|
|
||||||
p.pushFunc(tok, len(p.vals))
|
|
||||||
case TokenKindStartParameters, TokenKindStartGroup, TokenKindStartIndex, TokenKindLogicalOperator, TokenKindDereference:
|
|
||||||
if err := p.pushOp(tok); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case TokenKindSeparator:
|
|
||||||
if err := p.popGroup(TokenKindStartParameters); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case TokenKindEndParameters:
|
|
||||||
if err := p.pushFuncValue(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case TokenKindEndGroup:
|
|
||||||
if err := p.popGroup(TokenKindStartGroup); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.ops = p.ops[:len(p.ops)-1]
|
|
||||||
case TokenKindEndIndex:
|
|
||||||
if err := p.popGroup(TokenKindStartIndex); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// pop the start parameters
|
|
||||||
p.ops = p.ops[:len(p.ops)-1]
|
|
||||||
right := p.vals[len(p.vals)-1]
|
|
||||||
p.vals = p.vals[:len(p.vals)-1]
|
|
||||||
left := p.vals[len(p.vals)-1]
|
|
||||||
p.vals = p.vals[:len(p.vals)-1]
|
|
||||||
p.vals = append(p.vals, &BinaryNode{Op: "[", Left: left, Right: right})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for len(p.ops) > 0 {
|
|
||||||
if err := p.popOp(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(p.vals) != 1 {
|
|
||||||
return nil, errors.New("invalid expression")
|
|
||||||
}
|
|
||||||
return p.vals[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) pushFuncValue() error {
|
|
||||||
if err := p.popGroup(TokenKindStartParameters); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// pop the start parameters
|
|
||||||
p.ops = p.ops[:len(p.ops)-1]
|
|
||||||
// create function node
|
|
||||||
fnTok := p.ops[len(p.ops)-1]
|
|
||||||
if fnTok.Kind != TokenKindFunction {
|
|
||||||
return errors.New("expected function token")
|
|
||||||
}
|
|
||||||
p.ops = p.ops[:len(p.ops)-1]
|
|
||||||
// collect arguments
|
|
||||||
args := []Node{}
|
|
||||||
for len(p.vals) > fnTok.StartPos {
|
|
||||||
args = append([]Node{p.vals[len(p.vals)-1]}, args...)
|
|
||||||
p.vals = p.vals[:len(p.vals)-1]
|
|
||||||
}
|
|
||||||
p.pushValue(&FunctionNode{Name: fnTok.Raw, Args: args})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) initWithLexer(lexer *Lexer) error {
|
|
||||||
p.lexer = lexer
|
|
||||||
for {
|
|
||||||
tok := lexer.Next()
|
|
||||||
if tok == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if tok.Kind == TokenKindUnexpected {
|
|
||||||
return fmt.Errorf("unexpected token %s at position %d", tok.Raw, tok.Index)
|
|
||||||
}
|
|
||||||
p.tokens = append(p.tokens, *tok)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) popGroup(kind TokenKind) error {
|
|
||||||
for len(p.ops) > 0 && p.ops[len(p.ops)-1].Kind != kind {
|
|
||||||
if err := p.popOp(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(p.ops) == 0 {
|
|
||||||
return errors.New("mismatched parentheses")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) pushValue(v Node) {
|
|
||||||
p.vals = append(p.vals, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) pushOp(t Token) error {
|
|
||||||
for len(p.ops) > 0 {
|
|
||||||
top := p.ops[len(p.ops)-1]
|
|
||||||
if precedence(top.Token) >= precedence(t) &&
|
|
||||||
top.Kind != TokenKindStartGroup &&
|
|
||||||
top.Kind != TokenKindStartIndex &&
|
|
||||||
top.Kind != TokenKindStartParameters &&
|
|
||||||
top.Kind != TokenKindSeparator {
|
|
||||||
if err := p.popOp(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.ops = append(p.ops, OpToken{Token: t})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) pushFunc(t Token, start int) {
|
|
||||||
p.ops = append(p.ops, OpToken{Token: t, StartPos: start})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) popOp() error {
|
|
||||||
if len(p.ops) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
op := p.ops[len(p.ops)-1]
|
|
||||||
p.ops = p.ops[:len(p.ops)-1]
|
|
||||||
switch op.Kind {
|
|
||||||
case TokenKindLogicalOperator:
|
|
||||||
if op.Raw == "!" {
|
|
||||||
if len(p.vals) < 1 {
|
|
||||||
return errors.New("insufficient operands")
|
|
||||||
}
|
|
||||||
right := p.vals[len(p.vals)-1]
|
|
||||||
p.vals = p.vals[:len(p.vals)-1]
|
|
||||||
p.vals = append(p.vals, &UnaryNode{Op: op.Raw, Operand: right})
|
|
||||||
} else {
|
|
||||||
if len(p.vals) < 2 {
|
|
||||||
return errors.New("insufficient operands")
|
|
||||||
}
|
|
||||||
right := p.vals[len(p.vals)-1]
|
|
||||||
left := p.vals[len(p.vals)-2]
|
|
||||||
p.vals = p.vals[:len(p.vals)-2]
|
|
||||||
p.vals = append(p.vals, &BinaryNode{Op: op.Raw, Left: left, Right: right})
|
|
||||||
}
|
|
||||||
case TokenKindStartParameters:
|
|
||||||
// unary operator '!' handled elsewhere
|
|
||||||
case TokenKindDereference:
|
|
||||||
if len(p.vals) < 2 {
|
|
||||||
return errors.New("insufficient operands")
|
|
||||||
}
|
|
||||||
right := p.vals[len(p.vals)-1]
|
|
||||||
left := p.vals[len(p.vals)-2]
|
|
||||||
p.vals = p.vals[:len(p.vals)-2]
|
|
||||||
p.vals = append(p.vals, &BinaryNode{Op: ".", Left: left, Right: right})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the node.
|
|
||||||
func (n *ValueNode) String() string { return fmt.Sprintf("%v", n.Value) }
|
|
||||||
|
|
||||||
// String returns a string representation of the node.
|
|
||||||
func (n *FunctionNode) String() string {
|
|
||||||
return fmt.Sprintf("%s(%s)", n.Name, strings.Join(funcArgs(n.Args), ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func funcArgs(args []Node) []string {
|
|
||||||
res := []string{}
|
|
||||||
for _, a := range args {
|
|
||||||
res = append(res, a.String())
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the node.
|
|
||||||
func (n *BinaryNode) String() string {
|
|
||||||
return fmt.Sprintf("(%s %s %s)", n.Left.String(), n.Op, n.Right.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the node.
|
|
||||||
func (n *UnaryNode) String() string { return fmt.Sprintf("(%s%s)", n.Op, n.Operand.String()) }
|
|
||||||
|
|
||||||
func VisitNode(exprNode Node, callback func(node Node)) {
|
|
||||||
callback(exprNode)
|
|
||||||
switch node := exprNode.(type) {
|
|
||||||
case *FunctionNode:
|
|
||||||
for _, arg := range node.Args {
|
|
||||||
VisitNode(arg, callback)
|
|
||||||
}
|
|
||||||
case *UnaryNode:
|
|
||||||
VisitNode(node.Operand, callback)
|
|
||||||
case *BinaryNode:
|
|
||||||
VisitNode(node.Left, callback)
|
|
||||||
VisitNode(node.Right, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TokenKind represents the type of token returned by the lexer.
|
|
||||||
// The values mirror the C# TokenKind enum.
|
|
||||||
//
|
|
||||||
// Note: The names are kept identical to the C# implementation for
|
|
||||||
// easier mapping when porting the parser.
|
|
||||||
//
|
|
||||||
// The lexer is intentionally simple – it only tokenises the subset of
|
|
||||||
// expressions that are used in GitHub Actions workflow `if:` expressions.
|
|
||||||
// It does not evaluate the expression – that is left to the parser.
|
|
||||||
|
|
||||||
type TokenKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TokenKindStartGroup TokenKind = iota
|
|
||||||
TokenKindStartIndex
|
|
||||||
TokenKindEndGroup
|
|
||||||
TokenKindEndIndex
|
|
||||||
TokenKindSeparator
|
|
||||||
TokenKindDereference
|
|
||||||
TokenKindWildcard
|
|
||||||
TokenKindLogicalOperator
|
|
||||||
TokenKindNumber
|
|
||||||
TokenKindString
|
|
||||||
TokenKindBoolean
|
|
||||||
TokenKindNull
|
|
||||||
TokenKindPropertyName
|
|
||||||
TokenKindFunction
|
|
||||||
TokenKindNamedValue
|
|
||||||
TokenKindStartParameters
|
|
||||||
TokenKindEndParameters
|
|
||||||
TokenKindUnexpected
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token represents a single lexical token.
|
|
||||||
// Raw holds the original text, Value holds the parsed value when applicable.
|
|
||||||
// Index is the start position in the source string.
|
|
||||||
//
|
|
||||||
// The struct is intentionally minimal – it only contains what the parser
|
|
||||||
// needs. If you need more information (e.g. token length) you can add it.
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
Kind TokenKind
|
|
||||||
Raw string
|
|
||||||
Value any
|
|
||||||
Index int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lexer holds the state while tokenising an expression.
|
|
||||||
// It is a direct port of the C# LexicalAnalyzer.
|
|
||||||
//
|
|
||||||
// Flags can be used to enable/disable features – for now we only support
|
|
||||||
// a single flag that mirrors ExpressionFlags.DTExpressionsV1.
|
|
||||||
//
|
|
||||||
// The lexer is not thread‑safe – reuse a single instance per expression.
|
|
||||||
|
|
||||||
type Lexer struct {
|
|
||||||
expr string
|
|
||||||
flags int
|
|
||||||
index int
|
|
||||||
last *Token
|
|
||||||
stack []TokenKind // unclosed start tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLexer creates a new lexer for the given expression.
|
|
||||||
func NewLexer(expr string, flags int) *Lexer {
|
|
||||||
return &Lexer{expr: expr, flags: flags}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testTokenBoundary(c rune) bool {
|
|
||||||
switch c {
|
|
||||||
case '(', '[', ')', ']', ',', '.',
|
|
||||||
'!', '>', '<', '=', '&', '|':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return unicode.IsSpace(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns the next token or nil if the end of the expression is reached.
|
|
||||||
func (l *Lexer) Next() *Token {
|
|
||||||
// Skip whitespace
|
|
||||||
for l.index < len(l.expr) && unicode.IsSpace(rune(l.expr[l.index])) {
|
|
||||||
l.index++
|
|
||||||
}
|
|
||||||
if l.index >= len(l.expr) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := l.expr[l.index]
|
|
||||||
switch c {
|
|
||||||
case '(':
|
|
||||||
l.index++
|
|
||||||
// Function call or logical grouping
|
|
||||||
if l.last != nil && l.last.Kind == TokenKindFunction {
|
|
||||||
return l.createToken(TokenKindStartParameters, "(")
|
|
||||||
}
|
|
||||||
if l.flags&FlagV1 != 0 {
|
|
||||||
// V1 does not support grouping – treat as unexpected
|
|
||||||
return l.createToken(TokenKindUnexpected, "(")
|
|
||||||
}
|
|
||||||
return l.createToken(TokenKindStartGroup, "(")
|
|
||||||
case '[':
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindStartIndex, "[")
|
|
||||||
case ')':
|
|
||||||
l.index++
|
|
||||||
if len(l.stack) > 0 && l.stack[len(l.stack)-1] == TokenKindStartParameters {
|
|
||||||
return l.createToken(TokenKindEndParameters, ")")
|
|
||||||
}
|
|
||||||
return l.createToken(TokenKindEndGroup, ")")
|
|
||||||
case ']':
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindEndIndex, "]")
|
|
||||||
case ',':
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindSeparator, ",")
|
|
||||||
case '*':
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindWildcard, "*")
|
|
||||||
case '\'':
|
|
||||||
return l.readString()
|
|
||||||
case '!', '>', '<', '=', '&', '|':
|
|
||||||
if l.flags&FlagV1 != 0 {
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindUnexpected, string(c))
|
|
||||||
}
|
|
||||||
return l.readOperator()
|
|
||||||
default:
|
|
||||||
return l.defaultNext(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lexer) defaultNext(c byte) *Token {
|
|
||||||
if c == '.' {
|
|
||||||
// Could be number or dereference
|
|
||||||
if l.last == nil || l.last.Kind == TokenKindSeparator || l.last.Kind == TokenKindStartGroup || l.last.Kind == TokenKindStartIndex || l.last.Kind == TokenKindStartParameters || l.last.Kind == TokenKindLogicalOperator {
|
|
||||||
return l.readNumber()
|
|
||||||
}
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindDereference, ".")
|
|
||||||
}
|
|
||||||
if c == '-' || c == '+' || unicode.IsDigit(rune(c)) {
|
|
||||||
return l.readNumber()
|
|
||||||
}
|
|
||||||
return l.readKeyword()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create a token and update lexer state.
|
|
||||||
func (l *Lexer) createToken(kind TokenKind, raw string) *Token {
|
|
||||||
// Token order check
|
|
||||||
if !l.checkLastToken(kind, raw) {
|
|
||||||
// Illegal token sequence
|
|
||||||
return &Token{Kind: TokenKindUnexpected, Raw: raw, Index: l.index}
|
|
||||||
}
|
|
||||||
tok := &Token{Kind: kind, Raw: raw, Index: l.index}
|
|
||||||
l.last = tok
|
|
||||||
// Manage stack for grouping
|
|
||||||
switch kind {
|
|
||||||
case TokenKindStartGroup, TokenKindStartIndex, TokenKindStartParameters:
|
|
||||||
l.stack = append(l.stack, kind)
|
|
||||||
case TokenKindEndGroup, TokenKindEndIndex, TokenKindEndParameters:
|
|
||||||
if len(l.stack) > 0 {
|
|
||||||
l.stack = l.stack[:len(l.stack)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// nil last token represented by nil
|
|
||||||
func (l *Lexer) getLastKind() *TokenKind {
|
|
||||||
var lastKind *TokenKind
|
|
||||||
if l.last != nil {
|
|
||||||
lastKind = &l.last.Kind
|
|
||||||
}
|
|
||||||
return lastKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkLastToken verifies that the token sequence is legal based on the last token.
|
|
||||||
func (l *Lexer) checkLastToken(kind TokenKind, raw string) bool {
|
|
||||||
lastKind := l.getLastKind()
|
|
||||||
|
|
||||||
// Helper to check if lastKind is in allowed list
|
|
||||||
allowed := func(allowedKinds ...TokenKind) bool {
|
|
||||||
return lastKind != nil && slices.Contains(allowedKinds, *lastKind)
|
|
||||||
}
|
|
||||||
// For nil last, we treat as no previous token
|
|
||||||
// Define allowed previous kinds for each token kind
|
|
||||||
switch kind {
|
|
||||||
case TokenKindStartGroup:
|
|
||||||
return lastKind == nil || allowed(TokenKindSeparator, TokenKindStartGroup, TokenKindStartParameters, TokenKindStartIndex, TokenKindLogicalOperator)
|
|
||||||
case TokenKindStartIndex:
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindStartParameters:
|
|
||||||
return allowed(TokenKindFunction)
|
|
||||||
case TokenKindEndGroup:
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindEndIndex:
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindEndParameters:
|
|
||||||
return allowed(TokenKindStartParameters, TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindSeparator:
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindWildcard:
|
|
||||||
return allowed(TokenKindStartIndex, TokenKindDereference)
|
|
||||||
case TokenKindDereference:
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindLogicalOperator:
|
|
||||||
if raw == "!" { // "!"
|
|
||||||
return lastKind == nil || allowed(TokenKindSeparator, TokenKindStartGroup, TokenKindStartParameters, TokenKindStartIndex, TokenKindLogicalOperator)
|
|
||||||
}
|
|
||||||
return allowed(TokenKindEndGroup, TokenKindEndParameters, TokenKindEndIndex, TokenKindWildcard, TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString, TokenKindPropertyName, TokenKindNamedValue)
|
|
||||||
case TokenKindNull, TokenKindBoolean, TokenKindNumber, TokenKindString:
|
|
||||||
return lastKind == nil || allowed(TokenKindSeparator, TokenKindStartIndex, TokenKindStartGroup, TokenKindStartParameters, TokenKindLogicalOperator)
|
|
||||||
case TokenKindPropertyName:
|
|
||||||
return allowed(TokenKindDereference)
|
|
||||||
case TokenKindFunction, TokenKindNamedValue:
|
|
||||||
return lastKind == nil || allowed(TokenKindSeparator, TokenKindStartIndex, TokenKindStartGroup, TokenKindStartParameters, TokenKindLogicalOperator)
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readNumber parses a numeric literal.
|
|
||||||
func (l *Lexer) readNumber() *Token {
|
|
||||||
start := l.index
|
|
||||||
periods := 0
|
|
||||||
for l.index < len(l.expr) {
|
|
||||||
ch := l.expr[l.index]
|
|
||||||
if ch == '.' {
|
|
||||||
periods++
|
|
||||||
}
|
|
||||||
if testTokenBoundary(rune(ch)) && ch != '.' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
l.index++
|
|
||||||
}
|
|
||||||
raw := l.expr[start:l.index]
|
|
||||||
if len(raw) > 2 {
|
|
||||||
switch raw[:2] {
|
|
||||||
case "0x", "0o":
|
|
||||||
tok := l.createToken(TokenKindNumber, raw)
|
|
||||||
if i, err := strconv.ParseInt(raw, 0, 32); err == nil {
|
|
||||||
tok.Value = float64(i)
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Try to parse as float64
|
|
||||||
var val any = raw
|
|
||||||
if f, err := strconv.ParseFloat(raw, 64); err == nil {
|
|
||||||
val = f
|
|
||||||
}
|
|
||||||
tok := l.createToken(TokenKindNumber, raw)
|
|
||||||
tok.Value = val
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// readString parses a single‑quoted string literal.
|
|
||||||
func (l *Lexer) readString() *Token {
|
|
||||||
start := l.index
|
|
||||||
l.index++ // skip opening quote
|
|
||||||
var sb strings.Builder
|
|
||||||
closed := false
|
|
||||||
for l.index < len(l.expr) {
|
|
||||||
ch := l.expr[l.index]
|
|
||||||
l.index++
|
|
||||||
if ch == '\'' {
|
|
||||||
if l.index < len(l.expr) && l.expr[l.index] == '\'' {
|
|
||||||
// escaped quote
|
|
||||||
sb.WriteByte('\'')
|
|
||||||
l.index++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
closed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sb.WriteByte(ch)
|
|
||||||
}
|
|
||||||
raw := l.expr[start:l.index]
|
|
||||||
tok := l.createToken(TokenKindString, raw)
|
|
||||||
if closed {
|
|
||||||
tok.Value = sb.String()
|
|
||||||
} else {
|
|
||||||
tok.Kind = TokenKindUnexpected
|
|
||||||
}
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// readOperator parses logical operators (==, !=, >, >=, etc.).
|
|
||||||
func (l *Lexer) readOperator() *Token {
|
|
||||||
start := l.index
|
|
||||||
l.index++
|
|
||||||
if l.index < len(l.expr) {
|
|
||||||
two := l.expr[start : l.index+1]
|
|
||||||
switch two {
|
|
||||||
case "!=", ">=", "<=", "==", "&&", "||":
|
|
||||||
l.index++
|
|
||||||
return l.createToken(TokenKindLogicalOperator, two)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ch := l.expr[start]
|
|
||||||
switch ch {
|
|
||||||
case '!', '>', '<':
|
|
||||||
return l.createToken(TokenKindLogicalOperator, string(ch))
|
|
||||||
}
|
|
||||||
return l.createToken(TokenKindUnexpected, string(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// readKeyword parses identifiers, booleans, null, etc.
|
|
||||||
func (l *Lexer) readKeyword() *Token {
|
|
||||||
start := l.index
|
|
||||||
for l.index < len(l.expr) && !unicode.IsSpace(rune(l.expr[l.index])) && !strings.ContainsRune("()[],.!<>==&|*", rune(l.expr[l.index])) {
|
|
||||||
l.index++
|
|
||||||
}
|
|
||||||
raw := l.expr[start:l.index]
|
|
||||||
if l.last != nil && l.last.Kind == TokenKindDereference {
|
|
||||||
return l.createToken(TokenKindPropertyName, raw)
|
|
||||||
}
|
|
||||||
switch raw {
|
|
||||||
case "true":
|
|
||||||
tok := l.createToken(TokenKindBoolean, raw)
|
|
||||||
tok.Value = true
|
|
||||||
return tok
|
|
||||||
case "false":
|
|
||||||
tok := l.createToken(TokenKindBoolean, raw)
|
|
||||||
tok.Value = false
|
|
||||||
return tok
|
|
||||||
case "null":
|
|
||||||
return l.createToken(TokenKindNull, raw)
|
|
||||||
case "NaN":
|
|
||||||
tok := l.createToken(TokenKindNumber, raw)
|
|
||||||
tok.Value = math.NaN()
|
|
||||||
return tok
|
|
||||||
case "Infinity":
|
|
||||||
tok := l.createToken(TokenKindNumber, raw)
|
|
||||||
tok.Value = math.Inf(1)
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
if l.index < len(l.expr) && l.expr[l.index] == '(' {
|
|
||||||
return l.createToken(TokenKindFunction, raw)
|
|
||||||
}
|
|
||||||
return l.createToken(TokenKindNamedValue, raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag constants – only V1 is used for now.
|
|
||||||
const FlagV1 = 1
|
|
||||||
|
|
||||||
// UnclosedTokens returns the stack of unclosed start tokens.
|
|
||||||
func (l *Lexer) UnclosedTokens() []TokenKind {
|
|
||||||
return l.stack
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestLexerMultiple runs a set of expressions through the lexer and
|
|
||||||
// verifies that the produced token kinds and values match expectations.
|
|
||||||
func TestLexerMultiple(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
expr string
|
|
||||||
expected []TokenKind
|
|
||||||
values []any // optional, nil if not checking values
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
expr: "github.event_name == 'push'",
|
|
||||||
expected: []TokenKind{
|
|
||||||
TokenKindNamedValue, // github
|
|
||||||
TokenKindDereference,
|
|
||||||
TokenKindPropertyName, // event_name
|
|
||||||
TokenKindLogicalOperator, // ==
|
|
||||||
TokenKindString, // 'push'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "github.event_name == 'push' && github.ref == 'refs/heads/main'",
|
|
||||||
expected: []TokenKind{
|
|
||||||
TokenKindNamedValue, TokenKindDereference, TokenKindPropertyName, TokenKindLogicalOperator, TokenKindString,
|
|
||||||
TokenKindLogicalOperator, // &&
|
|
||||||
TokenKindNamedValue, TokenKindDereference, TokenKindPropertyName, TokenKindLogicalOperator, TokenKindString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "contains(github.ref, 'refs/heads/')",
|
|
||||||
expected: []TokenKind{
|
|
||||||
TokenKindFunction, // contains
|
|
||||||
TokenKindStartParameters,
|
|
||||||
TokenKindNamedValue, TokenKindDereference, TokenKindPropertyName, // github.ref
|
|
||||||
TokenKindSeparator,
|
|
||||||
TokenKindString,
|
|
||||||
TokenKindEndParameters,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "matrix[0].name",
|
|
||||||
expected: []TokenKind{
|
|
||||||
TokenKindNamedValue, // matrix
|
|
||||||
TokenKindStartIndex,
|
|
||||||
TokenKindNumber,
|
|
||||||
TokenKindEndIndex,
|
|
||||||
TokenKindDereference,
|
|
||||||
TokenKindPropertyName, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "github.*",
|
|
||||||
expected: []TokenKind{
|
|
||||||
TokenKindNamedValue, TokenKindDereference, TokenKindWildcard,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "null",
|
|
||||||
expected: []TokenKind{TokenKindNull},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "true",
|
|
||||||
expected: []TokenKind{TokenKindBoolean},
|
|
||||||
values: []any{true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "123",
|
|
||||||
expected: []TokenKind{TokenKindNumber},
|
|
||||||
values: []any{123.0},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "(a && b)",
|
|
||||||
expected: []TokenKind{TokenKindStartGroup, TokenKindNamedValue, TokenKindLogicalOperator, TokenKindNamedValue, TokenKindEndGroup},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "[1,2]", // Syntax Error
|
|
||||||
expected: []TokenKind{TokenKindUnexpected, TokenKindNumber, TokenKindSeparator, TokenKindNumber, TokenKindEndIndex},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: "'Hello i''s escaped'",
|
|
||||||
expected: []TokenKind{TokenKindString},
|
|
||||||
values: []any{"Hello i's escaped"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
lexer := NewLexer(tc.expr, 0)
|
|
||||||
var tokens []*Token
|
|
||||||
for {
|
|
||||||
tok := lexer.Next()
|
|
||||||
if tok == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens = append(tokens, tok)
|
|
||||||
}
|
|
||||||
assert.Len(t, tokens, len(tc.expected), "expression: %s", tc.expr)
|
|
||||||
for i, kind := range tc.expected {
|
|
||||||
assert.Equal(t, kind, tokens[i].Kind, "expr %s token %d", tc.expr, i)
|
|
||||||
}
|
|
||||||
if tc.values != nil {
|
|
||||||
for i, val := range tc.values {
|
|
||||||
assert.Equal(t, val, tokens[i].Value, "expr %s token %d value", tc.expr, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLexer(t *testing.T) {
|
|
||||||
input := "github.event_name == 'push' && github.ref == 'refs/heads/main'"
|
|
||||||
lexer := NewLexer(input, 0)
|
|
||||||
var tokens []*Token
|
|
||||||
for {
|
|
||||||
tok := lexer.Next()
|
|
||||||
if tok == nil || tok.Kind == TokenKindUnexpected {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens = append(tokens, tok)
|
|
||||||
}
|
|
||||||
for i, tok := range tokens {
|
|
||||||
t.Logf("Token %d: Kind=%v, Value=%v", i, tok.Kind, tok.Value)
|
|
||||||
}
|
|
||||||
assert.Equal(t, TokenKindDereference, tokens[1].Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLexerNumbers(t *testing.T) {
|
|
||||||
table := []struct {
|
|
||||||
in string
|
|
||||||
out any
|
|
||||||
}{
|
|
||||||
{"-Infinity", math.Inf(-1)},
|
|
||||||
{"Infinity", math.Inf(1)},
|
|
||||||
{"2.5", float64(2.5)},
|
|
||||||
{"3.3", float64(3.3)},
|
|
||||||
{"1", float64(1)},
|
|
||||||
{"-1", float64(-1)},
|
|
||||||
{"0x34", float64(0x34)},
|
|
||||||
{"0o34", float64(0o34)},
|
|
||||||
}
|
|
||||||
for _, cs := range table {
|
|
||||||
lexer := NewLexer(cs.in, 0)
|
|
||||||
var tokens []*Token
|
|
||||||
for {
|
|
||||||
tok := lexer.Next()
|
|
||||||
if tok == nil || tok.Kind == TokenKindUnexpected {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens = append(tokens, tok)
|
|
||||||
}
|
|
||||||
require.Len(t, tokens, 1)
|
|
||||||
assert.Equal(t, cs.out, tokens[0].Value)
|
|
||||||
assert.Equal(t, cs.in, tokens[0].Raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assumes there is no cycle ensured via test TestVerifyCycleIsInvalid
|
|
||||||
func resolveAliases(node *yaml.Node) error {
|
|
||||||
switch node.Kind {
|
|
||||||
case yaml.AliasNode:
|
|
||||||
aliasTarget := node.Alias
|
|
||||||
if aliasTarget == nil {
|
|
||||||
return errors.New("unresolved alias node")
|
|
||||||
}
|
|
||||||
*node = *aliasTarget
|
|
||||||
if err := resolveAliases(node); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
|
|
||||||
for _, child := range node.Content {
|
|
||||||
if err := resolveAliases(child); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"maps"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TraceWriter is an interface for logging trace information.
|
|
||||||
// Implementations can write to console, file, or any other sink.
|
|
||||||
type TraceWriter interface {
|
|
||||||
Info(format string, args ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrategyResult holds the result of expanding a strategy.
|
|
||||||
// FlatMatrix contains the expanded matrix entries.
|
|
||||||
// IncludeMatrix contains entries that were added via include.
|
|
||||||
// FailFast indicates whether the job should fail fast.
|
|
||||||
// MaxParallel is the maximum parallelism allowed.
|
|
||||||
// MatrixKeys is the set of keys present in the matrix.
|
|
||||||
type StrategyResult struct {
|
|
||||||
FlatMatrix []map[string]yaml.Node
|
|
||||||
IncludeMatrix []map[string]yaml.Node
|
|
||||||
FailFast bool
|
|
||||||
MaxParallel *float64
|
|
||||||
MatrixKeys map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type strategyContext struct {
|
|
||||||
jobTraceWriter TraceWriter
|
|
||||||
failFast bool
|
|
||||||
maxParallel float64
|
|
||||||
matrix map[string][]yaml.Node
|
|
||||||
|
|
||||||
flatMatrix []map[string]yaml.Node
|
|
||||||
includeMatrix []map[string]yaml.Node
|
|
||||||
|
|
||||||
include []yaml.Node
|
|
||||||
exclude []yaml.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (strategyContext *strategyContext) handleInclude() error {
|
|
||||||
// Handle include logic
|
|
||||||
if len(strategyContext.include) > 0 {
|
|
||||||
for _, incNode := range strategyContext.include {
|
|
||||||
if incNode.Kind != yaml.MappingNode {
|
|
||||||
return errors.New("include entry is not a mapping node")
|
|
||||||
}
|
|
||||||
incMap := make(map[string]yaml.Node)
|
|
||||||
for i := 0; i < len(incNode.Content); i += 2 {
|
|
||||||
keyNode := incNode.Content[i]
|
|
||||||
valNode := incNode.Content[i+1]
|
|
||||||
if keyNode.Kind != yaml.ScalarNode {
|
|
||||||
return errors.New("include key is not scalar")
|
|
||||||
}
|
|
||||||
incMap[keyNode.Value] = *valNode
|
|
||||||
}
|
|
||||||
matched := false
|
|
||||||
for _, row := range strategyContext.flatMatrix {
|
|
||||||
match := true
|
|
||||||
for k, v := range incMap {
|
|
||||||
if rv, ok := row[k]; ok && !nodesEqual(rv, v) {
|
|
||||||
match = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
matched = true
|
|
||||||
// Add missing keys
|
|
||||||
strategyContext.jobTraceWriter.Info("Add missing keys %v", incMap)
|
|
||||||
for k, v := range incMap {
|
|
||||||
if _, ok := row[k]; !ok {
|
|
||||||
row[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
if strategyContext.jobTraceWriter != nil {
|
|
||||||
strategyContext.jobTraceWriter.Info("Append include entry %v", incMap)
|
|
||||||
}
|
|
||||||
strategyContext.includeMatrix = append(strategyContext.includeMatrix, incMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (strategyContext *strategyContext) handleExclude() error {
|
|
||||||
// Handle exclude logic
|
|
||||||
if len(strategyContext.exclude) > 0 {
|
|
||||||
for _, exNode := range strategyContext.exclude {
|
|
||||||
// exNode is expected to be a mapping node
|
|
||||||
if exNode.Kind != yaml.MappingNode {
|
|
||||||
return errors.New("exclude entry is not a mapping node")
|
|
||||||
}
|
|
||||||
// Convert mapping to map[string]yaml.Node
|
|
||||||
exMap := make(map[string]yaml.Node)
|
|
||||||
for i := 0; i < len(exNode.Content); i += 2 {
|
|
||||||
keyNode := exNode.Content[i]
|
|
||||||
valNode := exNode.Content[i+1]
|
|
||||||
if keyNode.Kind != yaml.ScalarNode {
|
|
||||||
return errors.New("exclude key is not scalar")
|
|
||||||
}
|
|
||||||
exMap[keyNode.Value] = *valNode
|
|
||||||
}
|
|
||||||
// Remove matching rows
|
|
||||||
filtered := []map[string]yaml.Node{}
|
|
||||||
for _, row := range strategyContext.flatMatrix {
|
|
||||||
match := true
|
|
||||||
for k, v := range exMap {
|
|
||||||
if rv, ok := row[k]; !ok || !nodesEqual(rv, v) {
|
|
||||||
match = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !match {
|
|
||||||
filtered = append(filtered, row)
|
|
||||||
} else if strategyContext.jobTraceWriter != nil {
|
|
||||||
strategyContext.jobTraceWriter.Info("Removing %v from matrix due to exclude entry %v", row, exMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strategyContext.flatMatrix = filtered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandStrategy expands the given strategy into a flat matrix and include matrix.
|
|
||||||
// It mimics the behavior of the C# StrategyUtils. The strategy parameter is expected
|
|
||||||
// to be populated from a YAML mapping that follows the GitHub Actions strategy schema.
|
|
||||||
func ExpandStrategy(strategy *Strategy, jobTraceWriter TraceWriter) (*StrategyResult, error) {
|
|
||||||
if strategy == nil {
|
|
||||||
return &StrategyResult{FlatMatrix: []map[string]yaml.Node{{}}, IncludeMatrix: []map[string]yaml.Node{}, FailFast: true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize defaults
|
|
||||||
strategyContext := &strategyContext{
|
|
||||||
jobTraceWriter: jobTraceWriter,
|
|
||||||
failFast: strategy.FailFast,
|
|
||||||
maxParallel: strategy.MaxParallel,
|
|
||||||
matrix: strategy.Matrix,
|
|
||||||
flatMatrix: []map[string]yaml.Node{{}},
|
|
||||||
}
|
|
||||||
// Process matrix entries
|
|
||||||
for key, values := range strategyContext.matrix {
|
|
||||||
switch key {
|
|
||||||
case "include":
|
|
||||||
strategyContext.include = values
|
|
||||||
case "exclude":
|
|
||||||
strategyContext.exclude = values
|
|
||||||
default:
|
|
||||||
// Other keys are treated as matrix dimensions
|
|
||||||
// Expand each existing row with the new key/value pairs
|
|
||||||
next := []map[string]yaml.Node{}
|
|
||||||
for _, row := range strategyContext.flatMatrix {
|
|
||||||
for _, val := range values {
|
|
||||||
newRow := make(map[string]yaml.Node)
|
|
||||||
maps.Copy(newRow, row)
|
|
||||||
newRow[key] = val
|
|
||||||
next = append(next, newRow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strategyContext.flatMatrix = next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := strategyContext.handleExclude(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strategyContext.flatMatrix) == 0 {
|
|
||||||
if jobTraceWriter != nil {
|
|
||||||
jobTraceWriter.Info("Matrix is empty, adding an empty entry")
|
|
||||||
}
|
|
||||||
strategyContext.flatMatrix = []map[string]yaml.Node{{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce job matrix limit of github
|
|
||||||
if len(strategyContext.flatMatrix) > 256 {
|
|
||||||
if jobTraceWriter != nil {
|
|
||||||
jobTraceWriter.Info("Failure: Matrix contains more than 256 entries after exclude")
|
|
||||||
}
|
|
||||||
return nil, errors.New("matrix contains more than 256 entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build matrix keys set
|
|
||||||
matrixKeys := make(map[string]struct{})
|
|
||||||
if len(strategyContext.flatMatrix) > 0 {
|
|
||||||
for k := range strategyContext.flatMatrix[0] {
|
|
||||||
matrixKeys[k] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := strategyContext.handleInclude(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &StrategyResult{
|
|
||||||
FlatMatrix: strategyContext.flatMatrix,
|
|
||||||
IncludeMatrix: strategyContext.includeMatrix,
|
|
||||||
FailFast: strategyContext.failFast,
|
|
||||||
MaxParallel: &strategyContext.maxParallel,
|
|
||||||
MatrixKeys: matrixKeys,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodesEqual compares two yaml.Node values for equality.
|
|
||||||
func nodesEqual(a, b yaml.Node) bool {
|
|
||||||
return DeepEquals(a, b, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefaultDisplaySuffix returns a string like "(foo, bar, baz)".
|
|
||||||
// Empty items are ignored. If all items are empty the result is "".
|
|
||||||
func GetDefaultDisplaySuffix(items []string) string {
|
|
||||||
var b strings.Builder // efficient string concatenation
|
|
||||||
|
|
||||||
first := true // true until we write the first non‑empty item
|
|
||||||
|
|
||||||
for _, mk := range items {
|
|
||||||
if mk == "" { // Go has no null string, so we only need to check for empty
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if first {
|
|
||||||
b.WriteString("(")
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
b.WriteString(", ")
|
|
||||||
}
|
|
||||||
b.WriteString(mk)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first { // we wrote at least one item
|
|
||||||
b.WriteString(")")
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmptyTraceWriter struct{}
|
|
||||||
|
|
||||||
func (e *EmptyTraceWriter) Info(_ string, _ ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStrategy(t *testing.T) {
|
|
||||||
table := []struct {
|
|
||||||
content string
|
|
||||||
flatmatrix int
|
|
||||||
includematrix int
|
|
||||||
}{
|
|
||||||
{`
|
|
||||||
matrix:
|
|
||||||
label:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
fields:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
`, 4, 0},
|
|
||||||
{
|
|
||||||
`
|
|
||||||
matrix:
|
|
||||||
label:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
include:
|
|
||||||
- label: a
|
|
||||||
x: self`, 2, 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`
|
|
||||||
matrix:
|
|
||||||
label:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
include:
|
|
||||||
- label: c
|
|
||||||
x: self`, 2, 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`
|
|
||||||
matrix:
|
|
||||||
label:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
exclude:
|
|
||||||
- label: a`, 1, 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range table {
|
|
||||||
var strategy Strategy
|
|
||||||
err := yaml.Unmarshal([]byte(tc.content), &strategy)
|
|
||||||
require.NoError(t, err)
|
|
||||||
res, err := ExpandStrategy(&strategy, &EmptyTraceWriter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, res.FlatMatrix, tc.flatmatrix)
|
|
||||||
require.Len(t, res.IncludeMatrix, tc.includematrix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
v2 "gitea.com/gitea/act_runner/internal/eval/v2"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeepEquals compares two yaml.Node values recursively.
|
|
||||||
// It supports scalar, mapping and sequence nodes and allows
|
|
||||||
// an optional partial match for mappings and sequences.
|
|
||||||
func DeepEquals(a, b yaml.Node, partialMatch bool) bool {
|
|
||||||
// Scalar comparison
|
|
||||||
if a.Kind == yaml.ScalarNode && b.Kind == yaml.ScalarNode {
|
|
||||||
return scalarEquals(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapping comparison
|
|
||||||
if a.Kind == yaml.MappingNode && b.Kind == yaml.MappingNode {
|
|
||||||
return deepMapEquals(a, b, partialMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sequence comparison
|
|
||||||
if a.Kind == yaml.SequenceNode && b.Kind == yaml.SequenceNode {
|
|
||||||
return deepSequenceEquals(a, b, partialMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kinds are not equal
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func scalarEquals(a, b yaml.Node) bool {
|
|
||||||
var left, right any
|
|
||||||
return a.Decode(&left) == nil && b.Decode(&right) == nil && v2.CreateIntermediateResult(v2.NewEvaluationContext(), left).AbstractEqual(v2.CreateIntermediateResult(v2.NewEvaluationContext(), right))
|
|
||||||
}
|
|
||||||
|
|
||||||
func deepMapEquals(a, b yaml.Node, partialMatch bool) bool {
|
|
||||||
mapA := make(map[string]yaml.Node)
|
|
||||||
for i := 0; i < len(a.Content); i += 2 {
|
|
||||||
keyNode := a.Content[i]
|
|
||||||
valNode := a.Content[i+1]
|
|
||||||
if keyNode.Kind != yaml.ScalarNode {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mapA[strings.ToLower(keyNode.Value)] = *valNode
|
|
||||||
}
|
|
||||||
mapB := make(map[string]yaml.Node)
|
|
||||||
for i := 0; i < len(b.Content); i += 2 {
|
|
||||||
keyNode := b.Content[i]
|
|
||||||
valNode := b.Content[i+1]
|
|
||||||
if keyNode.Kind != yaml.ScalarNode {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mapB[strings.ToLower(keyNode.Value)] = *valNode
|
|
||||||
}
|
|
||||||
if partialMatch {
|
|
||||||
if len(mapA) < len(mapB) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(mapA) != len(mapB) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k, vB := range mapB {
|
|
||||||
vA, ok := mapA[k]
|
|
||||||
if !ok || !DeepEquals(vA, vB, partialMatch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func deepSequenceEquals(a, b yaml.Node, partialMatch bool) bool {
|
|
||||||
if partialMatch {
|
|
||||||
if len(a.Content) < len(b.Content) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(a.Content) != len(b.Content) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
limit := len(b.Content)
|
|
||||||
if !partialMatch {
|
|
||||||
limit = len(a.Content)
|
|
||||||
}
|
|
||||||
for i := 0; i < limit; i++ {
|
|
||||||
if !DeepEquals(*a.Content[i], *b.Content[i], partialMatch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// traverse walks a YAML node recursively.
|
|
||||||
func traverse(node *yaml.Node, omitKeys bool, result *[]*yaml.Node) {
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*result = append(*result, node)
|
|
||||||
|
|
||||||
switch node.Kind {
|
|
||||||
case yaml.MappingNode:
|
|
||||||
if omitKeys {
|
|
||||||
// node.Content: key0, val0, key1, val1, …
|
|
||||||
for i := 1; i < len(node.Content); i += 2 { // only the values
|
|
||||||
traverse(node.Content[i], omitKeys, result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, child := range node.Content {
|
|
||||||
traverse(child, omitKeys, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case yaml.SequenceNode:
|
|
||||||
// For all other node kinds (Scalar, Sequence, Alias, etc.)
|
|
||||||
for _, child := range node.Content {
|
|
||||||
traverse(child, omitKeys, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings implements the LINQ expression:
|
|
||||||
//
|
|
||||||
// from displayitem in keys.SelectMany(key => item[key].Traverse(true))
|
|
||||||
// where !(displayitem is SequenceToken || displayitem is MappingToken)
|
|
||||||
// select displayitem.ToString()
|
|
||||||
func GetDisplayStrings(keys []string, item map[string]*yaml.Node) []string {
|
|
||||||
var res []string
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
if node, ok := item[k]; ok {
|
|
||||||
var all []*yaml.Node
|
|
||||||
traverse(node, true, &all) // include the parent node itself
|
|
||||||
|
|
||||||
for _, n := range all {
|
|
||||||
// Keep only scalars – everything else is dropped
|
|
||||||
if n.Kind == yaml.ScalarNode {
|
|
||||||
res = append(res, n.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
type JobStatus int
|
|
||||||
|
|
||||||
const (
|
|
||||||
JobStatusPending JobStatus = iota
|
|
||||||
JobStatusDependenciesReady
|
|
||||||
JobStatusBlocked
|
|
||||||
JobStatusCompleted
|
|
||||||
)
|
|
||||||
|
|
||||||
type JobState struct {
|
|
||||||
JobID string // Workflow path to job, incl matrix and parent jobids
|
|
||||||
Result string // Actions Job Result
|
|
||||||
Outputs map[string]string // Returned Outputs
|
|
||||||
State JobStatus
|
|
||||||
Strategy []MatrixJobState
|
|
||||||
}
|
|
||||||
|
|
||||||
type MatrixJobState struct {
|
|
||||||
Matrix map[string]any
|
|
||||||
Name string
|
|
||||||
Result string
|
|
||||||
Outputs map[string]string // Returned Outputs
|
|
||||||
State JobStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkflowStatus int
|
|
||||||
|
|
||||||
const (
|
|
||||||
WorkflowStatusPending WorkflowStatus = iota
|
|
||||||
WorkflowStatusDependenciesReady
|
|
||||||
WorkflowStatusBlocked
|
|
||||||
WorkflowStatusCompleted
|
|
||||||
)
|
|
||||||
|
|
||||||
type WorkflowState struct {
|
|
||||||
Name string
|
|
||||||
RunName string
|
|
||||||
Jobs JobState
|
|
||||||
StateWorkflowStatus WorkflowStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
type Workflow struct {
|
|
||||||
On *On `yaml:"on,omitempty"`
|
|
||||||
Name string `yaml:"name,omitempty"`
|
|
||||||
Description string `yaml:"description,omitempty"`
|
|
||||||
RunName yaml.Node `yaml:"run-name,omitempty"`
|
|
||||||
Permissions *Permissions `yaml:"permissions,omitempty"`
|
|
||||||
Env yaml.Node `yaml:"env,omitempty"`
|
|
||||||
Defaults yaml.Node `yaml:"defaults,omitempty"`
|
|
||||||
Concurrency yaml.Node `yaml:"concurrency,omitempty"` // Two layouts
|
|
||||||
Jobs map[string]Job `yaml:"jobs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type On struct {
|
|
||||||
Data map[string]yaml.Node `yaml:"-"`
|
|
||||||
WorkflowDispatch *WorkflowDispatch `yaml:"workflow_dispatch,omitempty"`
|
|
||||||
WorkflowCall *WorkflowCall `yaml:"workflow_call,omitempty"`
|
|
||||||
Schedule []Cron `yaml:"schedule,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cron struct {
|
|
||||||
Cron string `yaml:"cron,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *On) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
switch node.Kind {
|
|
||||||
case yaml.ScalarNode:
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.Data = map[string]yaml.Node{}
|
|
||||||
a.Data[s] = yaml.Node{}
|
|
||||||
case yaml.SequenceNode:
|
|
||||||
var s []string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.Data = map[string]yaml.Node{}
|
|
||||||
for _, v := range s {
|
|
||||||
a.Data[v] = yaml.Node{}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := node.Decode(&a.Data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
type OnObj On
|
|
||||||
if err := node.Decode((*OnObj)(a)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *On) MarshalYAML() (any, error) {
|
|
||||||
return a.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ yaml.Unmarshaler = &On{}
|
|
||||||
_ yaml.Marshaler = &On{}
|
|
||||||
_ yaml.Unmarshaler = &Concurrency{}
|
|
||||||
_ yaml.Unmarshaler = &RunsOn{}
|
|
||||||
_ yaml.Unmarshaler = &ImplicitStringArray{}
|
|
||||||
_ yaml.Unmarshaler = &Environment{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type WorkflowDispatch struct {
|
|
||||||
Inputs map[string]Input `yaml:"inputs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Input struct {
|
|
||||||
Description string `yaml:"description,omitempty"`
|
|
||||||
Type string `yaml:"type,omitempty"`
|
|
||||||
Default string `yaml:"default,omitempty"`
|
|
||||||
Required bool `yaml:"required,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkflowCall struct {
|
|
||||||
Inputs map[string]Input `yaml:"inputs,omitempty"`
|
|
||||||
Secrets map[string]Secret `yaml:"secrets,omitempty"`
|
|
||||||
Outputs map[string]Output `yaml:"outputs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Secret struct {
|
|
||||||
Description string `yaml:"description,omitempty"`
|
|
||||||
Required bool `yaml:"required,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Output struct {
|
|
||||||
Description string `yaml:"description,omitempty"`
|
|
||||||
Value yaml.Node `yaml:"value,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Job struct {
|
|
||||||
Needs ImplicitStringArray `yaml:"needs,omitempty"`
|
|
||||||
Permissions *Permissions `yaml:"permissions,omitempty"`
|
|
||||||
Strategy yaml.Node `yaml:"strategy,omitempty"`
|
|
||||||
Name yaml.Node `yaml:"name,omitempty"`
|
|
||||||
Concurrency yaml.Node `yaml:"concurrency,omitempty"`
|
|
||||||
// Reusable Workflow
|
|
||||||
Uses yaml.Node `yaml:"uses,omitempty"`
|
|
||||||
With yaml.Node `yaml:"with,omitempty"`
|
|
||||||
Secrets yaml.Node `yaml:"secrets,omitempty"`
|
|
||||||
// Runner Job
|
|
||||||
RunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
|
||||||
Defaults yaml.Node `yaml:"defaults,omitempty"`
|
|
||||||
TimeoutMinutes yaml.Node `yaml:"timeout-minutes,omitempty"`
|
|
||||||
Container yaml.Node `yaml:"container,omitempty"`
|
|
||||||
Services yaml.Node `yaml:"services,omitempty"`
|
|
||||||
Env yaml.Node `yaml:"env,omitempty"`
|
|
||||||
Steps []yaml.Node `yaml:"steps,omitempty"`
|
|
||||||
Outputs yaml.Node `yaml:"outputs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImplicitStringArray []string
|
|
||||||
|
|
||||||
func (a *ImplicitStringArray) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*a = []string{s}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return node.Decode((*[]string)(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Permissions map[string]string
|
|
||||||
|
|
||||||
func (p *Permissions) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var perm string
|
|
||||||
switch s {
|
|
||||||
case "read-all":
|
|
||||||
perm = "read"
|
|
||||||
case "write-all":
|
|
||||||
perm = "write"
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
(*p)["actions"] = perm
|
|
||||||
(*p)["attestations"] = perm
|
|
||||||
(*p)["contents"] = perm
|
|
||||||
(*p)["checks"] = perm
|
|
||||||
(*p)["deployments"] = perm
|
|
||||||
(*p)["discussions"] = perm
|
|
||||||
(*p)["id-token"] = perm
|
|
||||||
(*p)["issues"] = perm
|
|
||||||
(*p)["models"] = perm
|
|
||||||
(*p)["packages"] = perm
|
|
||||||
(*p)["pages"] = perm
|
|
||||||
(*p)["pull-requests"] = perm
|
|
||||||
(*p)["repository-projects"] = perm
|
|
||||||
(*p)["security-events"] = perm
|
|
||||||
(*p)["statuses"] = perm
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return node.Decode((*map[string]string)(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Strategy struct {
|
|
||||||
Matrix map[string][]yaml.Node `yaml:"matrix"`
|
|
||||||
MaxParallel float64 `yaml:"max-parallel"`
|
|
||||||
FailFast bool `yaml:"fail-fast"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Concurrency struct {
|
|
||||||
Group string `yaml:"group"`
|
|
||||||
CancelInProgress bool `yaml:"cancel-in-progress"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Concurrency) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Group = s
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
type ConcurrencyObj Concurrency
|
|
||||||
return node.Decode((*ConcurrencyObj)(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Environment struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
URL yaml.Node `yaml:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.Name = s
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
type EnvironmentObj Environment
|
|
||||||
return node.Decode((*EnvironmentObj)(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunsOn struct {
|
|
||||||
Labels []string `yaml:"labels"`
|
|
||||||
Group string `yaml:"group,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *RunsOn) UnmarshalYAML(node *yaml.Node) error {
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
var s string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.Labels = []string{s}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if node.Kind == yaml.SequenceNode {
|
|
||||||
var s []string
|
|
||||||
if err := node.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.Labels = s
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
type RunsOnObj RunsOn
|
|
||||||
return node.Decode((*RunsOnObj)(a))
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v2 "gitea.com/gitea/act_runner/internal/eval/v2"
|
|
||||||
"gitea.com/gitea/act_runner/internal/templateeval"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseWorkflow(t *testing.T) {
|
|
||||||
ee := &templateeval.ExpressionEvaluator{
|
|
||||||
EvaluationContext: v2.EvaluationContext{
|
|
||||||
Variables: v2.CaseInsensitiveObject[any]{},
|
|
||||||
Functions: v2.GetFunctions(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var node yaml.Node
|
|
||||||
err := yaml.Unmarshal([]byte(`
|
|
||||||
on: push
|
|
||||||
run-name: ${{ fromjson('{}') }}
|
|
||||||
jobs:
|
|
||||||
_:
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- run: echo Hello World
|
|
||||||
env:
|
|
||||||
TAG: ${{ env.global }}
|
|
||||||
`), &node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ee.RestrictEval = true
|
|
||||||
ee.EvaluationContext.Variables = v2.CaseInsensitiveObject[any]{
|
|
||||||
"github": v2.CaseInsensitiveObject[any]{
|
|
||||||
"ref_name": "self",
|
|
||||||
},
|
|
||||||
"vars": v2.CaseInsensitiveObject[any]{},
|
|
||||||
"inputs": v2.CaseInsensitiveObject[any]{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
var myw Workflow
|
|
||||||
require.NoError(t, node.Decode(&myw))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseWorkflowCall(t *testing.T) {
|
|
||||||
ee := &templateeval.ExpressionEvaluator{
|
|
||||||
EvaluationContext: v2.EvaluationContext{
|
|
||||||
Variables: v2.CaseInsensitiveObject[any]{},
|
|
||||||
Functions: v2.GetFunctions(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var node yaml.Node
|
|
||||||
// jobs.test.outputs.test
|
|
||||||
err := yaml.Unmarshal([]byte(`
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
outputs:
|
|
||||||
test:
|
|
||||||
value: ${{ jobs.test.outputs.test }} # tojson(vars.raw)
|
|
||||||
run-name: ${{ github.ref_name }}
|
|
||||||
jobs:
|
|
||||||
_:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- run: echo Hello World
|
|
||||||
env:
|
|
||||||
TAG: ${{ env.global }}
|
|
||||||
`), &node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, resolveAliases(node.Content[0]))
|
|
||||||
require.NoError(t, (&schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
}).UnmarshalYAML(node.Content[0]))
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var raw any
|
|
||||||
err = node.Content[0].Decode(&raw)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ee.RestrictEval = true
|
|
||||||
ee.EvaluationContext.Variables = v2.CaseInsensitiveObject[any]{
|
|
||||||
"github": v2.CaseInsensitiveObject[any]{
|
|
||||||
"ref_name": "self",
|
|
||||||
},
|
|
||||||
"vars": v2.CaseInsensitiveObject[any]{
|
|
||||||
"raw": raw,
|
|
||||||
},
|
|
||||||
"inputs": v2.CaseInsensitiveObject[any]{},
|
|
||||||
"jobs": v2.CaseInsensitiveObject[any]{
|
|
||||||
"test": v2.CaseInsensitiveObject[any]{
|
|
||||||
"outputs": v2.CaseInsensitiveObject[any]{
|
|
||||||
"test": "Hello World",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
RestrictEval: true,
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
var myw Workflow
|
|
||||||
require.NoError(t, node.Decode(&myw))
|
|
||||||
workflowCall := myw.On.WorkflowCall
|
|
||||||
if workflowCall != nil {
|
|
||||||
for _, out := range workflowCall.Outputs {
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), &out.Value, &schema.Node{
|
|
||||||
RestrictEval: true,
|
|
||||||
Definition: "workflow-output-context",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "Hello World", out.Value.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, err := yaml.Marshal(&myw)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, out)
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||||
@@ -15,16 +16,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||||
|
transport := &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
MaxIdleConnsPerHost: 10, // All requests go to one host; default is 2 which causes frequent reconnects.
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
}
|
||||||
if strings.HasPrefix(endpoint, "https://") && insecure {
|
if strings.HasPrefix(endpoint, "https://") && insecure {
|
||||||
return &http.Client{
|
transport.TLSClientConfig = &tls.Config{
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return http.DefaultClient
|
return &http.Client{Transport: transport}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new runner client.
|
// New returns a new runner client.
|
||||||
@@ -47,14 +49,15 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
|||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
httpClient := getHTTPClient(endpoint, insecure)
|
||||||
return &HTTPClient{
|
return &HTTPClient{
|
||||||
PingServiceClient: pingv1connect.NewPingServiceClient(
|
PingServiceClient: pingv1connect.NewPingServiceClient(
|
||||||
getHTTPClient(endpoint, insecure),
|
httpClient,
|
||||||
baseURL,
|
baseURL,
|
||||||
opts...,
|
opts...,
|
||||||
),
|
),
|
||||||
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
|
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
|
||||||
getHTTPClient(endpoint, insecure),
|
httpClient,
|
||||||
baseURL,
|
baseURL,
|
||||||
opts...,
|
opts...,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ runner:
|
|||||||
fetch_timeout: 5s
|
fetch_timeout: 5s
|
||||||
# The interval for fetching the job from the Gitea instance.
|
# The interval for fetching the job from the Gitea instance.
|
||||||
fetch_interval: 2s
|
fetch_interval: 2s
|
||||||
|
# The maximum interval for fetching the job from the Gitea instance.
|
||||||
|
# The runner uses exponential backoff when idle, increasing the interval up to this maximum.
|
||||||
|
# Set to 0 or same as fetch_interval to disable backoff.
|
||||||
|
fetch_interval_max: 60s
|
||||||
|
# The base interval for periodic log flush to the Gitea instance.
|
||||||
|
# Logs may be sent earlier if the buffer reaches log_report_batch_size
|
||||||
|
# or if log_report_max_latency expires after the first buffered row.
|
||||||
|
log_report_interval: 5s
|
||||||
|
# The maximum time a log row can wait before being sent.
|
||||||
|
# This ensures even a single log line appears on the frontend within this duration.
|
||||||
|
# Must be less than log_report_interval to have any effect.
|
||||||
|
log_report_max_latency: 3s
|
||||||
|
# Flush logs immediately when the buffer reaches this many rows.
|
||||||
|
# This ensures bursty output (e.g., npm install) is delivered promptly.
|
||||||
|
log_report_batch_size: 100
|
||||||
|
# The interval for reporting task state (step status, timing) to the Gitea instance.
|
||||||
|
# State is also reported immediately on step transitions (start/stop).
|
||||||
|
state_report_interval: 5s
|
||||||
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
|
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
|
||||||
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
|
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
|
||||||
# and github_mirror is not empty. In this case,
|
# and github_mirror is not empty. In this case,
|
||||||
@@ -103,8 +121,23 @@ container:
|
|||||||
require_docker: false
|
require_docker: false
|
||||||
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
||||||
docker_timeout: 0s
|
docker_timeout: 0s
|
||||||
|
# Bind the workspace to the host filesystem instead of using Docker volumes.
|
||||||
|
# This is required for Docker-in-Docker (DinD) setups when jobs use docker compose
|
||||||
|
# with bind mounts (e.g., ".:/app"), as volume-based workspaces are not accessible
|
||||||
|
# from the DinD daemon's filesystem. When enabled, ensure the workspace parent
|
||||||
|
# directory is also mounted into the runner container and listed in valid_volumes.
|
||||||
|
bind_workdir: false
|
||||||
|
|
||||||
host:
|
host:
|
||||||
# The parent directory of a job's working directory.
|
# The parent directory of a job's working directory.
|
||||||
# If it's empty, $HOME/.cache/act/ will be used.
|
# If it's empty, $HOME/.cache/act/ will be used.
|
||||||
workdir_parent:
|
workdir_parent:
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
# Enable the Prometheus metrics endpoint.
|
||||||
|
# When enabled, metrics are served at http://<addr>/metrics and a liveness check at /healthz.
|
||||||
|
enabled: false
|
||||||
|
# The address for the metrics HTTP server to listen on.
|
||||||
|
# Defaults to localhost only. Set to ":9101" to allow external access,
|
||||||
|
# but ensure the port is firewall-protected as there is no authentication.
|
||||||
|
addr: "127.0.0.1:9101"
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ type Runner struct {
|
|||||||
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
|
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
|
||||||
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
|
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
|
||||||
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
|
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
|
||||||
|
FetchIntervalMax time.Duration `yaml:"fetch_interval_max"` // FetchIntervalMax specifies the maximum backoff interval when idle.
|
||||||
|
LogReportInterval time.Duration `yaml:"log_report_interval"` // LogReportInterval specifies the base interval for periodic log flush.
|
||||||
|
LogReportMaxLatency time.Duration `yaml:"log_report_max_latency"` // LogReportMaxLatency specifies the max time a log row can wait before being sent.
|
||||||
|
LogReportBatchSize int `yaml:"log_report_batch_size"` // LogReportBatchSize triggers immediate log flush when buffer reaches this size.
|
||||||
|
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
||||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
||||||
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
||||||
}
|
}
|
||||||
@@ -57,6 +62,7 @@ type Container struct {
|
|||||||
ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present
|
ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present
|
||||||
RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by act_runner
|
RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by act_runner
|
||||||
DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
||||||
|
BindWorkdir bool `yaml:"bind_workdir"` // BindWorkdir binds the workspace to the host filesystem instead of using Docker volumes. Required for DinD when jobs use docker compose with bind mounts.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host represents the configuration for the host.
|
// Host represents the configuration for the host.
|
||||||
@@ -64,6 +70,12 @@ type Host struct {
|
|||||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the host's working directory.
|
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the host's working directory.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metrics represents the configuration for the Prometheus metrics endpoint.
|
||||||
|
type Metrics struct {
|
||||||
|
Enabled bool `yaml:"enabled"` // Enabled indicates whether the metrics endpoint is exposed.
|
||||||
|
Addr string `yaml:"addr"` // Addr specifies the listen address for the metrics HTTP server (e.g., ":9101").
|
||||||
|
}
|
||||||
|
|
||||||
// Config represents the overall configuration.
|
// Config represents the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Log Log `yaml:"log"` // Log represents the configuration for logging.
|
Log Log `yaml:"log"` // Log represents the configuration for logging.
|
||||||
@@ -71,6 +83,7 @@ type Config struct {
|
|||||||
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
|
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
|
||||||
Container Container `yaml:"container"` // Container represents the configuration for the container.
|
Container Container `yaml:"container"` // Container represents the configuration for the container.
|
||||||
Host Host `yaml:"host"` // Host represents the configuration for the host.
|
Host Host `yaml:"host"` // Host represents the configuration for the host.
|
||||||
|
Metrics Metrics `yaml:"metrics"` // Metrics represents the configuration for the Prometheus metrics endpoint.
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefault returns the default configuration.
|
// LoadDefault returns the default configuration.
|
||||||
@@ -136,6 +149,35 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
if cfg.Runner.FetchInterval <= 0 {
|
if cfg.Runner.FetchInterval <= 0 {
|
||||||
cfg.Runner.FetchInterval = 2 * time.Second
|
cfg.Runner.FetchInterval = 2 * time.Second
|
||||||
}
|
}
|
||||||
|
if cfg.Runner.FetchIntervalMax <= 0 {
|
||||||
|
cfg.Runner.FetchIntervalMax = 60 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.Runner.LogReportInterval <= 0 {
|
||||||
|
cfg.Runner.LogReportInterval = 5 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.Runner.LogReportMaxLatency <= 0 {
|
||||||
|
cfg.Runner.LogReportMaxLatency = 3 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.Runner.LogReportBatchSize <= 0 {
|
||||||
|
cfg.Runner.LogReportBatchSize = 100
|
||||||
|
}
|
||||||
|
if cfg.Runner.StateReportInterval <= 0 {
|
||||||
|
cfg.Runner.StateReportInterval = 5 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.Metrics.Addr == "" {
|
||||||
|
cfg.Metrics.Addr = "127.0.0.1:9101"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and fix invalid config combinations to prevent confusing behavior.
|
||||||
|
if cfg.Runner.FetchIntervalMax < cfg.Runner.FetchInterval {
|
||||||
|
log.Warnf("fetch_interval_max (%v) is less than fetch_interval (%v), setting fetch_interval_max to fetch_interval",
|
||||||
|
cfg.Runner.FetchIntervalMax, cfg.Runner.FetchInterval)
|
||||||
|
cfg.Runner.FetchIntervalMax = cfg.Runner.FetchInterval
|
||||||
|
}
|
||||||
|
if cfg.Runner.LogReportMaxLatency >= cfg.Runner.LogReportInterval {
|
||||||
|
log.Warnf("log_report_max_latency (%v) >= log_report_interval (%v), the max-latency timer will never fire before the periodic ticker; consider lowering log_report_max_latency",
|
||||||
|
cfg.Runner.LogReportMaxLatency, cfg.Runner.LogReportInterval)
|
||||||
|
}
|
||||||
|
|
||||||
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
||||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ package labels
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
@@ -57,7 +57,7 @@ func TestParse(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.DeepEqual(t, got, tt.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
216
internal/pkg/metrics/metrics.go
Normal file
216
internal/pkg/metrics/metrics.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Namespace is the Prometheus namespace for all act_runner metrics.
|
||||||
|
const Namespace = "act_runner"
|
||||||
|
|
||||||
|
// Label value constants for Prometheus metrics.
|
||||||
|
// Using constants prevents typos from silently creating new time-series.
|
||||||
|
//
|
||||||
|
// LabelResult* values are used on metrics with label key "result" (RPC outcomes).
|
||||||
|
// LabelStatus* values are used on metrics with label key "status" (job outcomes).
|
||||||
|
const (
|
||||||
|
LabelResultTask = "task"
|
||||||
|
LabelResultEmpty = "empty"
|
||||||
|
LabelResultError = "error"
|
||||||
|
LabelResultSuccess = "success"
|
||||||
|
|
||||||
|
LabelMethodFetchTask = "FetchTask"
|
||||||
|
LabelMethodUpdateLog = "UpdateLog"
|
||||||
|
LabelMethodUpdateTask = "UpdateTask"
|
||||||
|
|
||||||
|
LabelStatusSuccess = "success"
|
||||||
|
LabelStatusFailure = "failure"
|
||||||
|
LabelStatusCancelled = "cancelled"
|
||||||
|
LabelStatusSkipped = "skipped"
|
||||||
|
LabelStatusUnknown = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rpcDurationBuckets covers the expected latency range for short-running
|
||||||
|
// UpdateLog / UpdateTask RPCs. FetchTask uses its own buckets (it has a 10s tail).
|
||||||
|
var rpcDurationBuckets = []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5}
|
||||||
|
|
||||||
|
// ResultToStatusLabel maps a runnerv1.Result to the "status" label value used on job metrics.
|
||||||
|
func ResultToStatusLabel(r runnerv1.Result) string {
|
||||||
|
switch r {
|
||||||
|
case runnerv1.Result_RESULT_SUCCESS:
|
||||||
|
return LabelStatusSuccess
|
||||||
|
case runnerv1.Result_RESULT_FAILURE:
|
||||||
|
return LabelStatusFailure
|
||||||
|
case runnerv1.Result_RESULT_CANCELLED:
|
||||||
|
return LabelStatusCancelled
|
||||||
|
case runnerv1.Result_RESULT_SKIPPED:
|
||||||
|
return LabelStatusSkipped
|
||||||
|
default:
|
||||||
|
return LabelStatusUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
RunnerInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Name: "info",
|
||||||
|
Help: "Runner metadata. Always 1. Labels carry version and name.",
|
||||||
|
}, []string{"version", "name"})
|
||||||
|
|
||||||
|
RunnerCapacity = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Name: "capacity",
|
||||||
|
Help: "Configured maximum concurrent jobs.",
|
||||||
|
})
|
||||||
|
|
||||||
|
PollFetchTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "poll",
|
||||||
|
Name: "fetch_total",
|
||||||
|
Help: "Total number of FetchTask RPCs by result (task, empty, error).",
|
||||||
|
}, []string{"result"})
|
||||||
|
|
||||||
|
PollFetchDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "poll",
|
||||||
|
Name: "fetch_duration_seconds",
|
||||||
|
Help: "Latency of FetchTask RPCs, excluding expected long-poll timeouts.",
|
||||||
|
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10},
|
||||||
|
})
|
||||||
|
|
||||||
|
PollBackoffSeconds = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "poll",
|
||||||
|
Name: "backoff_seconds",
|
||||||
|
Help: "Last observed polling backoff interval. With Capacity > 1, reflects whichever worker wrote last.",
|
||||||
|
})
|
||||||
|
|
||||||
|
JobsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "job",
|
||||||
|
Name: "total",
|
||||||
|
Help: "Total jobs processed by status (success, failure, cancelled, skipped, unknown).",
|
||||||
|
}, []string{"status"})
|
||||||
|
|
||||||
|
JobDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "job",
|
||||||
|
Name: "duration_seconds",
|
||||||
|
Help: "Duration of job execution from start to finish.",
|
||||||
|
Buckets: prometheus.ExponentialBuckets(1, 2, 14), // 1s to ~4.5h
|
||||||
|
})
|
||||||
|
|
||||||
|
ReportLogTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "report",
|
||||||
|
Name: "log_total",
|
||||||
|
Help: "Total UpdateLog RPCs by result (success, error).",
|
||||||
|
}, []string{"result"})
|
||||||
|
|
||||||
|
ReportLogDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "report",
|
||||||
|
Name: "log_duration_seconds",
|
||||||
|
Help: "Latency of UpdateLog RPCs.",
|
||||||
|
Buckets: rpcDurationBuckets,
|
||||||
|
})
|
||||||
|
|
||||||
|
ReportStateTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "report",
|
||||||
|
Name: "state_total",
|
||||||
|
Help: "Total UpdateTask (state) RPCs by result (success, error).",
|
||||||
|
}, []string{"result"})
|
||||||
|
|
||||||
|
ReportStateDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "report",
|
||||||
|
Name: "state_duration_seconds",
|
||||||
|
Help: "Latency of UpdateTask RPCs.",
|
||||||
|
Buckets: rpcDurationBuckets,
|
||||||
|
})
|
||||||
|
|
||||||
|
ReportLogBufferRows = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "report",
|
||||||
|
Name: "log_buffer_rows",
|
||||||
|
Help: "Current number of buffered log rows awaiting send.",
|
||||||
|
})
|
||||||
|
|
||||||
|
ClientErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "client",
|
||||||
|
Name: "errors_total",
|
||||||
|
Help: "Total client RPC errors by method.",
|
||||||
|
}, []string{"method"})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Registry is the custom Prometheus registry used by the runner.
|
||||||
|
var Registry = prometheus.NewRegistry()
|
||||||
|
|
||||||
|
var initOnce sync.Once
|
||||||
|
|
||||||
|
// Init registers all static metrics and the standard Go/process collectors.
|
||||||
|
// Safe to call multiple times; only the first call has effect.
|
||||||
|
func Init() {
|
||||||
|
initOnce.Do(func() {
|
||||||
|
Registry.MustRegister(
|
||||||
|
collectors.NewGoCollector(),
|
||||||
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||||
|
RunnerInfo, RunnerCapacity,
|
||||||
|
PollFetchTotal, PollFetchDuration, PollBackoffSeconds,
|
||||||
|
JobsTotal, JobDuration,
|
||||||
|
ReportLogTotal, ReportLogDuration,
|
||||||
|
ReportStateTotal, ReportStateDuration, ReportLogBufferRows,
|
||||||
|
ClientErrors,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterUptimeFunc registers a GaugeFunc that reports seconds since startTime.
|
||||||
|
func RegisterUptimeFunc(startTime time.Time) {
|
||||||
|
Registry.MustRegister(prometheus.NewGaugeFunc(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Name: "uptime_seconds",
|
||||||
|
Help: "Seconds since the runner daemon started.",
|
||||||
|
},
|
||||||
|
func() float64 { return time.Since(startTime).Seconds() },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRunningJobsFunc registers GaugeFuncs for the running job count and
|
||||||
|
// capacity utilisation ratio, evaluated lazily at Prometheus scrape time.
|
||||||
|
func RegisterRunningJobsFunc(countFn func() int64, capacity int) {
|
||||||
|
capF := float64(capacity)
|
||||||
|
Registry.MustRegister(prometheus.NewGaugeFunc(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "job",
|
||||||
|
Name: "running",
|
||||||
|
Help: "Number of jobs currently executing.",
|
||||||
|
},
|
||||||
|
func() float64 { return float64(countFn()) },
|
||||||
|
))
|
||||||
|
Registry.MustRegister(prometheus.NewGaugeFunc(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: Namespace,
|
||||||
|
Subsystem: "job",
|
||||||
|
Name: "capacity_utilization_ratio",
|
||||||
|
Help: "Ratio of running jobs to configured capacity (0.0-1.0).",
|
||||||
|
},
|
||||||
|
func() float64 {
|
||||||
|
if capF <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(countFn()) / capF
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
50
internal/pkg/metrics/server.go
Normal file
50
internal/pkg/metrics/server.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartServer starts an HTTP server that serves Prometheus metrics on /metrics
|
||||||
|
// and a liveness check on /healthz. The server shuts down when ctx is cancelled.
|
||||||
|
// Call Init() before StartServer to register metrics with the Registry.
|
||||||
|
func StartServer(ctx context.Context, addr string) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/metrics", promhttp.HandlerFor(Registry, promhttp.HandlerOpts{}))
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Infof("metrics server listening on %s", addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.WithError(err).Error("metrics server failed")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutCtx); err != nil {
|
||||||
|
log.WithError(err).Warn("metrics server shutdown error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,6 +20,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reporter struct {
|
type Reporter struct {
|
||||||
@@ -36,18 +37,32 @@ type Reporter struct {
|
|||||||
logReplacer *strings.Replacer
|
logReplacer *strings.Replacer
|
||||||
oldnew []string
|
oldnew []string
|
||||||
|
|
||||||
|
// lastLogBufferRows is the last value written to the ReportLogBufferRows
|
||||||
|
// gauge; guarded by clientM (the same lock held around each ReportLog call)
|
||||||
|
// so the gauge skips no-op Set calls when the buffer size is unchanged.
|
||||||
|
lastLogBufferRows int
|
||||||
|
|
||||||
state *runnerv1.TaskState
|
state *runnerv1.TaskState
|
||||||
|
stateChanged bool
|
||||||
stateMu sync.RWMutex
|
stateMu sync.RWMutex
|
||||||
outputs sync.Map
|
outputs sync.Map
|
||||||
daemon chan struct{}
|
daemon chan struct{}
|
||||||
|
|
||||||
|
// Adaptive batching control
|
||||||
|
logReportInterval time.Duration
|
||||||
|
logReportMaxLatency time.Duration
|
||||||
|
logBatchSize int
|
||||||
|
stateReportInterval time.Duration
|
||||||
|
|
||||||
|
// Event notification channels (non-blocking, buffered 1)
|
||||||
|
logNotify chan struct{} // signal: new log rows arrived
|
||||||
|
stateNotify chan struct{} // signal: step transition (start/stop)
|
||||||
|
|
||||||
debugOutputEnabled bool
|
debugOutputEnabled bool
|
||||||
stopCommandEndToken string
|
stopCommandEndToken string
|
||||||
|
|
||||||
stepIds []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task, cfg *config.Config) *Reporter {
|
||||||
var oldnew []string
|
var oldnew []string
|
||||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||||
oldnew = append(oldnew, v, "***")
|
oldnew = append(oldnew, v, "***")
|
||||||
@@ -65,6 +80,12 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
|||||||
client: client,
|
client: client,
|
||||||
oldnew: oldnew,
|
oldnew: oldnew,
|
||||||
logReplacer: strings.NewReplacer(oldnew...),
|
logReplacer: strings.NewReplacer(oldnew...),
|
||||||
|
logReportInterval: cfg.Runner.LogReportInterval,
|
||||||
|
logReportMaxLatency: cfg.Runner.LogReportMaxLatency,
|
||||||
|
logBatchSize: cfg.Runner.LogReportBatchSize,
|
||||||
|
stateReportInterval: cfg.Runner.StateReportInterval,
|
||||||
|
logNotify: make(chan struct{}, 1),
|
||||||
|
stateNotify: make(chan struct{}, 1),
|
||||||
state: &runnerv1.TaskState{
|
state: &runnerv1.TaskState{
|
||||||
Id: task.Id,
|
Id: task.Id,
|
||||||
},
|
},
|
||||||
@@ -78,6 +99,13 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
|||||||
return rv
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Result returns the final job result. Safe to call after Close() returns.
|
||||||
|
func (r *Reporter) Result() runnerv1.Result {
|
||||||
|
r.stateMu.RLock()
|
||||||
|
defer r.stateMu.RUnlock()
|
||||||
|
return r.state.Result
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) ResetSteps(l int) {
|
func (r *Reporter) ResetSteps(l int) {
|
||||||
r.stateMu.Lock()
|
r.stateMu.Lock()
|
||||||
defer r.stateMu.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
@@ -88,13 +116,6 @@ func (r *Reporter) ResetSteps(l int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) SetStepIdMapping(stepIDs ...string) {
|
|
||||||
r.ResetSteps(len(stepIDs))
|
|
||||||
r.stateMu.Lock()
|
|
||||||
defer r.stateMu.Unlock()
|
|
||||||
r.stepIds = stepIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reporter) Levels() []log.Level {
|
func (r *Reporter) Levels() []log.Level {
|
||||||
return log.AllLevels
|
return log.AllLevels
|
||||||
}
|
}
|
||||||
@@ -118,11 +139,42 @@ func isJobStepEntry(entry *log.Entry) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
// notifyLog sends a non-blocking signal that new log rows are available.
|
||||||
r.stateMu.Lock()
|
func (r *Reporter) notifyLog() {
|
||||||
defer r.stateMu.Unlock()
|
select {
|
||||||
|
case r.logNotify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyState sends a non-blocking signal that a UX-critical state change occurred (step start/stop, job result).
|
||||||
|
func (r *Reporter) notifyState() {
|
||||||
|
select {
|
||||||
|
case r.stateNotify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlockAndNotify releases stateMu and sends channel notifications.
|
||||||
|
// Must be called with stateMu held.
|
||||||
|
func (r *Reporter) unlockAndNotify(urgentState bool) {
|
||||||
|
r.stateMu.Unlock()
|
||||||
|
r.notifyLog()
|
||||||
|
if urgentState {
|
||||||
|
r.notifyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
|
urgentState := false
|
||||||
|
|
||||||
|
r.stateMu.Lock()
|
||||||
|
|
||||||
|
r.stateChanged = true
|
||||||
|
|
||||||
|
if log.IsLevelEnabled(log.TraceLevel) {
|
||||||
log.WithFields(entry.Data).Trace(entry.Message)
|
log.WithFields(entry.Data).Trace(entry.Message)
|
||||||
|
}
|
||||||
|
|
||||||
timestamp := entry.Time
|
timestamp := entry.Time
|
||||||
if r.state.StartedAt == nil {
|
if r.state.StartedAt == nil {
|
||||||
@@ -145,11 +197,13 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
urgentState = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
|
r.unlockAndNotify(urgentState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,22 +212,18 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
||||||
step = r.state.Steps[v]
|
step = r.state.Steps[v]
|
||||||
}
|
}
|
||||||
} else if v, ok := entry.Data["stepID"]; ok {
|
|
||||||
if v, ok := v.([]string); ok && len(v) >= 1 {
|
|
||||||
if no := slices.Index(r.stepIds, v[0]); no >= 0 && len(r.state.Steps) > no {
|
|
||||||
step = r.state.Steps[no]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if step == nil {
|
if step == nil {
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
|
r.unlockAndNotify(false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if step.StartedAt == nil {
|
if step.StartedAt == nil {
|
||||||
step.StartedAt = timestamppb.New(timestamp)
|
step.StartedAt = timestamppb.New(timestamp)
|
||||||
|
urgentState = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force reporting log errors as raw output to prevent silent failures
|
// Force reporting log errors as raw output to prevent silent failures
|
||||||
@@ -201,26 +251,91 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
step.Result = stepResult
|
step.Result = stepResult
|
||||||
step.StoppedAt = timestamppb.New(timestamp)
|
step.StoppedAt = timestamppb.New(timestamp)
|
||||||
|
urgentState = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.unlockAndNotify(urgentState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) RunDaemon() {
|
func (r *Reporter) RunDaemon() {
|
||||||
|
go r.runDaemonLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) stopLatencyTimer(active *bool, timer *time.Timer) {
|
||||||
|
if *active {
|
||||||
|
if !timer.Stop() {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) runDaemonLoop() {
|
||||||
|
logTicker := time.NewTicker(r.logReportInterval)
|
||||||
|
stateTicker := time.NewTicker(r.stateReportInterval)
|
||||||
|
|
||||||
|
// maxLatencyTimer ensures the first buffered log row is sent within logReportMaxLatency.
|
||||||
|
// Start inactive — it is armed when the first log row arrives in an empty buffer.
|
||||||
|
maxLatencyTimer := time.NewTimer(0)
|
||||||
|
if !maxLatencyTimer.Stop() {
|
||||||
|
<-maxLatencyTimer.C
|
||||||
|
}
|
||||||
|
maxLatencyActive := false
|
||||||
|
|
||||||
|
defer logTicker.Stop()
|
||||||
|
defer stateTicker.Stop()
|
||||||
|
defer maxLatencyTimer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-logTicker.C:
|
||||||
|
_ = r.ReportLog(false)
|
||||||
|
r.stopLatencyTimer(&maxLatencyActive, maxLatencyTimer)
|
||||||
|
|
||||||
|
case <-stateTicker.C:
|
||||||
|
_ = r.ReportState(false)
|
||||||
|
|
||||||
|
case <-r.logNotify:
|
||||||
r.stateMu.RLock()
|
r.stateMu.RLock()
|
||||||
closed := r.closed
|
n := len(r.logRows)
|
||||||
r.stateMu.RUnlock()
|
r.stateMu.RUnlock()
|
||||||
if closed || r.ctx.Err() != nil {
|
|
||||||
// Acknowledge close
|
if n >= r.logBatchSize {
|
||||||
|
_ = r.ReportLog(false)
|
||||||
|
r.stopLatencyTimer(&maxLatencyActive, maxLatencyTimer)
|
||||||
|
} else if !maxLatencyActive && n > 0 {
|
||||||
|
maxLatencyTimer.Reset(r.logReportMaxLatency)
|
||||||
|
maxLatencyActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-r.stateNotify:
|
||||||
|
// Step transition or job result — flush both immediately for frontend UX.
|
||||||
|
_ = r.ReportLog(false)
|
||||||
|
_ = r.ReportState(false)
|
||||||
|
r.stopLatencyTimer(&maxLatencyActive, maxLatencyTimer)
|
||||||
|
|
||||||
|
case <-maxLatencyTimer.C:
|
||||||
|
maxLatencyActive = false
|
||||||
|
_ = r.ReportLog(false)
|
||||||
|
|
||||||
|
case <-r.ctx.Done():
|
||||||
close(r.daemon)
|
close(r.daemon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = r.ReportLog(false)
|
r.stateMu.RLock()
|
||||||
_ = r.ReportState(false)
|
closed := r.closed
|
||||||
|
r.stateMu.RUnlock()
|
||||||
time.AfterFunc(time.Second, r.RunDaemon)
|
if closed {
|
||||||
|
close(r.daemon)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Logf(format string, a ...any) {
|
func (r *Reporter) Logf(format string, a ...any) {
|
||||||
@@ -284,6 +399,10 @@ func (r *Reporter) Close(lastWords string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
r.stateMu.Unlock()
|
r.stateMu.Unlock()
|
||||||
|
|
||||||
|
// Wake up the daemon loop so it detects closed promptly.
|
||||||
|
r.notifyLog()
|
||||||
|
|
||||||
// Wait for Acknowledge
|
// Wait for Acknowledge
|
||||||
select {
|
select {
|
||||||
case <-r.daemon:
|
case <-r.daemon:
|
||||||
@@ -311,15 +430,24 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
|||||||
rows := r.logRows
|
rows := r.logRows
|
||||||
r.stateMu.RUnlock()
|
r.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if !noMore && len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
resp, err := r.client.UpdateLog(r.ctx, connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||||
TaskId: r.state.Id,
|
TaskId: r.state.Id,
|
||||||
Index: int64(r.logOffset),
|
Index: int64(r.logOffset),
|
||||||
Rows: rows,
|
Rows: rows,
|
||||||
NoMore: noMore,
|
NoMore: noMore,
|
||||||
}))
|
}))
|
||||||
|
metrics.ReportLogDuration.Observe(time.Since(start).Seconds())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metrics.ReportLogTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
||||||
|
metrics.ClientErrors.WithLabelValues(metrics.LabelMethodUpdateLog).Inc()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
metrics.ReportLogTotal.WithLabelValues(metrics.LabelResultSuccess).Inc()
|
||||||
|
|
||||||
ack := int(resp.Msg.AckIndex)
|
ack := int(resp.Msg.AckIndex)
|
||||||
if ack < r.logOffset {
|
if ack < r.logOffset {
|
||||||
@@ -330,7 +458,12 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
|||||||
r.logRows = r.logRows[ack-r.logOffset:]
|
r.logRows = r.logRows[ack-r.logOffset:]
|
||||||
submitted := r.logOffset + len(rows)
|
submitted := r.logOffset + len(rows)
|
||||||
r.logOffset = ack
|
r.logOffset = ack
|
||||||
|
remaining := len(r.logRows)
|
||||||
r.stateMu.Unlock()
|
r.stateMu.Unlock()
|
||||||
|
if remaining != r.lastLogBufferRows {
|
||||||
|
metrics.ReportLogBufferRows.Set(float64(remaining))
|
||||||
|
r.lastLogBufferRows = remaining
|
||||||
|
}
|
||||||
|
|
||||||
if noMore && ack < submitted {
|
if noMore && ack < submitted {
|
||||||
return errors.New("not all logs are submitted")
|
return errors.New("not all logs are submitted")
|
||||||
@@ -345,15 +478,7 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
|||||||
r.clientM.Lock()
|
r.clientM.Lock()
|
||||||
defer r.clientM.Unlock()
|
defer r.clientM.Unlock()
|
||||||
|
|
||||||
r.stateMu.RLock()
|
// Build the outputs map first (single Range pass instead of two).
|
||||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
|
||||||
r.stateMu.RUnlock()
|
|
||||||
|
|
||||||
// Only report result from Close to reliable sent logs
|
|
||||||
if !reportResult {
|
|
||||||
state.Result = runnerv1.Result_RESULT_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs := make(map[string]string)
|
outputs := make(map[string]string)
|
||||||
r.outputs.Range(func(k, v any) bool {
|
r.outputs.Range(func(k, v any) bool {
|
||||||
if val, ok := v.(string); ok {
|
if val, ok := v.(string); ok {
|
||||||
@@ -362,13 +487,36 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Consume stateChanged atomically with the snapshot; restored on error
|
||||||
|
// below so a concurrent Fire() during UpdateTask isn't silently lost.
|
||||||
|
r.stateMu.Lock()
|
||||||
|
if !reportResult && !r.stateChanged && len(outputs) == 0 {
|
||||||
|
r.stateMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||||
|
r.stateChanged = false
|
||||||
|
r.stateMu.Unlock()
|
||||||
|
|
||||||
|
if !reportResult {
|
||||||
|
state.Result = runnerv1.Result_RESULT_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
resp, err := r.client.UpdateTask(r.ctx, connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||||
State: state,
|
State: state,
|
||||||
Outputs: outputs,
|
Outputs: outputs,
|
||||||
}))
|
}))
|
||||||
|
metrics.ReportStateDuration.Observe(time.Since(start).Seconds())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metrics.ReportStateTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
||||||
|
metrics.ClientErrors.WithLabelValues(metrics.LabelMethodUpdateTask).Inc()
|
||||||
|
r.stateMu.Lock()
|
||||||
|
r.stateChanged = true
|
||||||
|
r.stateMu.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
metrics.ReportStateTotal.WithLabelValues(metrics.LabelResultSuccess).Inc()
|
||||||
|
|
||||||
for _, k := range resp.Msg.SentOutputs {
|
for _, k := range resp.Msg.SentOutputs {
|
||||||
r.outputs.Store(k, struct{}{})
|
r.outputs.Store(k, struct{}{})
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ package report
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReporter_parseLogRow(t *testing.T) {
|
func TestReporter_parseLogRow(t *testing.T) {
|
||||||
@@ -175,9 +177,10 @@ func TestReporter_Fire(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||||
Context: taskCtx,
|
Context: taskCtx,
|
||||||
})
|
}, cfg)
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(t, reporter.Close(""))
|
require.NoError(t, reporter.Close(""))
|
||||||
@@ -252,7 +255,8 @@ func TestReporter_EphemeralRunnerDeletion(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx})
|
cfg, _ := config.LoadDefault("")
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
// Fire a log entry to create pending data
|
// Fire a log entry to create pending data
|
||||||
@@ -315,23 +319,281 @@ func TestReporter_RunDaemonClose_Race(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||||
Context: taskCtx,
|
Context: taskCtx,
|
||||||
})
|
}, cfg)
|
||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
// Start the daemon loop in a separate goroutine.
|
// Start the daemon loop — RunDaemon spawns a goroutine internally.
|
||||||
// RunDaemon reads r.closed and reschedules itself via time.AfterFunc.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Go(func() {
|
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
})
|
|
||||||
|
|
||||||
// Close concurrently — this races with RunDaemon on r.closed.
|
// Close concurrently — this races with the daemon goroutine on r.closed.
|
||||||
require.NoError(t, reporter.Close(""))
|
require.NoError(t, reporter.Close(""))
|
||||||
|
|
||||||
// Cancel context so pending AfterFunc callbacks exit quickly.
|
// Cancel context so the daemon goroutine exits cleanly.
|
||||||
cancel()
|
cancel()
|
||||||
wg.Wait()
|
}
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
// TestReporter_MaxLatencyTimer verifies that the maxLatencyTimer flushes a
|
||||||
|
// single buffered log row before the periodic logTicker fires.
|
||||||
|
//
|
||||||
|
// Setup: logReportInterval=10s (effectively never), maxLatency=100ms.
|
||||||
|
// Fire one log line, then assert UpdateLog is called within 500ms.
|
||||||
|
func TestReporter_MaxLatencyTimer(t *testing.T) {
|
||||||
|
var updateLogCalls atomic.Int64
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
updateLogCalls.Add(1)
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Custom config: logTicker=10s (won't fire during test), maxLatency=100ms
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportMaxLatency = 100 * time.Millisecond
|
||||||
|
cfg.Runner.LogReportBatchSize = 1000 // won't trigger batch flush
|
||||||
|
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(1)
|
||||||
|
reporter.RunDaemon()
|
||||||
|
defer func() {
|
||||||
|
_ = reporter.Close("")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Fire a single log line — not enough to trigger batch flush
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{
|
||||||
|
Message: "single log line",
|
||||||
|
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// maxLatencyTimer should flush within ~100ms. Wait up to 500ms.
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
return updateLogCalls.Load() > 0
|
||||||
|
}, 500*time.Millisecond, 10*time.Millisecond,
|
||||||
|
"maxLatencyTimer should have flushed the log before logTicker (10s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReporter_BatchSizeFlush verifies that reaching logBatchSize triggers
|
||||||
|
// an immediate log flush without waiting for any timer.
|
||||||
|
func TestReporter_BatchSizeFlush(t *testing.T) {
|
||||||
|
var updateLogCalls atomic.Int64
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
updateLogCalls.Add(1)
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Custom config: large timers, small batch size
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportMaxLatency = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportBatchSize = 5
|
||||||
|
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(1)
|
||||||
|
reporter.RunDaemon()
|
||||||
|
defer func() {
|
||||||
|
_ = reporter.Close("")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Fire exactly batchSize log lines
|
||||||
|
for i := range 5 {
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{
|
||||||
|
Message: fmt.Sprintf("log line %d", i),
|
||||||
|
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch threshold should trigger immediate flush
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
return updateLogCalls.Load() > 0
|
||||||
|
}, 500*time.Millisecond, 10*time.Millisecond,
|
||||||
|
"batch size threshold should have triggered immediate flush")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReporter_StateChangedNotLostDuringReport asserts that a Fire() arriving
|
||||||
|
// mid-UpdateTask re-dirties the flag so the change is picked up by the next report.
|
||||||
|
func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
inFlight := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
n := updateTaskCalls.Add(1)
|
||||||
|
if n == 1 {
|
||||||
|
// Signal that the first UpdateTask is in flight, then block until released.
|
||||||
|
close(inFlight)
|
||||||
|
<-release
|
||||||
|
}
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(2)
|
||||||
|
|
||||||
|
// Mark stateChanged=true so the first ReportState proceeds to UpdateTask.
|
||||||
|
reporter.stateMu.Lock()
|
||||||
|
reporter.stateChanged = true
|
||||||
|
reporter.stateMu.Unlock()
|
||||||
|
|
||||||
|
// Kick off the first ReportState in a goroutine — it will block in UpdateTask.
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- reporter.ReportState(false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait until UpdateTask is in flight (snapshot taken, flag consumed).
|
||||||
|
<-inFlight
|
||||||
|
|
||||||
|
// Concurrent Fire() modifies state — must re-flip stateChanged so the
|
||||||
|
// change is not lost when the in-flight ReportState finishes.
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{
|
||||||
|
Message: "step starts",
|
||||||
|
Data: log.Fields{"stage": "Main", "stepNumber": 1, "raw_output": true},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Release the in-flight UpdateTask and wait for it to return.
|
||||||
|
close(release)
|
||||||
|
require.NoError(t, <-done)
|
||||||
|
|
||||||
|
// stateChanged must still be true so the next ReportState picks up the
|
||||||
|
// concurrent Fire()'s change instead of skipping via the early-return path.
|
||||||
|
reporter.stateMu.RLock()
|
||||||
|
changed := reporter.stateChanged
|
||||||
|
reporter.stateMu.RUnlock()
|
||||||
|
assert.True(t, changed, "stateChanged must remain true after a concurrent Fire() during in-flight ReportState")
|
||||||
|
|
||||||
|
// And the next ReportState must actually send a second UpdateTask.
|
||||||
|
require.NoError(t, reporter.ReportState(false))
|
||||||
|
assert.Equal(t, int64(2), updateTaskCalls.Load(), "concurrent Fire() change must trigger a second UpdateTask, not be silently lost")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReporter_StateChangedRestoredOnError verifies that when UpdateTask fails,
|
||||||
|
// the dirty flag is restored so the snapshotted change isn't silently lost.
|
||||||
|
func TestReporter_StateChangedRestoredOnError(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
n := updateTaskCalls.Add(1)
|
||||||
|
if n == 1 {
|
||||||
|
return nil, errors.New("transient network error")
|
||||||
|
}
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
|
reporter.stateMu.Lock()
|
||||||
|
reporter.stateChanged = true
|
||||||
|
reporter.stateMu.Unlock()
|
||||||
|
|
||||||
|
// First ReportState fails — flag must be restored to true.
|
||||||
|
require.Error(t, reporter.ReportState(false))
|
||||||
|
|
||||||
|
reporter.stateMu.RLock()
|
||||||
|
changed := reporter.stateChanged
|
||||||
|
reporter.stateMu.RUnlock()
|
||||||
|
assert.True(t, changed, "stateChanged must be restored to true after UpdateTask error so the change is retried")
|
||||||
|
|
||||||
|
// The next ReportState should still issue a request because the flag was restored.
|
||||||
|
require.NoError(t, reporter.ReportState(false))
|
||||||
|
assert.Equal(t, int64(2), updateTaskCalls.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReporter_StateNotifyFlush verifies that step transitions trigger
|
||||||
|
// an immediate state flush via the stateNotify channel.
|
||||||
|
func TestReporter_StateNotifyFlush(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Maybe().Return(
|
||||||
|
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
updateTaskCalls.Add(1)
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Custom config: large state interval so only stateNotify can trigger
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
cfg.Runner.StateReportInterval = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
|
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(1)
|
||||||
|
reporter.RunDaemon()
|
||||||
|
defer func() {
|
||||||
|
_ = reporter.Close("")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Fire a log entry that starts a step — this triggers stateNotify
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{
|
||||||
|
Message: "step starting",
|
||||||
|
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// stateNotify should trigger immediate UpdateTask call
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
return updateTaskCalls.Load() > 0
|
||||||
|
}, 500*time.Millisecond, 10*time.Millisecond,
|
||||||
|
"step transition should have triggered immediate state flush via stateNotify")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
package templateeval
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
v2 "gitea.com/gitea/act_runner/internal/eval/v2"
|
|
||||||
exprparser "gitea.com/gitea/act_runner/internal/expr"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ExpressionEvaluator struct {
|
|
||||||
RestrictEval bool
|
|
||||||
EvaluationContext v2.EvaluationContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func isImplExpr(snode *schema.Node) bool {
|
|
||||||
def := snode.Schema.GetDefinition(snode.Definition)
|
|
||||||
return def.String != nil && def.String.IsExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) evaluateScalarYamlNode(_ context.Context, node *yaml.Node, snode *schema.Node) (*yaml.Node, error) {
|
|
||||||
var in string
|
|
||||||
if err := node.Decode(&in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
expr, isExpr, err := rewriteSubExpression(in, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if snode == nil || !isExpr && !isImplExpr(snode) || snode.Schema.GetDefinition(snode.Definition).String.IsExpression || ee.RestrictEval && node.Tag != "!!expr" {
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
parsed, err := exprparser.Parse(expr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
canEvaluate := ee.canEvaluate(parsed, snode)
|
|
||||||
if !canEvaluate {
|
|
||||||
node.Tag = "!!expr"
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
eval := v2.NewEvaluator(&ee.EvaluationContext)
|
|
||||||
res, err := eval.EvaluateRaw(expr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret := &yaml.Node{}
|
|
||||||
if err := ret.Encode(res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret.Line = node.Line
|
|
||||||
ret.Column = node.Column
|
|
||||||
// Finally check if we found a schema validation error
|
|
||||||
return ret, snode.UnmarshalYAML(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) canEvaluate(parsed exprparser.Node, snode *schema.Node) bool {
|
|
||||||
canEvaluate := true
|
|
||||||
for _, v := range snode.GetVariables() {
|
|
||||||
canEvaluate = canEvaluate && ee.EvaluationContext.Variables.Get(v) != nil
|
|
||||||
}
|
|
||||||
for _, v := range snode.GetFunctions() {
|
|
||||||
canEvaluate = canEvaluate && ee.EvaluationContext.Functions.Get(v.GetName()) != nil
|
|
||||||
}
|
|
||||||
exprparser.VisitNode(parsed, func(node exprparser.Node) {
|
|
||||||
switch el := node.(type) {
|
|
||||||
case *exprparser.FunctionNode:
|
|
||||||
canEvaluate = canEvaluate && ee.EvaluationContext.Functions.Get(el.Name) != nil
|
|
||||||
case *exprparser.ValueNode:
|
|
||||||
canEvaluate = canEvaluate && (el.Kind != exprparser.TokenKindNamedValue || ee.EvaluationContext.Variables.Get(el.Value.(string)) != nil)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return canEvaluate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) evaluateMappingYamlNode(ctx context.Context, node *yaml.Node, snode *schema.Node) (*yaml.Node, error) {
|
|
||||||
var ret *yaml.Node
|
|
||||||
// GitHub has this undocumented feature to merge maps, called insert directive
|
|
||||||
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
|
||||||
for i := 0; i < len(node.Content)/2; i++ {
|
|
||||||
k := node.Content[i*2]
|
|
||||||
var sk string
|
|
||||||
shouldInsert := k.Decode(&sk) == nil && insertDirective.MatchString(sk)
|
|
||||||
changed := func() error {
|
|
||||||
if ret == nil {
|
|
||||||
ret = &yaml.Node{}
|
|
||||||
if err := ret.Encode(node); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ret.Content = ret.Content[:i*2]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var ek *yaml.Node
|
|
||||||
if !shouldInsert {
|
|
||||||
var err error
|
|
||||||
ek, err = ee.evaluateYamlNodeInternal(ctx, k, snode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ek != nil {
|
|
||||||
if err := changed(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ek = k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v := node.Content[i*2+1]
|
|
||||||
ev, err := ee.evaluateYamlNodeInternal(ctx, v, snode.GetNestedNode(ek.Value))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ev != nil {
|
|
||||||
if err := changed(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ev = v
|
|
||||||
}
|
|
||||||
// Merge the nested map of the insert directive
|
|
||||||
if shouldInsert {
|
|
||||||
if ev.Kind != yaml.MappingNode {
|
|
||||||
return nil, fmt.Errorf("failed to insert node %v into mapping %v unexpected type %v expected MappingNode", ev, node, ev.Kind)
|
|
||||||
}
|
|
||||||
if err := changed(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret.Content = append(ret.Content, ev.Content...)
|
|
||||||
} else if ret != nil {
|
|
||||||
ret.Content = append(ret.Content, ek, ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) evaluateSequenceYamlNode(ctx context.Context, node *yaml.Node, snode *schema.Node) (*yaml.Node, error) {
|
|
||||||
var ret *yaml.Node
|
|
||||||
for i := 0; i < len(node.Content); i++ {
|
|
||||||
v := node.Content[i]
|
|
||||||
// Preserve nested sequences
|
|
||||||
wasseq := v.Kind == yaml.SequenceNode
|
|
||||||
ev, err := ee.evaluateYamlNodeInternal(ctx, v, snode.GetNestedNode("*"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ev != nil {
|
|
||||||
if ret == nil {
|
|
||||||
ret = &yaml.Node{}
|
|
||||||
if err := ret.Encode(node); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret.Content = ret.Content[:i]
|
|
||||||
}
|
|
||||||
// GitHub has this undocumented feature to merge sequences / arrays
|
|
||||||
// We have a nested sequence via evaluation, merge the arrays
|
|
||||||
if ev.Kind == yaml.SequenceNode && !wasseq {
|
|
||||||
ret.Content = append(ret.Content, ev.Content...)
|
|
||||||
} else {
|
|
||||||
ret.Content = append(ret.Content, ev)
|
|
||||||
}
|
|
||||||
} else if ret != nil {
|
|
||||||
ret.Content = append(ret.Content, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) evaluateYamlNodeInternal(ctx context.Context, node *yaml.Node, snode *schema.Node) (*yaml.Node, error) {
|
|
||||||
switch node.Kind {
|
|
||||||
case yaml.ScalarNode:
|
|
||||||
return ee.evaluateScalarYamlNode(ctx, node, snode)
|
|
||||||
case yaml.MappingNode:
|
|
||||||
return ee.evaluateMappingYamlNode(ctx, node, snode)
|
|
||||||
case yaml.SequenceNode:
|
|
||||||
return ee.evaluateSequenceYamlNode(ctx, node, snode)
|
|
||||||
default:
|
|
||||||
return nil, nil //nolint:nilnil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee ExpressionEvaluator) EvaluateYamlNode(ctx context.Context, node *yaml.Node, snode *schema.Node) error {
|
|
||||||
ret, err := ee.evaluateYamlNodeInternal(ctx, node, snode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ret != nil {
|
|
||||||
return ret.Decode(node)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package templateeval
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v2 "gitea.com/gitea/act_runner/internal/eval/v2"
|
|
||||||
"gitea.com/gitea/act_runner/pkg/schema"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEval(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
yamlInput string
|
|
||||||
restrict bool
|
|
||||||
variables v2.CaseInsensitiveObject[any]
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "NoError",
|
|
||||||
yamlInput: `on: push
|
|
||||||
run-name: ${{ github.ref_name }}
|
|
||||||
jobs:
|
|
||||||
_:
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- run: echo Hello World
|
|
||||||
env:
|
|
||||||
TAG: ${{ env.global }}`,
|
|
||||||
restrict: false,
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error",
|
|
||||||
yamlInput: `on: push
|
|
||||||
run-name: ${{ fromjson('{}') }}
|
|
||||||
jobs:
|
|
||||||
_:
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- run: echo Hello World
|
|
||||||
env:
|
|
||||||
TAG: ${{ env.global }}`,
|
|
||||||
restrict: true,
|
|
||||||
variables: v2.CaseInsensitiveObject[any]{
|
|
||||||
"github": v2.CaseInsensitiveObject[any]{
|
|
||||||
"ref_name": "self",
|
|
||||||
},
|
|
||||||
"vars": v2.CaseInsensitiveObject[any]{},
|
|
||||||
"inputs": v2.CaseInsensitiveObject[any]{},
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
ee := &ExpressionEvaluator{
|
|
||||||
EvaluationContext: v2.EvaluationContext{
|
|
||||||
Variables: v2.CaseInsensitiveObject[any]{},
|
|
||||||
Functions: v2.GetFunctions(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var node yaml.Node
|
|
||||||
err := yaml.Unmarshal([]byte(tc.yamlInput), &node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if tc.restrict {
|
|
||||||
ee.RestrictEval = true
|
|
||||||
}
|
|
||||||
if tc.variables != nil {
|
|
||||||
ee.EvaluationContext.Variables = tc.variables
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
|
||||||
Definition: "workflow-root",
|
|
||||||
Schema: schema.GetWorkflowSchema(),
|
|
||||||
})
|
|
||||||
if tc.expectErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package templateeval
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func escapeFormatString(in string) string {
|
|
||||||
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteSubExpression(in string, forceFormat bool) (result string, isExpr bool, err error) {
|
|
||||||
// missing closing pair is an error
|
|
||||||
if !strings.Contains(in, "${{") {
|
|
||||||
return in, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
strPattern := regexp.MustCompile("(?:''|[^'])*'")
|
|
||||||
pos := 0
|
|
||||||
exprStart := -1
|
|
||||||
strStart := -1
|
|
||||||
var results []string
|
|
||||||
formatOut := ""
|
|
||||||
for pos < len(in) {
|
|
||||||
if strStart > -1 {
|
|
||||||
matches := strPattern.FindStringIndex(in[pos:])
|
|
||||||
if matches == nil {
|
|
||||||
return "", false, fmt.Errorf("unclosed string at position %d in %s", pos, in)
|
|
||||||
}
|
|
||||||
|
|
||||||
strStart = -1
|
|
||||||
pos += matches[1]
|
|
||||||
} else if exprStart > -1 {
|
|
||||||
exprEnd := strings.Index(in[pos:], "}}")
|
|
||||||
strStart = strings.Index(in[pos:], "'")
|
|
||||||
|
|
||||||
if exprEnd > -1 && strStart > -1 {
|
|
||||||
if exprEnd < strStart {
|
|
||||||
strStart = -1
|
|
||||||
} else {
|
|
||||||
exprEnd = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if exprEnd > -1 {
|
|
||||||
formatOut += fmt.Sprintf("{%d}", len(results))
|
|
||||||
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
|
|
||||||
pos += exprEnd + 2
|
|
||||||
exprStart = -1
|
|
||||||
} else if strStart > -1 {
|
|
||||||
pos += strStart + 1
|
|
||||||
} else {
|
|
||||||
return "", false, fmt.Errorf("unclosed expression at position %d in %s", pos, in)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exprStart = strings.Index(in[pos:], "${{")
|
|
||||||
if exprStart != -1 {
|
|
||||||
formatOut += escapeFormatString(in[pos : pos+exprStart])
|
|
||||||
exprStart = pos + exprStart + 3
|
|
||||||
pos = exprStart
|
|
||||||
} else {
|
|
||||||
formatOut += escapeFormatString(in[pos:])
|
|
||||||
pos = len(in)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
|
||||||
return results[0], true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
|
|
||||||
return out, true, nil
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package templateeval
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_NoExpression(t *testing.T) {
|
|
||||||
in := "Hello world"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("expected ok=false for no expression, got true with output %q", out)
|
|
||||||
}
|
|
||||||
if out != in {
|
|
||||||
t.Fatalf("expected output %q, got %q", in, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_SingleExpression(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ok=true for single expression, got false")
|
|
||||||
}
|
|
||||||
expected := "format('Hello {0}', 'world')"
|
|
||||||
if out != expected {
|
|
||||||
t.Fatalf("expected %q, got %q", expected, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_MultipleExpressions(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world' }}, you are ${{ 'awesome' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ok=true for multiple expressions, got false")
|
|
||||||
}
|
|
||||||
expected := "format('Hello {0}, you are {1}', 'world', 'awesome')"
|
|
||||||
if out != expected {
|
|
||||||
t.Fatalf("expected %q, got %q", expected, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_ForceFormatSingle(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, true)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ok=true when forceFormat, got false")
|
|
||||||
}
|
|
||||||
expected := "format('Hello {0}', 'world')"
|
|
||||||
if out != expected {
|
|
||||||
t.Fatalf("expected %q, got %q", expected, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_ForceFormatMultiple(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world' }}, you are ${{ 'awesome' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, true)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ok=true when forceFormat, got false")
|
|
||||||
}
|
|
||||||
expected := "format('Hello {0}, you are {1}', 'world', 'awesome')"
|
|
||||||
if out != expected {
|
|
||||||
t.Fatalf("expected %q, got %q", expected, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_UnclosedExpression(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world' " // missing closing }}
|
|
||||||
_, _, err := rewriteSubExpression(in, false)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "unclosed expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_UnclosedString(t *testing.T) {
|
|
||||||
in := "Hello ${{ 'world }}, you are ${{ 'awesome' }}"
|
|
||||||
_, _, err := rewriteSubExpression(in, false)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "unclosed string")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_EscapedStringLiteral(t *testing.T) {
|
|
||||||
// Two single quotes represent an escaped quote inside a string
|
|
||||||
in := "Hello ${{ 'It''s a test' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, ok)
|
|
||||||
expected := "format('Hello {0}', 'It''s a test')"
|
|
||||||
assert.Equal(t, expected, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_ExpressionAtEnd(t *testing.T) {
|
|
||||||
// Expression ends exactly at the string end – should be valid
|
|
||||||
in := "Hello ${{ 'world' }}"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, ok)
|
|
||||||
expected := "format('Hello {0}', 'world')"
|
|
||||||
assert.Equal(t, expected, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewriteSubExpression_ExpressionNotAtEnd(t *testing.T) {
|
|
||||||
// Expression followed by additional text – should still be valid
|
|
||||||
in := "Hello ${{ 'world' }}, how are you?"
|
|
||||||
out, ok, err := rewriteSubExpression(in, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, ok)
|
|
||||||
expected := "format('Hello {0}, how are you?', 'world')"
|
|
||||||
assert.Equal(t, expected, out)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// Package artifactcache provides a cache handler for the runner.
|
|
||||||
//
|
|
||||||
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
|
|
||||||
//
|
|
||||||
// TODO: Authorization
|
|
||||||
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
|
|
||||||
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
|
||||||
package artifactcache
|
|
||||||
@@ -1,613 +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"
|
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/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
|
|
||||||
externalAddress 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, errors.New("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 CreateHandler(dir, externalAddress string, logger logrus.FieldLogger) (*Handler, http.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, nil, err
|
|
||||||
}
|
|
||||||
dir = filepath.Join(home, ".cache", "actcache")
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.dir = dir
|
|
||||||
|
|
||||||
storage, err := NewStorage(filepath.Join(dir, "cache"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
h.storage = storage
|
|
||||||
|
|
||||||
if externalAddress != "" {
|
|
||||||
h.externalAddress = externalAddress
|
|
||||||
} else if ip := common.GetOutboundIP(); ip == nil {
|
|
||||||
return nil, nil, errors.New("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()
|
|
||||||
|
|
||||||
return h, router, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ExternalURL() string {
|
|
||||||
if h.externalAddress != "" {
|
|
||||||
return h.externalAddress
|
|
||||||
}
|
|
||||||
// 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.ParseUint(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.ParseUint(params.ByName("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
h.responseJSON(w, r, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.useCache(id)
|
|
||||||
h.storage.Serve(w, r, 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 := "^" + 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 //nolint:nilnil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 uint64) {
|
|
||||||
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,747 +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)
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
|
|
||||||
if resp != nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}
|
|
||||||
require.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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/clean", "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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,
|
|
||||||
base+"/caches/invalid_id", 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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("commit with bad id", func(t *testing.T) {
|
|
||||||
{
|
|
||||||
resp, err := http.Post(base+"/caches/invalid_id", "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 500, resp.StatusCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get with bad id", func(t *testing.T) {
|
|
||||||
resp, err := http.Get(base + "/artifacts/invalid_id")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer contentResp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer contentResp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer contentResp.Body.Close()
|
|
||||||
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) { //nolint:unparam
|
|
||||||
var id uint64
|
|
||||||
{
|
|
||||||
body, err := json.Marshal(&Request{
|
|
||||||
Key: key,
|
|
||||||
Version: version,
|
|
||||||
Size: int64(len(content)),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
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 {
|
|
||||||
require.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
assert.ErrorIs(t, err, bolthold.ErrNotFound)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
require.NoError(t, db.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateHandler(t *testing.T) {
|
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
|
||||||
handler, router, err := CreateHandler(dir, "http://localhost:8080", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, handler)
|
|
||||||
require.NotNil(t, router)
|
|
||||||
|
|
||||||
require.Equal(t, "http://localhost:8080", handler.ExternalURL())
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package artifactcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
rootDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStorage(rootDir string) (*Storage, error) {
|
|
||||||
if err := os.MkdirAll(rootDir, 0o755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Storage{
|
|
||||||
rootDir: rootDir,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Exist(id uint64) (bool, error) {
|
|
||||||
name := s.filename(id)
|
|
||||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Write(id uint64, offset int64, reader io.Reader) error {
|
|
||||||
name := s.tempName(id, offset)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := os.Create(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(file, reader)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Commit(id uint64, size int64) (int64, error) {
|
|
||||||
defer func() {
|
|
||||||
_ = os.RemoveAll(s.tempDir(id))
|
|
||||||
}()
|
|
||||||
|
|
||||||
name := s.filename(id)
|
|
||||||
tempNames, err := s.tempNames(id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
file, err := os.Create(name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var written int64
|
|
||||||
for _, v := range tempNames {
|
|
||||||
f, err := os.Open(v)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
n, err := io.Copy(file, f)
|
|
||||||
_ = f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return 0, 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 {
|
|
||||||
_ = file.Close()
|
|
||||||
_ = os.Remove(name)
|
|
||||||
return 0, fmt.Errorf("broken file: %v != %v", written, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return written, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id uint64) {
|
|
||||||
name := s.filename(id)
|
|
||||||
http.ServeFile(w, r, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Remove(id uint64) {
|
|
||||||
_ = os.Remove(s.filename(id))
|
|
||||||
_ = os.RemoveAll(s.tempDir(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) filename(id uint64) string {
|
|
||||||
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), strconv.FormatUint(id, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempDir(id uint64) string {
|
|
||||||
return filepath.Join(s.rootDir, "tmp", strconv.FormatUint(id, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempName(id uint64, offset int64) string {
|
|
||||||
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) tempNames(id uint64) ([]string, error) {
|
|
||||||
dir := s.tempDir(id)
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for _, v := range files {
|
|
||||||
if !v.IsDir() {
|
|
||||||
names = append(names, filepath.Join(dir, v.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names, nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user