Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1b9b277ee | |||
| 826ecfb433 | |||
| 5ac01b2dc9 | |||
| f984198d4d | |||
| 607c332313 | |||
| 50480c989c | |||
| bf71b55cb7 | |||
| 26b4e7497f | |||
| b2922e332a | |||
| d388ec5519 | |||
| cb1c1a3264 | |||
| 63967eb6fa | |||
| 22f1ea6e76 | |||
|
|
4d6900b7a3 | ||
|
|
898ef596ae | ||
|
|
eb37073861 | ||
|
|
ec9b323318 | ||
|
|
d955727863 | ||
|
|
3addd66efa | ||
|
|
b6d700af60 | ||
|
|
7c0d11c353 | ||
|
|
b9ae4d5f36 | ||
|
|
3a66563c1e | ||
|
|
e0feb6bd4e | ||
|
|
0db86bc6a4 | ||
|
|
f5b22c4149 | ||
|
|
0ba2e0c3d5 | ||
|
|
8a54ec62d4 | ||
|
|
587ac42be4 | ||
|
|
56dcda0d5e | ||
|
|
e44f0c403b | ||
|
|
fb1498bf7a | ||
|
|
fa69213d15 | ||
|
|
f92e50f35b | ||
|
|
a792b47b41 | ||
|
|
68ec7efde0 | ||
|
|
f314ffb036 | ||
|
|
b303a83a77 | ||
|
|
66d0b1e608 | ||
|
|
48a589eb79 | ||
|
|
fef300dd5b | ||
|
|
49a0b6f167 | ||
|
|
e5fdaadbd2 | ||
|
|
ab382dc256 | ||
|
|
2c66de4df2 | ||
|
|
9de33d4252 | ||
|
|
ff8375c6e1 | ||
| 3e22214bbd | |||
| b4e90db372 | |||
| c6b9e0c2d1 | |||
| aefab79307 | |||
| a6c08576a9 | |||
|
|
90d11b8692 | ||
|
|
c4b57fbcb2 | ||
|
|
e6dbe2a1ca | ||
|
|
774f316c8b | ||
|
|
823e9489d6 | ||
|
|
dc38bf1895 | ||
|
|
96b866a3a8 | ||
|
|
3b11bac2ad | ||
|
|
47caafd037 | ||
|
|
50e0509007 | ||
|
|
8920c4a170 | ||
|
|
bbf9d7e90f | ||
|
|
46f471a900 | ||
|
|
aa28f8d99c | ||
|
|
6a7e18b124 | ||
|
|
53329c46ff | ||
|
|
6b1aea9c04 | ||
|
|
edec9a8f91 | ||
|
|
6a9a447f86 | ||
|
|
5302c25feb | ||
|
|
a616ed1a10 | ||
|
|
f0b5aff3bb | ||
|
|
44b4736703 | ||
|
|
b1ae30dda8 | ||
|
|
0d687268c7 | ||
|
|
425a570261 | ||
|
|
4c8179ee12 | ||
|
|
5ae13f0bd7 | ||
|
|
3510152e36 | ||
|
|
8dfb805c62 | ||
|
|
a7080f5457 | ||
|
|
8b72d1c7ae | ||
|
|
8bc0275e74 | ||
|
|
0348aaac59 | ||
|
|
9712481bed | ||
|
|
b5f901b2d9 | ||
|
|
0e2a3e00f5 | ||
|
|
b282356e9e |
@@ -1,88 +0,0 @@
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
args: release --nightly
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: "gitea"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
DOCKER_ORG: gitea
|
||||
DOCKER_LATEST: nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
|
||||
- name: Build and push dind-rootless
|
||||
uses: docker/build-push-action@v5
|
||||
env:
|
||||
ACTIONS_RUNTIME_TOKEN: "" # See https://gitea.com/gitea/act_runner/issues/119
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.rootless
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless
|
||||
@@ -1,97 +1,79 @@
|
||||
name: release-tag
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: linux-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v5
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
args: release
|
||||
go-version-file: 'go.mod'
|
||||
cache: false
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: "gitea"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
DOCKER_ORG: gitea
|
||||
DOCKER_LATEST: latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
GOPRIVATE: git.marketally.com
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
|
||||
# Strip the v prefix from tag
|
||||
VERSION="${VERSION#v}"
|
||||
EXT=""
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
EXT=".exe"
|
||||
fi
|
||||
echo "Building version: ${VERSION}"
|
||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
|
||||
go build -a -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=${VERSION}" \
|
||||
-o gitcaddy-runner-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
name: gitcaddy-runner-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: gitcaddy-runner-*
|
||||
|
||||
- name: Build and push dind-rootless
|
||||
uses: docker/build-push-action@v5
|
||||
release:
|
||||
needs: build
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
find artifacts -type f -name 'gitcaddy-runner-*' -exec mv {} release/ \;
|
||||
cd release && sha256sum * > checksums.txt
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: release/*
|
||||
generate_release_notes: true
|
||||
env:
|
||||
ACTIONS_RUNTIME_TOKEN: "" # See https://gitea.com/gitea/act_runner/issues/119
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.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
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
name: checks
|
||||
name: CI
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: check and test
|
||||
runs-on: ubuntu-latest
|
||||
build-and-test:
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: vet checks
|
||||
cache: false
|
||||
|
||||
- name: Clear stale module cache
|
||||
run: go clean -modcache
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
- name: build
|
||||
env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: test
|
||||
env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
act_runner
|
||||
/act_runner
|
||||
*.exe
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
@@ -12,3 +13,4 @@ coverage.txt
|
||||
__debug_bin
|
||||
# gorelease binary folder
|
||||
dist
|
||||
act_runner-*
|
||||
|
||||
14
.gitsecrets-ignore
Normal file
14
.gitsecrets-ignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# GitSecrets Ignore File
|
||||
# This file tracks false positives identified by AI evaluation or manually marked.
|
||||
# Each line is a JSON object with the following fields:
|
||||
# - contentHash: SHA256 hash prefix of the secret content
|
||||
# - patternId: The pattern that detected this secret
|
||||
# - filePath: Relative path where the secret was found
|
||||
# - reason: Why this was marked as a false positive
|
||||
# - confidence: AI confidence level (if from AI evaluation)
|
||||
# - addedAt: Timestamp when this entry was added
|
||||
#
|
||||
# You can safely commit this file to share false positive markers with your team.
|
||||
# To remove an entry, simply delete the corresponding line.
|
||||
|
||||
{"contentHash":"5af30500c6463ec4","patternId":"password-assignment","filePath":"..\\gitcaddy\\internal\\app\\cmd\\register.go","reason":"Manually marked as false positive","addedAt":1769249840525}
|
||||
100
.golangci.yml
100
.golangci.yml
@@ -1,53 +1,42 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- gosimple
|
||||
- typecheck
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- dupl
|
||||
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
|
||||
- gofmt
|
||||
- misspell
|
||||
- gocritic
|
||||
- bidichk
|
||||
- ineffassign
|
||||
- revive
|
||||
- gofumpt
|
||||
- depguard
|
||||
- nakedret
|
||||
- unconvert
|
||||
- wastedassign
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
fast: false
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
|
||||
run:
|
||||
go: 1.18
|
||||
go: "1.23"
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- node_modules
|
||||
- public
|
||||
- web_src
|
||||
|
||||
linters-settings:
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1005", "-ST1003"]
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
|
||||
- singleCaseSwitch
|
||||
revive:
|
||||
ignore-generated-header: false
|
||||
severity: warning
|
||||
confidence: 0.8
|
||||
errorCode: 1
|
||||
warningCode: 1
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
@@ -72,94 +61,25 @@ linters-settings:
|
||||
- name: modifies-value-receiver
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
lang-version: "1.18"
|
||||
depguard:
|
||||
# TODO: use depguard to replace import checks in gitea-vet
|
||||
list-type: denylist
|
||||
# Check the list against standard lib.
|
||||
include-go-root: true
|
||||
packages-with-error-message:
|
||||
- github.com/unknwon/com: "use gitea's util and replacements"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- unparam
|
||||
- staticcheck
|
||||
- path: models/migrations/v
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- linters:
|
||||
- dupl
|
||||
text: "webhook"
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "`ID' should not be capitalized"
|
||||
- path: modules/templates/helper.go
|
||||
linters:
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
text: "swagger"
|
||||
- path: contrib/pr/checkout.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/issue.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: modules/log/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: routers/api/v1/repo/issue_subscription.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: routers/repo/view.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- unused
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "argument x is overwritten before first use"
|
||||
- path: modules/httplib/httplib.go
|
||||
linters:
|
||||
- staticcheck
|
||||
# Enabling this would require refactoring the methods and how they are called.
|
||||
- path: models/issue_comment_list.go
|
||||
linters:
|
||||
- dupl
|
||||
- linters:
|
||||
- misspell
|
||||
text: '`Unknwon` is a misspelling of `Unknown`'
|
||||
- path: models/update.go
|
||||
linters:
|
||||
- unused
|
||||
- path: cmd/dump.go
|
||||
linters:
|
||||
- dupl
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "exitAfterDefer:"
|
||||
linters:
|
||||
- gocritic
|
||||
- path: modules/graceful/manager_windows.go
|
||||
linters:
|
||||
- staticcheck
|
||||
text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
|
||||
- path: models/user/openid.go
|
||||
linters:
|
||||
- golint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
version: 2
|
||||
|
||||
project_name: gitcaddy-runner
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -16,6 +18,9 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- loong64
|
||||
- s390x
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
@@ -60,7 +65,7 @@ builds:
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
- -s -w -X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version={{ .Summary }}
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- .Version }}-
|
||||
@@ -83,7 +88,7 @@ blobs:
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
directory: "act_runner/{{.Version}}"
|
||||
directory: "gitcaddy-runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
@@ -105,8 +110,8 @@ nightly:
|
||||
version_template: "nightly"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
download: https://gitea.com
|
||||
api: https://git.marketally.com/api/v1
|
||||
download: https://git.marketally.com
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
|
||||
58
Dockerfile
58
Dockerfile
@@ -1,16 +1,64 @@
|
||||
FROM golang:1.23-alpine AS builder
|
||||
### BUILDER STAGE
|
||||
#
|
||||
#
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-}
|
||||
|
||||
COPY . /opt/src/act_runner
|
||||
WORKDIR /opt/src/act_runner
|
||||
|
||||
RUN make clean && make build
|
||||
|
||||
FROM alpine
|
||||
RUN apk add --no-cache git bash tini
|
||||
### DIND VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:28-dind AS dind
|
||||
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /opt/act/run.sh
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
ENTRYPOINT ["/sbin/tini","--","/opt/act/run.sh"]
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
|
||||
### DIND-ROOTLESS VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:28-dind-rootless AS dind-rootless
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
VOLUME /data
|
||||
|
||||
RUN mkdir -p /data && chown -R rootless:rootless /etc/s6 /data
|
||||
|
||||
ENV DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
|
||||
USER rootless
|
||||
ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
|
||||
### BASIC VARIANT
|
||||
#
|
||||
#
|
||||
FROM alpine AS basic
|
||||
RUN apk add --no-cache tini bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["/sbin/tini","--","run.sh"]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM golang:1.23-alpine AS builder
|
||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
COPY . /opt/src/act_runner
|
||||
WORKDIR /opt/src/act_runner
|
||||
|
||||
RUN make clean && make build
|
||||
|
||||
FROM docker:dind-rootless
|
||||
USER root
|
||||
RUN apk add --no-cache \
|
||||
git bash supervisor
|
||||
|
||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
||||
COPY /scripts/supervisord.conf /etc/supervisord.conf
|
||||
COPY /scripts/run.sh /opt/act/run.sh
|
||||
COPY /scripts/rootless.sh /opt/act/rootless.sh
|
||||
|
||||
RUN mkdir /data \
|
||||
&& chown rootless:rootless /data
|
||||
|
||||
USER rootless
|
||||
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||
121
HOWTOSTART.md
Normal file
121
HOWTOSTART.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# How to Start a GitCaddy Runner
|
||||
|
||||
This guide explains how to set up and start a GitCaddy Actions runner (act_runner) to execute your CI/CD workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Linux, macOS, or Windows machine
|
||||
- Network access to your GitCaddy/Gitea instance
|
||||
- (Optional) Docker installed for container-based workflows
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Download the Runner
|
||||
|
||||
Download the latest release from the [releases page](https://git.marketally.com/gitcaddy/act_runner/releases) or build from source:
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/act_runner.git
|
||||
cd act_runner
|
||||
make build
|
||||
```
|
||||
|
||||
### 2. Register the Runner
|
||||
|
||||
Get a registration token from your GitCaddy instance:
|
||||
- **Global runners**: Admin Area → Actions → Runners → Create Runner
|
||||
- **Organization runners**: Organization Settings → Actions → Runners
|
||||
- **Repository runners**: Repository Settings → Actions → Runners
|
||||
|
||||
Then register:
|
||||
|
||||
```bash
|
||||
./act_runner register --no-interactive \
|
||||
--instance https://your-gitea-instance.com \
|
||||
--token YOUR_REGISTRATION_TOKEN \
|
||||
--name my-runner \
|
||||
--labels linux,ubuntu-latest
|
||||
```
|
||||
|
||||
### 3. Start the Runner
|
||||
|
||||
```bash
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Runner Labels
|
||||
|
||||
Labels determine which jobs the runner can execute. Configure labels during registration or edit them in the admin UI.
|
||||
|
||||
Common labels:
|
||||
- `linux`, `linux-latest` - Linux runners
|
||||
- `windows`, `windows-latest` - Windows runners
|
||||
- `macos`, `macos-latest` - macOS runners
|
||||
- `ubuntu`, `ubuntu-latest` - Ubuntu-specific
|
||||
- `self-hosted` - Self-hosted runners
|
||||
|
||||
### Running as a Service
|
||||
|
||||
#### Linux (systemd)
|
||||
|
||||
```bash
|
||||
sudo cat > /etc/systemd/system/act_runner.service << 'SERVICE'
|
||||
[Unit]
|
||||
Description=GitCaddy Actions Runner
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=runner
|
||||
WorkingDirectory=/opt/act_runner
|
||||
ExecStart=/opt/act_runner/act_runner daemon
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE
|
||||
|
||||
sudo systemctl enable act_runner
|
||||
sudo systemctl start act_runner
|
||||
```
|
||||
|
||||
### Docker Support
|
||||
|
||||
For workflows that use container actions, ensure Docker is installed and the runner user has access:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
## Capabilities Detection
|
||||
|
||||
The runner automatically detects and reports:
|
||||
- Operating system and architecture
|
||||
- Available shells (bash, sh, powershell)
|
||||
- Installed tools (node, python, go, etc.)
|
||||
- Docker availability
|
||||
- Disk space and network bandwidth
|
||||
|
||||
These capabilities help admins understand what each runner can handle.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Runner not connecting
|
||||
|
||||
1. Check network connectivity to your GitCaddy instance
|
||||
2. Verify the registration token is valid
|
||||
3. Check firewall rules allow outbound HTTPS
|
||||
|
||||
### Jobs not running
|
||||
|
||||
1. Verify runner labels match the job's `runs-on` requirement
|
||||
2. Check runner is online in the admin panel
|
||||
3. Review runner logs: `journalctl -u act_runner -f`
|
||||
|
||||
## More Information
|
||||
|
||||
- [act_runner Repository](https://git.marketally.com/gitcaddy/act_runner)
|
||||
- [GitCaddy Documentation](https://git.marketally.com/gitcaddy/gitea)
|
||||
18
LICENSE.md
Normal file
18
LICENSE.md
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 gitcaddy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
27
Makefile
27
Makefile
@@ -1,13 +1,12 @@
|
||||
DIST := dist
|
||||
EXECUTABLE := act_runner
|
||||
EXECUTABLE := gitcaddy-runner
|
||||
GOFMT ?= gofumpt -l
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
XGO_VERSION := go-1.18.x
|
||||
XGO_VERSION := go-1.24.x
|
||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||
@@ -16,11 +15,13 @@ WINDOWS_ARCHS ?= windows/amd64
|
||||
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||
|
||||
DOCKER_IMAGE ?= gitea/act_runner
|
||||
DOCKER_IMAGE ?= marketally/gitcaddy-runner
|
||||
DOCKER_TAG ?= nightly
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
|
||||
ifneq ($(shell uname), Darwin)
|
||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||
else
|
||||
@@ -66,11 +67,11 @@ else
|
||||
endif
|
||||
endif
|
||||
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
|
||||
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
LDFLAGS ?= -X "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
|
||||
all: build
|
||||
|
||||
@@ -102,14 +103,21 @@ fmt-check:
|
||||
exit 1; \
|
||||
fi;
|
||||
|
||||
test: fmt-check
|
||||
.PHONY: deps-tools
|
||||
deps-tools: ## install tool dependencies
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE)
|
||||
|
||||
.PHONY: security-check
|
||||
security-check: deps-tools
|
||||
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
test: fmt-check security-check
|
||||
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@$(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
|
||||
@$(GO) vet $(GO_PACKAGES_TO_VET)
|
||||
|
||||
install: $(GOFILES)
|
||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||
@@ -170,7 +178,6 @@ docker:
|
||||
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||
fi; \
|
||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_ROOTLESS_REF) -f Dockerfile.rootless .
|
||||
|
||||
clean:
|
||||
$(GO) clean -x -i ./...
|
||||
|
||||
527
README.md
527
README.md
@@ -1,108 +1,529 @@
|
||||
# act runner
|
||||
# GitCaddy Runner
|
||||
|
||||
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act).
|
||||
A Gitea Actions runner with enhanced capability detection and reporting for AI-friendly workflow generation.
|
||||
|
||||
GitCaddy Runner is a hard fork of Gitea's act_runner, rebranded and enhanced with automated capability detection to enable AI tools to generate compatible workflows based on available resources.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automated Capability Detection**: Automatically identifies OS, architecture, installed tools, and available resources
|
||||
- **Concurrent Job Execution**: Configure runner capacity to handle multiple jobs simultaneously
|
||||
- **Docker Support**: Full support for Docker and Docker Compose workflows
|
||||
- **Xcode Integration**: Detects Xcode installations, SDKs, and simulators on macOS
|
||||
- **Tool Detection**: Identifies installed tools (Node.js, Python, .NET, Go, Ruby, Swift, etc.)
|
||||
- **AI-Friendly API**: Exposes capabilities through Gitea's API for automated workflow generation
|
||||
- **Cache Support**: Built-in workflow cache support for faster builds
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
### Pre-built Binaries
|
||||
|
||||
Docker Engine Community version is required for docker mode. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
|
||||
Download the latest release for your platform from the [releases page](https://git.marketally.com/gitcaddy/gitcaddy-runner/releases):
|
||||
|
||||
### Download pre-built binary
|
||||
**macOS:**
|
||||
```bash
|
||||
# Apple Silicon (M1/M2/M3/M4)
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-darwin-arm64
|
||||
chmod +x gitcaddy-runner
|
||||
|
||||
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
|
||||
# Intel
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-darwin-amd64
|
||||
chmod +x gitcaddy-runner
|
||||
```
|
||||
|
||||
### Build from source
|
||||
**Linux:**
|
||||
```bash
|
||||
# x86_64
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-linux-amd64
|
||||
chmod +x gitcaddy-runner
|
||||
|
||||
# ARM64
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-linux-arm64
|
||||
chmod +x gitcaddy-runner
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
# Download the Windows executable
|
||||
# https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-windows-amd64.exe
|
||||
```
|
||||
|
||||
### Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/gitcaddy-runner.git
|
||||
cd gitcaddy-runner
|
||||
make build
|
||||
```
|
||||
|
||||
### Build a docker image
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
make docker
|
||||
```
|
||||
### 1. Enable Gitea Actions
|
||||
|
||||
## Quickstart
|
||||
In your Gitea `app.ini`:
|
||||
|
||||
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED=true
|
||||
ENABLED = true
|
||||
```
|
||||
|
||||
### Register
|
||||
### 2. Generate Configuration
|
||||
|
||||
```bash
|
||||
./act_runner register
|
||||
./gitcaddy-runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
And you will be asked to input:
|
||||
### 3. Configure the Runner
|
||||
|
||||
1. Gitea instance URL, like `http://192.168.8.8:3000/`. You should use your gitea instance ROOT_URL as the instance argument
|
||||
and you should not use `localhost` or `127.0.0.1` as instance IP;
|
||||
2. Runner token, you can get it from `http://192.168.8.8:3000/admin/actions/runners`;
|
||||
3. Runner name, you can just leave it blank;
|
||||
4. Runner labels, you can just leave it blank.
|
||||
Edit `config.yaml` to customize settings. **Important configuration options:**
|
||||
|
||||
The process looks like:
|
||||
```yaml
|
||||
log:
|
||||
level: info
|
||||
|
||||
```text
|
||||
INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
|
||||
WARN Runner in user-mode.
|
||||
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
|
||||
http://192.168.8.8:3000/
|
||||
INFO Enter the runner token:
|
||||
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
|
||||
INFO Enter the runner name (if set empty, use hostname: Test.local):
|
||||
runner:
|
||||
file: .runner
|
||||
capacity: 2 # Number of concurrent jobs (default: 1)
|
||||
timeout: 3h
|
||||
shutdown_timeout: 3m # Grace period for running jobs on shutdown
|
||||
insecure: false
|
||||
fetch_timeout: 5s
|
||||
fetch_interval: 2s
|
||||
labels:
|
||||
- "ubuntu-latest:docker://node:16-bullseye"
|
||||
- "ubuntu-22.04:docker://node:16-bullseye"
|
||||
|
||||
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest):
|
||||
cache:
|
||||
enabled: true
|
||||
dir: ""
|
||||
|
||||
INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://gitea/runner-images:ubuntu-latest ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04 ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04].
|
||||
DEBU Successfully pinged the Gitea instance server
|
||||
INFO Runner registered successfully.
|
||||
container:
|
||||
network: ""
|
||||
privileged: false
|
||||
options: ""
|
||||
valid_volumes: []
|
||||
docker_host: ""
|
||||
force_pull: false
|
||||
|
||||
host:
|
||||
workdir_parent: ""
|
||||
```
|
||||
|
||||
You can also register with command line arguments.
|
||||
#### Capacity Configuration
|
||||
|
||||
The `capacity` setting controls how many jobs the runner can execute simultaneously:
|
||||
|
||||
- **Default**: 1 (one job at a time)
|
||||
- **Recommended**: 2-4 for multi-core systems
|
||||
- **Considerations**:
|
||||
- Each job consumes CPU, memory, and disk I/O
|
||||
- iOS/macOS builds are resource-intensive (start with 2)
|
||||
- Lighter builds (Node.js, Go) can handle higher capacity (4-6)
|
||||
- Monitor system load and adjust accordingly
|
||||
|
||||
**Example for different workloads:**
|
||||
|
||||
```yaml
|
||||
# Light builds (web apps, APIs)
|
||||
runner:
|
||||
capacity: 4
|
||||
|
||||
# Mixed builds
|
||||
runner:
|
||||
capacity: 2
|
||||
|
||||
# Heavy builds (iOS/macOS, large containers)
|
||||
runner:
|
||||
capacity: 1
|
||||
```
|
||||
|
||||
### 4. Register the Runner
|
||||
|
||||
```bash
|
||||
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
./gitcaddy-runner register \
|
||||
--instance https://your-gitea-instance.com \
|
||||
--token YOUR_REGISTRATION_TOKEN \
|
||||
--name my-runner \
|
||||
--labels ubuntu-latest:docker://node:16-bullseye
|
||||
```
|
||||
|
||||
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
||||
The registration token can be obtained from:
|
||||
- Gitea Admin Panel > Actions > Runners
|
||||
- Or repository Settings > Actions > Runners
|
||||
|
||||
### Run
|
||||
### 5. Start the Runner
|
||||
|
||||
**Important:** Always specify the config file path with `-c` flag:
|
||||
|
||||
```bash
|
||||
./act_runner daemon
|
||||
./gitcaddy-runner daemon -c config.yaml
|
||||
```
|
||||
|
||||
### Run with docker
|
||||
**Without the `-c` flag, the runner will use default settings and ignore your config.yaml!**
|
||||
|
||||
## Running as a Service
|
||||
|
||||
### macOS (launchd)
|
||||
|
||||
Create `~/Library/LaunchAgents/com.gitcaddy.runner.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.gitcaddy.runner</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/path/to/gitcaddy-runner</string>
|
||||
<string>daemon</string>
|
||||
<string>-c</string>
|
||||
<string>/path/to/config.yaml</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/path/to/runner/directory</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/path/to/runner.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/path/to/runner.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
Load the service:
|
||||
|
||||
```bash
|
||||
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/act_runner:nightly
|
||||
launchctl load ~/Library/LaunchAgents/com.gitcaddy.runner.plist
|
||||
```
|
||||
|
||||
### Configuration
|
||||
### Linux (systemd)
|
||||
|
||||
You can also configure the runner with a configuration file.
|
||||
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
|
||||
Create `/etc/systemd/system/gitcaddy-runner.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GitCaddy Actions Runner
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=runner
|
||||
WorkingDirectory=/home/runner/gitcaddy-runner
|
||||
ExecStart=/home/runner/gitcaddy-runner/gitcaddy-runner daemon -c /home/runner/gitcaddy-runner/config.yaml
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start:
|
||||
|
||||
```bash
|
||||
./act_runner generate-config > config.yaml
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable gitcaddy-runner
|
||||
sudo systemctl start gitcaddy-runner
|
||||
```
|
||||
|
||||
You can specify the configuration file path with `-c`/`--config` argument.
|
||||
### Windows (NSSM or Native Service)
|
||||
|
||||
GitCaddy Runner has native Windows service support. When running as a service, it automatically detects the Windows Service Control Manager (SCM) and handles stop/shutdown signals properly.
|
||||
|
||||
**Option 1: Using NSSM (Recommended)**
|
||||
|
||||
Install NSSM via Chocolatey:
|
||||
|
||||
```powershell
|
||||
choco install nssm -y
|
||||
```
|
||||
|
||||
Create the service:
|
||||
|
||||
```powershell
|
||||
# Install the service
|
||||
nssm install GiteaRunnerSvc C:\gitea-runner\gitcaddy-runner.exe daemon --config C:\gitea-runner\config.yaml
|
||||
|
||||
# Set working directory
|
||||
nssm set GiteaRunnerSvc AppDirectory C:\gitea-runner
|
||||
|
||||
# Set environment variables
|
||||
nssm set GiteaRunnerSvc AppEnvironmentExtra HOME=C:\gitea-runner USERPROFILE=C:\gitea-runner
|
||||
|
||||
# Configure auto-restart on failure
|
||||
sc failure GiteaRunnerSvc reset=86400 actions=restart/60000/restart/60000/restart/60000
|
||||
|
||||
# Start the service
|
||||
sc start GiteaRunnerSvc
|
||||
```
|
||||
|
||||
**Option 2: Native sc.exe (requires wrapper)**
|
||||
|
||||
Create a wrapper batch file `C:\gitea-runner\start-runner.bat`:
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
set HOME=C:\gitea-runner
|
||||
set USERPROFILE=C:\gitea-runner
|
||||
cd /d C:\gitea-runner
|
||||
C:\gitea-runner\gitcaddy-runner.exe daemon --config C:\gitea-runner\config.yaml
|
||||
```
|
||||
|
||||
**Service Management:**
|
||||
|
||||
```powershell
|
||||
# Check service status
|
||||
sc query GiteaRunnerSvc
|
||||
|
||||
# Start service
|
||||
sc start GiteaRunnerSvc
|
||||
|
||||
# Stop service
|
||||
sc stop GiteaRunnerSvc
|
||||
|
||||
# View service logs (if using NSSM with log rotation)
|
||||
Get-Content C:\gitea-runner\logs\runner.log -Tail 50
|
||||
```
|
||||
|
||||
**Environment Variables for Windows Services:**
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `GITEA_RUNNER_SERVICE_NAME` | Override service name detection | `GiteaRunnerSvc` |
|
||||
|
||||
## Capability Detection
|
||||
|
||||
GitCaddy Runner automatically detects and reports system capabilities:
|
||||
|
||||
### Platform Information
|
||||
- Operating system (darwin, linux, windows)
|
||||
- Architecture (amd64, arm64)
|
||||
|
||||
### Container Runtime
|
||||
- Docker availability and version
|
||||
- Docker Compose support
|
||||
- Container runtime type
|
||||
|
||||
### Development Tools
|
||||
- Node.js, npm, yarn
|
||||
- Python, pip
|
||||
- Go
|
||||
- .NET
|
||||
- Ruby
|
||||
- Rust
|
||||
- Java
|
||||
- Swift (macOS)
|
||||
- Git, Make
|
||||
|
||||
### macOS-Specific
|
||||
- Xcode version and build
|
||||
- Available SDKs (iOS, macOS, tvOS, watchOS, visionOS)
|
||||
- Simulators
|
||||
- Code signing tools (codesign, pkgbuild)
|
||||
- Package managers (Homebrew, CocoaPods, Fastlane)
|
||||
|
||||
### System Resources
|
||||
- CPU cores
|
||||
- Load average
|
||||
- Disk space and usage
|
||||
- Network bandwidth
|
||||
|
||||
### Example Capabilities Output
|
||||
|
||||
```json
|
||||
{
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"capacity": 2,
|
||||
"docker": true,
|
||||
"docker_compose": true,
|
||||
"container_runtime": "docker",
|
||||
"xcode": {
|
||||
"version": "15.2",
|
||||
"build": "15C500b",
|
||||
"sdks": ["iOS 17.2", "macOS 14.2"]
|
||||
},
|
||||
"tools": {
|
||||
"node": ["20.11"],
|
||||
"python": ["3.11"],
|
||||
"swift": ["5.9"]
|
||||
},
|
||||
"build_tools": ["fastlane", "cocoapods", "codesign"],
|
||||
"cpu": {
|
||||
"num_cpu": 10,
|
||||
"load_percent": 25.5
|
||||
},
|
||||
"disk": {
|
||||
"free_bytes": 54199226368,
|
||||
"used_percent": 77.89
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Runner Section
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `capacity` | int | 1 | Maximum concurrent jobs |
|
||||
| `timeout` | duration | 3h | Maximum job execution time |
|
||||
| `shutdown_timeout` | duration | 3m | Grace period for jobs to complete on shutdown |
|
||||
| `insecure` | bool | false | Allow insecure HTTPS |
|
||||
| `fetch_timeout` | duration | 5s | Timeout for fetching tasks |
|
||||
| `fetch_interval` | duration | 2s | Interval between task fetches |
|
||||
| `labels` | []string | [] | Runner labels for job matching |
|
||||
| `env_file` | string | .env | Environment variables file |
|
||||
|
||||
### Cache Section
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `enabled` | bool | true | Enable cache support |
|
||||
| `dir` | string | "" | Cache directory path |
|
||||
| `host` | string | "" | External cache server host |
|
||||
| `port` | int | 0 | External cache server port |
|
||||
|
||||
### Container Section
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `network` | string | "" | Docker network for containers |
|
||||
| `privileged` | bool | false | Run containers in privileged mode |
|
||||
| `docker_host` | string | "" | Custom Docker host |
|
||||
| `force_pull` | bool | false | Always pull latest images |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Capacity Not Being Applied
|
||||
|
||||
**Problem:** Runner shows `"capacity":1` even though config.yaml has `capacity: 2`
|
||||
|
||||
**Solution:** Ensure you're using the `-c` flag when starting the daemon:
|
||||
|
||||
```bash
|
||||
./act_runner -c config.yaml register # register with config file
|
||||
./act_runner -c config.yaml daemon # run with config file
|
||||
# ✅ Correct
|
||||
./gitcaddy-runner daemon -c /path/to/config.yaml
|
||||
|
||||
# ❌ Wrong - uses defaults
|
||||
./gitcaddy-runner daemon
|
||||
```
|
||||
|
||||
You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml).
|
||||
Verify the config is being loaded:
|
||||
```bash
|
||||
# Check runner process
|
||||
ps aux | grep gitcaddy-runner
|
||||
|
||||
### Example Deployments
|
||||
# Should show: gitcaddy-runner daemon -c /path/to/config.yaml
|
||||
```
|
||||
|
||||
Check out the [examples](examples) directory for sample deployment types.
|
||||
### Docker Not Detected
|
||||
|
||||
**Problem:** Capabilities show `"docker":false` but Docker is installed
|
||||
|
||||
**Solution:**
|
||||
1. Ensure Docker Desktop/daemon is running:
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
2. Restart the runner after starting Docker:
|
||||
```bash
|
||||
./gitcaddy-runner daemon -c config.yaml
|
||||
```
|
||||
|
||||
3. Check Docker socket permissions:
|
||||
```bash
|
||||
ls -l /var/run/docker.sock
|
||||
```
|
||||
|
||||
### Jobs Not Running Concurrently
|
||||
|
||||
**Problem:** Jobs queue instead of running in parallel
|
||||
|
||||
**Checklist:**
|
||||
1. Verify capacity in capabilities output (check runner logs)
|
||||
2. Confirm config.yaml has `capacity > 1`
|
||||
3. Ensure runner was started with `-c config.yaml` flag
|
||||
4. Check system resources aren't maxed out
|
||||
5. Restart runner after config changes
|
||||
|
||||
### Runner Not Starting
|
||||
|
||||
**Problem:** Runner exits immediately or fails to start
|
||||
|
||||
**Common causes:**
|
||||
1. Invalid config.yaml syntax
|
||||
2. `.runner` file missing (run `register` first)
|
||||
3. Permission issues on working directory
|
||||
4. Invalid Gitea instance URL or token
|
||||
|
||||
**Debug steps:**
|
||||
```bash
|
||||
# Check config syntax
|
||||
./gitcaddy-runner generate-config > test-config.yaml
|
||||
diff config.yaml test-config.yaml
|
||||
|
||||
# Test with verbose logging
|
||||
./gitcaddy-runner daemon -c config.yaml --log-level debug
|
||||
|
||||
# Verify registration
|
||||
cat .runner
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
GitCaddy Runner supports environment variable configuration:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `GITEA_RUNNER_CAPACITY` | Override capacity setting | `GITEA_RUNNER_CAPACITY=2` |
|
||||
| `GITEA_RUNNER_ENV_FILE` | Custom env file path | `GITEA_RUNNER_ENV_FILE=.env.prod` |
|
||||
|
||||
## API Integration
|
||||
|
||||
Query runner capabilities via Gitea API:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: token YOUR_TOKEN" \
|
||||
https://your-gitea.com/api/v1/runners
|
||||
```
|
||||
|
||||
Use capabilities to generate compatible workflows:
|
||||
|
||||
```yaml
|
||||
# Example: Use capabilities to select appropriate runner
|
||||
name: Build
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ capabilities.os == 'darwin' && 'macos-latest' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
## Support
|
||||
|
||||
- Issues: https://git.marketally.com/gitcaddy/gitcaddy-runner/issues
|
||||
- Discussions: https://git.marketally.com/gitcaddy/gitcaddy-runner/discussions
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
GitCaddy Runner is a hard fork of [Gitea's act_runner](https://gitea.com/gitea/act_runner), rebranded and enhanced with automated capability detection and reporting features for AI-friendly workflow generation.
|
||||
|
||||
BIN
act_runner_test
Executable file
BIN
act_runner_test
Executable file
Binary file not shown.
39
cmd/upload-helper/main.go
Normal file
39
cmd/upload-helper/main.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package main provides the upload-helper CLI tool for reliable file uploads.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/artifact"
|
||||
)
|
||||
|
||||
func main() {
|
||||
url := flag.String("url", "", "Upload URL")
|
||||
token := flag.String("token", "", "Auth token")
|
||||
file := flag.String("file", "", "File to upload")
|
||||
retries := flag.Int("retries", 5, "Maximum retry attempts")
|
||||
flag.Parse()
|
||||
|
||||
if *url == "" || *token == "" || *file == "" {
|
||||
fmt.Fprintf(os.Stderr, "GitCaddy Upload Helper - Reliable file uploads with retry\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: gitcaddy-upload -url URL -token TOKEN -file FILE\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Options:\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
helper := artifact.NewUploadHelper()
|
||||
helper.MaxRetries = *retries
|
||||
|
||||
if err := helper.UploadWithRetry(*url, *token, *file); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Upload failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Upload succeeded!")
|
||||
}
|
||||
@@ -5,12 +5,27 @@
|
||||
gitea:
|
||||
image: gitea/gitea
|
||||
...
|
||||
healthcheck:
|
||||
# checks availability of Gitea's front-end with curl
|
||||
test: ["CMD", "curl", "-f", "<instance_url>"]
|
||||
interval: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
environment:
|
||||
# GITEA_RUNNER_REGISTRATION_TOKEN can be used to set a global runner registration token.
|
||||
# The Gitea version must be v1.23 or higher.
|
||||
# It's also possible to use GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
|
||||
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
|
||||
|
||||
runner:
|
||||
image: gitea/act_runner
|
||||
restart: always
|
||||
depends_on:
|
||||
- gitea
|
||||
gitea:
|
||||
# required so runner can attach to gitea, see "healthcheck"
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
volumes:
|
||||
- ./data/act_runner:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -12,6 +12,9 @@ spec:
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
# The registration token can be obtained from the web UI, API or command-line.
|
||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -46,7 +49,7 @@ spec:
|
||||
containers:
|
||||
- name: runner
|
||||
image: gitea/act_runner:nightly
|
||||
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
|
||||
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: tcp://localhost:2376
|
||||
|
||||
@@ -12,7 +12,10 @@ spec:
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
token: << runner registration token goes here >>
|
||||
# The registration token can be obtained from the web UI, API or command-line.
|
||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: runner-secret
|
||||
|
||||
@@ -11,7 +11,8 @@ As `root`:
|
||||
```bash
|
||||
useradd -m rootless
|
||||
passwd rootless
|
||||
```
|
||||
apt-get install -y uidmap # Not mentioned but needed for docker rootless.
|
||||
```
|
||||
|
||||
- Install [`docker-ce`](https://docs.docker.com/engine/install/)
|
||||
- (Recommended) Disable the system-wide Docker daemon
|
||||
@@ -21,12 +22,19 @@ As `root`:
|
||||
As the `rootless` user:
|
||||
|
||||
- Follow the instructions for [enabling rootless mode](https://docs.docker.com/engine/security/rootless/)
|
||||
- Add the following lines to the `/home/rootless/.bashrc`:
|
||||
- Add the following line to the `/home/rootless/.bashrc`:
|
||||
|
||||
```bash
|
||||
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
|
||||
export PATH=/home/rootless/bin:$PATH
|
||||
export DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||
for f in ./.bashrc.d/*.bash; do echo "Processing $f file..."; . "$f"; done
|
||||
```
|
||||
|
||||
- Create the .bashrc.d directory `mkdir ~/.bashrc.d`
|
||||
- Add the following lines to the `/home/rootless/.bashrc.d/rootless-docker.bash`:
|
||||
|
||||
```bash
|
||||
export XDG_RUNTIME_DIR=/home/rootless/.docker/run
|
||||
export PATH=/home/rootless/bin:$PATH
|
||||
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
||||
```
|
||||
|
||||
- Reboot. Ensure that the Docker process is working.
|
||||
|
||||
63
go.mod
63
go.mod
@@ -1,39 +1,42 @@
|
||||
module gitea.com/gitea/act_runner
|
||||
module git.marketally.com/gitcaddy/gitcaddy-runner
|
||||
|
||||
go 1.23
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.4.0
|
||||
code.gitea.io/actions-proto-go v0.5.2
|
||||
code.gitea.io/gitea-vet v0.2.3
|
||||
connectrpc.com/connect v1.16.2
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/docker/docker v25.0.5+incompatible
|
||||
github.com/docker/docker v25.0.13+incompatible
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/nektos/act v0.0.0 // will be replaced
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/time v0.12.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.37.0
|
||||
|
||||
require (
|
||||
cyphar.com/go-pathrs v0.2.1 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/containerd/containerd v1.7.13 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // 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.8.2 // indirect
|
||||
@@ -43,14 +46,14 @@ require (
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // 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
|
||||
@@ -65,18 +68,19 @@ require (
|
||||
github.com/moby/buildkit v0.12.5 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/selinux v1.13.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rhysd/actionlint v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 // indirect
|
||||
@@ -89,13 +93,20 @@ require (
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.2
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.16.2
|
||||
|
||||
// Remove after
|
||||
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0
|
||||
|
||||
// Use GitCaddy fork with capability support
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
|
||||
|
||||
182
go.sum
182
go.sum
@@ -1,13 +1,15 @@
|
||||
code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU=
|
||||
code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
||||
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||
connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
|
||||
connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc=
|
||||
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
|
||||
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
gitea.com/gitea/act v0.261.2 h1:yAhxlt11gpRmF7CeVsVjgLg1Ph0xxroJ/l2fWaYyl84=
|
||||
gitea.com/gitea/act v0.261.2/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.8/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
||||
gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742 h1:ulcquQluJbmNASkh6ina70LvcHEa9eWYfQ+DeAZ0VEE=
|
||||
gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
@@ -17,31 +19,29 @@ 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.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
||||
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
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/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/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
|
||||
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
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/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is=
|
||||
github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
||||
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyphar/filepath-securejoin v0.3.0 h1:tXpmbiaeBrS/K2US8nhgwdKYnfAOnVfkcLPKFgFHeA0=
|
||||
github.com/cyphar/filepath-securejoin v0.3.0/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
@@ -51,32 +51,32 @@ github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6
|
||||
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
|
||||
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
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.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
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/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -86,12 +86,12 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
@@ -136,22 +136,24 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84=
|
||||
github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -163,16 +165,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -189,8 +191,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 h1:zjNCuOOhh1TKRU0Ru3PPPJt80z7eReswCao91gBLk00=
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928/go.mod h1:PCFYfAEfKT+Nd6zWvUpsXduMR1bXFLf0uGSlEF05MCI=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -205,7 +207,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
@@ -220,8 +221,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMey
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
|
||||
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
@@ -229,37 +230,27 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8
|
||||
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
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.3.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
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-20201020160332-67f06af15bc9/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -269,58 +260,43 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20210616094352-59db8d763f22/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
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-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-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
|
||||
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -22,8 +22,8 @@ type cacheServerArgs struct {
|
||||
Port uint16
|
||||
}
|
||||
|
||||
func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
func runCacheServer(_ context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cmd provides the CLI commands for gitcaddy-runner.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -10,14 +11,16 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Execute runs the root command for gitcaddy-runner CLI.
|
||||
func Execute(ctx context.Context) {
|
||||
// ./act_runner
|
||||
// ./gitcaddy-runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Use: "gitcaddy-runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: ver.Version(),
|
||||
@@ -26,7 +29,7 @@ func Execute(ctx context.Context) {
|
||||
configFile := ""
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||
|
||||
// ./act_runner register
|
||||
// ./gitcaddy-runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
@@ -35,25 +38,28 @@ func Execute(ctx context.Context) {
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs
|
||||
}
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "GitCaddy instance address")
|
||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token")
|
||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||
registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
// ./act_runner daemon
|
||||
// ./gitcaddy-runner daemon
|
||||
var daemArgs daemonArgs
|
||||
daemonCmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Run as a runner daemon",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runDaemon(ctx, &configFile),
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: runDaemon(ctx, &daemArgs, &configFile),
|
||||
}
|
||||
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit")
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// ./act_runner exec
|
||||
// ./gitcaddy-runner exec
|
||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||
|
||||
// ./act_runner config
|
||||
// ./gitcaddy-runner config
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "Generate an example config file",
|
||||
@@ -63,7 +69,7 @@ func Execute(ctx context.Context) {
|
||||
},
|
||||
})
|
||||
|
||||
// ./act_runner cache-server
|
||||
// ./gitcaddy-runner cache-server
|
||||
var cacheArgs cacheServerArgs
|
||||
cacheCmd := &cobra.Command{
|
||||
Use: "cache-server",
|
||||
@@ -76,6 +82,31 @@ func Execute(ctx context.Context) {
|
||||
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server")
|
||||
rootCmd.AddCommand(cacheCmd)
|
||||
|
||||
// ./gitcaddy-runner cleanup
|
||||
cleanupCmd := &cobra.Command{
|
||||
Use: "cleanup",
|
||||
Short: "Manually trigger cleanup to free disk space",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
result, err := cleanup.RunCleanup(ctx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cleanup failed: %w", err)
|
||||
}
|
||||
fmt.Printf("Cleanup completed: freed %d bytes, deleted %d files in %s\n", result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
if len(result.Errors) > 0 {
|
||||
fmt.Printf("Warnings: %d errors occurred\n", len(result.Errors))
|
||||
for _, e := range result.Errors {
|
||||
fmt.Printf(" - %s\n", e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(cleanupCmd)
|
||||
// hide completion command
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -13,28 +14,59 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/poll"
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/poll"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
const (
|
||||
// DiskSpaceWarningThreshold is the percentage at which to warn about low disk space
|
||||
DiskSpaceWarningThreshold = 85.0
|
||||
// DiskSpaceCriticalThreshold is the percentage at which to log critical warnings
|
||||
DiskSpaceCriticalThreshold = 95.0
|
||||
// DiskSpaceAutoCleanupThreshold is the percentage at which to trigger automatic cleanup
|
||||
DiskSpaceAutoCleanupThreshold = 85.0
|
||||
// CleanupCooldown is the minimum time between automatic cleanups
|
||||
CleanupCooldown = 10 * time.Minute
|
||||
// CapabilitiesUpdateInterval is how often to update capabilities (including disk space)
|
||||
CapabilitiesUpdateInterval = 5 * time.Minute
|
||||
// BandwidthTestInterval is how often to run bandwidth tests (hourly)
|
||||
BandwidthTestInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
// Global bandwidth manager - accessible for triggering manual tests
|
||||
var bandwidthManager *envcheck.BandwidthManager
|
||||
|
||||
// Global cleanup state
|
||||
var (
|
||||
lastCleanupTime time.Time
|
||||
cleanupMutex sync.Mutex
|
||||
globalConfig *config.Config
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
// Store config globally for auto-cleanup
|
||||
globalConfig = cfg
|
||||
|
||||
initLogging(cfg)
|
||||
log.Infoln("Starting runner daemon")
|
||||
|
||||
@@ -64,7 +96,34 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||
log.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||
}
|
||||
|
||||
if ls.RequireDocker() {
|
||||
if ls.RequireDocker() || cfg.Container.RequireDocker {
|
||||
// Wait for dockerd be ready
|
||||
if timeout := cfg.Container.DockerTimeout; timeout > 0 {
|
||||
tctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
keepRunning := true
|
||||
for keepRunning {
|
||||
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get socket path: %s", err.Error())
|
||||
} else if err = envcheck.CheckIfDockerRunning(tctx, dockerSocketPath); errors.Is(err, context.Canceled) {
|
||||
log.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Errorf("Docker connection failed: %s", err.Error())
|
||||
} else {
|
||||
log.Infof("Docker is ready")
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
case <-tctx.Done():
|
||||
log.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
keepRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Require dockerd be ready
|
||||
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -73,7 +132,7 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||
return err
|
||||
}
|
||||
// if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath
|
||||
os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
_ = os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
// empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
|
||||
// and assign the path to cfg.Container.DockerHost
|
||||
if cfg.Container.DockerHost == "" {
|
||||
@@ -107,24 +166,79 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
|
||||
// Detect runner capabilities for AI-friendly workflow generation
|
||||
dockerHost := cfg.Container.DockerHost
|
||||
if dockerHost == "" {
|
||||
if dh, err := getDockerSocketPath(""); err == nil {
|
||||
dockerHost = dh
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize bandwidth manager with the Gitea server address
|
||||
bandwidthManager = envcheck.NewBandwidthManager(reg.Address, BandwidthTestInterval)
|
||||
bandwidthManager.Start(ctx)
|
||||
log.Infof("bandwidth manager started, testing against: %s", reg.Address)
|
||||
|
||||
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, cfg.Container.WorkdirParent, globalConfig.Runner.Capacity)
|
||||
// Include initial bandwidth result if available
|
||||
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||
capabilitiesJSON := capabilities.ToJSON()
|
||||
log.Infof("detected capabilities: %s", capabilitiesJSON)
|
||||
|
||||
// Check disk space and warn if low
|
||||
checkDiskSpaceAndCleanup(ctx, capabilities)
|
||||
|
||||
// declare the labels of the runner before fetching tasks
|
||||
resp, err := runner.Declare(ctx, ls.Names())
|
||||
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||
log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJSON)
|
||||
switch {
|
||||
case err != nil && connect.CodeOf(err) == connect.CodeUnimplemented:
|
||||
log.Errorf("Your GitCaddy version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
return err
|
||||
} else if err != nil {
|
||||
case err != nil:
|
||||
log.WithError(err).Error("fail to invoke Declare")
|
||||
return err
|
||||
} else {
|
||||
default:
|
||||
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||
}
|
||||
|
||||
poller := poll.New(cfg, cli, runner)
|
||||
// Start periodic capabilities update goroutine
|
||||
go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost, cfg.Container.WorkdirParent)
|
||||
// Start periodic stale job cache cleanup (every hour, remove caches older than 2 hours)
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runner.CleanStaleJobCaches(2 * time.Hour)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go poller.Poll()
|
||||
poller := poll.New(ctx, cfg, cli, runner)
|
||||
poller.SetBandwidthManager(bandwidthManager)
|
||||
|
||||
if daemArgs.Once || reg.Ephemeral {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
poller.PollOnce()
|
||||
}()
|
||||
|
||||
// shutdown when we complete a job or cancel is requested
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-done:
|
||||
}
|
||||
} else {
|
||||
go poller.Poll()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
|
||||
@@ -134,45 +248,159 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||
if err != nil {
|
||||
log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// checkDiskSpaceAndCleanup logs warnings if disk space is low and triggers cleanup if needed
|
||||
func checkDiskSpaceAndCleanup(ctx context.Context, capabilities *envcheck.RunnerCapabilities) {
|
||||
if capabilities.Disk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
usedPercent := capabilities.Disk.UsedPercent
|
||||
freeGB := float64(capabilities.Disk.Free) / (1024 * 1024 * 1024)
|
||||
|
||||
switch {
|
||||
case usedPercent >= DiskSpaceCriticalThreshold:
|
||||
log.Errorf("CRITICAL: Disk space critically low! %.1f%% used, only %.2f GB free. Runner may fail to execute jobs!", usedPercent, freeGB)
|
||||
// Always try cleanup at critical level
|
||||
triggerAutoCleanup(ctx)
|
||||
case usedPercent >= DiskSpaceAutoCleanupThreshold:
|
||||
log.Warnf("WARNING: Disk space at %.1f%% used (%.2f GB free). Triggering automatic cleanup.", usedPercent, freeGB)
|
||||
triggerAutoCleanup(ctx)
|
||||
case usedPercent >= DiskSpaceWarningThreshold:
|
||||
log.Warnf("WARNING: Disk space running low. %.1f%% used, %.2f GB free. Consider cleaning up disk space.", usedPercent, freeGB)
|
||||
}
|
||||
}
|
||||
|
||||
// triggerAutoCleanup runs cleanup if cooldown has passed
|
||||
func triggerAutoCleanup(ctx context.Context) {
|
||||
cleanupMutex.Lock()
|
||||
defer cleanupMutex.Unlock()
|
||||
|
||||
// Check cooldown (except for first run)
|
||||
if !lastCleanupTime.IsZero() && time.Since(lastCleanupTime) < CleanupCooldown {
|
||||
log.Debugf("Skipping auto-cleanup, cooldown not expired (last cleanup: %s ago)", time.Since(lastCleanupTime))
|
||||
return
|
||||
}
|
||||
|
||||
if globalConfig == nil {
|
||||
log.Warn("Cannot run auto-cleanup: config not available")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Starting automatic disk cleanup...")
|
||||
lastCleanupTime = time.Now()
|
||||
|
||||
go func() {
|
||||
result, err := cleanup.RunCleanup(ctx, globalConfig)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Auto-cleanup failed")
|
||||
return
|
||||
}
|
||||
log.Infof("Auto-cleanup completed: freed %d bytes, deleted %d files in %s",
|
||||
result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
if len(result.Errors) > 0 {
|
||||
for _, e := range result.Errors {
|
||||
log.WithError(e).Warn("Cleanup error")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth
|
||||
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) {
|
||||
ticker := time.NewTicker(CapabilitiesUpdateInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("stopping periodic capabilities update")
|
||||
if bandwidthManager != nil {
|
||||
bandwidthManager.Stop()
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Detect updated capabilities (disk space changes over time)
|
||||
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, workingDir, globalConfig.Runner.Capacity)
|
||||
|
||||
// Include latest bandwidth result
|
||||
if bandwidthManager != nil {
|
||||
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||
}
|
||||
|
||||
capabilitiesJSON := capabilities.ToJSON()
|
||||
|
||||
// Check for disk space warnings
|
||||
checkDiskSpaceAndCleanup(ctx, capabilities)
|
||||
|
||||
// Send updated capabilities to server
|
||||
_, err := runner.Declare(ctx, labelNames, capabilitiesJSON)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failed to update capabilities")
|
||||
} else {
|
||||
bandwidthInfo := ""
|
||||
if capabilities.Bandwidth != nil {
|
||||
bandwidthInfo = fmt.Sprintf(", bandwidth: %.1f Mbps", capabilities.Bandwidth.DownloadMbps)
|
||||
}
|
||||
log.Debugf("capabilities updated: disk %.1f%% used, %.2f GB free%s",
|
||||
capabilities.Disk.UsedPercent,
|
||||
float64(capabilities.Disk.Free)/(1024*1024*1024),
|
||||
bandwidthInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type daemonArgs struct {
|
||||
Once bool
|
||||
}
|
||||
|
||||
// initLogging setup the global logrus logger.
|
||||
func initLogging(cfg *config.Config) {
|
||||
callPrettyfier := func(f *runtime.Frame) (string, string) {
|
||||
// get function name
|
||||
s := strings.Split(f.Function, ".")
|
||||
funcname := "[" + s[len(s)-1] + "]"
|
||||
// get file name and line number
|
||||
_, filename := path.Split(f.File)
|
||||
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
|
||||
return funcname, filename
|
||||
}
|
||||
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
format := &log.TextFormatter{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
CallerPrettyfier: callPrettyfier,
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
|
||||
if l := cfg.Log.Level; l != "" {
|
||||
level, err := log.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorf("invalid log level: %q", l)
|
||||
}
|
||||
l := cfg.Log.Level
|
||||
if l == "" {
|
||||
log.Infof("Log level not set, sticking to info")
|
||||
return
|
||||
}
|
||||
|
||||
// debug level
|
||||
if level == log.DebugLevel {
|
||||
log.SetReportCaller(true)
|
||||
format.CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||
// get function name
|
||||
s := strings.Split(f.Function, ".")
|
||||
funcname := "[" + s[len(s)-1] + "]"
|
||||
// get file name and line number
|
||||
_, filename := path.Split(f.File)
|
||||
filename = "[" + filename + ":" + strconv.Itoa(f.Line) + "]"
|
||||
return funcname, filename
|
||||
}
|
||||
log.SetFormatter(format)
|
||||
}
|
||||
level, err := log.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorf("invalid log level: %q", l)
|
||||
}
|
||||
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("log level changed to %v", level)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
// debug level
|
||||
switch level {
|
||||
case log.DebugLevel, log.TraceLevel:
|
||||
log.SetReportCaller(true) // Only in debug or trace because it takes a performance toll
|
||||
log.Infof("Log level %s requested, setting up report caller for further debugging", level)
|
||||
}
|
||||
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("log level set to %v", level)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ type executeArgs struct {
|
||||
envs []string
|
||||
envfile string
|
||||
secrets []string
|
||||
vars []string
|
||||
defaultActionsURL string
|
||||
insecureSecrets bool
|
||||
privileged bool
|
||||
@@ -130,6 +131,22 @@ func (i *executeArgs) LoadEnvs() map[string]string {
|
||||
return envs
|
||||
}
|
||||
|
||||
func (i *executeArgs) LoadVars() map[string]string {
|
||||
vars := make(map[string]string)
|
||||
if i.vars != nil {
|
||||
for _, runVar := range i.vars {
|
||||
e := strings.SplitN(runVar, `=`, 2)
|
||||
if len(e) == 2 {
|
||||
vars[e[0]] = e[1]
|
||||
} else {
|
||||
vars[e[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// Workdir returns path to workdir
|
||||
func (i *executeArgs) Workdir() string {
|
||||
return i.resolve(".")
|
||||
@@ -247,7 +264,7 @@ func printList(plan *model.Plan) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
func runExecList(_ context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
// plan with filtered jobs - to be used for filtering only
|
||||
var filterPlan *model.Plan
|
||||
|
||||
@@ -269,19 +286,20 @@ func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *e
|
||||
}
|
||||
|
||||
var err error
|
||||
if execArgs.job != "" {
|
||||
switch {
|
||||
case execArgs.job != "":
|
||||
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if filterEventName != "" {
|
||||
case filterEventName != "":
|
||||
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
log.Infof("Preparing plan with all jobs")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
@@ -295,7 +313,7 @@ func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *e
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -314,18 +332,19 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
// collect all events from loaded workflows
|
||||
events := planner.GetEvents()
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
switch {
|
||||
case len(execArgs.event) > 0:
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
eventName = execArgs.event
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
case len(events) == 1 && len(events[0]) > 0:
|
||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
case execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0:
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
default:
|
||||
log.Infof("Using default workflow event: push")
|
||||
eventName = "push"
|
||||
}
|
||||
@@ -371,7 +390,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
@@ -386,6 +405,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
LogOutput: true,
|
||||
JSONLogger: execArgs.jsonLogger,
|
||||
Env: execArgs.LoadEnvs(),
|
||||
Vars: execArgs.LoadVars(),
|
||||
Secrets: execArgs.LoadSecrets(),
|
||||
InsecureSecrets: execArgs.insecureSecrets,
|
||||
Privileged: execArgs.privileged,
|
||||
@@ -436,7 +456,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
|
||||
artifactCancel()
|
||||
return nil
|
||||
})
|
||||
@@ -468,6 +488,7 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
|
||||
execCmd.Flags().StringArrayVarP(&execArg.vars, "var", "", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
|
||||
execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")
|
||||
execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")
|
||||
@@ -484,9 +505,9 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "gitea/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "docker.gitea.com/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "GitCaddy instance to use.")
|
||||
|
||||
return execCmd
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
log.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
@@ -52,7 +52,7 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(ctx, *configFile); err != nil {
|
||||
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
@@ -75,10 +75,12 @@ type registerArgs struct {
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels string
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
type registerStage int8
|
||||
|
||||
// Register stage constants define the steps in the registration workflow.
|
||||
const (
|
||||
StageUnknown registerStage = -1
|
||||
StageOverwriteLocalConfig registerStage = iota + 1
|
||||
@@ -91,9 +93,9 @@ const (
|
||||
)
|
||||
|
||||
var defaultLabels = []string{
|
||||
"ubuntu-latest:docker://gitea/runner-images:ubuntu-latest",
|
||||
"ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04",
|
||||
"ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04",
|
||||
"ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest",
|
||||
"ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04",
|
||||
"ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04",
|
||||
}
|
||||
|
||||
type registerInputs struct {
|
||||
@@ -101,6 +103,7 @@ type registerInputs struct {
|
||||
Token string
|
||||
RunnerName string
|
||||
Labels []string
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
func (r *registerInputs) validate() error {
|
||||
@@ -125,6 +128,22 @@ func validateLabels(ls []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registerInputs) stageValue(stage registerStage) string {
|
||||
switch stage {
|
||||
case StageInputInstance:
|
||||
return r.InstanceAddr
|
||||
case StageInputToken:
|
||||
return r.Token
|
||||
case StageInputRunnerName:
|
||||
return r.RunnerName
|
||||
case StageInputLabels:
|
||||
if len(r.Labels) > 0 {
|
||||
return strings.Join(r.Labels, ",")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *config.Config) registerStage {
|
||||
// must set instance address and token.
|
||||
// if empty, keep current stage.
|
||||
@@ -178,7 +197,8 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
|
||||
}
|
||||
|
||||
if validateLabels(r.Labels) != nil {
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest)")
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest)")
|
||||
r.Labels = nil
|
||||
return StageInputLabels
|
||||
}
|
||||
return StageWaitingForRegistration
|
||||
@@ -186,11 +206,25 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
|
||||
return StageUnknown
|
||||
}
|
||||
|
||||
func registerInteractive(ctx context.Context, configFile string) error {
|
||||
func initInputs(regArgs *registerArgs) *registerInputs {
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
Ephemeral: regArgs.Ephemeral,
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
// command line flag.
|
||||
if regArgs.Labels != "" {
|
||||
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
||||
var (
|
||||
reader = bufio.NewReader(os.Stdin)
|
||||
stage = StageInputInstance
|
||||
inputs = new(registerInputs)
|
||||
)
|
||||
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
@@ -200,20 +234,24 @@ func registerInteractive(ctx context.Context, configFile string) error {
|
||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||
stage = StageOverwriteLocalConfig
|
||||
}
|
||||
inputs := initInputs(regArgs)
|
||||
|
||||
for {
|
||||
printStageHelp(stage)
|
||||
|
||||
cmdString, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
cmdString := inputs.stageValue(stage)
|
||||
if cmdString == "" {
|
||||
printStageHelp(stage)
|
||||
var err error
|
||||
cmdString, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
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.")
|
||||
return nil
|
||||
@@ -235,14 +273,14 @@ func printStageHelp(stage registerStage) {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
log.Infoln("Enter the GitCaddy instance URL (for example, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||
case StageInputLabels:
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://gitea/runner-images:ubuntu-latest):")
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("Waiting for registration...")
|
||||
}
|
||||
@@ -253,17 +291,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: regArgs.InstanceAddr,
|
||||
Token: regArgs.Token,
|
||||
RunnerName: regArgs.RunnerName,
|
||||
Labels: defaultLabels,
|
||||
}
|
||||
regArgs.Labels = strings.TrimSpace(regArgs.Labels)
|
||||
// command line flag.
|
||||
if regArgs.Labels != "" {
|
||||
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
||||
}
|
||||
inputs := initInputs(regArgs)
|
||||
// specify labels in config file.
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
if regArgs.Labels != "" {
|
||||
@@ -271,6 +299,9 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
}
|
||||
inputs.Labels = cfg.Runner.Labels
|
||||
}
|
||||
if len(inputs.Labels) == 0 {
|
||||
inputs.Labels = defaultLabels
|
||||
}
|
||||
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
@@ -278,10 +309,10 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
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.")
|
||||
return nil
|
||||
@@ -311,7 +342,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("Cannot ping the Gitea instance server")
|
||||
Errorln("Cannot ping the GitCaddy instance server")
|
||||
// TODO: if ping failed, retry or exit
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
@@ -321,10 +352,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
}
|
||||
|
||||
reg := &config.Registration{
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.Labels,
|
||||
Name: inputs.RunnerName,
|
||||
Token: inputs.Token,
|
||||
Address: inputs.InstanceAddr,
|
||||
Labels: inputs.Labels,
|
||||
Ephemeral: inputs.Ephemeral,
|
||||
}
|
||||
|
||||
ls := make([]string, len(reg.Labels))
|
||||
@@ -339,6 +371,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
Version: ver.Version(),
|
||||
AgentLabels: ls, // Could be removed after Gitea 1.20
|
||||
Labels: ls,
|
||||
Ephemeral: reg.Ephemeral,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
@@ -350,6 +383,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
reg.Name = resp.Msg.Runner.Name
|
||||
reg.Token = resp.Msg.Runner.Token
|
||||
|
||||
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
|
||||
// TODO we cannot remove the configuration via runner api, if we return an error here we just fill the database
|
||||
log.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
|
||||
}
|
||||
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("failed to save runner config: %w", err)
|
||||
}
|
||||
|
||||
19
internal/app/cmd/register_test.go
Normal file
19
internal/app/cmd/register_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
||||
err := registerNoInteractive(t.Context(), "", ®isterArgs{
|
||||
Labels: "label:invalid",
|
||||
Token: "token",
|
||||
InstanceAddr: "http://localhost:3000",
|
||||
})
|
||||
assert.Error(t, err, "unsupported schema: invalid")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package poll provides task polling functionality for CI runners.
|
||||
package poll
|
||||
|
||||
import (
|
||||
@@ -15,16 +16,20 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
|
||||
)
|
||||
|
||||
// Poller handles task polling from the Gitea server.
|
||||
type Poller struct {
|
||||
client client.Client
|
||||
runner *run.Runner
|
||||
cfg *config.Config
|
||||
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
|
||||
client client.Client
|
||||
runner *run.Runner
|
||||
cfg *config.Config
|
||||
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
|
||||
bandwidthManager *envcheck.BandwidthManager
|
||||
|
||||
pollingCtx context.Context
|
||||
shutdownPolling context.CancelFunc
|
||||
@@ -35,10 +40,11 @@ type Poller struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
|
||||
|
||||
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
|
||||
// New creates a new Poller instance with the given context for shutdown propagation.
|
||||
func New(ctx context.Context, cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
// Inherit from parent context so shutdown signals propagate properly
|
||||
pollingCtx, shutdownPolling := context.WithCancel(ctx)
|
||||
jobsCtx, shutdownJobs := context.WithCancel(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
@@ -57,6 +63,12 @@ func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
|
||||
}
|
||||
}
|
||||
|
||||
// SetBandwidthManager sets the bandwidth manager for on-demand testing
|
||||
func (p *Poller) SetBandwidthManager(bm *envcheck.BandwidthManager) {
|
||||
p.bandwidthManager = bm
|
||||
}
|
||||
|
||||
// Poll starts polling for tasks with the configured capacity.
|
||||
func (p *Poller) Poll() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
@@ -70,18 +82,29 @@ func (p *Poller) Poll() {
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// PollOnce polls for a single task and then exits.
|
||||
func (p *Poller) PollOnce() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
|
||||
p.pollOnce(limiter)
|
||||
|
||||
// signal that we're done
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the poller.
|
||||
func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownPolling()
|
||||
|
||||
select {
|
||||
// graceful shutdown completed succesfully
|
||||
// graceful shutdown completed successfully
|
||||
case <-p.done:
|
||||
return nil
|
||||
|
||||
// our timeout for shutting down ran out
|
||||
case <-ctx.Done():
|
||||
// when both the timeout fires and the graceful shutdown
|
||||
// completed succsfully, this branch of the select may
|
||||
// completed successfully, this branch of the select may
|
||||
// fire. Do a non-blocking check here against the graceful
|
||||
// shutdown status to avoid sending an error if we don't need to.
|
||||
_, ok := <-p.done
|
||||
@@ -93,7 +116,7 @@ func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownJobs()
|
||||
|
||||
// wait for running jobs to report their status to Gitea
|
||||
_, _ = <-p.done
|
||||
<-p.done
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -101,6 +124,19 @@ func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
|
||||
func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
p.pollOnce(limiter)
|
||||
|
||||
select {
|
||||
case <-p.pollingCtx.Done():
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) pollOnce(limiter *rate.Limiter) {
|
||||
for {
|
||||
if err := limiter.Wait(p.pollingCtx); err != nil {
|
||||
if p.pollingCtx.Err() != nil {
|
||||
@@ -114,6 +150,7 @@ func (p *Poller) poll(wg *sync.WaitGroup, limiter *rate.Limiter) {
|
||||
}
|
||||
|
||||
p.runTaskWithRecover(p.jobsCtx, task)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +171,23 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Detect capabilities including current disk space
|
||||
caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost, p.cfg.Container.WorkdirParent, p.cfg.Runner.Capacity)
|
||||
|
||||
// Include latest bandwidth result if available
|
||||
if p.bandwidthManager != nil {
|
||||
caps.Bandwidth = p.bandwidthManager.GetLastResult()
|
||||
}
|
||||
|
||||
capsJSON := caps.ToJSON()
|
||||
|
||||
// Load the version value that was in the cache when the request was sent.
|
||||
v := p.tasksVersion.Load()
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: v,
|
||||
}))
|
||||
fetchReq := &runnerv1.FetchTaskRequest{
|
||||
TasksVersion: v,
|
||||
CapabilitiesJson: capsJSON,
|
||||
}
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(fetchReq))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
err = nil
|
||||
}
|
||||
@@ -151,6 +200,32 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check if server requested a bandwidth test
|
||||
if resp.Msg.RequestBandwidthTest && p.bandwidthManager != nil {
|
||||
log.Info("Server requested bandwidth test, running now...")
|
||||
go func() {
|
||||
result := p.bandwidthManager.RunTest(ctx)
|
||||
if result != nil {
|
||||
log.Infof("Bandwidth test completed: %.1f Mbps download, %.0f ms latency",
|
||||
result.DownloadMbps, result.Latency)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Check if server requested a cleanup
|
||||
if resp.Msg.RequestCleanup {
|
||||
log.Info("Server requested cleanup, running now...")
|
||||
go func() {
|
||||
result, err := cleanup.RunCleanup(ctx, p.cfg)
|
||||
if err != nil {
|
||||
log.Errorf("Cleanup failed: %v", err)
|
||||
} else if result != nil {
|
||||
log.Infof("Cleanup completed: freed %d bytes, deleted %d files in %s",
|
||||
result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if resp.Msg.TasksVersion > v {
|
||||
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
||||
}
|
||||
@@ -159,7 +234,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
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)
|
||||
|
||||
return resp.Msg.Task, true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package run provides the core runner functionality for executing tasks.
|
||||
package run
|
||||
|
||||
import (
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -21,11 +22,11 @@ import (
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/report"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Runner runs the pipeline.
|
||||
@@ -41,6 +42,50 @@ type Runner struct {
|
||||
runningTasks sync.Map
|
||||
}
|
||||
|
||||
// getJobCacheDir returns a job-isolated cache directory
|
||||
func (r *Runner) getJobCacheDir(taskID int64) string {
|
||||
return filepath.Join(r.cfg.Host.WorkdirParent, "jobs", fmt.Sprintf("%d", taskID))
|
||||
}
|
||||
|
||||
// cleanupJobCache removes the job-specific cache directory after completion
|
||||
func (r *Runner) cleanupJobCache(taskID int64) {
|
||||
jobCacheDir := r.getJobCacheDir(taskID)
|
||||
if err := os.RemoveAll(jobCacheDir); err != nil {
|
||||
log.Warnf("failed to cleanup job cache %s: %v", jobCacheDir, err)
|
||||
} else {
|
||||
log.Infof("cleaned up job cache: %s", jobCacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanStaleJobCaches removes job cache directories older than maxAge
|
||||
func (r *Runner) CleanStaleJobCaches(maxAge time.Duration) {
|
||||
jobsDir := filepath.Join(r.cfg.Host.WorkdirParent, "jobs")
|
||||
entries, err := os.ReadDir(jobsDir)
|
||||
if err != nil {
|
||||
return // directory may not exist yet
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
jobPath := filepath.Join(jobsDir, entry.Name())
|
||||
if err := os.RemoveAll(jobPath); err != nil {
|
||||
log.Warnf("failed to remove stale job cache %s: %v", jobPath, err)
|
||||
} else {
|
||||
log.Infof("evicted stale job cache: %s", jobPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewRunner creates a new Runner with the given configuration, registration, and client.
|
||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||
ls := labels.Labels{}
|
||||
for _, v := range reg.Labels {
|
||||
@@ -89,12 +134,14 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes a task from the server.
|
||||
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||
return fmt.Errorf("task %d is already running", task.Id)
|
||||
}
|
||||
r.runningTasks.Store(task.Id, struct{}{})
|
||||
defer r.runningTasks.Delete(task.Id)
|
||||
defer r.cleanupJobCache(task.Id)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||
defer cancel()
|
||||
@@ -113,6 +160,17 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultActionsURL
|
||||
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
|
||||
// it should be set to GithubMirror first.
|
||||
func (r *Runner) getDefaultActionsURL(_ context.Context, task *runnerv1.Task) string {
|
||||
giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue()
|
||||
if giteaDefaultActionsURL == "https://github.com" && r.cfg.Runner.GithubMirror != "" {
|
||||
return r.cfg.Runner.GithubMirror
|
||||
}
|
||||
return giteaDefaultActionsURL
|
||||
}
|
||||
|
||||
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -137,7 +195,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
taskContext := task.Context.Fields
|
||||
|
||||
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||
taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
r.getDefaultActionsURL(ctx, task),
|
||||
r.client.Address())
|
||||
|
||||
preset := &model.GithubContext{
|
||||
@@ -163,6 +221,12 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
preset.Token = t
|
||||
}
|
||||
|
||||
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
||||
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
||||
}
|
||||
|
||||
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
|
||||
if giteaRuntimeToken == "" {
|
||||
// use task token to action api token for previous Gitea Server Versions
|
||||
@@ -180,19 +244,30 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
maxLifetime = time.Until(deadline)
|
||||
}
|
||||
|
||||
// Create job-specific environment with isolated cache directories
|
||||
jobCacheDir := r.getJobCacheDir(task.Id)
|
||||
jobEnvs := make(map[string]string, len(r.envs)+2)
|
||||
for k, v := range r.envs {
|
||||
jobEnvs[k] = v
|
||||
}
|
||||
// Isolate golangci-lint cache to prevent parallel job conflicts
|
||||
jobEnvs["GOLANGCI_LINT_CACHE"] = filepath.Join(jobCacheDir, "golangci-lint")
|
||||
// Set XDG_CACHE_HOME to isolate other tools that respect it
|
||||
jobEnvs["XDG_CACHE_HOME"] = jobCacheDir
|
||||
|
||||
runnerConfig := &runner.Config{
|
||||
// On Linux, 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)),
|
||||
BindWorkdir: false,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
ActionCacheDir: filepath.FromSlash(jobCacheDir),
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: r.cfg.Container.ForcePull,
|
||||
ForceRebuild: r.cfg.Container.ForceRebuild,
|
||||
LogOutput: true,
|
||||
JSONLogger: false,
|
||||
Env: r.envs,
|
||||
Env: jobEnvs,
|
||||
Secrets: task.Secrets,
|
||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||
AutoRemove: true,
|
||||
@@ -205,7 +280,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
ContainerOptions: r.cfg.Container.Options,
|
||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||
Privileged: r.cfg.Container.Privileged,
|
||||
DefaultActionInstance: taskContext["gitea_default_actions_url"].GetStringValue(),
|
||||
DefaultActionInstance: r.getDefaultActionsURL(ctx, task),
|
||||
PlatformPicker: r.labels.PickPlatform,
|
||||
Vars: task.Vars,
|
||||
ValidVolumes: r.cfg.Container.ValidVolumes,
|
||||
@@ -232,9 +307,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
return execErr
|
||||
}
|
||||
|
||||
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
// Declare sends the runner's labels and capabilities to the server.
|
||||
func (r *Runner) Declare(ctx context.Context, labels []string, capabilitiesJSON string) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
|
||||
Version: ver.Version(),
|
||||
Labels: labels,
|
||||
Version: ver.Version(),
|
||||
Labels: labels,
|
||||
CapabilitiesJson: capabilitiesJSON,
|
||||
}))
|
||||
}
|
||||
|
||||
146
internal/pkg/artifact/upload_helper.go
Normal file
146
internal/pkg/artifact/upload_helper.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package artifact provides utilities for handling artifact uploads.
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// UploadHelper handles reliable file uploads with retry logic
|
||||
type UploadHelper struct {
|
||||
MaxRetries int
|
||||
RetryDelay time.Duration
|
||||
ChunkSize int64
|
||||
ConnectTimeout time.Duration
|
||||
MaxTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewUploadHelper creates a new upload helper with sensible defaults
|
||||
func NewUploadHelper() *UploadHelper {
|
||||
return &UploadHelper{
|
||||
MaxRetries: 5,
|
||||
RetryDelay: 10 * time.Second,
|
||||
ChunkSize: 10 * 1024 * 1024, // 10MB
|
||||
ConnectTimeout: 120 * time.Second,
|
||||
MaxTimeout: 3600 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadWithRetry uploads a file with automatic retry on failure
|
||||
func (u *UploadHelper) UploadWithRetry(url, token, filepath string) error {
|
||||
client := &http.Client{
|
||||
Timeout: u.MaxTimeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
MaxIdleConnsPerHost: 5,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableKeepAlives: false, // Keep connections alive
|
||||
ForceAttemptHTTP2: false, // Use HTTP/1.1 for large uploads
|
||||
},
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < u.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
delay := u.RetryDelay * time.Duration(attempt)
|
||||
log.Infof("Upload attempt %d/%d, waiting %v before retry...", attempt+1, u.MaxRetries, delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
// Pre-resolve DNS / warm connection
|
||||
if err := u.prewarmConnection(url); err != nil {
|
||||
lastErr = fmt.Errorf("connection prewarm failed: %w", err)
|
||||
log.Warnf("Prewarm failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt upload
|
||||
if err := u.doUpload(client, url, token, filepath); err != nil {
|
||||
lastErr = err
|
||||
log.Warnf("Upload attempt %d failed: %v", attempt+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Upload succeeded on attempt %d", attempt+1)
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
return fmt.Errorf("upload failed after %d attempts: %w", u.MaxRetries, lastErr)
|
||||
}
|
||||
|
||||
// prewarmConnection establishes a connection to help with DNS and TCP setup
|
||||
func (u *UploadHelper) prewarmConnection(url string) error {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// doUpload performs the actual file upload
|
||||
func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string) error {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Uploading %s (%d bytes) to %s", filepath, stat.Size(), url)
|
||||
|
||||
// Create multipart form
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", stat.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create form file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return fmt.Errorf("failed to copy file to form: %w", err)
|
||||
}
|
||||
_ = writer.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upload request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
log.Infof("Upload completed successfully, status: %d", resp.StatusCode)
|
||||
return nil
|
||||
}
|
||||
390
internal/pkg/cleanup/cleanup.go
Normal file
390
internal/pkg/cleanup/cleanup.go
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cleanup provides disk cleanup utilities for CI runners.
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Result contains the results of a cleanup operation.
|
||||
type Result struct {
|
||||
BytesFreed int64
|
||||
FilesDeleted int
|
||||
Errors []error
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// RunCleanup performs cleanup operations to free disk space.
|
||||
func RunCleanup(_ context.Context, cfg *config.Config) (*Result, error) {
|
||||
start := time.Now()
|
||||
result := &Result{}
|
||||
|
||||
log.Info("Starting runner cleanup...")
|
||||
|
||||
// 1. Clean old cache directories
|
||||
cacheDir := filepath.Join(cfg.Cache.Dir, "_cache")
|
||||
if cacheDir != "" {
|
||||
if bytes, files, err := cleanOldDir(cacheDir, 24*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("cache cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned cache: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clean old work directories
|
||||
workDir := cfg.Container.WorkdirParent
|
||||
if workDir != "" {
|
||||
if bytes, files, err := cleanOldWorkDirs(workDir, 48*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("workdir cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned work dirs: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clean old artifact staging directories
|
||||
artifactDir := cfg.Cache.Dir
|
||||
if bytes, files, err := cleanOldArtifacts(artifactDir, 72*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("artifact cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned artifacts: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
// 4. Clean system temp files (older than 24h)
|
||||
if bytes, files, err := cleanTempDir(24 * time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("temp cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned temp: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
// 5. Clean build tool caches (older than 7 days)
|
||||
// These can grow very large from Go, npm, nuget, gradle, maven builds
|
||||
if bytes, files, err := cleanBuildCaches(7 * 24 * time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("build cache cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned build caches: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
result.Duration = time.Since(start)
|
||||
log.Infof("Cleanup completed: freed %s in %s", formatBytes(result.BytesFreed), result.Duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// cleanOldDir removes files older than maxAge from a directory
|
||||
func cleanOldDir(dir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := info.Size()
|
||||
if err := os.Remove(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return bytesFreed, filesDeleted, err
|
||||
}
|
||||
|
||||
// cleanOldWorkDirs removes work directories older than maxAge
|
||||
func cleanOldWorkDirs(baseDir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
entries, err := os.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(baseDir, entry.Name())
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := dirSize(path)
|
||||
if err := os.RemoveAll(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
log.Debugf("Removed old work dir: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// cleanOldArtifacts removes artifact staging files older than maxAge
|
||||
func cleanOldArtifacts(baseDir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
// Look for artifact staging dirs
|
||||
patterns := []string{"artifact-*", "upload-*", "download-*"}
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(baseDir, pattern))
|
||||
for _, path := range matches {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
var size int64
|
||||
if info.IsDir() {
|
||||
size = dirSize(path)
|
||||
err = os.RemoveAll(path)
|
||||
} else {
|
||||
size = info.Size()
|
||||
err = os.Remove(path)
|
||||
}
|
||||
if err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// cleanTempDir removes old files from system temp directory
|
||||
func cleanTempDir(maxAge time.Duration) (int64, int, error) {
|
||||
tmpDir := os.TempDir()
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Only clean files/dirs that look like runner/act artifacts or build tool temp files
|
||||
runnerPatterns := []string{
|
||||
"act-", "runner-", "gitea-", "workflow-",
|
||||
"go-build", "go-link",
|
||||
"node-compile-cache", "npm-", "yarn-", "yarn--", "pnpm-",
|
||||
"ts-node-", "tsx-", "jiti", "v8-compile-cache",
|
||||
"text-diff-expansion-test", "DiagOutputDir",
|
||||
"dugite-native-", "reorderCommitMessage-", "squashCommitMessage-",
|
||||
}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
isRunner := false
|
||||
for _, p := range runnerPatterns {
|
||||
if len(name) >= len(p) && name[:len(p)] == p {
|
||||
isRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isRunner {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(tmpDir, name)
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
var size int64
|
||||
if info.IsDir() {
|
||||
size = dirSize(path)
|
||||
err = os.RemoveAll(path)
|
||||
} else {
|
||||
size = info.Size()
|
||||
err = os.Remove(path)
|
||||
}
|
||||
if err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// dirSize calculates the total size of a directory.
|
||||
func dirSize(path string) int64 {
|
||||
var size int64
|
||||
_ = filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size
|
||||
}
|
||||
|
||||
// cleanBuildCaches removes old build tool caches that accumulate from CI jobs
|
||||
// These are cleaned more aggressively (files older than 7 days) since they can grow very large
|
||||
func cleanBuildCaches(maxAge time.Duration) (int64, int, error) {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE") // Windows
|
||||
}
|
||||
if home == "" {
|
||||
home = "/root" // fallback for runners typically running as root
|
||||
}
|
||||
|
||||
var totalBytesFreed int64
|
||||
var totalFilesDeleted int
|
||||
|
||||
// Build cache directories to clean
|
||||
// Format: {path, description, maxAge (0 = use default)}
|
||||
// Go build cache cleaned more aggressively (3 days) as it grows very fast
|
||||
goBuildMaxAge := 3 * 24 * time.Hour
|
||||
cacheDirs := []struct {
|
||||
path string
|
||||
desc string
|
||||
maxAge time.Duration
|
||||
}{
|
||||
// Linux paths
|
||||
{filepath.Join(home, ".cache", "go-build"), "Go build cache", goBuildMaxAge},
|
||||
{filepath.Join(home, ".cache", "golangci-lint"), "golangci-lint cache", 0},
|
||||
{filepath.Join(home, ".npm", "_cacache"), "npm cache", 0},
|
||||
{filepath.Join(home, ".cache", "pnpm"), "pnpm cache", 0},
|
||||
{filepath.Join(home, ".cache", "yarn"), "yarn cache", 0},
|
||||
{filepath.Join(home, ".nuget", "packages"), "NuGet cache", 0},
|
||||
{filepath.Join(home, ".gradle", "caches"), "Gradle cache", 0},
|
||||
{filepath.Join(home, ".m2", "repository"), "Maven cache", 0},
|
||||
{filepath.Join(home, ".cache", "pip"), "pip cache", 0},
|
||||
{filepath.Join(home, ".cargo", "registry", "cache"), "Cargo cache", 0},
|
||||
{filepath.Join(home, ".rustup", "tmp"), "Rustup temp", 0},
|
||||
// macOS paths (Library/Caches)
|
||||
{filepath.Join(home, "Library", "Caches", "go-build"), "Go build cache (macOS)", goBuildMaxAge},
|
||||
{filepath.Join(home, "Library", "Caches", "Yarn"), "Yarn cache (macOS)", 0},
|
||||
{filepath.Join(home, "Library", "Caches", "pip"), "pip cache (macOS)", 0},
|
||||
{filepath.Join(home, "Library", "Caches", "Homebrew"), "Homebrew cache (macOS)", 0},
|
||||
// Windows paths (LOCALAPPDATA)
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "go-build"), "Go build cache (Windows)", goBuildMaxAge},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "npm-cache"), "npm cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "pnpm"), "pnpm cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "Yarn", "Cache"), "Yarn cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "NuGet", "v3-cache"), "NuGet cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "pip", "Cache"), "pip cache (Windows)", 0},
|
||||
// Windows custom paths used by some CI setups
|
||||
{"C:\\L\\Yarn", "Yarn global cache (Windows)", 0},
|
||||
{filepath.Join(os.TempDir(), "chocolatey"), "Chocolatey temp cache", 0},
|
||||
}
|
||||
|
||||
for _, cache := range cacheDirs {
|
||||
if _, err := os.Stat(cache.path); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use cache-specific maxAge if set, otherwise use default
|
||||
cacheMaxAge := cache.maxAge
|
||||
if cacheMaxAge == 0 {
|
||||
cacheMaxAge = maxAge
|
||||
}
|
||||
cutoff := time.Now().Add(-cacheMaxAge)
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
|
||||
err := filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := info.Size()
|
||||
if err := os.Remove(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && (bytesFreed > 0 || filesDeleted > 0) {
|
||||
log.Infof("Cleaned %s: freed %s, deleted %d files", cache.desc, formatBytes(bytesFreed), filesDeleted)
|
||||
totalBytesFreed += bytesFreed
|
||||
totalFilesDeleted += filesDeleted
|
||||
}
|
||||
|
||||
// Also remove empty directories
|
||||
_ = filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || !info.IsDir() || path == cache.path {
|
||||
return nil
|
||||
}
|
||||
entries, _ := os.ReadDir(path)
|
||||
if len(entries) == 0 {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return totalBytesFreed, totalFilesDeleted, nil
|
||||
}
|
||||
|
||||
// formatBytes formats bytes into human readable string
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package client provides the HTTP client for communicating with the runner API.
|
||||
package client
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
package client
|
||||
|
||||
// HTTP header constants for runner authentication and identification.
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
@@ -15,16 +16,24 @@ import (
|
||||
)
|
||||
|
||||
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
MaxIdleConnsPerHost: 5,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint, "https://") && insecure {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
return http.DefaultClient
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new runner client.
|
||||
@@ -63,10 +72,12 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
||||
}
|
||||
}
|
||||
|
||||
// Address returns the endpoint URL of the client.
|
||||
func (c *HTTPClient) Address() string {
|
||||
return c.endpoint
|
||||
}
|
||||
|
||||
// Insecure returns whether TLS verification is disabled.
|
||||
func (c *HTTPClient) Insecure() bool {
|
||||
return c.insecure
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ runner:
|
||||
env_file: .env
|
||||
# The timeout for a job to be finished.
|
||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
|
||||
# So the job could be stopped by the Gitea instance if its timeout is shorter than this.
|
||||
timeout: 3h
|
||||
# The timeout for the runner to wait for running jobs to finish when shutting down.
|
||||
# Any running jobs that haven't finished after this timeout will be cancelled.
|
||||
@@ -32,15 +32,20 @@ runner:
|
||||
fetch_timeout: 5s
|
||||
# The interval for fetching the job from the Gitea instance.
|
||||
fetch_interval: 2s
|
||||
# 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,
|
||||
# and github_mirror is not empty. In this case,
|
||||
# it replaces https://github.com with the value here, which is useful for some special network environments.
|
||||
github_mirror: ''
|
||||
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
|
||||
# Like: "macos-arm64:host" or "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
|
||||
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
|
||||
# If it's empty when registering, it will ask for inputting labels.
|
||||
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
|
||||
- "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
|
||||
- "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"
|
||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
@@ -67,7 +72,7 @@ container:
|
||||
network: ""
|
||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||
privileged: false
|
||||
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||
# Any other options to be used when the container is started (e.g., --add-host=my.gitea.url:host-gateway).
|
||||
options:
|
||||
# The parent directory of a job's working directory.
|
||||
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
|
||||
@@ -85,7 +90,7 @@ container:
|
||||
# valid_volumes:
|
||||
# - '**'
|
||||
valid_volumes: []
|
||||
# overrides the docker client host with the specified one.
|
||||
# Overrides the docker client host with the specified one.
|
||||
# If it's empty, act_runner will find an available docker host automatically.
|
||||
# If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
|
||||
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
||||
@@ -94,6 +99,10 @@ container:
|
||||
force_pull: true
|
||||
# Rebuild docker image(s) even if already present
|
||||
force_rebuild: false
|
||||
# Always require a reachable docker daemon, even if not required by act_runner
|
||||
require_docker: false
|
||||
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
||||
docker_timeout: 0s
|
||||
|
||||
host:
|
||||
# The parent directory of a job's working directory.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package config provides configuration loading and management for the runner.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -31,6 +32,7 @@ type Runner struct {
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// Cache represents the configuration for caching.
|
||||
@@ -44,15 +46,17 @@ type Cache struct {
|
||||
|
||||
// Container represents the configuration for the container.
|
||||
type Container struct {
|
||||
Network string `yaml:"network"` // Network specifies the network for the container.
|
||||
NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
|
||||
Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
|
||||
Options string `yaml:"options"` // Options specifies additional options for the container.
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
|
||||
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers.
|
||||
DockerHost string `yaml:"docker_host"` // DockerHost specifies the Docker host. It overrides the value specified in environment variable DOCKER_HOST.
|
||||
ForcePull bool `yaml:"force_pull"` // Pull docker image(s) even if already present
|
||||
ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present
|
||||
Network string `yaml:"network"` // Network specifies the network for the container.
|
||||
NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20
|
||||
Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode.
|
||||
Options string `yaml:"options"` // Options specifies additional options for the container.
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory.
|
||||
ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers.
|
||||
DockerHost string `yaml:"docker_host"` // DockerHost specifies the Docker host. It overrides the value specified in environment variable DOCKER_HOST.
|
||||
ForcePull bool `yaml:"force_pull"` // Pull 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
|
||||
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
|
||||
}
|
||||
|
||||
// Host represents the configuration for the host.
|
||||
@@ -134,6 +138,9 @@ func LoadDefault(file string) (*Config, error) {
|
||||
if cfg.Runner.FetchInterval <= 0 {
|
||||
cfg.Runner.FetchInterval = 2 * time.Second
|
||||
}
|
||||
if cfg.Runner.ShutdownTimeout <= 0 {
|
||||
cfg.Runner.ShutdownTimeout = 3 * time.Minute
|
||||
}
|
||||
|
||||
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||
|
||||
@@ -5,5 +5,7 @@ package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// Example contains the example configuration file content.
|
||||
//
|
||||
//go:embed config.example.yaml
|
||||
var Example []byte
|
||||
|
||||
@@ -14,20 +14,22 @@ const registrationWarning = "This file is automatically generated by act-runner.
|
||||
type Registration struct {
|
||||
Warning string `json:"WARNING"` // Warning message to display, it's always the registrationWarning constant
|
||||
|
||||
ID int64 `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Labels []string `json:"labels"`
|
||||
ID int64 `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Labels []string `json:"labels"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
}
|
||||
|
||||
// LoadRegistration loads the runner registration from a JSON file.
|
||||
func LoadRegistration(file string) (*Registration, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
var reg Registration
|
||||
if err := json.NewDecoder(f).Decode(®); err != nil {
|
||||
@@ -39,12 +41,13 @@ func LoadRegistration(file string) (*Registration, error) {
|
||||
return ®, nil
|
||||
}
|
||||
|
||||
// SaveRegistration saves the runner registration to a JSON file.
|
||||
func SaveRegistration(file string, reg *Registration) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
reg.Warning = registrationWarning
|
||||
|
||||
|
||||
209
internal/pkg/envcheck/bandwidth.go
Normal file
209
internal/pkg/envcheck/bandwidth.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BandwidthInfo holds network bandwidth test results
|
||||
type BandwidthInfo struct {
|
||||
DownloadMbps float64 `json:"download_mbps"`
|
||||
UploadMbps float64 `json:"upload_mbps,omitempty"`
|
||||
Latency float64 `json:"latency_ms,omitempty"`
|
||||
TestedAt time.Time `json:"tested_at"`
|
||||
}
|
||||
|
||||
// BandwidthManager handles periodic bandwidth testing
|
||||
type BandwidthManager struct {
|
||||
serverURL string
|
||||
lastResult *BandwidthInfo
|
||||
mu sync.RWMutex
|
||||
testInterval time.Duration
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewBandwidthManager creates a new bandwidth manager
|
||||
func NewBandwidthManager(serverURL string, testInterval time.Duration) *BandwidthManager {
|
||||
return &BandwidthManager{
|
||||
serverURL: serverURL,
|
||||
testInterval: testInterval,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins periodic bandwidth testing
|
||||
func (bm *BandwidthManager) Start(ctx context.Context) {
|
||||
// Run initial test
|
||||
bm.RunTest(ctx)
|
||||
|
||||
// Start periodic testing
|
||||
go func() {
|
||||
ticker := time.NewTicker(bm.testInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
bm.RunTest(ctx)
|
||||
case <-bm.stopChan:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the periodic testing
|
||||
func (bm *BandwidthManager) Stop() {
|
||||
close(bm.stopChan)
|
||||
}
|
||||
|
||||
// RunTest runs a bandwidth test and stores the result
|
||||
func (bm *BandwidthManager) RunTest(ctx context.Context) *BandwidthInfo {
|
||||
result := TestBandwidth(ctx, bm.serverURL)
|
||||
|
||||
bm.mu.Lock()
|
||||
bm.lastResult = result
|
||||
bm.mu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLastResult returns the most recent bandwidth test result
|
||||
func (bm *BandwidthManager) GetLastResult() *BandwidthInfo {
|
||||
bm.mu.RLock()
|
||||
defer bm.mu.RUnlock()
|
||||
return bm.lastResult
|
||||
}
|
||||
|
||||
// TestBandwidth tests network bandwidth to the GitCaddy server
|
||||
func TestBandwidth(ctx context.Context, serverURL string) *BandwidthInfo {
|
||||
if serverURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := &BandwidthInfo{
|
||||
TestedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Test latency first
|
||||
info.Latency = testLatency(ctx, serverURL)
|
||||
|
||||
// Test download speed
|
||||
info.DownloadMbps = testDownloadSpeed(ctx, serverURL)
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func testLatency(ctx context.Context, serverURL string) float64 {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, "HEAD", serverURL, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
latency := time.Since(start).Seconds() * 1000 // Convert to ms
|
||||
return float64(int(latency*100)) / 100 // Round to 2 decimals
|
||||
}
|
||||
|
||||
func testDownloadSpeed(ctx context.Context, serverURL string) float64 {
|
||||
// Try multiple endpoints to accumulate ~1MB of data
|
||||
endpoints := []string{
|
||||
"/assets/css/index.css",
|
||||
"/assets/js/index.js",
|
||||
"/assets/img/logo.svg",
|
||||
"/assets/img/logo.png",
|
||||
"/",
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
var totalBytes int64
|
||||
var totalDuration time.Duration
|
||||
targetBytes := int64(1024 * 1024) // 1MB target
|
||||
maxAttempts := 10 // Limit iterations
|
||||
|
||||
for attempt := 0; attempt < maxAttempts && totalBytes < targetBytes; attempt++ {
|
||||
for _, endpoint := range endpoints {
|
||||
if totalBytes >= targetBytes {
|
||||
break
|
||||
}
|
||||
|
||||
url := serverURL + endpoint
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
req, err := http.NewRequestWithContext(reqCtx, "GET", url, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
n, _ := io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
cancel()
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
if n > 0 {
|
||||
totalBytes += n
|
||||
totalDuration += duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalBytes == 0 || totalDuration == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate speed in Mbps
|
||||
seconds := totalDuration.Seconds()
|
||||
if seconds == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytesPerSecond := float64(totalBytes) / seconds
|
||||
mbps := (bytesPerSecond * 8) / (1024 * 1024)
|
||||
|
||||
return float64(int(mbps*100)) / 100
|
||||
}
|
||||
|
||||
// FormatBandwidth formats bandwidth for display
|
||||
func FormatBandwidth(mbps float64) string {
|
||||
if mbps == 0 {
|
||||
return "Unknown"
|
||||
}
|
||||
if mbps >= 1000 {
|
||||
return fmt.Sprintf("%.1f Gbps", mbps/1000)
|
||||
}
|
||||
return fmt.Sprintf("%.1f Mbps", mbps)
|
||||
}
|
||||
1054
internal/pkg/envcheck/capabilities.go
Normal file
1054
internal/pkg/envcheck/capabilities.go
Normal file
File diff suppressed because it is too large
Load Diff
43
internal/pkg/envcheck/disk_unix.go
Normal file
43
internal/pkg/envcheck/disk_unix.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build unix
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// detectDiskSpace detects disk space on the specified path's filesystem (Unix version)
|
||||
// If path is empty, defaults to "/"
|
||||
func detectDiskSpace(path string) *DiskInfo {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
var stat unix.Statfs_t
|
||||
|
||||
err := unix.Statfs(path, &stat)
|
||||
if err != nil {
|
||||
// Fallback to root if the path doesn't exist
|
||||
err = unix.Statfs("/", &stat)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = "/"
|
||||
}
|
||||
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bavail * uint64(stat.Bsize)
|
||||
used := total - free
|
||||
usedPercent := float64(used) / float64(total) * 100
|
||||
|
||||
return &DiskInfo{
|
||||
Path: path,
|
||||
Total: total,
|
||||
Free: free,
|
||||
Used: used,
|
||||
UsedPercent: usedPercent,
|
||||
}
|
||||
}
|
||||
57
internal/pkg/envcheck/disk_windows.go
Normal file
57
internal/pkg/envcheck/disk_windows.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// detectDiskSpace detects disk space on the specified path's drive (Windows version)
|
||||
// If path is empty, defaults to "C:\"
|
||||
func detectDiskSpace(path string) *DiskInfo {
|
||||
if path == "" {
|
||||
path = "C:\\"
|
||||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
absPath = "C:\\"
|
||||
}
|
||||
|
||||
// Extract drive letter (e.g., "D:\" from "D:\builds\runner")
|
||||
drivePath := filepath.VolumeName(absPath) + "\\"
|
||||
if drivePath == "\\" {
|
||||
drivePath = "C:\\"
|
||||
}
|
||||
|
||||
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64
|
||||
|
||||
pathPtr := windows.StringToUTF16Ptr(drivePath)
|
||||
err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
|
||||
if err != nil {
|
||||
// Fallback to C: drive
|
||||
pathPtr = windows.StringToUTF16Ptr("C:\\")
|
||||
err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
drivePath = "C:\\"
|
||||
}
|
||||
|
||||
used := totalNumberOfBytes - totalNumberOfFreeBytes
|
||||
usedPercent := float64(used) / float64(totalNumberOfBytes) * 100
|
||||
|
||||
return &DiskInfo{
|
||||
Path: drivePath,
|
||||
Total: totalNumberOfBytes,
|
||||
Free: totalNumberOfFreeBytes,
|
||||
Used: used,
|
||||
UsedPercent: usedPercent,
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// CheckIfDockerRunning verifies that the Docker daemon is running and accessible.
|
||||
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
opts := []client.Opt{
|
||||
client.FromEnv,
|
||||
@@ -23,7 +24,7 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package labels provides utilities for parsing and managing runner labels.
|
||||
package labels
|
||||
|
||||
import (
|
||||
@@ -8,17 +9,20 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Label scheme constants define the execution environments.
|
||||
const (
|
||||
SchemeHost = "host"
|
||||
SchemeDocker = "docker"
|
||||
)
|
||||
|
||||
// Label represents a parsed runner label with name, schema, and optional argument.
|
||||
type Label struct {
|
||||
Name string
|
||||
Schema string
|
||||
Arg string
|
||||
}
|
||||
|
||||
// Parse parses a label string in the format "name:schema:arg" and returns a Label.
|
||||
func Parse(str string) (*Label, error) {
|
||||
splits := strings.SplitN(str, ":", 3)
|
||||
label := &Label{
|
||||
@@ -38,8 +42,10 @@ func Parse(str string) (*Label, error) {
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// Labels is a slice of Label pointers.
|
||||
type Labels []*Label
|
||||
|
||||
// RequireDocker returns true if any label uses the docker schema.
|
||||
func (l Labels) RequireDocker() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeDocker {
|
||||
@@ -49,6 +55,7 @@ func (l Labels) RequireDocker() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PickPlatform selects the appropriate platform based on the runsOn requirements.
|
||||
func (l Labels) PickPlatform(runsOn []string) string {
|
||||
platforms := make(map[string]string, len(l))
|
||||
for _, label := range l {
|
||||
@@ -79,9 +86,10 @@ func (l Labels) PickPlatform(runsOn []string) string {
|
||||
// So the runner receives a task with a label that the runner doesn't have,
|
||||
// it happens when the user have edited the label of the runner in the web UI.
|
||||
// TODO: it may be not correct, what if the runner is used as host mode only?
|
||||
return "gitea/runner-images:ubuntu-latest"
|
||||
return "docker.gitea.com/runner-images:ubuntu-latest"
|
||||
}
|
||||
|
||||
// Names returns the names of all labels.
|
||||
func (l Labels) Names() []string {
|
||||
names := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
@@ -90,6 +98,7 @@ func (l Labels) Names() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// ToStrings converts labels back to their string representation.
|
||||
func (l Labels) ToStrings() []string {
|
||||
ls := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package report provides task reporting functionality for communicating with the server.
|
||||
package report
|
||||
|
||||
import (
|
||||
@@ -18,9 +19,10 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
)
|
||||
|
||||
// Reporter handles logging and state reporting for running tasks.
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -42,6 +44,7 @@ type Reporter struct {
|
||||
stopCommandEndToken string
|
||||
}
|
||||
|
||||
// NewReporter creates a new Reporter for the given task.
|
||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
@@ -72,6 +75,7 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
||||
return rv
|
||||
}
|
||||
|
||||
// ResetSteps initializes the step states with the given number of steps.
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -82,6 +86,7 @@ func (r *Reporter) ResetSteps(l int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Levels returns all log levels that this hook should fire for.
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
@@ -93,6 +98,7 @@ func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||
return s
|
||||
}
|
||||
|
||||
// Fire processes a log entry and updates the task state accordingly.
|
||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -143,6 +149,12 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
if step.StartedAt == nil {
|
||||
step.StartedAt = timestamppb.New(timestamp)
|
||||
}
|
||||
|
||||
// Force reporting log errors as raw output to prevent silent failures
|
||||
if entry.Level == log.ErrorLevel {
|
||||
entry.Data["raw_output"] = true
|
||||
}
|
||||
|
||||
if v, ok := entry.Data["raw_output"]; ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if row := r.parseLogRow(entry); row != nil {
|
||||
@@ -169,6 +181,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunDaemon starts the periodic reporting of logs and state.
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
@@ -177,12 +190,23 @@ func (r *Reporter) RunDaemon() {
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ReportLog(false)
|
||||
_ = r.ReportState()
|
||||
if err := r.ReportLog(false); err != nil {
|
||||
log.WithError(err).Warn("failed to report log")
|
||||
}
|
||||
if err := r.ReportState(); err != nil {
|
||||
log.WithError(err).Warn("failed to report state")
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
// Use select with context to allow clean shutdown
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
r.RunDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
// Logf adds a formatted log message to the report.
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -199,6 +223,7 @@ func (r *Reporter) logf(format string, a ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetOutputs stores the job outputs to be reported to the server.
|
||||
func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -219,6 +244,7 @@ func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Close finalizes the report and sends any remaining logs and state.
|
||||
func (r *Reporter) Close(lastWords string) error {
|
||||
r.closed = true
|
||||
|
||||
@@ -254,6 +280,7 @@ func (r *Reporter) Close(lastWords string) error {
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
// ReportLog sends accumulated log rows to the server.
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
@@ -289,6 +316,7 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportState sends the current task state to the server.
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
@@ -367,7 +395,7 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
|
||||
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||
|
||||
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||
func (r *Reporter) handleCommand(originalContent, command, _ /* parameters */, value string) *string {
|
||||
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client/mocks"
|
||||
)
|
||||
|
||||
func TestReporter_parseLogRow(t *testing.T) {
|
||||
|
||||
27
internal/pkg/service/service_other.go
Normal file
27
internal/pkg/service/service_other.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
|
||||
// Package service provides Windows service integration for the runner.
|
||||
// On non-Windows platforms, these functions are no-ops.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// IsWindowsService returns false on non-Windows platforms.
|
||||
func IsWindowsService() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RunAsService is a no-op on non-Windows platforms.
|
||||
func RunAsService(_ string, _ func(ctx context.Context)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServiceName returns empty on non-Windows platforms.
|
||||
func GetServiceName() string {
|
||||
return ""
|
||||
}
|
||||
103
internal/pkg/service/service_windows.go
Normal file
103
internal/pkg/service/service_windows.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
// Package service provides Windows service integration for the runner.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
)
|
||||
|
||||
// runnerService implements svc.Handler for Windows service management.
|
||||
type runnerService struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Execute is called by the Windows Service Control Manager.
|
||||
func (s *runnerService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
|
||||
log.Info("Windows service started")
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
// Windows wants two responses for interrogate
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
log.Info("Windows service stop/shutdown requested")
|
||||
s.cancel()
|
||||
break loop
|
||||
default:
|
||||
log.Warnf("unexpected control request #%d", c)
|
||||
}
|
||||
case <-s.ctx.Done():
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// IsWindowsService returns true if the process is running as a Windows service.
|
||||
func IsWindowsService() bool {
|
||||
// Check if we're running interactively
|
||||
isInteractive, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failed to detect if running as Windows service")
|
||||
return false
|
||||
}
|
||||
return isInteractive
|
||||
}
|
||||
|
||||
// RunAsService runs the application as a Windows service.
|
||||
func RunAsService(serviceName string, run func(ctx context.Context)) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Start the actual runner in a goroutine
|
||||
go run(ctx)
|
||||
|
||||
// Run the service handler - this blocks until service stops
|
||||
err := svc.Run(serviceName, &runnerService{ctx: ctx, cancel: cancel})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Windows service run failed")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServiceName returns the service name from environment or default.
|
||||
func GetServiceName() string {
|
||||
if name := os.Getenv("GITEA_RUNNER_SERVICE_NAME"); name != "" {
|
||||
return name
|
||||
}
|
||||
// Try to detect from executable name
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
base := strings.TrimSuffix(exe, ".exe")
|
||||
if idx := strings.LastIndex(base, string(os.PathSeparator)); idx >= 0 {
|
||||
return base[idx+1:]
|
||||
}
|
||||
return base
|
||||
}
|
||||
return "GiteaRunnerSvc"
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package ver provides version information for the runner.
|
||||
package ver
|
||||
|
||||
// go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
|
||||
// go build -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
// Version returns the current runner version.
|
||||
func Version() string {
|
||||
return version
|
||||
}
|
||||
|
||||
14
main.go
14
main.go
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// GitCaddy Runner is a CI/CD runner for Gitea Actions.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,10 +9,21 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/cmd"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/cmd"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check if running as Windows service
|
||||
if service.IsWindowsService() {
|
||||
// Run as Windows service with proper SCM handling
|
||||
_ = service.RunAsService(service.GetServiceName(), func(ctx context.Context) {
|
||||
cmd.Execute(ctx)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Normal interactive mode with signal handling
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// run the command
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# wait for docker daemon
|
||||
while ! nc -z localhost 2376 </dev/null; do
|
||||
echo 'waiting for docker daemon...'
|
||||
sleep 5
|
||||
done
|
||||
|
||||
. /opt/act/run.sh
|
||||
@@ -16,6 +16,13 @@ EXTRA_ARGS=""
|
||||
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
|
||||
EXTRA_ARGS="${EXTRA_ARGS} --labels ${GITEA_RUNNER_LABELS}"
|
||||
fi
|
||||
if [[ ! -z "${GITEA_RUNNER_EPHEMERAL}" ]]; then
|
||||
EXTRA_ARGS="${EXTRA_ARGS} --ephemeral"
|
||||
fi
|
||||
RUN_ARGS=""
|
||||
if [[ ! -z "${GITEA_RUNNER_ONCE}" ]]; then
|
||||
RUN_ARGS="${RUN_ARGS} --once"
|
||||
fi
|
||||
|
||||
# In case no token is set, it's possible to read the token from a file, i.e. a Docker Secret
|
||||
if [[ -z "${GITEA_RUNNER_REGISTRATION_TOKEN}" ]] && [[ -f "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE}" ]]; then
|
||||
@@ -54,4 +61,4 @@ fi
|
||||
unset GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
unset GITEA_RUNNER_REGISTRATION_TOKEN_FILE
|
||||
|
||||
exec act_runner daemon ${CONFIG_ARG}
|
||||
exec act_runner daemon ${CONFIG_ARG} ${RUN_ARGS}
|
||||
|
||||
3
scripts/s6/act_runner/finish
Executable file
3
scripts/s6/act_runner/finish
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-svscanctl -t /etc/s6
|
||||
5
scripts/s6/act_runner/run
Executable file
5
scripts/s6/act_runner/run
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
s6-svwait -U /etc/s6/docker
|
||||
|
||||
exec run.sh
|
||||
6
scripts/s6/docker/data/check
Executable file
6
scripts/s6/docker/data/check
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
echo "Waiting for Docker daemon to start..."
|
||||
exit 1
|
||||
fi
|
||||
4
scripts/s6/docker/finish
Executable file
4
scripts/s6/docker/finish
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-svscanctl -t /etc/s6
|
||||
|
||||
1
scripts/s6/docker/notification-fd
Normal file
1
scripts/s6/docker/notification-fd
Normal file
@@ -0,0 +1 @@
|
||||
3
|
||||
3
scripts/s6/docker/run
Executable file
3
scripts/s6/docker/run
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec s6-notifyoncheck dockerd-entrypoint.sh
|
||||
@@ -1,17 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
|
||||
[program:dockerd]
|
||||
command=/usr/local/bin/dockerd-entrypoint.sh
|
||||
|
||||
[program:act_runner]
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
command=/opt/act/rootless.sh
|
||||
|
||||
[eventlistener:processes]
|
||||
command=bash -c "echo READY && read line && kill -SIGQUIT $PPID"
|
||||
events=PROCESS_STATE_STOPPED,PROCESS_STATE_EXITED,PROCESS_STATE_FATAL
|
||||
Reference in New Issue
Block a user