2
0

Compare commits

...

31 Commits

Author SHA1 Message Date
ee5fd838b8 feat(labels): support schema validation in runs-on matching
Some checks failed
CI / build-and-test (push) Has been cancelled
Release / build (amd64, darwin) (push) Successful in 53s
Release / build (amd64, linux) (push) Successful in 1m4s
Release / build (amd64, windows) (push) Successful in 50s
Release / build (arm64, darwin) (push) Successful in 48s
Release / build (arm64, linux) (push) Successful in 51s
Release / release (push) Successful in 21s
Enhances PickPlatform to validate schema when runs-on includes explicit mode (e.g., "linux:host" or "ubuntu:docker"). Parses schema suffix from runs-on values and ensures it matches the runner's configured schema for that label. Returns empty string on schema mismatch to prevent jobs from running in wrong environment (e.g., workflow requesting :host but runner only has :docker). Adds test coverage for schema matching, mismatches, and backward compatibility with schema-less runs-on values.
2026-02-09 02:30:24 -05:00
17f78a5e4c feat(runner): merge admin-added labels from server on declare
Some checks failed
CI / build-and-test (push) Has been cancelled
Release / build (amd64, darwin) (push) Successful in 49s
Release / build (amd64, linux) (push) Successful in 1m4s
Release / build (amd64, windows) (push) Successful in 52s
Release / build (arm64, darwin) (push) Successful in 54s
Release / build (arm64, linux) (push) Successful in 47s
Release / release (push) Successful in 21s
Adds MergeServerLabels method to sync labels added by admins in Gitea UI with runner's local configuration. Called after successful declare response to incorporate any server-side label changes. Skips duplicate labels and logs invalid entries. Enables dynamic label management without requiring runner restart or config file edits.
2026-02-09 02:15:23 -05:00
522ee44718 fix(labels): return empty string on label mismatch instead of fallback
Some checks failed
Release / build (amd64, darwin) (push) Successful in 53s
Release / build (amd64, linux) (push) Successful in 1m8s
Release / build (amd64, windows) (push) Successful in 50s
Release / build (arm64, darwin) (push) Successful in 1m1s
Release / build (arm64, linux) (push) Successful in 53s
Release / release (push) Successful in 21s
CI / build-and-test (push) Failing after 45s
Changes PickPlatform to return empty string when no matching label is found, causing the job to fail with a clear error rather than silently falling back to ubuntu-latest. This prevents jobs from running in incorrect environments when runner labels are edited in Gitea admin UI after registration. Adds comprehensive test coverage for label matching scenarios including exact matches, no matches, empty labels, and multiple runsOn values.
2026-02-09 01:56:57 -05:00
d87b08c559 Merge branch 'main' of https://git.marketally.com/gitcaddy/gitcaddy-runner
All checks were successful
CI / build-and-test (push) Successful in 57s
2026-01-27 22:50:26 -05:00
259238eedf docs(detached-note): add runner user guide and update deployment examples
Add comprehensive GUIDE.md (1000+ lines) covering GitCaddy Runner installation, registration, configuration, deployment options (Docker, Kubernetes, VM), workflow examples, artifact handling, cache server setup, and troubleshooting.

Update all deployment example READMEs with improved instructions and clarifications for Docker Compose, Kubernetes (DinD and rootless), and VM deployments. Enhance YAML configurations with better comments and security practices.
2026-01-27 22:50:23 -05:00
f33d0a54c4 refactor(client): simplify HTTP client and reporter daemon implementation
Some checks failed
CI / build-and-test (push) Has been cancelled
Release / build (amd64, darwin) (push) Successful in 57s
Release / build (amd64, linux) (push) Successful in 1m4s
Release / build (amd64, windows) (push) Successful in 1m13s
Release / build (arm64, linux) (push) Successful in 53s
Release / build (arm64, darwin) (push) Successful in 1m16s
Release / release (push) Successful in 24s
Use http.DefaultClient when TLS verification is not skipped, removing unnecessary custom transport configuration. Replace select-based daemon loop with time.AfterFunc for cleaner implementation and remove verbose error logging in RunDaemon.
2026-01-25 14:31:47 -05:00
899ca015b1 fix(poll): revert context inheritance to prevent deadlock
All checks were successful
CI / build-and-test (push) Successful in 1m0s
Release / build (amd64, linux) (push) Successful in 1m8s
Release / build (amd64, darwin) (push) Successful in 1m25s
Release / build (amd64, windows) (push) Successful in 51s
Release / build (arm64, darwin) (push) Successful in 1m0s
Release / build (arm64, linux) (push) Successful in 1m9s
Release / release (push) Successful in 26s
The v1.0.3 change that made poller contexts inherit from the parent
context caused a deadlock where runners would start but never poll
for tasks.

Reverted to using context.Background() for pollingCtx and jobsCtx.
Graceful shutdown still works via explicit Shutdown() call which
cancels the polling context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 13:51:47 -05:00
e1b9b277ee build(actions): update go version to 1.25.5
All checks were successful
CI / build-and-test (push) Successful in 1m2s
Release / build (amd64, linux) (push) Successful in 55s
Release / build (amd64, darwin) (push) Successful in 1m7s
Release / build (amd64, windows) (push) Successful in 1m16s
Release / build (arm64, darwin) (push) Successful in 1m3s
Release / build (arm64, linux) (push) Successful in 50s
Release / release (push) Successful in 17s
2026-01-25 12:48:20 -05:00
826ecfb433 chore(ci): clarify cache clearing and remove verbose flag
Some checks failed
CI / build-and-test (push) Failing after 46s
Update step name to better describe purpose and remove -x flag from go mod download to reduce log noise
2026-01-25 12:43:32 -05:00
5ac01b2dc9 ci(deps): clear module cache before downloading deps
Some checks failed
CI / build-and-test (push) Failing after 57s
Add module cache clearing step and enable verbose output for dependency downloads to help diagnose potential caching issues with private modules
2026-01-25 12:27:22 -05:00
f984198d4d ci(deps): add explicit dependency download step
Some checks failed
CI / build-and-test (push) Failing after 23s
Download Go modules before running vet to ensure all dependencies are available, especially private modules from git.marketally.com
2026-01-25 12:23:12 -05:00
607c332313 build(deps): sync go.sum with Go 1.25.0
Some checks failed
CI / build-and-test (push) Failing after 23s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:17:20 -05:00
50480c989c build(deps): update go version to 1.25.0
Some checks failed
CI / build-and-test (push) Failing after 23s
Remove explicit toolchain directive and update to Go 1.25.0
2026-01-25 12:12:49 -05:00
bf71b55cb7 style(service): improve comments and fix linter warnings
Some checks failed
CI / build-and-test (push) Failing after 24s
Release / build (amd64, darwin) (push) Failing after 38s
Release / build (amd64, linux) (push) Failing after 40s
Release / build (amd64, windows) (push) Failing after 32s
Release / build (arm64, darwin) (push) Failing after 31s
Release / build (arm64, linux) (push) Failing after 39s
Release / release (push) Has been skipped
- Add package documentation comments
- Use blank identifiers for unused parameters
- Add periods to comment sentences for consistency
- Fix naked return statement
2026-01-25 11:44:38 -05:00
26b4e7497f Merge branch 'main' of https://git.marketally.com/gitcaddy/gitcaddy-runner 2026-01-25 11:42:24 -05:00
b2922e332a feat(i18n): add windows service support and graceful shutdown
- Add native Windows service detection and signal handling
- Implement configurable shutdown timeout for graceful job completion
- Improve HTTP client with connection pooling and timeouts
- Propagate context through poller for proper shutdown coordination
- Add documentation for Windows service installation (NSSM and sc.exe)
- Add *.exe to .gitignore for Windows builds
2026-01-25 11:40:30 -05:00
d388ec5519 chore(scanner): add gitsecrets ignore file
Some checks failed
CI / build-and-test (push) Has been cancelled
Initializes .gitsecrets-ignore file to track false positives from secret scanning. Includes documentation header explaining the file format and usage.
2026-01-24 14:42:10 -05:00
cb1c1a3264 Add LICENSE.md (MIT)
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-01-23 00:53:55 +00:00
63967eb6fa style(ui): add package docs and mark unused parameters
Some checks failed
CI / build-and-test (push) Has been cancelled
Release / build (arm64, darwin) (push) Has been cancelled
Release / build (amd64, windows) (push) Has been cancelled
Release / build (arm64, linux) (push) Has been cancelled
Release / build (amd64, darwin) (push) Has been cancelled
Release / build (amd64, linux) (push) Has been cancelled
Release / release (push) Has been cancelled
Adds package-level documentation comments across cmd and internal packages. Marks unused function parameters with underscore prefix to satisfy linter requirements. Replaces if-else chains with switch statements for better readability. Explicitly ignores os.Setenv return value where error handling is not needed.
2026-01-19 01:16:47 -05:00
22f1ea6e76 chore(ui): update golangci-lint config and cleanup package docs
Updates golangci-lint configuration to v2 format with Go 1.23, streamlines linter settings by removing deprecated options and unnecessary exclusions. Adds package documentation and renames CleanupResult to Result for consistency. Marks unused context parameter with underscore.
2026-01-19 01:03:07 -05:00
GitCaddy Bot
4d6900b7a3 Update README download URLs to v1.0.0
Some checks failed
Release / release (push) Has been cancelled
Release / build (amd64, darwin) (push) Has been cancelled
Release / build (amd64, windows) (push) Has been cancelled
CI / build-and-test (push) Has been cancelled
Release / build (amd64, linux) (push) Has been cancelled
Release / build (arm64, darwin) (push) Has been cancelled
Release / build (arm64, linux) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:52:04 -05:00
GitCaddy Bot
898ef596ae Fix release workflow to use gitcaddy-runner naming
Some checks failed
CI / build-and-test (push) Successful in 1m15s
Release / build (arm64, darwin) (push) Has been cancelled
Release / build (arm64, linux) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / build (amd64, windows) (push) Has been cancelled
Release / build (amd64, linux) (push) Has been cancelled
Release / build (amd64, darwin) (push) Has been cancelled
- Update ldflags to use git.marketally.com/gitcaddy/gitcaddy-runner path
- Rename output binaries from act_runner to gitcaddy-runner
- Update artifact names to match new naming convention

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:49:46 -05:00
GitCaddy Bot
eb37073861 Fix project name in goreleaser config
All checks were successful
CI / build-and-test (push) Successful in 53s
Release / build (amd64, linux) (push) Successful in 1m7s
Release / build (amd64, darwin) (push) Successful in 1m8s
Release / build (amd64, windows) (push) Successful in 1m17s
Release / build (arm64, darwin) (push) Successful in 48s
Release / build (arm64, linux) (push) Successful in 54s
Release / release (push) Successful in 17s
- Add project_name: gitcaddy-runner so binaries are named correctly
- Update gitea_urls to point to git.marketally.com instead of gitea.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:41:56 -05:00
GitCaddy Bot
ec9b323318 Rebrand from act_runner to gitcaddy-runner v1.0.0
All checks were successful
Release / build (amd64, linux) (push) Successful in 1m15s
CI / build-and-test (push) Successful in 1m7s
Release / build (amd64, windows) (push) Successful in 1m3s
Release / build (amd64, darwin) (push) Successful in 1m8s
Release / build (arm64, darwin) (push) Successful in 46s
Release / build (arm64, linux) (push) Successful in 50s
Release / release (push) Successful in 26s
- Update module path: gitea.com/gitea/act_runner → git.marketally.com/gitcaddy/gitcaddy-runner
- Update all import paths across Go source files
- Update Makefile LDFLAGS and package references
- Update .goreleaser.yaml ldflags and S3 directory path
- Add comprehensive README with capacity configuration documentation
- Document troubleshooting for capacity feature and Docker detection
- Bump version to v1.0.0 for major rebrand
- All linting checks passed (fmt-check, go mod tidy, go vet)
2026-01-16 10:31:58 -05:00
GitCaddy
d955727863 Fix formatting (gofmt, remove BOM)
All checks were successful
CI / build-and-test (push) Successful in 1m13s
Release / build (amd64, darwin) (push) Successful in 57s
Release / build (amd64, linux) (push) Successful in 55s
Release / build (amd64, windows) (push) Successful in 54s
Release / build (arm64, darwin) (push) Successful in 53s
Release / build (arm64, linux) (push) Successful in 52s
Release / release (push) Successful in 19s
2026-01-15 13:09:06 +00:00
GitCaddy
3addd66efa Report runner capacity in capabilities JSON
Some checks failed
CI / build-and-test (push) Failing after 20s
2026-01-15 13:06:30 +00:00
GitCaddy
b6d700af60 fix: Use PowerShell instead of deprecated wmic for Windows CPU detection
Some checks failed
CI / build-and-test (push) Failing after 37s
Release / build (amd64, linux) (push) Successful in 1m6s
Release / build (amd64, darwin) (push) Successful in 1m22s
Release / build (amd64, windows) (push) Successful in 49s
Release / build (arm64, darwin) (push) Successful in 1m1s
Release / build (arm64, linux) (push) Successful in 49s
Release / release (push) Successful in 18s
wmic is deprecated in newer Windows versions and returns empty results.
Use Get-CimInstance Win32_Processor via PowerShell instead.
2026-01-14 18:00:21 +00:00
GitCaddy
7c0d11c353 chore: Reduce go-build cache retention to 3 days
Some checks failed
CI / build-and-test (push) Failing after 33s
2026-01-14 12:19:38 +00:00
GitCaddy
b9ae4d5f36 feat: Add auto-cleanup and fix container CPU detection
Some checks failed
CI / build-and-test (push) Failing after 37s
- Add automatic disk cleanup when usage exceeds 85%
- Fix false CPU readings in LXC containers (was showing host load)
- Add cross-platform cache cleanup (Linux, macOS, Windows)
- Extend temp file patterns for go-build, node-compile-cache, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 12:12:34 +00:00
GitCaddy
3a66563c1e chore: Fix gofmt formatting in runner.go
All checks were successful
CI / build-and-test (push) Successful in 1m1s
Release / build (amd64, darwin) (push) Successful in 50s
Release / build (amd64, linux) (push) Successful in 1m0s
Release / build (amd64, windows) (push) Successful in 1m7s
Release / build (arm64, darwin) (push) Successful in 1m27s
Release / build (arm64, linux) (push) Successful in 1m2s
Release / release (push) Successful in 54s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:57:48 +00:00
GitCaddy
e0feb6bd4e chore: Remove gitea-vet from build process
Some checks failed
CI / build-and-test (push) Failing after 30s
Release / build (amd64, darwin) (push) Failing after 1m20s
Release / build (arm64, darwin) (push) Failing after 1m32s
Release / build (amd64, windows) (push) Failing after 1m40s
Release / build (arm64, linux) (push) Successful in 1m30s
Release / release (push) Has been cancelled
Release / build (amd64, linux) (push) Has been cancelled
Use standard go vet instead of gitea-vet for copyright checks.
This allows MarketAlly copyright headers in new files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:53:17 +00:00
51 changed files with 2345 additions and 533 deletions

View File

@@ -44,14 +44,14 @@ jobs:
fi fi
echo "Building version: ${VERSION}" echo "Building version: ${VERSION}"
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
go build -a -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=${VERSION}" \ go build -a -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=${VERSION}" \
-o act_runner-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} -o gitcaddy-runner-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: act_runner-${{ matrix.goos }}-${{ matrix.goarch }} name: gitcaddy-runner-${{ matrix.goos }}-${{ matrix.goarch }}
path: act_runner-* path: gitcaddy-runner-*
release: release:
needs: build needs: build
@@ -67,7 +67,7 @@ jobs:
- name: Prepare release files - name: Prepare release files
run: | run: |
mkdir -p release mkdir -p release
find artifacts -type f -name 'act_runner-*' -exec mv {} release/ \; find artifacts -type f -name 'gitcaddy-runner-*' -exec mv {} release/ \;
cd release && sha256sum * > checksums.txt cd release && sha256sum * > checksums.txt
- name: Create Release - name: Create Release

View File

@@ -17,6 +17,14 @@ jobs:
go-version-file: 'go.mod' go-version-file: 'go.mod'
cache: false 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 - name: Vet
run: make vet run: make vet
env: env:

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/act_runner /act_runner
*.exe
.env .env
.runner .runner
coverage.txt coverage.txt

14
.gitsecrets-ignore Normal file
View 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}

View File

@@ -1,53 +1,42 @@
version: "2"
linters: linters:
default: none
enable: enable:
- gosimple
- typecheck
- govet - govet
- errcheck - errcheck
- staticcheck - staticcheck
- unused - unused
- dupl - dupl
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
- gofmt
- misspell - misspell
- gocritic - gocritic
- bidichk - bidichk
- ineffassign - ineffassign
- revive - revive
- gofumpt
- depguard
- nakedret - nakedret
- unconvert - unconvert
- wastedassign - wastedassign
- nolintlint - nolintlint
- stylecheck
enable-all: false formatters:
disable-all: true enable:
fast: false - gofmt
- gofumpt
run: run:
go: 1.18 go: "1.23"
timeout: 10m timeout: 10m
skip-dirs:
- node_modules
- public
- web_src
linters-settings: linters-settings:
stylecheck:
checks: ["all", "-ST1005", "-ST1003"]
nakedret: nakedret:
max-func-lines: 0 max-func-lines: 0
gocritic: gocritic:
disabled-checks: disabled-checks:
- ifElseChain - ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way. - singleCaseSwitch
revive: revive:
ignore-generated-header: false
severity: warning severity: warning
confidence: 0.8 confidence: 0.8
errorCode: 1
warningCode: 1
rules: rules:
- name: blank-imports - name: blank-imports
- name: context-as-argument - name: context-as-argument
@@ -72,94 +61,25 @@ linters-settings:
- name: modifies-value-receiver - name: modifies-value-receiver
gofumpt: gofumpt:
extra-rules: true 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: issues:
exclude-rules: exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
linters: linters:
- gocyclo
- errcheck - errcheck
- dupl - dupl
- gosec
- unparam
- staticcheck
- path: models/migrations/v
linters:
- gocyclo
- errcheck
- dupl
- gosec
- linters: - linters:
- dupl - dupl
text: "webhook" text: "webhook"
- linters: - linters:
- gocritic - gocritic
text: "`ID' should not be capitalized" 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: - linters:
- misspell - misspell
text: '`Unknwon` is a misspelling of `Unknown`' 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" - text: "commentFormatting: put a space between `//` and comment text"
linters: linters:
- gocritic - gocritic
- text: "exitAfterDefer:" - text: "exitAfterDefer:"
linters: linters:
- gocritic - gocritic
- path: modules/graceful/manager_windows.go
linters:
- staticcheck
text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
- path: models/user/openid.go
linters:
- golint

View File

@@ -1,5 +1,7 @@
version: 2 version: 2
project_name: gitcaddy-runner
before: before:
hooks: hooks:
- go mod tidy - go mod tidy
@@ -63,7 +65,7 @@ builds:
flags: flags:
- -trimpath - -trimpath
ldflags: 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: >- binary: >-
{{ .ProjectName }}- {{ .ProjectName }}-
{{- .Version }}- {{- .Version }}-
@@ -86,7 +88,7 @@ blobs:
provider: s3 provider: s3
bucket: "{{ .Env.S3_BUCKET }}" bucket: "{{ .Env.S3_BUCKET }}"
region: "{{ .Env.S3_REGION }}" region: "{{ .Env.S3_REGION }}"
directory: "act_runner/{{.Version}}" directory: "gitcaddy-runner/{{.Version}}"
extra_files: extra_files:
- glob: ./**.xz - glob: ./**.xz
- glob: ./**.sha256 - glob: ./**.sha256
@@ -108,8 +110,8 @@ nightly:
version_template: "nightly" version_template: "nightly"
gitea_urls: gitea_urls:
api: https://gitea.com/api/v1 api: https://git.marketally.com/api/v1
download: https://gitea.com download: https://git.marketally.com
release: release:
extra_files: extra_files:

1039
GUIDE.md Normal file
View File

File diff suppressed because it is too large Load Diff

18
LICENSE.md Normal file
View 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.

View File

@@ -67,11 +67,11 @@ else
endif endif
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 ?= 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 all: build
@@ -117,8 +117,7 @@ test: fmt-check security-check
.PHONY: vet .PHONY: vet
vet: vet:
@echo "Running go vet..." @echo "Running go vet..."
@$(GO) build code.gitea.io/gitea-vet @$(GO) vet $(GO_PACKAGES_TO_VET)
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
install: $(GOFILES) install: $(GOFILES)
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' $(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'

590
README.md
View File

@@ -1,121 +1,81 @@
# GitCaddy Act Runner # GitCaddy Runner
A Gitea Actions runner with enhanced capability detection and reporting for AI-friendly workflow generation. A Gitea Actions runner with enhanced capability detection and reporting for AI-friendly workflow generation.
> **This is a GitCaddy fork** of [gitea.com/gitea/act_runner](https://gitea.com/gitea/act_runner) with runner capability discovery features. 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.
## Overview ## Features
Act Runner executes Gitea Actions workflows using [act](https://github.com/nektos/act). This fork adds automatic capability detection, enabling Gitea to expose runner capabilities via API for AI tools to query before generating workflows. - **Automated Capability Detection**: Automatically identifies OS, architecture, installed tools, and available resources
- **Concurrent Job Execution**: Configure runner capacity to handle multiple jobs simultaneously
## Key Features - **Docker Support**: Full support for Docker and Docker Compose workflows
- **Xcode Integration**: Detects Xcode installations, SDKs, and simulators on macOS
- **Capability Detection**: Automatically detects OS, architecture, Docker support, available shells, and installed tools - **Tool Detection**: Identifies installed tools (Node.js, Python, .NET, Go, Ruby, Swift, etc.)
- **Capability Reporting**: Reports capabilities to Gitea server during runner declaration - **AI-Friendly API**: Exposes capabilities through Gitea's API for automated workflow generation
- **Full Compatibility**: Drop-in replacement for standard act_runner - **Cache Support**: Built-in workflow cache support for faster builds
- **Multi-Platform**: Supports Linux, macOS, and Windows
## Installation ## Installation
### Download Pre-built Binary ### Pre-built Binaries
Download from [Releases](https://git.marketally.com/gitcaddy/act_runner/releases): Download the latest release for your platform from the [releases page](https://git.marketally.com/gitcaddy/gitcaddy-runner/releases):
**macOS:**
```bash ```bash
# Linux (amd64) # Apple Silicon (M1/M2/M3/M4)
curl -L -o act_runner https://git.marketally.com/gitcaddy/act_runner/releases/download/v0.3.1-gitcaddy/act_runner-linux-amd64 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 act_runner chmod +x gitcaddy-runner
# macOS (Apple Silicon) # Intel
curl -L -o act_runner https://git.marketally.com/gitcaddy/act_runner/releases/download/v0.3.1-gitcaddy/act_runner-darwin-arm64 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 act_runner chmod +x gitcaddy-runner
```
**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 ### Build from Source
```bash ```bash
git clone https://git.marketally.com/gitcaddy/act_runner.git git clone https://git.marketally.com/gitcaddy/gitcaddy-runner.git
cd act_runner cd gitcaddy-runner
make build make build
``` ```
## Quick Start ## Quick Start
### 1. Enable Actions in Gitea ### 1. Enable Gitea Actions
Add to your Gitea `app.ini`: In your Gitea `app.ini`:
```ini ```ini
[actions] [actions]
ENABLED = true ENABLED = true
``` ```
### 2. Register the Runner ### 2. Generate Configuration
```bash ```bash
./act_runner register \ ./gitcaddy-runner generate-config > config.yaml
--instance https://your-gitea-instance.com \
--token YOUR_RUNNER_TOKEN \
--name my-runner \
--labels ubuntu-latest,docker
``` ```
### 3. Start the Runner ### 3. Configure the Runner
```bash Edit `config.yaml` to customize settings. **Important configuration options:**
./act_runner daemon
```
On startup, the runner will:
1. Detect system capabilities (OS, arch, Docker, shells, tools)
2. Report capabilities to Gitea via the Declare API
3. Begin polling for jobs
## Capability Detection
The runner automatically detects:
| Category | Examples |
|----------|----------|
| **OS/Arch** | linux/amd64, darwin/arm64, windows/amd64 |
| **Container Runtime** | Docker, Podman |
| **Shells** | bash, sh, zsh, powershell, cmd |
| **Tools** | Node.js, Go, Python, Java, .NET, Rust |
| **Features** | Cache support, Docker Compose |
### Example Capabilities JSON
```json
{
"os": "linux",
"arch": "amd64",
"docker": true,
"docker_compose": true,
"container_runtime": "docker",
"shell": ["bash", "sh"],
"tools": {
"node": ["18.19.0"],
"go": ["1.21.5"],
"python": ["3.11.6"]
},
"features": {
"cache": true,
"docker_services": true
},
"limitations": []
}
```
## Configuration
Create a config file or use command-line flags:
```bash
./act_runner generate-config > config.yaml
./act_runner -c config.yaml daemon
```
Example configuration:
```yaml ```yaml
log: log:
@@ -123,73 +83,447 @@ log:
runner: runner:
file: .runner file: .runner
capacity: 1 capacity: 2 # Number of concurrent jobs (default: 1)
timeout: 3h timeout: 3h
shutdown_timeout: 3m # Grace period for running jobs on shutdown
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
labels: labels:
- ubuntu-latest:docker://node:18-bullseye - "ubuntu-latest:docker://node:16-bullseye"
- ubuntu-22.04:docker://ubuntu:22.04 - "ubuntu-22.04:docker://node:16-bullseye"
container:
docker_host: ""
force_pull: false
privileged: false
cache: cache:
enabled: true enabled: true
dir: ~/.cache/actcache dir: ""
container:
network: ""
privileged: false
options: ""
valid_volumes: []
docker_host: ""
force_pull: false
host:
workdir_parent: ""
``` ```
## Docker Deployment #### 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 ```bash
docker run -d \ ./gitcaddy-runner register \
--name act_runner \ --instance https://your-gitea-instance.com \
-e GITEA_INSTANCE_URL=https://your-gitea.com \ --token YOUR_REGISTRATION_TOKEN \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \ --name my-runner \
-v /var/run/docker.sock:/var/run/docker.sock \ --labels ubuntu-latest:docker://node:16-bullseye
-v ./data:/data \
gitcaddy/act_runner:latest
``` ```
## GitCaddy Integration The registration token can be obtained from:
- Gitea Admin Panel > Actions > Runners
- Or repository Settings > Actions > Runners
This runner is designed to work with the [GitCaddy Gitea fork](https://git.marketally.com/gitcaddy/gitea), which provides: ### 5. Start the Runner
- **Runner Capabilities API** (`/api/v2/repos/{owner}/{repo}/actions/runners/capabilities`) **Important:** Always specify the config file path with `-c` flag:
- **Workflow Validation API** for pre-flight checks
- **Action Compatibility Database** for GitHub Actions mapping
### How It Works
```bash
./gitcaddy-runner daemon -c config.yaml
``` ```
act_runner Gitea AI Tool **Without the `-c` flag, the runner will use default settings and ignore your config.yaml!**
| | |
| Declare + Capabilities | |
|---------------------------->| |
| | |
| | GET /api/v2/.../caps |
| |<------------------------|
| | |
| | Runner capabilities |
| |------------------------>|
| | |
| | Generates workflow |
| | with correct config |
## 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>
``` ```
## Related Projects Load the service:
| Project | Description | ```bash
|---------|-------------| launchctl load ~/Library/LaunchAgents/com.gitcaddy.runner.plist
| [gitcaddy/gitea](https://git.marketally.com/gitcaddy/gitea) | Gitea with AI-friendly enhancements | ```
| [gitcaddy/actions-proto-go](https://git.marketally.com/gitcaddy/actions-proto-go) | Protocol definitions with capability support |
## Upstream ### Linux (systemd)
This project is a fork of [gitea.com/gitea/act_runner](https://gitea.com/gitea/act_runner). We contribute enhancements back to upstream where appropriate. 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
sudo systemctl daemon-reload
sudo systemctl enable gitcaddy-runner
sudo systemctl start gitcaddy-runner
```
### 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
# ✅ Correct
./gitcaddy-runner daemon -c /path/to/config.yaml
# ❌ Wrong - uses defaults
./gitcaddy-runner daemon
```
Verify the config is being loaded:
```bash
# Check runner process
ps aux | grep gitcaddy-runner
# Should show: gitcaddy-runner daemon -c /path/to/config.yaml
```
### 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 ## License
MIT License - see [LICENSE](LICENSE) for details. 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.

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -1,6 +1,7 @@
// Copyright 2026 MarketAlly. All rights reserved. // Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package main provides the upload-helper CLI tool for reliable file uploads.
package main package main
import ( import (
@@ -8,7 +9,7 @@ import (
"fmt" "fmt"
"os" "os"
"gitea.com/gitea/act_runner/internal/pkg/artifact" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/artifact"
) )
func main() { func main() {

View File

@@ -1,12 +1,12 @@
# Usage Examples for `act_runner` # Usage Examples for `gitcaddy-runner`
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable. A collection of usage and deployment examples for GitCaddy Runner. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to your GitCaddy setup. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
| Section | Description | | Section | Description |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`docker`](docker) | This section provides you with scripts and instructions tailored for running containers on a workstation or server where Docker is installed. It simplifies the process of setting up and managing your Gitea deployment using Docker. | | [`docker`](docker) | Scripts and instructions for running GitCaddy Runner in a Docker container on a workstation or server where Docker is installed. |
| [`docker-compose`](docker-compose) | In this section, you'll discover examples demonstrating how to utilize docker-compose to efficiently handle your Gitea deployments. It offers a straightforward approach to managing multiple containerized components of your Gitea setup. | | [`docker-compose`](docker-compose) | Examples demonstrating how to use docker-compose to run GitCaddy Runner alongside a GitCaddy server instance. |
| [`kubernetes`](kubernetes) | If you're utilizing Kubernetes clusters for your infrastructure, this section is specifically designed for you. It presents examples and guidelines for configuring Gitea deployments within Kubernetes clusters, enabling you to leverage the scalability and flexibility of Kubernetes. | | [`kubernetes`](kubernetes) | Examples and guidelines for deploying GitCaddy Runner within Kubernetes clusters, leveraging scalability and flexibility. |
| [`vm`](vm) | This section is dedicated to examples that assist you in setting up Gitea on virtual or physical servers. Whether you're working with virtual machines or physical hardware, you'll find helpful resources to guide you through the deployment process. | | [`vm`](vm) | Examples for setting up GitCaddy Runner on virtual or physical servers directly, without containerization. |
We hope these resources provide you with valuable insights and solutions for your Gitea setup. Feel free to explore, contribute, and adapt these examples to suit your specific requirements. Feel free to explore, contribute, and adapt these examples to suit your specific requirements.

View File

@@ -1,12 +1,12 @@
### Running `act_runner` using `docker-compose` ### Running `gitcaddy-runner` using `docker-compose`
```yml ```yml
... ...
gitea: gitcaddy:
image: gitea/gitea image: git.marketally.com/gitcaddy/server:latest
... ...
healthcheck: healthcheck:
# checks availability of Gitea's front-end with curl # checks availability of GitCaddy's front-end with curl
test: ["CMD", "curl", "-f", "<instance_url>"] test: ["CMD", "curl", "-f", "<instance_url>"]
interval: 10s interval: 10s
retries: 3 retries: 3
@@ -14,20 +14,19 @@
timeout: 10s timeout: 10s
environment: environment:
# GITEA_RUNNER_REGISTRATION_TOKEN can be used to set a global runner registration token. # 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. # It's also possible to use GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token> # - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
runner: runner:
image: gitea/act_runner image: git.marketally.com/gitcaddy/gitcaddy-runner:latest
restart: always restart: always
depends_on: depends_on:
gitea: gitcaddy:
# required so runner can attach to gitea, see "healthcheck" # required so runner can attach to GitCaddy, see "healthcheck"
condition: service_healthy condition: service_healthy
restart: true restart: true
volumes: volumes:
- ./data/act_runner:/data - ./data/gitcaddy-runner:/data
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
- GITEA_INSTANCE_URL=<instance url> - GITEA_INSTANCE_URL=<instance url>
@@ -38,18 +37,18 @@
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token> - GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
``` ```
### Running `act_runner` using Docker-in-Docker (DIND) ### Running `gitcaddy-runner` using Docker-in-Docker (DIND)
```yml ```yml
... ...
runner: runner:
image: gitea/act_runner:latest-dind-rootless image: git.marketally.com/gitcaddy/gitcaddy-runner:latest-dind-rootless
restart: always restart: always
privileged: true privileged: true
depends_on: depends_on:
- gitea - gitcaddy
volumes: volumes:
- ./data/act_runner:/data - ./data/gitcaddy-runner:/data
environment: environment:
- GITEA_INSTANCE_URL=<instance url> - GITEA_INSTANCE_URL=<instance url>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock - DOCKER_HOST=unix:///var/run/user/1000/docker.sock

View File

@@ -1,7 +1,7 @@
### Run `act_runner` in a Docker Container ### Run `gitcaddy-runner` in a Docker Container
```sh ```sh
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/act_runner:nightly docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner git.marketally.com/gitcaddy/gitcaddy-runner:latest
``` ```
The `/data` directory inside the docker container contains the runner API keys after registration. The `/data` directory inside the docker container contains the runner API keys after registration.

View File

@@ -1,4 +1,4 @@
## Kubernetes Docker in Docker Deployment with `act_runner` ## Kubernetes Docker in Docker Deployment with `gitcaddy-runner`
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context. NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.

View File

@@ -1,7 +1,7 @@
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: act-runner-vol name: gitcaddy-runner-vol
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@@ -13,7 +13,7 @@ spec:
apiVersion: v1 apiVersion: v1
data: data:
# The registration token can be obtained from the web UI, API or command-line. # 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 # You can also set a pre-defined global runner registration token for the GitCaddy instance via
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable. # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >> token: << base64 encoded registration token >>
kind: Secret kind: Secret
@@ -25,19 +25,19 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
labels: labels:
app: act-runner app: gitcaddy-runner
name: act-runner name: gitcaddy-runner
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: act-runner app: gitcaddy-runner
strategy: {} strategy: {}
template: template:
metadata: metadata:
creationTimestamp: null creationTimestamp: null
labels: labels:
app: act-runner app: gitcaddy-runner
spec: spec:
restartPolicy: Always restartPolicy: Always
volumes: volumes:
@@ -45,10 +45,10 @@ spec:
emptyDir: {} emptyDir: {}
- name: runner-data - name: runner-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: act-runner-vol claimName: gitcaddy-runner-vol
containers: containers:
- name: runner - name: runner
image: gitea/act_runner:nightly image: git.marketally.com/gitcaddy/gitcaddy-runner:latest
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- 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: env:
- name: DOCKER_HOST - name: DOCKER_HOST
@@ -58,7 +58,7 @@ spec:
- name: DOCKER_TLS_VERIFY - name: DOCKER_TLS_VERIFY
value: "1" value: "1"
- name: GITEA_INSTANCE_URL - name: GITEA_INSTANCE_URL
value: http://gitea-http.gitea.svc.cluster.local:3000 value: http://gitcaddy-http.gitcaddy.svc.cluster.local:3000
- name: GITEA_RUNNER_REGISTRATION_TOKEN - name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -1,7 +1,7 @@
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: act-runner-vol name: gitcaddy-runner-vol
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@@ -13,7 +13,7 @@ spec:
apiVersion: v1 apiVersion: v1
data: data:
# The registration token can be obtained from the web UI, API or command-line. # 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 # You can also set a pre-defined global runner registration token for the GitCaddy instance via
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable. # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >> token: << base64 encoded registration token >>
kind: Secret kind: Secret
@@ -25,30 +25,30 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
labels: labels:
app: act-runner app: gitcaddy-runner
name: act-runner name: gitcaddy-runner
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: act-runner app: gitcaddy-runner
strategy: {} strategy: {}
template: template:
metadata: metadata:
creationTimestamp: null creationTimestamp: null
labels: labels:
app: act-runner app: gitcaddy-runner
spec: spec:
restartPolicy: Always restartPolicy: Always
volumes: volumes:
- name: runner-data - name: runner-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: act-runner-vol claimName: gitcaddy-runner-vol
securityContext: securityContext:
fsGroup: 1000 fsGroup: 1000
containers: containers:
- name: runner - name: runner
image: gitea/act_runner:nightly-dind-rootless image: git.marketally.com/gitcaddy/gitcaddy-runner:latest-dind-rootless
imagePullPolicy: Always imagePullPolicy: Always
# 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 -- /opt/act/run.sh"]
env: env:
@@ -59,7 +59,7 @@ spec:
- name: DOCKER_TLS_VERIFY - name: DOCKER_TLS_VERIFY
value: "1" value: "1"
- name: GITEA_INSTANCE_URL - name: GITEA_INSTANCE_URL
value: http://gitea-http.gitea.svc.cluster.local:3000 value: http://gitcaddy-http.gitcaddy.svc.cluster.local:3000
- name: GITEA_RUNNER_REGISTRATION_TOKEN - name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -70,4 +70,3 @@ spec:
volumeMounts: volumeMounts:
- name: runner-data - name: runner-data
mountPath: /data mountPath: /data

View File

@@ -1,4 +1,4 @@
## `act_runner` on Virtual or Physical Servers ## `gitcaddy-runner` on Virtual or Physical Servers
Files in this directory: Files in this directory:

View File

@@ -1,12 +1,12 @@
## Using Rootless Docker with`act_runner` ## Using Rootless Docker with `gitcaddy-runner`
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way. Here is a simple example of how to set up `gitcaddy-runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below. Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
As `root`: As `root`:
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`. - Create a user to run both `docker` and `gitcaddy-runner`. In this example, we use a non-privileged account called `rootless`.
```bash ```bash
useradd -m rootless useradd -m rootless
@@ -38,36 +38,36 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
``` ```
- Reboot. Ensure that the Docker process is working. - Reboot. Ensure that the Docker process is working.
- Create a directory for saving `act_runner` data between restarts - Create a directory for saving `gitcaddy-runner` data between restarts
`mkdir /home/rootless/act_runner` `mkdir /home/rootless/gitcaddy-runner`
- Register the runner from the data directory - Register the runner from the data directory
```bash ```bash
cd /home/rootless/act_runner cd /home/rootless/gitcaddy-runner
act_runner register gitcaddy-runner register
``` ```
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system. - Generate a `gitcaddy-runner` configuration file in the data directory. Edit the file to adjust for the system.
```bash ```bash
act_runner generate-config >/home/rootless/act_runner/config gitcaddy-runner generate-config >/home/rootless/gitcaddy-runner/config
``` ```
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents: - Create a new user-level `systemd` unit file as `/home/rootless/.config/systemd/user/gitcaddy-runner.service` with the following contents:
```bash ```bash
Description=Gitea Actions runner Description=GitCaddy Actions runner
Documentation=https://gitea.com/gitea/act_runner Documentation=https://git.marketally.com/gitcaddy/gitcaddy-runner
After=docker.service After=docker.service
[Service] [Service]
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config ExecStart=/usr/bin/gitcaddy-runner daemon -c /home/rootless/gitcaddy-runner/config
ExecReload=/bin/kill -s HUP $MAINPID ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/rootless/act_runner WorkingDirectory=/home/rootless/gitcaddy-runner
TimeoutSec=0 TimeoutSec=0
RestartSec=2 RestartSec=2
Restart=always Restart=always
@@ -88,8 +88,9 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
- Reboot - Reboot
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea. After the system restarts, check that `gitcaddy-runner` is working and that the runner is connected to GitCaddy.
````bash ```bash
systemctl --user status act_runner systemctl --user status gitcaddy-runner
journalctl --user -xeu act_runner journalctl --user -xeu gitcaddy-runner
```

6
go.mod
View File

@@ -1,8 +1,6 @@
module gitea.com/gitea/act_runner module git.marketally.com/gitcaddy/gitcaddy-runner
go 1.24.0 go 1.25.5
toolchain go1.24.11
require ( require (
code.gitea.io/actions-proto-go v0.5.2 code.gitea.io/actions-proto-go v0.5.2

2
go.sum
View File

@@ -6,8 +6,6 @@ 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= 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 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
git.marketally.com/gitcaddy/actions-proto-go v0.5.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4= 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= 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 h1:ulcquQluJbmNASkh6ina70LvcHEa9eWYfQ+DeAZ0VEE=

View File

@@ -9,7 +9,7 @@ import (
"os" "os"
"os/signal" "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" "github.com/nektos/act/pkg/artifactcache"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -22,8 +22,8 @@ type cacheServerArgs struct {
Port uint16 Port uint16
} }
func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error { func runCacheServer(_ context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error { return func(_ *cobra.Command, _ []string) error {
cfg, err := config.LoadDefault(*configFile) cfg, err := config.LoadDefault(*configFile)
if err != nil { if err != nil {
return fmt.Errorf("invalid configuration: %w", err) return fmt.Errorf("invalid configuration: %w", err)

View File

@@ -1,6 +1,7 @@
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved. // Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package cmd provides the CLI commands for gitcaddy-runner.
package cmd package cmd
import ( import (
@@ -10,11 +11,12 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/pkg/cleanup" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/ver" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
) )
// Execute runs the root command for gitcaddy-runner CLI.
func Execute(ctx context.Context) { func Execute(ctx context.Context) {
// ./gitcaddy-runner // ./gitcaddy-runner
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{

View File

@@ -14,6 +14,7 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"connectrpc.com/connect" "connectrpc.com/connect"
@@ -21,13 +22,14 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/app/poll" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/poll"
"gitea.com/gitea/act_runner/internal/app/run" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/client" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/envcheck" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
"gitea.com/gitea/act_runner/internal/pkg/ver" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
) )
const ( const (
@@ -35,6 +37,10 @@ const (
DiskSpaceWarningThreshold = 85.0 DiskSpaceWarningThreshold = 85.0
// DiskSpaceCriticalThreshold is the percentage at which to log critical warnings // DiskSpaceCriticalThreshold is the percentage at which to log critical warnings
DiskSpaceCriticalThreshold = 95.0 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 is how often to update capabilities (including disk space)
CapabilitiesUpdateInterval = 5 * time.Minute CapabilitiesUpdateInterval = 5 * time.Minute
// BandwidthTestInterval is how often to run bandwidth tests (hourly) // BandwidthTestInterval is how often to run bandwidth tests (hourly)
@@ -44,13 +50,23 @@ const (
// Global bandwidth manager - accessible for triggering manual tests // Global bandwidth manager - accessible for triggering manual tests
var bandwidthManager *envcheck.BandwidthManager 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 { func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error { return func(_ *cobra.Command, _ []string) error {
cfg, err := config.LoadDefault(*configFile) cfg, err := config.LoadDefault(*configFile)
if err != nil { if err != nil {
return fmt.Errorf("invalid configuration: %w", err) return fmt.Errorf("invalid configuration: %w", err)
} }
// Store config globally for auto-cleanup
globalConfig = cfg
initLogging(cfg) initLogging(cfg)
log.Infoln("Starting runner daemon") log.Infoln("Starting runner daemon")
@@ -116,7 +132,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
return err return err
} }
// if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath // 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 // empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
// and assign the path to cfg.Container.DockerHost // and assign the path to cfg.Container.DockerHost
if cfg.Container.DockerHost == "" { if cfg.Container.DockerHost == "" {
@@ -163,26 +179,29 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
bandwidthManager.Start(ctx) bandwidthManager.Start(ctx)
log.Infof("bandwidth manager started, testing against: %s", reg.Address) log.Infof("bandwidth manager started, testing against: %s", reg.Address)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, cfg.Container.WorkdirParent) capabilities := envcheck.DetectCapabilities(ctx, dockerHost, cfg.Container.WorkdirParent, globalConfig.Runner.Capacity)
// Include initial bandwidth result if available // Include initial bandwidth result if available
capabilities.Bandwidth = bandwidthManager.GetLastResult() capabilities.Bandwidth = bandwidthManager.GetLastResult()
capabilitiesJson := capabilities.ToJSON() capabilitiesJSON := capabilities.ToJSON()
log.Infof("detected capabilities: %s", capabilitiesJson) log.Infof("detected capabilities: %s", capabilitiesJSON)
// Check disk space and warn if low // Check disk space and warn if low
checkDiskSpaceWarnings(capabilities) checkDiskSpaceAndCleanup(ctx, capabilities)
// declare the labels of the runner before fetching tasks // declare the labels of the runner before fetching tasks
resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJson) resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJSON)
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented { 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") log.Errorf("Your GitCaddy version is too old to support runner declare, please upgrade to v1.21 or later")
return err return err
} else if err != nil { case err != nil:
log.WithError(err).Error("fail to invoke Declare") log.WithError(err).Error("fail to invoke Declare")
return err return err
} else { default:
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully", log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels) resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
// Merge any admin-added labels from the server
runner.MergeServerLabels(resp.Msg.Runner.Labels)
} }
// Start periodic capabilities update goroutine // Start periodic capabilities update goroutine
@@ -236,8 +255,8 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
} }
} }
// checkDiskSpaceWarnings logs warnings if disk space is low // checkDiskSpaceAndCleanup logs warnings if disk space is low and triggers cleanup if needed
func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) { func checkDiskSpaceAndCleanup(ctx context.Context, capabilities *envcheck.RunnerCapabilities) {
if capabilities.Disk == nil { if capabilities.Disk == nil {
return return
} }
@@ -245,13 +264,54 @@ func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
usedPercent := capabilities.Disk.UsedPercent usedPercent := capabilities.Disk.UsedPercent
freeGB := float64(capabilities.Disk.Free) / (1024 * 1024 * 1024) freeGB := float64(capabilities.Disk.Free) / (1024 * 1024 * 1024)
if usedPercent >= DiskSpaceCriticalThreshold { 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) log.Errorf("CRITICAL: Disk space critically low! %.1f%% used, only %.2f GB free. Runner may fail to execute jobs!", usedPercent, freeGB)
} else if usedPercent >= DiskSpaceWarningThreshold { // 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) 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 // periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) { func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) {
ticker := time.NewTicker(CapabilitiesUpdateInterval) ticker := time.NewTicker(CapabilitiesUpdateInterval)
@@ -267,20 +327,20 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa
return return
case <-ticker.C: case <-ticker.C:
// Detect updated capabilities (disk space changes over time) // Detect updated capabilities (disk space changes over time)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, workingDir) capabilities := envcheck.DetectCapabilities(ctx, dockerHost, workingDir, globalConfig.Runner.Capacity)
// Include latest bandwidth result // Include latest bandwidth result
if bandwidthManager != nil { if bandwidthManager != nil {
capabilities.Bandwidth = bandwidthManager.GetLastResult() capabilities.Bandwidth = bandwidthManager.GetLastResult()
} }
capabilitiesJson := capabilities.ToJSON() capabilitiesJSON := capabilities.ToJSON()
// Check for disk space warnings // Check for disk space warnings
checkDiskSpaceWarnings(capabilities) checkDiskSpaceAndCleanup(ctx, capabilities)
// Send updated capabilities to server // Send updated capabilities to server
_, err := runner.Declare(ctx, labelNames, capabilitiesJson) _, err := runner.Declare(ctx, labelNames, capabilitiesJSON)
if err != nil { if err != nil {
log.WithError(err).Debug("failed to update capabilities") log.WithError(err).Debug("failed to update capabilities")
} else { } else {

View File

@@ -264,7 +264,7 @@ func printList(plan *model.Plan) error {
return nil 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 // plan with filtered jobs - to be used for filtering only
var filterPlan *model.Plan var filterPlan *model.Plan
@@ -286,19 +286,20 @@ func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *e
} }
var err error var err error
if execArgs.job != "" { switch {
case execArgs.job != "":
log.Infof("Preparing plan with a job: %s", execArgs.job) log.Infof("Preparing plan with a job: %s", execArgs.job)
filterPlan, err = planner.PlanJob(execArgs.job) filterPlan, err = planner.PlanJob(execArgs.job)
if err != nil { if err != nil {
return err return err
} }
} else if filterEventName != "" { case filterEventName != "":
log.Infof("Preparing plan for a event: %s", filterEventName) log.Infof("Preparing plan for a event: %s", filterEventName)
filterPlan, err = planner.PlanEvent(filterEventName) filterPlan, err = planner.PlanEvent(filterEventName)
if err != nil { if err != nil {
return err return err
} }
} else { default:
log.Infof("Preparing plan with all jobs") log.Infof("Preparing plan with all jobs")
filterPlan, err = planner.PlanAll() filterPlan, err = planner.PlanAll()
if err != nil { if err != nil {
@@ -312,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 { 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) planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
if err != nil { if err != nil {
return err return err
@@ -331,18 +332,19 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
// collect all events from loaded workflows // collect all events from loaded workflows
events := planner.GetEvents() events := planner.GetEvents()
if len(execArgs.event) > 0 { switch {
case len(execArgs.event) > 0:
log.Infof("Using chosed event for filtering: %s", execArgs.event) log.Infof("Using chosed event for filtering: %s", execArgs.event)
eventName = 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]) log.Infof("Using the only detected workflow event: %s", events[0])
eventName = 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 // set default event type to first event from many available
// this way user dont have to specify the event. // this way user dont have to specify the event.
log.Infof("Using first detected workflow event: %s", events[0]) log.Infof("Using first detected workflow event: %s", events[0])
eventName = events[0] eventName = events[0]
} else { default:
log.Infof("Using default workflow event: push") log.Infof("Using default workflow event: push")
eventName = "push" eventName = "push"
} }
@@ -388,7 +390,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
defer os.RemoveAll(tempDir) defer func() { _ = os.RemoveAll(tempDir) }()
execArgs.artifactServerPath = tempDir execArgs.artifactServerPath = tempDir
} }
@@ -454,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) log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
ctx = common.WithDryrun(ctx, execArgs.dryrun) ctx = common.WithDryrun(ctx, execArgs.dryrun)
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
artifactCancel() artifactCancel()
return nil return nil
}) })

View File

@@ -20,15 +20,15 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gitea.com/gitea/act_runner/internal/pkg/client" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/ver" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
) )
// runRegister registers a runner to the server // runRegister registers a runner to the server
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error { func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error { return func(_ *cobra.Command, _ []string) error {
log.SetReportCaller(false) log.SetReportCaller(false)
isTerm := isatty.IsTerminal(os.Stdout.Fd()) isTerm := isatty.IsTerminal(os.Stdout.Fd())
log.SetFormatter(&log.TextFormatter{ log.SetFormatter(&log.TextFormatter{
@@ -80,6 +80,7 @@ type registerArgs struct {
type registerStage int8 type registerStage int8
// Register stage constants define the steps in the registration workflow.
const ( const (
StageUnknown registerStage = -1 StageUnknown registerStage = -1
StageOverwriteLocalConfig registerStage = iota + 1 StageOverwriteLocalConfig registerStage = iota + 1
@@ -250,7 +251,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
if stage == StageWaitingForRegistration { if stage == StageWaitingForRegistration {
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels) log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
if err := doRegister(ctx, cfg, inputs); err != nil { if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("Failed to register runner: %w", err) return fmt.Errorf("failed to register runner: %w", err)
} }
log.Infof("Runner registered successfully.") log.Infof("Runner registered successfully.")
return nil return nil
@@ -311,7 +312,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
return err return err
} }
if err := doRegister(ctx, cfg, inputs); err != nil { if err := doRegister(ctx, cfg, inputs); err != nil {
return fmt.Errorf("Failed to register runner: %w", err) return fmt.Errorf("failed to register runner: %w", err)
} }
log.Infof("Runner registered successfully.") log.Infof("Runner registered successfully.")
return nil return nil

View File

@@ -1,6 +1,7 @@
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved. // Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package poll provides task polling functionality for CI runners.
package poll package poll
import ( import (
@@ -15,13 +16,14 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"gitea.com/gitea/act_runner/internal/app/run" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/cleanup" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
"gitea.com/gitea/act_runner/internal/pkg/client" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/envcheck" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
) )
// Poller handles task polling from the Gitea server.
type Poller struct { type Poller struct {
client client.Client client client.Client
runner *run.Runner runner *run.Runner
@@ -38,9 +40,10 @@ type Poller struct {
done chan struct{} done chan struct{}
} }
// New creates a new Poller instance.
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller { func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
// Use independent contexts - shutdown is handled explicitly via Shutdown()
pollingCtx, shutdownPolling := context.WithCancel(context.Background()) pollingCtx, shutdownPolling := context.WithCancel(context.Background())
jobsCtx, shutdownJobs := context.WithCancel(context.Background()) jobsCtx, shutdownJobs := context.WithCancel(context.Background())
done := make(chan struct{}) done := make(chan struct{})
@@ -65,6 +68,7 @@ func (p *Poller) SetBandwidthManager(bm *envcheck.BandwidthManager) {
p.bandwidthManager = bm p.bandwidthManager = bm
} }
// Poll starts polling for tasks with the configured capacity.
func (p *Poller) Poll() { func (p *Poller) Poll() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1) limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
@@ -78,6 +82,7 @@ func (p *Poller) Poll() {
close(p.done) close(p.done)
} }
// PollOnce polls for a single task and then exits.
func (p *Poller) PollOnce() { func (p *Poller) PollOnce() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1) limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
@@ -87,18 +92,19 @@ func (p *Poller) PollOnce() {
close(p.done) close(p.done)
} }
// Shutdown gracefully stops the poller.
func (p *Poller) Shutdown(ctx context.Context) error { func (p *Poller) Shutdown(ctx context.Context) error {
p.shutdownPolling() p.shutdownPolling()
select { select {
// graceful shutdown completed succesfully // graceful shutdown completed successfully
case <-p.done: case <-p.done:
return nil return nil
// our timeout for shutting down ran out // our timeout for shutting down ran out
case <-ctx.Done(): case <-ctx.Done():
// when both the timeout fires and the graceful shutdown // 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 // fire. Do a non-blocking check here against the graceful
// shutdown status to avoid sending an error if we don't need to. // shutdown status to avoid sending an error if we don't need to.
_, ok := <-p.done _, ok := <-p.done
@@ -110,7 +116,7 @@ func (p *Poller) Shutdown(ctx context.Context) error {
p.shutdownJobs() p.shutdownJobs()
// wait for running jobs to report their status to Gitea // wait for running jobs to report their status to Gitea
_, _ = <-p.done <-p.done
return ctx.Err() return ctx.Err()
} }
@@ -166,20 +172,20 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
defer cancel() defer cancel()
// Detect capabilities including current disk space // Detect capabilities including current disk space
caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost, p.cfg.Container.WorkdirParent) caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost, p.cfg.Container.WorkdirParent, p.cfg.Runner.Capacity)
// Include latest bandwidth result if available // Include latest bandwidth result if available
if p.bandwidthManager != nil { if p.bandwidthManager != nil {
caps.Bandwidth = p.bandwidthManager.GetLastResult() caps.Bandwidth = p.bandwidthManager.GetLastResult()
} }
capsJson := caps.ToJSON() capsJSON := caps.ToJSON()
// Load the version value that was in the cache when the request was sent. // Load the version value that was in the cache when the request was sent.
v := p.tasksVersion.Load() v := p.tasksVersion.Load()
fetchReq := &runnerv1.FetchTaskRequest{ fetchReq := &runnerv1.FetchTaskRequest{
TasksVersion: v, TasksVersion: v,
CapabilitiesJson: capsJson, CapabilitiesJson: capsJSON,
} }
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(fetchReq)) resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(fetchReq))
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {

View File

@@ -1,6 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package run provides the core runner functionality for executing tasks.
package run package run
import ( import (

View File

@@ -22,11 +22,11 @@ import (
"github.com/nektos/act/pkg/runner" "github.com/nektos/act/pkg/runner"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gitea.com/gitea/act_runner/internal/pkg/client" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/labels" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/report" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/report"
"gitea.com/gitea/act_runner/internal/pkg/ver" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
) )
// Runner runs the pipeline. // Runner runs the pipeline.
@@ -84,6 +84,8 @@ func (r *Runner) CleanStaleJobCaches(maxAge time.Duration) {
} }
} }
} }
// NewRunner creates a new Runner with the given configuration, registration, and client.
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner { func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
ls := labels.Labels{} ls := labels.Labels{}
for _, v := range reg.Labels { for _, v := range reg.Labels {
@@ -132,6 +134,7 @@ 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 { func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
if _, ok := r.runningTasks.Load(task.Id); ok { if _, ok := r.runningTasks.Load(task.Id); ok {
return fmt.Errorf("task %d is already running", task.Id) return fmt.Errorf("task %d is already running", task.Id)
@@ -160,7 +163,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
// getDefaultActionsURL // getDefaultActionsURL
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank, // when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
// it should be set to GithubMirror first. // it should be set to GithubMirror first.
func (r *Runner) getDefaultActionsURL(ctx context.Context, task *runnerv1.Task) string { func (r *Runner) getDefaultActionsURL(_ context.Context, task *runnerv1.Task) string {
giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue() giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue()
if giteaDefaultActionsURL == "https://github.com" && r.cfg.Runner.GithubMirror != "" { if giteaDefaultActionsURL == "https://github.com" && r.cfg.Runner.GithubMirror != "" {
return r.cfg.Runner.GithubMirror return r.cfg.Runner.GithubMirror
@@ -218,8 +221,8 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
preset.Token = t preset.Token = t
} }
if actionsIdTokenRequestUrl := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIdTokenRequestUrl != "" { if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIdTokenRequestUrl r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue() 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"] task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
} }
@@ -304,10 +307,32 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
return execErr return execErr
} }
func (r *Runner) Declare(ctx context.Context, labels []string, capabilitiesJson string) (*connect.Response[runnerv1.DeclareResponse], error) { // Declare sends the runner's labels and capabilities to the server.
func (r *Runner) Declare(ctx context.Context, declareLabels []string, capabilitiesJSON string) (*connect.Response[runnerv1.DeclareResponse], error) {
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{ return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
Version: ver.Version(), Version: ver.Version(),
Labels: labels, Labels: declareLabels,
CapabilitiesJson: capabilitiesJson, CapabilitiesJson: capabilitiesJSON,
})) }))
} }
// MergeServerLabels merges labels returned from the server (which may include admin-added labels)
// with the runner's existing labels. This allows admins to add labels via the Gitea UI.
func (r *Runner) MergeServerLabels(serverLabels []string) {
existing := make(map[string]bool)
for _, l := range r.labels {
existing[l.Name] = true
}
for _, labelStr := range serverLabels {
label, err := labels.Parse(labelStr)
if err != nil {
log.Warnf("ignoring invalid server label %q: %v", labelStr, err)
continue
}
if !existing[label.Name] {
r.labels = append(r.labels, label)
log.Infof("merged server label: %s", labelStr)
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2026 MarketAlly. All rights reserved. // Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package artifact provides utilities for handling artifact uploads.
package artifact package artifact
import ( import (
@@ -88,7 +89,7 @@ func (u *UploadHelper) prewarmConnection(url string) error {
if err != nil { if err != nil {
return err return err
} }
resp.Body.Close() _ = resp.Body.Close()
return nil return nil
} }
@@ -98,7 +99,7 @@ func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string
if err != nil { if err != nil {
return fmt.Errorf("failed to open file: %w", err) return fmt.Errorf("failed to open file: %w", err)
} }
defer file.Close() defer func() { _ = file.Close() }()
stat, err := file.Stat() stat, err := file.Stat()
if err != nil { if err != nil {
@@ -118,7 +119,7 @@ func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string
if _, err := io.Copy(part, file); err != nil { if _, err := io.Copy(part, file); err != nil {
return fmt.Errorf("failed to copy file to form: %w", err) return fmt.Errorf("failed to copy file to form: %w", err)
} }
writer.Close() _ = writer.Close()
req, err := http.NewRequest("POST", url, body) req, err := http.NewRequest("POST", url, body)
if err != nil { if err != nil {
@@ -133,7 +134,7 @@ func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string
if err != nil { if err != nil {
return fmt.Errorf("upload request failed: %w", err) return fmt.Errorf("upload request failed: %w", err)
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body) respBody, _ := io.ReadAll(resp.Body)

View File

@@ -1,6 +1,7 @@
// Copyright 2026 MarketAlly. All rights reserved. // Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package cleanup provides disk cleanup utilities for CI runners.
package cleanup package cleanup
import ( import (
@@ -10,23 +11,23 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"gitea.com/gitea/act_runner/internal/pkg/config" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// CleanupResult contains the results of a cleanup operation // Result contains the results of a cleanup operation.
type CleanupResult struct { type Result struct {
BytesFreed int64 BytesFreed int64
FilesDeleted int FilesDeleted int
Errors []error Errors []error
Duration time.Duration Duration time.Duration
} }
// RunCleanup performs cleanup operations to free disk space // RunCleanup performs cleanup operations to free disk space.
func RunCleanup(ctx context.Context, cfg *config.Config) (*CleanupResult, error) { func RunCleanup(_ context.Context, cfg *config.Config) (*Result, error) {
start := time.Now() start := time.Now()
result := &CleanupResult{} result := &Result{}
log.Info("Starting runner cleanup...") log.Info("Starting runner cleanup...")
@@ -207,8 +208,15 @@ func cleanTempDir(maxAge time.Duration) (int64, int, error) {
return 0, 0, err return 0, 0, err
} }
// Only clean files/dirs that look like runner/act artifacts // Only clean files/dirs that look like runner/act artifacts or build tool temp files
runnerPatterns := []string{"act-", "runner-", "gitea-", "workflow-"} 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 { for _, entry := range entries {
name := entry.Name() name := entry.Name()
isRunner := false isRunner := false
@@ -246,10 +254,10 @@ func cleanTempDir(maxAge time.Duration) (int64, int, error) {
return bytesFreed, filesDeleted, nil return bytesFreed, filesDeleted, nil
} }
// dirSize calculates the total size of a directory // dirSize calculates the total size of a directory.
func dirSize(path string) int64 { func dirSize(path string) int64 {
var size int64 var size int64
filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { _ = filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return nil return nil
} }
@@ -265,6 +273,9 @@ func dirSize(path string) int64 {
// These are cleaned more aggressively (files older than 7 days) since they can grow very large // These are cleaned more aggressively (files older than 7 days) since they can grow very large
func cleanBuildCaches(maxAge time.Duration) (int64, int, error) { func cleanBuildCaches(maxAge time.Duration) (int64, int, error) {
home := os.Getenv("HOME") home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE") // Windows
}
if home == "" { if home == "" {
home = "/root" // fallback for runners typically running as root home = "/root" // fallback for runners typically running as root
} }
@@ -273,31 +284,55 @@ func cleanBuildCaches(maxAge time.Duration) (int64, int, error) {
var totalFilesDeleted int var totalFilesDeleted int
// Build cache directories to clean // Build cache directories to clean
// Format: {path, description} // 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 { cacheDirs := []struct {
path string path string
desc string desc string
maxAge time.Duration
}{ }{
{filepath.Join(home, ".cache", "go-build"), "Go build cache"}, // Linux paths
{filepath.Join(home, ".cache", "golangci-lint"), "golangci-lint cache"}, {filepath.Join(home, ".cache", "go-build"), "Go build cache", goBuildMaxAge},
{filepath.Join(home, ".npm", "_cacache"), "npm cache"}, {filepath.Join(home, ".cache", "golangci-lint"), "golangci-lint cache", 0},
{filepath.Join(home, ".cache", "pnpm"), "pnpm cache"}, {filepath.Join(home, ".npm", "_cacache"), "npm cache", 0},
{filepath.Join(home, ".cache", "yarn"), "yarn cache"}, {filepath.Join(home, ".cache", "pnpm"), "pnpm cache", 0},
{filepath.Join(home, ".nuget", "packages"), "NuGet cache"}, {filepath.Join(home, ".cache", "yarn"), "yarn cache", 0},
{filepath.Join(home, ".gradle", "caches"), "Gradle cache"}, {filepath.Join(home, ".nuget", "packages"), "NuGet cache", 0},
{filepath.Join(home, ".m2", "repository"), "Maven cache"}, {filepath.Join(home, ".gradle", "caches"), "Gradle cache", 0},
{filepath.Join(home, ".cache", "pip"), "pip cache"}, {filepath.Join(home, ".m2", "repository"), "Maven cache", 0},
{filepath.Join(home, ".cargo", "registry", "cache"), "Cargo cache"}, {filepath.Join(home, ".cache", "pip"), "pip cache", 0},
{filepath.Join(home, ".rustup", "tmp"), "Rustup temp"}, {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},
} }
cutoff := time.Now().Add(-maxAge)
for _, cache := range cacheDirs { for _, cache := range cacheDirs {
if _, err := os.Stat(cache.path); os.IsNotExist(err) { if _, err := os.Stat(cache.path); os.IsNotExist(err) {
continue 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 bytesFreed int64
var filesDeleted int var filesDeleted int
@@ -325,13 +360,13 @@ func cleanBuildCaches(maxAge time.Duration) (int64, int, error) {
} }
// Also remove empty directories // Also remove empty directories
filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error { _ = filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error {
if err != nil || !info.IsDir() || path == cache.path { if err != nil || !info.IsDir() || path == cache.path {
return nil return nil
} }
entries, _ := os.ReadDir(path) entries, _ := os.ReadDir(path)
if len(entries) == 0 { if len(entries) == 0 {
os.Remove(path) _ = os.Remove(path)
} }
return nil return nil
}) })

View File

@@ -1,6 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package client provides the HTTP client for communicating with the runner API.
package client package client
import ( import (

View File

@@ -3,6 +3,7 @@
package client package client
// HTTP header constants for runner authentication and identification.
const ( const (
UUIDHeader = "x-runner-uuid" UUIDHeader = "x-runner-uuid"
TokenHeader = "x-runner-token" TokenHeader = "x-runner-token"

View File

@@ -63,10 +63,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 { func (c *HTTPClient) Address() string {
return c.endpoint return c.endpoint
} }
// Insecure returns whether TLS verification is disabled.
func (c *HTTPClient) Insecure() bool { func (c *HTTPClient) Insecure() bool {
return c.insecure return c.insecure
} }

View File

@@ -1,6 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package config provides configuration loading and management for the runner.
package config package config
import ( import (
@@ -137,6 +138,9 @@ func LoadDefault(file string) (*Config, error) {
if cfg.Runner.FetchInterval <= 0 { if cfg.Runner.FetchInterval <= 0 {
cfg.Runner.FetchInterval = 2 * time.Second cfg.Runner.FetchInterval = 2 * time.Second
} }
if cfg.Runner.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. // although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" { if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {

View File

@@ -5,5 +5,7 @@ package config
import _ "embed" import _ "embed"
// Example contains the example configuration file content.
//
//go:embed config.example.yaml //go:embed config.example.yaml
var Example []byte var Example []byte

View File

@@ -23,12 +23,13 @@ type Registration struct {
Ephemeral bool `json:"ephemeral"` Ephemeral bool `json:"ephemeral"`
} }
// LoadRegistration loads the runner registration from a JSON file.
func LoadRegistration(file string) (*Registration, error) { func LoadRegistration(file string) (*Registration, error) {
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer func() { _ = f.Close() }()
var reg Registration var reg Registration
if err := json.NewDecoder(f).Decode(&reg); err != nil { if err := json.NewDecoder(f).Decode(&reg); err != nil {
@@ -40,12 +41,13 @@ func LoadRegistration(file string) (*Registration, error) {
return &reg, nil return &reg, nil
} }
// SaveRegistration saves the runner registration to a JSON file.
func SaveRegistration(file string, reg *Registration) error { func SaveRegistration(file string, reg *Registration) error {
f, err := os.Create(file) f, err := os.Create(file)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer func() { _ = f.Close() }()
reg.Warning = registrationWarning reg.Warning = registrationWarning

View File

@@ -121,7 +121,7 @@ func testLatency(ctx context.Context, serverURL string) float64 {
if err != nil { if err != nil {
return 0 return 0
} }
resp.Body.Close() _ = resp.Body.Close()
latency := time.Since(start).Seconds() * 1000 // Convert to ms latency := time.Since(start).Seconds() * 1000 // Convert to ms
return float64(int(latency*100)) / 100 // Round to 2 decimals return float64(int(latency*100)) / 100 // Round to 2 decimals
@@ -169,7 +169,7 @@ func testDownloadSpeed(ctx context.Context, serverURL string) float64 {
} }
n, _ := io.Copy(io.Discard, resp.Body) n, _ := io.Copy(io.Discard, resp.Body)
resp.Body.Close() _ = resp.Body.Close()
cancel() cancel()
duration := time.Since(start) duration := time.Since(start)

View File

@@ -69,6 +69,7 @@ type RunnerCapabilities struct {
CPU *CPUInfo `json:"cpu,omitempty"` CPU *CPUInfo `json:"cpu,omitempty"`
Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"` Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"`
SuggestedLabels []string `json:"suggested_labels,omitempty"` SuggestedLabels []string `json:"suggested_labels,omitempty"`
Capacity int `json:"capacity,omitempty"` // Number of concurrent jobs this runner can handle
} }
// CapabilityFeatures represents feature support flags // CapabilityFeatures represents feature support flags
@@ -81,8 +82,9 @@ type CapabilityFeatures struct {
// DetectCapabilities detects the runner's capabilities // DetectCapabilities detects the runner's capabilities
// workingDir is the directory where builds will run (for disk space detection) // workingDir is the directory where builds will run (for disk space detection)
func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string) *RunnerCapabilities { func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string, capacity int) *RunnerCapabilities {
cap := &RunnerCapabilities{ caps := &RunnerCapabilities{
Capacity: capacity,
OS: runtime.GOOS, OS: runtime.GOOS,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Tools: make(map[string][]string), Tools: make(map[string][]string),
@@ -103,40 +105,40 @@ func DetectCapabilities(ctx context.Context, dockerHost string, workingDir strin
// Detect Linux distribution // Detect Linux distribution
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
cap.Distro = detectLinuxDistro() caps.Distro = detectLinuxDistro()
} }
// Detect macOS Xcode/iOS // Detect macOS Xcode/iOS
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
cap.Xcode = detectXcode(ctx) caps.Xcode = detectXcode(ctx)
} }
// Detect Docker // Detect Docker
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost) caps.Docker, caps.ContainerRuntime = detectDocker(ctx, dockerHost)
if cap.Docker { if caps.Docker {
cap.DockerCompose = detectDockerCompose(ctx) caps.DockerCompose = detectDockerCompose(ctx)
cap.Features.Services = true caps.Features.Services = true
} }
// Detect common tools // Detect common tools
detectTools(ctx, cap) detectTools(ctx, caps)
// Detect build tools // Detect build tools
detectBuildTools(ctx, cap) detectBuildTools(ctx, caps)
// Detect package managers // Detect package managers
detectPackageManagers(ctx, cap) detectPackageManagers(ctx, caps)
// Detect disk space on the working directory's filesystem // Detect disk space on the working directory's filesystem
cap.Disk = detectDiskSpace(workingDir) caps.Disk = detectDiskSpace(workingDir)
// Detect CPU load // Detect CPU load
cap.CPU = detectCPULoad() caps.CPU = detectCPULoad()
// Generate suggested labels based on detected capabilities // Generate suggested labels based on detected capabilities
cap.SuggestedLabels = generateSuggestedLabels(cap) caps.SuggestedLabels = generateSuggestedLabels(caps)
return cap return caps
} }
// detectXcode detects Xcode and iOS development capabilities on macOS // detectXcode detects Xcode and iOS development capabilities on macOS
@@ -225,18 +227,19 @@ func detectLinuxDistro() *DistroInfo {
if err != nil { if err != nil {
return nil return nil
} }
defer file.Close() defer func() { _ = file.Close() }()
distro := &DistroInfo{} distro := &DistroInfo{}
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.HasPrefix(line, "ID=") { switch {
case strings.HasPrefix(line, "ID="):
distro.ID = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") distro.ID = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
} else if strings.HasPrefix(line, "VERSION_ID=") { case strings.HasPrefix(line, "VERSION_ID="):
distro.VersionID = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") distro.VersionID = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
} else if strings.HasPrefix(line, "PRETTY_NAME=") { case strings.HasPrefix(line, "PRETTY_NAME="):
distro.PrettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") distro.PrettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
} }
} }
@@ -249,7 +252,7 @@ func detectLinuxDistro() *DistroInfo {
} }
// generateSuggestedLabels creates industry-standard labels based on capabilities // generateSuggestedLabels creates industry-standard labels based on capabilities
func generateSuggestedLabels(cap *RunnerCapabilities) []string { func generateSuggestedLabels(caps *RunnerCapabilities) []string {
labels := []string{} labels := []string{}
seen := make(map[string]bool) seen := make(map[string]bool)
@@ -261,7 +264,7 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
} }
// OS labels // OS labels
switch cap.OS { switch caps.OS {
case "linux": case "linux":
addLabel("linux") addLabel("linux")
addLabel("linux-latest") addLabel("linux-latest")
@@ -274,17 +277,17 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
} }
// Distro labels (Linux only) // Distro labels (Linux only)
if cap.Distro != nil && cap.Distro.ID != "" { if caps.Distro != nil && caps.Distro.ID != "" {
distro := strings.ToLower(cap.Distro.ID) distro := strings.ToLower(caps.Distro.ID)
addLabel(distro) addLabel(distro)
addLabel(distro + "-latest") addLabel(distro + "-latest")
} }
// Xcode/iOS labels (macOS only) // Xcode/iOS labels (macOS only)
if cap.Xcode != nil { if caps.Xcode != nil {
addLabel("xcode") addLabel("xcode")
// Check for SDKs // Check for SDKs
for _, sdk := range cap.Xcode.SDKs { for _, sdk := range caps.Xcode.SDKs {
sdkLower := strings.ToLower(sdk) sdkLower := strings.ToLower(sdk)
if strings.Contains(sdkLower, "ios") { if strings.Contains(sdkLower, "ios") {
addLabel("ios") addLabel("ios")
@@ -300,24 +303,24 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
} }
} }
// If simulators available, add simulator label // If simulators available, add simulator label
if len(cap.Xcode.Simulators) > 0 { if len(caps.Xcode.Simulators) > 0 {
addLabel("ios-simulator") addLabel("ios-simulator")
} }
} }
// Tool-based labels // Tool-based labels
if _, ok := cap.Tools["dotnet"]; ok { if _, ok := caps.Tools["dotnet"]; ok {
addLabel("dotnet") addLabel("dotnet")
} }
if _, ok := cap.Tools["java"]; ok { if _, ok := caps.Tools["java"]; ok {
addLabel("java") addLabel("java")
} }
if _, ok := cap.Tools["node"]; ok { if _, ok := caps.Tools["node"]; ok {
addLabel("node") addLabel("node")
} }
// Build tool labels // Build tool labels
for _, tool := range cap.BuildTools { for _, tool := range caps.BuildTools {
switch tool { switch tool {
case "msbuild": case "msbuild":
addLabel("msbuild") addLabel("msbuild")
@@ -382,7 +385,7 @@ func detectDocker(ctx context.Context, dockerHost string) (bool, string) {
if err != nil { if err != nil {
return false, "" return false, ""
} }
defer cli.Close() defer func() { _ = cli.Close() }()
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
@@ -418,7 +421,7 @@ func detectDockerCompose(ctx context.Context) bool {
return false return false
} }
func detectTools(ctx context.Context, cap *RunnerCapabilities) { func detectTools(ctx context.Context, caps *RunnerCapabilities) {
toolDetectors := map[string]func(context.Context) []string{ toolDetectors := map[string]func(context.Context) []string{
"node": detectNodeVersions, "node": detectNodeVersions,
"go": detectGoVersions, "go": detectGoVersions,
@@ -437,7 +440,7 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
for tool, detector := range toolDetectors { for tool, detector := range toolDetectors {
if versions := detector(ctx); len(versions) > 0 { if versions := detector(ctx); len(versions) > 0 {
cap.Tools[tool] = versions caps.Tools[tool] = versions
} }
} }
@@ -458,23 +461,23 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
for name, cmd := range simpleTools { for name, cmd := range simpleTools {
if v := detectSimpleToolVersion(ctx, cmd); v != "" { if v := detectSimpleToolVersion(ctx, cmd); v != "" {
cap.Tools[name] = []string{v} caps.Tools[name] = []string{v}
} }
} }
} }
func detectBuildTools(ctx context.Context, cap *RunnerCapabilities) { func detectBuildTools(ctx context.Context, caps *RunnerCapabilities) {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
detectWindowsBuildTools(ctx, cap) detectWindowsBuildTools(ctx, caps)
case "darwin": case "darwin":
detectMacOSBuildTools(ctx, cap) detectMacOSBuildTools(caps)
case "linux": case "linux":
detectLinuxBuildTools(ctx, cap) detectLinuxBuildTools(caps)
} }
} }
func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) { func detectWindowsBuildTools(ctx context.Context, caps *RunnerCapabilities) {
// Check for Visual Studio via vswhere // Check for Visual Studio via vswhere
vswherePaths := []string{ vswherePaths := []string{
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`, `C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`,
@@ -484,7 +487,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
if _, err := os.Stat(vswhere); err == nil { if _, err := os.Stat(vswhere); err == nil {
cmd := exec.CommandContext(ctx, vswhere, "-latest", "-property", "displayName") cmd := exec.CommandContext(ctx, vswhere, "-latest", "-property", "displayName")
if output, err := cmd.Output(); err == nil && len(output) > 0 { if output, err := cmd.Output(); err == nil && len(output) > 0 {
cap.BuildTools = append(cap.BuildTools, "visual-studio") caps.BuildTools = append(caps.BuildTools, "visual-studio")
break break
} }
} }
@@ -501,7 +504,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
} }
for _, msbuild := range msbuildPaths { for _, msbuild := range msbuildPaths {
if _, err := os.Stat(msbuild); err == nil { if _, err := os.Stat(msbuild); err == nil {
cap.BuildTools = append(cap.BuildTools, "msbuild") caps.BuildTools = append(caps.BuildTools, "msbuild")
break break
} }
} }
@@ -515,14 +518,14 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
} }
for _, iscc := range innoSetupPaths { for _, iscc := range innoSetupPaths {
if _, err := os.Stat(iscc); err == nil { if _, err := os.Stat(iscc); err == nil {
cap.BuildTools = append(cap.BuildTools, "inno-setup") caps.BuildTools = append(caps.BuildTools, "inno-setup")
break break
} }
} }
// Also check PATH // Also check PATH
if _, err := exec.LookPath("iscc"); err == nil { if _, err := exec.LookPath("iscc"); err == nil {
if !contains(cap.BuildTools, "inno-setup") { if !contains(caps.BuildTools, "inno-setup") {
cap.BuildTools = append(cap.BuildTools, "inno-setup") caps.BuildTools = append(caps.BuildTools, "inno-setup")
} }
} }
@@ -533,13 +536,13 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
} }
for _, nsis := range nsisPaths { for _, nsis := range nsisPaths {
if _, err := os.Stat(nsis); err == nil { if _, err := os.Stat(nsis); err == nil {
cap.BuildTools = append(cap.BuildTools, "nsis") caps.BuildTools = append(caps.BuildTools, "nsis")
break break
} }
} }
if _, err := exec.LookPath("makensis"); err == nil { if _, err := exec.LookPath("makensis"); err == nil {
if !contains(cap.BuildTools, "nsis") { if !contains(caps.BuildTools, "nsis") {
cap.BuildTools = append(cap.BuildTools, "nsis") caps.BuildTools = append(caps.BuildTools, "nsis")
} }
} }
@@ -550,7 +553,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
} }
for _, wix := range wixPaths { for _, wix := range wixPaths {
if _, err := os.Stat(wix); err == nil { if _, err := os.Stat(wix); err == nil {
cap.BuildTools = append(cap.BuildTools, "wix") caps.BuildTools = append(caps.BuildTools, "wix")
break break
} }
} }
@@ -558,63 +561,63 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
// Check for signtool (Windows SDK) // Check for signtool (Windows SDK)
signtoolPaths, _ := filepath.Glob(`C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe`) signtoolPaths, _ := filepath.Glob(`C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe`)
if len(signtoolPaths) > 0 { if len(signtoolPaths) > 0 {
cap.BuildTools = append(cap.BuildTools, "signtool") caps.BuildTools = append(caps.BuildTools, "signtool")
} }
} }
func detectMacOSBuildTools(ctx context.Context, cap *RunnerCapabilities) { func detectMacOSBuildTools(caps *RunnerCapabilities) {
// Check for xcpretty // Check for xcpretty
if _, err := exec.LookPath("xcpretty"); err == nil { if _, err := exec.LookPath("xcpretty"); err == nil {
cap.BuildTools = append(cap.BuildTools, "xcpretty") caps.BuildTools = append(caps.BuildTools, "xcpretty")
} }
// Check for fastlane // Check for fastlane
if _, err := exec.LookPath("fastlane"); err == nil { if _, err := exec.LookPath("fastlane"); err == nil {
cap.BuildTools = append(cap.BuildTools, "fastlane") caps.BuildTools = append(caps.BuildTools, "fastlane")
} }
// Check for CocoaPods // Check for CocoaPods
if _, err := exec.LookPath("pod"); err == nil { if _, err := exec.LookPath("pod"); err == nil {
cap.BuildTools = append(cap.BuildTools, "cocoapods") caps.BuildTools = append(caps.BuildTools, "cocoapods")
} }
// Check for Carthage // Check for Carthage
if _, err := exec.LookPath("carthage"); err == nil { if _, err := exec.LookPath("carthage"); err == nil {
cap.BuildTools = append(cap.BuildTools, "carthage") caps.BuildTools = append(caps.BuildTools, "carthage")
} }
// Check for SwiftLint // Check for SwiftLint
if _, err := exec.LookPath("swiftlint"); err == nil { if _, err := exec.LookPath("swiftlint"); err == nil {
cap.BuildTools = append(cap.BuildTools, "swiftlint") caps.BuildTools = append(caps.BuildTools, "swiftlint")
} }
// Check for create-dmg or similar // Check for create-dmg or similar
if _, err := exec.LookPath("create-dmg"); err == nil { if _, err := exec.LookPath("create-dmg"); err == nil {
cap.BuildTools = append(cap.BuildTools, "create-dmg") caps.BuildTools = append(caps.BuildTools, "create-dmg")
} }
// Check for Packages (packagesbuild) // Check for Packages (packagesbuild)
if _, err := exec.LookPath("packagesbuild"); err == nil { if _, err := exec.LookPath("packagesbuild"); err == nil {
cap.BuildTools = append(cap.BuildTools, "packages") caps.BuildTools = append(caps.BuildTools, "packages")
} }
// Check for pkgbuild (built-in) // Check for pkgbuild (built-in)
if _, err := exec.LookPath("pkgbuild"); err == nil { if _, err := exec.LookPath("pkgbuild"); err == nil {
cap.BuildTools = append(cap.BuildTools, "pkgbuild") caps.BuildTools = append(caps.BuildTools, "pkgbuild")
} }
// Check for codesign (built-in) // Check for codesign (built-in)
if _, err := exec.LookPath("codesign"); err == nil { if _, err := exec.LookPath("codesign"); err == nil {
cap.BuildTools = append(cap.BuildTools, "codesign") caps.BuildTools = append(caps.BuildTools, "codesign")
} }
// Check for notarytool (built-in with Xcode) // Check for notarytool (built-in with Xcode)
if _, err := exec.LookPath("notarytool"); err == nil { if _, err := exec.LookPath("notarytool"); err == nil {
cap.BuildTools = append(cap.BuildTools, "notarytool") caps.BuildTools = append(caps.BuildTools, "notarytool")
} }
} }
func detectLinuxBuildTools(ctx context.Context, cap *RunnerCapabilities) { func detectLinuxBuildTools(caps *RunnerCapabilities) {
// Check for common Linux build tools // Check for common Linux build tools
tools := []string{ tools := []string{
"gcc", "g++", "clang", "clang++", "gcc", "g++", "clang", "clang++",
@@ -626,54 +629,54 @@ func detectLinuxBuildTools(ctx context.Context, cap *RunnerCapabilities) {
for _, tool := range tools { for _, tool := range tools {
if _, err := exec.LookPath(tool); err == nil { if _, err := exec.LookPath(tool); err == nil {
cap.BuildTools = append(cap.BuildTools, tool) caps.BuildTools = append(caps.BuildTools, tool)
} }
} }
} }
func detectPackageManagers(ctx context.Context, cap *RunnerCapabilities) { func detectPackageManagers(_ context.Context, caps *RunnerCapabilities) {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
if _, err := exec.LookPath("choco"); err == nil { if _, err := exec.LookPath("choco"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "chocolatey") caps.PackageManagers = append(caps.PackageManagers, "chocolatey")
} }
if _, err := exec.LookPath("scoop"); err == nil { if _, err := exec.LookPath("scoop"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "scoop") caps.PackageManagers = append(caps.PackageManagers, "scoop")
} }
if _, err := exec.LookPath("winget"); err == nil { if _, err := exec.LookPath("winget"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "winget") caps.PackageManagers = append(caps.PackageManagers, "winget")
} }
case "darwin": case "darwin":
if _, err := exec.LookPath("brew"); err == nil { if _, err := exec.LookPath("brew"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "homebrew") caps.PackageManagers = append(caps.PackageManagers, "homebrew")
} }
if _, err := exec.LookPath("port"); err == nil { if _, err := exec.LookPath("port"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "macports") caps.PackageManagers = append(caps.PackageManagers, "macports")
} }
case "linux": case "linux":
if _, err := exec.LookPath("apt"); err == nil { if _, err := exec.LookPath("apt"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "apt") caps.PackageManagers = append(caps.PackageManagers, "apt")
} }
if _, err := exec.LookPath("yum"); err == nil { if _, err := exec.LookPath("yum"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "yum") caps.PackageManagers = append(caps.PackageManagers, "yum")
} }
if _, err := exec.LookPath("dnf"); err == nil { if _, err := exec.LookPath("dnf"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "dnf") caps.PackageManagers = append(caps.PackageManagers, "dnf")
} }
if _, err := exec.LookPath("pacman"); err == nil { if _, err := exec.LookPath("pacman"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "pacman") caps.PackageManagers = append(caps.PackageManagers, "pacman")
} }
if _, err := exec.LookPath("zypper"); err == nil { if _, err := exec.LookPath("zypper"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "zypper") caps.PackageManagers = append(caps.PackageManagers, "zypper")
} }
if _, err := exec.LookPath("apk"); err == nil { if _, err := exec.LookPath("apk"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "apk") caps.PackageManagers = append(caps.PackageManagers, "apk")
} }
if _, err := exec.LookPath("snap"); err == nil { if _, err := exec.LookPath("snap"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "snap") caps.PackageManagers = append(caps.PackageManagers, "snap")
} }
if _, err := exec.LookPath("flatpak"); err == nil { if _, err := exec.LookPath("flatpak"); err == nil {
cap.PackageManagers = append(cap.PackageManagers, "flatpak") caps.PackageManagers = append(caps.PackageManagers, "flatpak")
} }
} }
} }
@@ -811,13 +814,8 @@ func detectPwshVersion(ctx context.Context, cmd string) string {
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
// Use -Command to get version // Use -Command to get version (same command works for both pwsh and powershell)
var c *exec.Cmd c := exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
if cmd == "pwsh" {
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
} else {
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
}
output, err := c.Output() output, err := c.Output()
if err != nil { if err != nil {
@@ -910,7 +908,24 @@ func detectCPULoad() *CPUInfo {
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
// Read from /proc/loadavg // Check if running in a container (LXC/Docker)
// Containers share /proc/loadavg with host, giving inaccurate readings
inContainer := isInContainer()
if inContainer {
// Try to get CPU usage from cgroups (more accurate for containers)
if cgroupCPU := getContainerCPUUsage(); cgroupCPU >= 0 {
info.LoadPercent = cgroupCPU
info.LoadAvg1m = cgroupCPU * float64(numCPU) / 100.0
return info
}
// If cgroup reading failed, report 0 - better than host's load
info.LoadPercent = 0
info.LoadAvg1m = 0
return info
}
// Not in container - use traditional /proc/loadavg
data, err := os.ReadFile("/proc/loadavg") data, err := os.ReadFile("/proc/loadavg")
if err != nil { if err != nil {
return info return info
@@ -950,23 +965,19 @@ func detectCPULoad() *CPUInfo {
} }
} }
case "windows": case "windows":
// Windows doesn't have load average, use CPU usage via wmic // Windows doesn't have load average, use PowerShell to get CPU usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // wmic is deprecated, use Get-CimInstance instead
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, "wmic", "cpu", "get", "loadpercentage") cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
"(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average")
output, err := cmd.Output() output, err := cmd.Output()
if err == nil { if err == nil {
lines := strings.Split(string(output), "\n") line := strings.TrimSpace(string(output))
for _, line := range lines { if load, err := parseFloat(line); err == nil {
line = strings.TrimSpace(line) info.LoadPercent = load
if line != "" && line != "LoadPercentage" { info.LoadAvg1m = load * float64(numCPU) / 100.0
if load, err := parseFloat(line); err == nil { return info
// Convert percentage to "load" equivalent
info.LoadPercent = load
info.LoadAvg1m = load * float64(numCPU) / 100.0
return info
}
}
} }
} }
} }
@@ -979,6 +990,61 @@ func detectCPULoad() *CPUInfo {
return info return info
} }
// isInContainer checks if we're running inside a container (LXC/Docker)
func isInContainer() bool {
// Check for Docker
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check PID 1's environment for container type (works for LXC on Proxmox)
if data, err := os.ReadFile("/proc/1/environ"); err == nil {
// environ uses null bytes as separators
content := string(data)
if strings.Contains(content, "container=lxc") || strings.Contains(content, "container=docker") {
return true
}
}
// Check for LXC/Docker in cgroup path (cgroup v1)
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
content := string(data)
if strings.Contains(content, "/lxc/") || strings.Contains(content, "/docker/") {
return true
}
}
// Check for container environment variable in current process
if os.Getenv("container") != "" {
return true
}
// Check for systemd-nspawn or other containers
if _, err := os.Stat("/run/.containerenv"); err == nil {
return true
}
return false
}
// getContainerCPUUsage tries to get CPU usage from cgroups
// Returns -1 if unable to determine
func getContainerCPUUsage() float64 {
// Try cgroup v2 first
if data, err := os.ReadFile("/sys/fs/cgroup/cpu.stat"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "usage_usec ") {
// This gives total CPU time, not current usage
// For now, we can't easily calculate percentage without storing previous value
// Return -1 to fall back to reporting 0
break
}
}
}
// Note: Reading /proc/self/stat could give us utime and stime (fields 14 and 15),
// but these are cumulative values, not instantaneous. For containers, we report 0
// rather than misleading host data.
return -1 // Unable to determine - caller should handle
}
// parseFloat parses a string to float64 // parseFloat parses a string to float64
func parseFloat(s string) (float64, error) { func parseFloat(s string) (float64, error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)

View File

@@ -1,8 +1,8 @@
//go:build unix // Copyright 2026 MarketAlly. All rights reserved.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build unix
package envcheck package envcheck
import ( import (

View File

@@ -1,8 +1,8 @@
//go:build windows // Copyright 2026 MarketAlly. All rights reserved.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build windows
package envcheck package envcheck
import ( import (

View File

@@ -10,6 +10,7 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
// CheckIfDockerRunning verifies that the Docker daemon is running and accessible.
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error { func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
opts := []client.Opt{ opts := []client.Opt{
client.FromEnv, client.FromEnv,
@@ -23,7 +24,7 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
if err != nil { if err != nil {
return err return err
} }
defer cli.Close() defer func() { _ = cli.Close() }()
_, err = cli.Ping(ctx) _, err = cli.Ping(ctx)
if err != nil { if err != nil {

View File

@@ -1,6 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package labels provides utilities for parsing and managing runner labels.
package labels package labels
import ( import (
@@ -8,17 +9,20 @@ import (
"strings" "strings"
) )
// Label scheme constants define the execution environments.
const ( const (
SchemeHost = "host" SchemeHost = "host"
SchemeDocker = "docker" SchemeDocker = "docker"
) )
// Label represents a parsed runner label with name, schema, and optional argument.
type Label struct { type Label struct {
Name string Name string
Schema string Schema string
Arg string Arg string
} }
// Parse parses a label string in the format "name:schema:arg" and returns a Label.
func Parse(str string) (*Label, error) { func Parse(str string) (*Label, error) {
splits := strings.SplitN(str, ":", 3) splits := strings.SplitN(str, ":", 3)
label := &Label{ label := &Label{
@@ -38,8 +42,10 @@ func Parse(str string) (*Label, error) {
return label, nil return label, nil
} }
// Labels is a slice of Label pointers.
type Labels []*Label type Labels []*Label
// RequireDocker returns true if any label uses the docker schema.
func (l Labels) RequireDocker() bool { func (l Labels) RequireDocker() bool {
for _, label := range l { for _, label := range l {
if label.Schema == SchemeDocker { if label.Schema == SchemeDocker {
@@ -49,39 +55,56 @@ func (l Labels) RequireDocker() bool {
return false return false
} }
// PickPlatform selects the appropriate platform based on the runsOn requirements.
// Returns empty string if no matching label is found, which will cause the job to fail.
// If runs-on includes a schema (e.g., "linux:host" or "linux:docker"), it must match
// the runner's configured schema for that label.
func (l Labels) PickPlatform(runsOn []string) string { func (l Labels) PickPlatform(runsOn []string) string {
// Build maps for both platform values and schemas
platforms := make(map[string]string, len(l)) platforms := make(map[string]string, len(l))
schemas := make(map[string]string, len(l))
for _, label := range l { for _, label := range l {
switch label.Schema { switch label.Schema {
case SchemeDocker: case SchemeDocker:
// "//" will be ignored
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//") platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
schemas[label.Name] = SchemeDocker
case SchemeHost: case SchemeHost:
platforms[label.Name] = "-self-hosted" platforms[label.Name] = "-self-hosted"
schemas[label.Name] = SchemeHost
default: default:
// It should not happen, because Parse has checked it.
continue continue
} }
} }
for _, v := range runsOn { for _, v := range runsOn {
if v, ok := platforms[v]; ok { name := v
return v requestedSchema := ""
// Parse schema if present (e.g., "germany-linux:host" -> name="germany-linux", schema="host")
if idx := strings.Index(v, ":"); idx != -1 {
name = v[:idx]
requestedSchema = v[idx+1:]
// Handle docker:// prefix
if strings.HasPrefix(requestedSchema, "docker") {
requestedSchema = SchemeDocker
}
}
if platform, ok := platforms[name]; ok {
// If schema was specified, validate it matches
if requestedSchema != "" && requestedSchema != schemas[name] {
// Schema mismatch - workflow asked for different mode than runner provides
continue
}
return platform
} }
} }
// TODO: support multiple labels // No matching label found
// like: return ""
// ["ubuntu-22.04"] => "ubuntu:22.04"
// ["with-gpu"] => "linux:with-gpu"
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
// return default.
// So the runner receives a task with a label that the runner doesn't have,
// it happens when the user have edited the label of the runner in the web UI.
// TODO: it may be not correct, what if the runner is used as host mode only?
return "docker.gitea.com/runner-images:ubuntu-latest"
} }
// Names returns the names of all labels.
func (l Labels) Names() []string { func (l Labels) Names() []string {
names := make([]string, 0, len(l)) names := make([]string, 0, len(l))
for _, label := range l { for _, label := range l {
@@ -90,6 +113,7 @@ func (l Labels) Names() []string {
return names return names
} }
// ToStrings converts labels back to their string representation.
func (l Labels) ToStrings() []string { func (l Labels) ToStrings() []string {
ls := make([]string, 0, len(l)) ls := make([]string, 0, len(l))
for _, label := range l { for _, label := range l {

View File

@@ -10,6 +10,94 @@ import (
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
func TestPickPlatform(t *testing.T) {
tests := []struct {
name string
labels []string
runsOn []string
want string
}{
{
name: "exact match host label",
labels: []string{"linux-latest:host", "ubuntu:host"},
runsOn: []string{"linux-latest"},
want: "-self-hosted",
},
{
name: "exact match docker label",
labels: []string{"ubuntu:docker://node:18"},
runsOn: []string{"ubuntu"},
want: "node:18",
},
{
name: "no match returns empty string to fail job",
labels: []string{"linux:host", "debian:host"},
runsOn: []string{"unknown-label"},
want: "",
},
{
name: "no match on docker runner returns empty string",
labels: []string{"ubuntu:docker://node:18", "linux:host"},
runsOn: []string{"unknown-label"},
want: "",
},
{
name: "empty labels returns empty string",
labels: []string{},
runsOn: []string{"anything"},
want: "",
},
{
name: "multiple runsOn matches first available",
labels: []string{"linux:host", "ubuntu:docker://node:18"},
runsOn: []string{"windows", "ubuntu"},
want: "node:18",
},
{
name: "runsOn with :host suffix matches host label",
labels: []string{"germany-linux:host", "linux:host"},
runsOn: []string{"germany-linux:host"},
want: "-self-hosted",
},
{
name: "runsOn with :docker suffix matches docker label",
labels: []string{"ubuntu:docker://node:18"},
runsOn: []string{"ubuntu:docker"},
want: "node:18",
},
{
name: "runsOn without suffix matches any schema",
labels: []string{"linux:host"},
runsOn: []string{"linux"},
want: "-self-hosted",
},
{
name: "runsOn :docker does not match host label",
labels: []string{"linux:host"},
runsOn: []string{"linux:docker"},
want: "",
},
{
name: "runsOn :host does not match docker label",
labels: []string{"ubuntu:docker://node:18"},
runsOn: []string{"ubuntu:host"},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ls := Labels{}
for _, l := range tt.labels {
label, err := Parse(l)
require.NoError(t, err)
ls = append(ls, label)
}
got := ls.PickPlatform(tt.runsOn)
assert.Equal(t, got, tt.want)
})
}
}
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
tests := []struct { tests := []struct {
args string args string

View File

@@ -1,6 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package report provides task reporting functionality for communicating with the server.
package report package report
import ( import (
@@ -18,9 +19,10 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"gitea.com/gitea/act_runner/internal/pkg/client" "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
) )
// Reporter handles logging and state reporting for running tasks.
type Reporter struct { type Reporter struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@@ -42,6 +44,7 @@ type Reporter struct {
stopCommandEndToken string 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 { func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
var oldnew []string var oldnew []string
if v := task.Context.Fields["token"].GetStringValue(); v != "" { 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 return rv
} }
// ResetSteps initializes the step states with the given number of steps.
func (r *Reporter) ResetSteps(l int) { func (r *Reporter) ResetSteps(l int) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@@ -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 { func (r *Reporter) Levels() []log.Level {
return log.AllLevels return log.AllLevels
} }
@@ -93,6 +98,7 @@ func appendIfNotNil[T any](s []*T, v *T) []*T {
return s return s
} }
// Fire processes a log entry and updates the task state accordingly.
func (r *Reporter) Fire(entry *log.Entry) error { func (r *Reporter) Fire(entry *log.Entry) error {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@@ -175,6 +181,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
return nil return nil
} }
// RunDaemon starts the periodic reporting of logs and state.
func (r *Reporter) RunDaemon() { func (r *Reporter) RunDaemon() {
if r.closed { if r.closed {
return return
@@ -189,6 +196,7 @@ func (r *Reporter) RunDaemon() {
time.AfterFunc(time.Second, r.RunDaemon) time.AfterFunc(time.Second, r.RunDaemon)
} }
// Logf adds a formatted log message to the report.
func (r *Reporter) Logf(format string, a ...interface{}) { func (r *Reporter) Logf(format string, a ...interface{}) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@@ -205,6 +213,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) { func (r *Reporter) SetOutputs(outputs map[string]string) {
r.stateMu.Lock() r.stateMu.Lock()
defer r.stateMu.Unlock() defer r.stateMu.Unlock()
@@ -225,6 +234,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 { func (r *Reporter) Close(lastWords string) error {
r.closed = true r.closed = true
@@ -260,6 +270,7 @@ func (r *Reporter) Close(lastWords string) error {
}, retry.Context(r.ctx)) }, retry.Context(r.ctx))
} }
// ReportLog sends accumulated log rows to the server.
func (r *Reporter) ReportLog(noMore bool) error { func (r *Reporter) ReportLog(noMore bool) error {
r.clientM.Lock() r.clientM.Lock()
defer r.clientM.Unlock() defer r.clientM.Unlock()
@@ -295,6 +306,7 @@ func (r *Reporter) ReportLog(noMore bool) error {
return nil return nil
} }
// ReportState sends the current task state to the server.
func (r *Reporter) ReportState() error { func (r *Reporter) ReportState() error {
r.clientM.Lock() r.clientM.Lock()
defer r.clientM.Unlock() defer r.clientM.Unlock()
@@ -373,7 +385,7 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`) 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 { if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
return &originalContent return &originalContent
} }

View File

@@ -16,7 +16,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb" "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) { func TestReporter_parseLogRow(t *testing.T) {

View 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 ""
}

View 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"
}

View File

@@ -1,11 +1,13 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package ver provides version information for the runner.
package ver 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" var version = "dev"
// Version returns the current runner version.
func Version() string { func Version() string {
return version return version
} }

14
main.go
View File

@@ -1,6 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// GitCaddy Runner is a CI/CD runner for Gitea Actions.
package main package main
import ( import (
@@ -8,10 +9,21 @@ import (
"os/signal" "os/signal"
"syscall" "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() { 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) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop() defer stop()
// run the command // run the command