Compare commits
38 Commits
v0.3.11-gitcaddy
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee5fd838b8 | |||
| 17f78a5e4c | |||
| 522ee44718 | |||
| d87b08c559 | |||
| 259238eedf | |||
| f33d0a54c4 | |||
| 899ca015b1 | |||
| e1b9b277ee | |||
| 826ecfb433 | |||
| 5ac01b2dc9 | |||
| f984198d4d | |||
| 607c332313 | |||
| 50480c989c | |||
| bf71b55cb7 | |||
| 26b4e7497f | |||
| b2922e332a | |||
| d388ec5519 | |||
| cb1c1a3264 | |||
| 63967eb6fa | |||
| 22f1ea6e76 | |||
|
|
4d6900b7a3 | ||
|
|
898ef596ae | ||
|
|
eb37073861 | ||
|
|
ec9b323318 | ||
|
|
d955727863 | ||
|
|
3addd66efa | ||
|
|
b6d700af60 | ||
|
|
7c0d11c353 | ||
|
|
b9ae4d5f36 | ||
|
|
3a66563c1e | ||
|
|
e0feb6bd4e | ||
|
|
0db86bc6a4 | ||
|
|
f5b22c4149 | ||
|
|
0ba2e0c3d5 | ||
|
|
8a54ec62d4 | ||
|
|
587ac42be4 | ||
|
|
56dcda0d5e | ||
|
|
e44f0c403b |
@@ -44,14 +44,14 @@ jobs:
|
||||
fi
|
||||
echo "Building version: ${VERSION}"
|
||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
|
||||
go build -a -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=${VERSION}" \
|
||||
-o act_runner-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
|
||||
go build -a -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=${VERSION}" \
|
||||
-o gitcaddy-runner-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: act_runner-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: act_runner-*
|
||||
name: gitcaddy-runner-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: gitcaddy-runner-*
|
||||
|
||||
release:
|
||||
needs: build
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Create Release
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -17,6 +17,14 @@ jobs:
|
||||
go-version-file: 'go.mod'
|
||||
cache: false
|
||||
|
||||
- name: Clear stale module cache
|
||||
run: go clean -modcache
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/act_runner
|
||||
*.exe
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
@@ -12,3 +13,4 @@ coverage.txt
|
||||
__debug_bin
|
||||
# gorelease binary folder
|
||||
dist
|
||||
act_runner-*
|
||||
|
||||
14
.gitsecrets-ignore
Normal file
14
.gitsecrets-ignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# GitSecrets Ignore File
|
||||
# This file tracks false positives identified by AI evaluation or manually marked.
|
||||
# Each line is a JSON object with the following fields:
|
||||
# - contentHash: SHA256 hash prefix of the secret content
|
||||
# - patternId: The pattern that detected this secret
|
||||
# - filePath: Relative path where the secret was found
|
||||
# - reason: Why this was marked as a false positive
|
||||
# - confidence: AI confidence level (if from AI evaluation)
|
||||
# - addedAt: Timestamp when this entry was added
|
||||
#
|
||||
# You can safely commit this file to share false positive markers with your team.
|
||||
# To remove an entry, simply delete the corresponding line.
|
||||
|
||||
{"contentHash":"5af30500c6463ec4","patternId":"password-assignment","filePath":"..\\gitcaddy\\internal\\app\\cmd\\register.go","reason":"Manually marked as false positive","addedAt":1769249840525}
|
||||
100
.golangci.yml
100
.golangci.yml
@@ -1,53 +1,42 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- gosimple
|
||||
- typecheck
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- dupl
|
||||
#- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
|
||||
- gofmt
|
||||
- misspell
|
||||
- gocritic
|
||||
- bidichk
|
||||
- ineffassign
|
||||
- revive
|
||||
- gofumpt
|
||||
- depguard
|
||||
- nakedret
|
||||
- unconvert
|
||||
- wastedassign
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
fast: false
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
|
||||
run:
|
||||
go: 1.18
|
||||
go: "1.23"
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- node_modules
|
||||
- public
|
||||
- web_src
|
||||
|
||||
linters-settings:
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1005", "-ST1003"]
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
|
||||
- singleCaseSwitch
|
||||
revive:
|
||||
ignore-generated-header: false
|
||||
severity: warning
|
||||
confidence: 0.8
|
||||
errorCode: 1
|
||||
warningCode: 1
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
@@ -72,94 +61,25 @@ linters-settings:
|
||||
- name: modifies-value-receiver
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
lang-version: "1.18"
|
||||
depguard:
|
||||
# TODO: use depguard to replace import checks in gitea-vet
|
||||
list-type: denylist
|
||||
# Check the list against standard lib.
|
||||
include-go-root: true
|
||||
packages-with-error-message:
|
||||
- github.com/unknwon/com: "use gitea's util and replacements"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- unparam
|
||||
- staticcheck
|
||||
- path: models/migrations/v
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- linters:
|
||||
- dupl
|
||||
text: "webhook"
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "`ID' should not be capitalized"
|
||||
- path: modules/templates/helper.go
|
||||
linters:
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
text: "swagger"
|
||||
- path: contrib/pr/checkout.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/issue.go
|
||||
linters:
|
||||
- errcheck
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: modules/log/
|
||||
linters:
|
||||
- errcheck
|
||||
- path: routers/api/v1/repo/issue_subscription.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: routers/repo/view.go
|
||||
linters:
|
||||
- dupl
|
||||
- path: models/migrations/
|
||||
linters:
|
||||
- unused
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "argument x is overwritten before first use"
|
||||
- path: modules/httplib/httplib.go
|
||||
linters:
|
||||
- staticcheck
|
||||
# Enabling this would require refactoring the methods and how they are called.
|
||||
- path: models/issue_comment_list.go
|
||||
linters:
|
||||
- dupl
|
||||
- linters:
|
||||
- misspell
|
||||
text: '`Unknwon` is a misspelling of `Unknown`'
|
||||
- path: models/update.go
|
||||
linters:
|
||||
- unused
|
||||
- path: cmd/dump.go
|
||||
linters:
|
||||
- dupl
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "exitAfterDefer:"
|
||||
linters:
|
||||
- gocritic
|
||||
- path: modules/graceful/manager_windows.go
|
||||
linters:
|
||||
- staticcheck
|
||||
text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
|
||||
- path: models/user/openid.go
|
||||
linters:
|
||||
- golint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
version: 2
|
||||
|
||||
project_name: gitcaddy-runner
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -63,7 +65,7 @@ builds:
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
||||
- -s -w -X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version={{ .Summary }}
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- .Version }}-
|
||||
@@ -86,7 +88,7 @@ blobs:
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
directory: "act_runner/{{.Version}}"
|
||||
directory: "gitcaddy-runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
@@ -108,8 +110,8 @@ nightly:
|
||||
version_template: "nightly"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
download: https://gitea.com
|
||||
api: https://git.marketally.com/api/v1
|
||||
download: https://git.marketally.com
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
|
||||
18
LICENSE.md
Normal file
18
LICENSE.md
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 gitcaddy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
11
Makefile
11
Makefile
@@ -1,5 +1,5 @@
|
||||
DIST := dist
|
||||
EXECUTABLE := act_runner
|
||||
EXECUTABLE := gitcaddy-runner
|
||||
GOFMT ?= gofumpt -l
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
GO ?= go
|
||||
@@ -15,7 +15,7 @@ WINDOWS_ARCHS ?= windows/amd64
|
||||
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
|
||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||
|
||||
DOCKER_IMAGE ?= gitea/act_runner
|
||||
DOCKER_IMAGE ?= marketally/gitcaddy-runner
|
||||
DOCKER_TAG ?= nightly
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||
@@ -67,11 +67,11 @@ else
|
||||
endif
|
||||
endif
|
||||
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
GO_PACKAGES_TO_VET ?= $(filter-out git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
|
||||
|
||||
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
LDFLAGS ?= -X "git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||
|
||||
all: build
|
||||
|
||||
@@ -117,8 +117,7 @@ test: fmt-check security-check
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@$(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
|
||||
@$(GO) vet $(GO_PACKAGES_TO_VET)
|
||||
|
||||
install: $(GOFILES)
|
||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||
|
||||
590
README.md
590
README.md
@@ -1,121 +1,81 @@
|
||||
# GitCaddy Act Runner
|
||||
# GitCaddy Runner
|
||||
|
||||
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.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Capability Detection**: Automatically detects OS, architecture, Docker support, available shells, and installed tools
|
||||
- **Capability Reporting**: Reports capabilities to Gitea server during runner declaration
|
||||
- **Full Compatibility**: Drop-in replacement for standard act_runner
|
||||
- **Multi-Platform**: Supports Linux, macOS, and Windows
|
||||
- **Automated Capability Detection**: Automatically identifies OS, architecture, installed tools, and available resources
|
||||
- **Concurrent Job Execution**: Configure runner capacity to handle multiple jobs simultaneously
|
||||
- **Docker Support**: Full support for Docker and Docker Compose workflows
|
||||
- **Xcode Integration**: Detects Xcode installations, SDKs, and simulators on macOS
|
||||
- **Tool Detection**: Identifies installed tools (Node.js, Python, .NET, Go, Ruby, Swift, etc.)
|
||||
- **AI-Friendly API**: Exposes capabilities through Gitea's API for automated workflow generation
|
||||
- **Cache Support**: Built-in workflow cache support for faster builds
|
||||
|
||||
## Installation
|
||||
|
||||
### 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
|
||||
# Linux (amd64)
|
||||
curl -L -o act_runner https://git.marketally.com/gitcaddy/act_runner/releases/download/v0.3.1-gitcaddy/act_runner-linux-amd64
|
||||
chmod +x act_runner
|
||||
# Apple Silicon (M1/M2/M3/M4)
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-darwin-arm64
|
||||
chmod +x gitcaddy-runner
|
||||
|
||||
# macOS (Apple Silicon)
|
||||
curl -L -o act_runner https://git.marketally.com/gitcaddy/act_runner/releases/download/v0.3.1-gitcaddy/act_runner-darwin-arm64
|
||||
chmod +x act_runner
|
||||
# Intel
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-darwin-amd64
|
||||
chmod +x gitcaddy-runner
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
# x86_64
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-linux-amd64
|
||||
chmod +x gitcaddy-runner
|
||||
|
||||
# ARM64
|
||||
curl -L -o gitcaddy-runner https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-linux-arm64
|
||||
chmod +x gitcaddy-runner
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
# Download the Windows executable
|
||||
# https://git.marketally.com/gitcaddy/gitcaddy-runner/releases/download/v1.0.0/gitcaddy-runner-1.0.0-windows-amd64.exe
|
||||
```
|
||||
|
||||
### Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/act_runner.git
|
||||
cd act_runner
|
||||
git clone https://git.marketally.com/gitcaddy/gitcaddy-runner.git
|
||||
cd gitcaddy-runner
|
||||
make build
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Enable Actions in Gitea
|
||||
### 1. Enable Gitea Actions
|
||||
|
||||
Add to your Gitea `app.ini`:
|
||||
In your Gitea `app.ini`:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED = true
|
||||
```
|
||||
|
||||
### 2. Register the Runner
|
||||
### 2. Generate Configuration
|
||||
|
||||
```bash
|
||||
./act_runner register \
|
||||
--instance https://your-gitea-instance.com \
|
||||
--token YOUR_RUNNER_TOKEN \
|
||||
--name my-runner \
|
||||
--labels ubuntu-latest,docker
|
||||
./gitcaddy-runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
### 3. Start the Runner
|
||||
### 3. Configure the Runner
|
||||
|
||||
```bash
|
||||
./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:
|
||||
Edit `config.yaml` to customize settings. **Important configuration options:**
|
||||
|
||||
```yaml
|
||||
log:
|
||||
@@ -123,73 +83,447 @@ log:
|
||||
|
||||
runner:
|
||||
file: .runner
|
||||
capacity: 1
|
||||
capacity: 2 # Number of concurrent jobs (default: 1)
|
||||
timeout: 3h
|
||||
shutdown_timeout: 3m # Grace period for running jobs on shutdown
|
||||
insecure: false
|
||||
fetch_timeout: 5s
|
||||
fetch_interval: 2s
|
||||
labels:
|
||||
- ubuntu-latest:docker://node:18-bullseye
|
||||
- ubuntu-22.04:docker://ubuntu:22.04
|
||||
|
||||
container:
|
||||
docker_host: ""
|
||||
force_pull: false
|
||||
privileged: false
|
||||
- "ubuntu-latest:docker://node:16-bullseye"
|
||||
- "ubuntu-22.04:docker://node:16-bullseye"
|
||||
|
||||
cache:
|
||||
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
|
||||
docker run -d \
|
||||
--name act_runner \
|
||||
-e GITEA_INSTANCE_URL=https://your-gitea.com \
|
||||
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ./data:/data \
|
||||
gitcaddy/act_runner:latest
|
||||
./gitcaddy-runner register \
|
||||
--instance https://your-gitea-instance.com \
|
||||
--token YOUR_REGISTRATION_TOKEN \
|
||||
--name my-runner \
|
||||
--labels ubuntu-latest:docker://node:16-bullseye
|
||||
```
|
||||
|
||||
## 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`)
|
||||
- **Workflow Validation API** for pre-flight checks
|
||||
- **Action Compatibility Database** for GitHub Actions mapping
|
||||
|
||||
### How It Works
|
||||
**Important:** Always specify the config file path with `-c` flag:
|
||||
|
||||
```bash
|
||||
./gitcaddy-runner daemon -c config.yaml
|
||||
```
|
||||
|
||||
act_runner Gitea AI Tool
|
||||
| | |
|
||||
| Declare + Capabilities | |
|
||||
|---------------------------->| |
|
||||
| | |
|
||||
| | GET /api/v2/.../caps |
|
||||
| |<------------------------|
|
||||
| | |
|
||||
| | Runner capabilities |
|
||||
| |------------------------>|
|
||||
| | |
|
||||
| | Generates workflow |
|
||||
| | with correct config |
|
||||
**Without the `-c` flag, the runner will use default settings and ignore your config.yaml!**
|
||||
|
||||
## Running as a Service
|
||||
|
||||
### macOS (launchd)
|
||||
|
||||
Create `~/Library/LaunchAgents/com.gitcaddy.runner.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.gitcaddy.runner</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/path/to/gitcaddy-runner</string>
|
||||
<string>daemon</string>
|
||||
<string>-c</string>
|
||||
<string>/path/to/config.yaml</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/path/to/runner/directory</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/path/to/runner.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/path/to/runner.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
Load the service:
|
||||
|
||||
| Project | Description |
|
||||
|---------|-------------|
|
||||
| [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 |
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/com.gitcaddy.runner.plist
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
39
cmd/upload-helper/main.go
Normal file
39
cmd/upload-helper/main.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package main provides the upload-helper CLI tool for reliable file uploads.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/artifact"
|
||||
)
|
||||
|
||||
func main() {
|
||||
url := flag.String("url", "", "Upload URL")
|
||||
token := flag.String("token", "", "Auth token")
|
||||
file := flag.String("file", "", "File to upload")
|
||||
retries := flag.Int("retries", 5, "Maximum retry attempts")
|
||||
flag.Parse()
|
||||
|
||||
if *url == "" || *token == "" || *file == "" {
|
||||
fmt.Fprintf(os.Stderr, "GitCaddy Upload Helper - Reliable file uploads with retry\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: gitcaddy-upload -url URL -token TOKEN -file FILE\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Options:\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
helper := artifact.NewUploadHelper()
|
||||
helper.MaxRetries = *retries
|
||||
|
||||
if err := helper.UploadWithRetry(*url, *token, *file); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Upload failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Upload succeeded!")
|
||||
}
|
||||
@@ -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 |
|
||||
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [`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-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. |
|
||||
| [`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. |
|
||||
| [`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. |
|
||||
| [`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) | Examples demonstrating how to use docker-compose to run GitCaddy Runner alongside a GitCaddy server instance. |
|
||||
| [`kubernetes`](kubernetes) | Examples and guidelines for deploying GitCaddy Runner within Kubernetes clusters, leveraging scalability and flexibility. |
|
||||
| [`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.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
### Running `act_runner` using `docker-compose`
|
||||
### Running `gitcaddy-runner` using `docker-compose`
|
||||
|
||||
```yml
|
||||
...
|
||||
gitea:
|
||||
image: gitea/gitea
|
||||
gitcaddy:
|
||||
image: git.marketally.com/gitcaddy/server:latest
|
||||
...
|
||||
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>"]
|
||||
interval: 10s
|
||||
retries: 3
|
||||
@@ -14,20 +14,19 @@
|
||||
timeout: 10s
|
||||
environment:
|
||||
# GITEA_RUNNER_REGISTRATION_TOKEN can be used to set a global runner registration token.
|
||||
# The Gitea version must be v1.23 or higher.
|
||||
# It's also possible to use GITEA_RUNNER_REGISTRATION_TOKEN_FILE to pass the location.
|
||||
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
|
||||
|
||||
runner:
|
||||
image: gitea/act_runner
|
||||
image: git.marketally.com/gitcaddy/gitcaddy-runner:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
gitea:
|
||||
# required so runner can attach to gitea, see "healthcheck"
|
||||
condition: service_healthy
|
||||
gitcaddy:
|
||||
# required so runner can attach to GitCaddy, see "healthcheck"
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
volumes:
|
||||
- ./data/act_runner:/data
|
||||
- ./data/gitcaddy-runner:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=<instance url>
|
||||
@@ -38,18 +37,18 @@
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
||||
```
|
||||
|
||||
### Running `act_runner` using Docker-in-Docker (DIND)
|
||||
### Running `gitcaddy-runner` using Docker-in-Docker (DIND)
|
||||
|
||||
```yml
|
||||
...
|
||||
runner:
|
||||
image: gitea/act_runner:latest-dind-rootless
|
||||
image: git.marketally.com/gitcaddy/gitcaddy-runner:latest-dind-rootless
|
||||
restart: always
|
||||
privileged: true
|
||||
depends_on:
|
||||
- gitea
|
||||
- gitcaddy
|
||||
volumes:
|
||||
- ./data/act_runner:/data
|
||||
- ./data/gitcaddy-runner:/data
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=<instance url>
|
||||
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### Run `act_runner` in a Docker Container
|
||||
### Run `gitcaddy-runner` in a Docker Container
|
||||
|
||||
```sh
|
||||
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/act_runner:nightly
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: act-runner-vol
|
||||
name: gitcaddy-runner-vol
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
@@ -13,7 +13,7 @@ spec:
|
||||
apiVersion: v1
|
||||
data:
|
||||
# The registration token can be obtained from the web UI, API or command-line.
|
||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||
# 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.
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
@@ -25,19 +25,19 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: act-runner
|
||||
name: act-runner
|
||||
app: gitcaddy-runner
|
||||
name: gitcaddy-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: act-runner
|
||||
app: gitcaddy-runner
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: act-runner
|
||||
app: gitcaddy-runner
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
@@ -45,10 +45,10 @@ spec:
|
||||
emptyDir: {}
|
||||
- name: runner-data
|
||||
persistentVolumeClaim:
|
||||
claimName: act-runner-vol
|
||||
claimName: gitcaddy-runner-vol
|
||||
containers:
|
||||
- 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"]
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
@@ -58,7 +58,7 @@ spec:
|
||||
- name: DOCKER_TLS_VERIFY
|
||||
value: "1"
|
||||
- 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
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: act-runner-vol
|
||||
name: gitcaddy-runner-vol
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
@@ -13,7 +13,7 @@ spec:
|
||||
apiVersion: v1
|
||||
data:
|
||||
# The registration token can be obtained from the web UI, API or command-line.
|
||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||
# 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.
|
||||
token: << base64 encoded registration token >>
|
||||
kind: Secret
|
||||
@@ -25,30 +25,30 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: act-runner
|
||||
name: act-runner
|
||||
app: gitcaddy-runner
|
||||
name: gitcaddy-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: act-runner
|
||||
app: gitcaddy-runner
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: act-runner
|
||||
app: gitcaddy-runner
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: runner-data
|
||||
persistentVolumeClaim:
|
||||
claimName: act-runner-vol
|
||||
claimName: gitcaddy-runner-vol
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: runner
|
||||
image: gitea/act_runner:nightly-dind-rootless
|
||||
image: git.marketally.com/gitcaddy/gitcaddy-runner:latest-dind-rootless
|
||||
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"]
|
||||
env:
|
||||
@@ -59,7 +59,7 @@ spec:
|
||||
- name: DOCKER_TLS_VERIFY
|
||||
value: "1"
|
||||
- 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
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -70,4 +70,3 @@ spec:
|
||||
volumeMounts:
|
||||
- name: runner-data
|
||||
mountPath: /data
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## `act_runner` on Virtual or Physical Servers
|
||||
## `gitcaddy-runner` on Virtual or Physical Servers
|
||||
|
||||
Files in this directory:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
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.
|
||||
- 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
|
||||
|
||||
```bash
|
||||
cd /home/rootless/act_runner
|
||||
act_runner register
|
||||
cd /home/rootless/gitcaddy-runner
|
||||
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
|
||||
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
|
||||
Description=Gitea Actions runner
|
||||
Documentation=https://gitea.com/gitea/act_runner
|
||||
Description=GitCaddy Actions runner
|
||||
Documentation=https://git.marketally.com/gitcaddy/gitcaddy-runner
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
|
||||
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config
|
||||
ExecStart=/usr/bin/gitcaddy-runner daemon -c /home/rootless/gitcaddy-runner/config
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
WorkingDirectory=/home/rootless/act_runner
|
||||
WorkingDirectory=/home/rootless/gitcaddy-runner
|
||||
TimeoutSec=0
|
||||
RestartSec=2
|
||||
Restart=always
|
||||
@@ -88,8 +88,9 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
||||
|
||||
- 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
|
||||
systemctl --user status act_runner
|
||||
journalctl --user -xeu act_runner
|
||||
```bash
|
||||
systemctl --user status gitcaddy-runner
|
||||
journalctl --user -xeu gitcaddy-runner
|
||||
```
|
||||
|
||||
8
go.mod
8
go.mod
@@ -1,8 +1,6 @@
|
||||
module gitea.com/gitea/act_runner
|
||||
module git.marketally.com/gitcaddy/gitcaddy-runner
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.11
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.5.2
|
||||
@@ -111,4 +109,4 @@ replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.16.2
|
||||
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0
|
||||
|
||||
// Use GitCaddy fork with capability support
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.7
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
|
||||
|
||||
4
go.sum
4
go.sum
@@ -6,8 +6,8 @@ cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
|
||||
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
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/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
||||
gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742 h1:ulcquQluJbmNASkh6ina70LvcHEa9eWYfQ+DeAZ0VEE=
|
||||
gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
|
||||
"github.com/nektos/act/pkg/artifactcache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -22,8 +22,8 @@ type cacheServerArgs struct {
|
||||
Port uint16
|
||||
}
|
||||
|
||||
func runCacheServer(ctx context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
func runCacheServer(_ context.Context, configFile *string, cacheArgs *cacheServerArgs) func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cmd provides the CLI commands for gitcaddy-runner.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -10,14 +11,16 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Execute runs the root command for gitcaddy-runner CLI.
|
||||
func Execute(ctx context.Context) {
|
||||
// ./act_runner
|
||||
// ./gitcaddy-runner
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Use: "gitcaddy-runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Version: ver.Version(),
|
||||
@@ -26,7 +29,7 @@ func Execute(ctx context.Context) {
|
||||
configFile := ""
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
|
||||
|
||||
// ./act_runner register
|
||||
// ./gitcaddy-runner register
|
||||
var regArgs registerArgs
|
||||
registerCmd := &cobra.Command{
|
||||
Use: "register",
|
||||
@@ -35,14 +38,14 @@ func Execute(ctx context.Context) {
|
||||
RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs
|
||||
}
|
||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "GitCaddy instance address")
|
||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token")
|
||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||
registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
// ./act_runner daemon
|
||||
// ./gitcaddy-runner daemon
|
||||
var daemArgs daemonArgs
|
||||
daemonCmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
@@ -53,10 +56,10 @@ func Execute(ctx context.Context) {
|
||||
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit")
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
|
||||
// ./act_runner exec
|
||||
// ./gitcaddy-runner exec
|
||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||
|
||||
// ./act_runner config
|
||||
// ./gitcaddy-runner config
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "Generate an example config file",
|
||||
@@ -66,7 +69,7 @@ func Execute(ctx context.Context) {
|
||||
},
|
||||
})
|
||||
|
||||
// ./act_runner cache-server
|
||||
// ./gitcaddy-runner cache-server
|
||||
var cacheArgs cacheServerArgs
|
||||
cacheCmd := &cobra.Command{
|
||||
Use: "cache-server",
|
||||
@@ -79,6 +82,31 @@ func Execute(ctx context.Context) {
|
||||
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server")
|
||||
rootCmd.AddCommand(cacheCmd)
|
||||
|
||||
// ./gitcaddy-runner cleanup
|
||||
cleanupCmd := &cobra.Command{
|
||||
Use: "cleanup",
|
||||
Short: "Manually trigger cleanup to free disk space",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
result, err := cleanup.RunCleanup(ctx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cleanup failed: %w", err)
|
||||
}
|
||||
fmt.Printf("Cleanup completed: freed %d bytes, deleted %d files in %s\n", result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
if len(result.Errors) > 0 {
|
||||
fmt.Printf("Warnings: %d errors occurred\n", len(result.Errors))
|
||||
for _, e := range result.Errors {
|
||||
fmt.Printf(" - %s\n", e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(cleanupCmd)
|
||||
// hide completion command
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
@@ -21,13 +22,14 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/poll"
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/poll"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,6 +37,10 @@ const (
|
||||
DiskSpaceWarningThreshold = 85.0
|
||||
// DiskSpaceCriticalThreshold is the percentage at which to log critical warnings
|
||||
DiskSpaceCriticalThreshold = 95.0
|
||||
// DiskSpaceAutoCleanupThreshold is the percentage at which to trigger automatic cleanup
|
||||
DiskSpaceAutoCleanupThreshold = 85.0
|
||||
// CleanupCooldown is the minimum time between automatic cleanups
|
||||
CleanupCooldown = 10 * time.Minute
|
||||
// CapabilitiesUpdateInterval is how often to update capabilities (including disk space)
|
||||
CapabilitiesUpdateInterval = 5 * time.Minute
|
||||
// BandwidthTestInterval is how often to run bandwidth tests (hourly)
|
||||
@@ -44,13 +50,23 @@ const (
|
||||
// Global bandwidth manager - accessible for triggering manual tests
|
||||
var bandwidthManager *envcheck.BandwidthManager
|
||||
|
||||
// Global cleanup state
|
||||
var (
|
||||
lastCleanupTime time.Time
|
||||
cleanupMutex sync.Mutex
|
||||
globalConfig *config.Config
|
||||
)
|
||||
|
||||
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadDefault(*configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
// Store config globally for auto-cleanup
|
||||
globalConfig = cfg
|
||||
|
||||
initLogging(cfg)
|
||||
log.Infoln("Starting runner daemon")
|
||||
|
||||
@@ -116,7 +132,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
return err
|
||||
}
|
||||
// if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath
|
||||
os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
_ = os.Setenv("DOCKER_HOST", dockerSocketPath)
|
||||
// empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
|
||||
// and assign the path to cfg.Container.DockerHost
|
||||
if cfg.Container.DockerHost == "" {
|
||||
@@ -163,26 +179,29 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
bandwidthManager.Start(ctx)
|
||||
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
|
||||
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||
capabilitiesJson := capabilities.ToJSON()
|
||||
log.Infof("detected capabilities: %s", capabilitiesJson)
|
||||
capabilitiesJSON := capabilities.ToJSON()
|
||||
log.Infof("detected capabilities: %s", capabilitiesJSON)
|
||||
|
||||
// Check disk space and warn if low
|
||||
checkDiskSpaceWarnings(capabilities)
|
||||
checkDiskSpaceAndCleanup(ctx, capabilities)
|
||||
|
||||
// declare the labels of the runner before fetching tasks
|
||||
resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJson)
|
||||
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||
log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJSON)
|
||||
switch {
|
||||
case err != nil && connect.CodeOf(err) == connect.CodeUnimplemented:
|
||||
log.Errorf("Your GitCaddy version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
return err
|
||||
} else if err != nil {
|
||||
case err != nil:
|
||||
log.WithError(err).Error("fail to invoke Declare")
|
||||
return err
|
||||
} else {
|
||||
default:
|
||||
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||
// Merge any admin-added labels from the server
|
||||
runner.MergeServerLabels(resp.Msg.Runner.Labels)
|
||||
}
|
||||
|
||||
// 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
|
||||
func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
|
||||
// checkDiskSpaceAndCleanup logs warnings if disk space is low and triggers cleanup if needed
|
||||
func checkDiskSpaceAndCleanup(ctx context.Context, capabilities *envcheck.RunnerCapabilities) {
|
||||
if capabilities.Disk == nil {
|
||||
return
|
||||
}
|
||||
@@ -245,13 +264,54 @@ func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
|
||||
usedPercent := capabilities.Disk.UsedPercent
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// triggerAutoCleanup runs cleanup if cooldown has passed
|
||||
func triggerAutoCleanup(ctx context.Context) {
|
||||
cleanupMutex.Lock()
|
||||
defer cleanupMutex.Unlock()
|
||||
|
||||
// Check cooldown (except for first run)
|
||||
if !lastCleanupTime.IsZero() && time.Since(lastCleanupTime) < CleanupCooldown {
|
||||
log.Debugf("Skipping auto-cleanup, cooldown not expired (last cleanup: %s ago)", time.Since(lastCleanupTime))
|
||||
return
|
||||
}
|
||||
|
||||
if globalConfig == nil {
|
||||
log.Warn("Cannot run auto-cleanup: config not available")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Starting automatic disk cleanup...")
|
||||
lastCleanupTime = time.Now()
|
||||
|
||||
go func() {
|
||||
result, err := cleanup.RunCleanup(ctx, globalConfig)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Auto-cleanup failed")
|
||||
return
|
||||
}
|
||||
log.Infof("Auto-cleanup completed: freed %d bytes, deleted %d files in %s",
|
||||
result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
if len(result.Errors) > 0 {
|
||||
for _, e := range result.Errors {
|
||||
log.WithError(e).Warn("Cleanup error")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth
|
||||
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) {
|
||||
ticker := time.NewTicker(CapabilitiesUpdateInterval)
|
||||
@@ -267,20 +327,20 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 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
|
||||
if bandwidthManager != nil {
|
||||
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||
}
|
||||
|
||||
capabilitiesJson := capabilities.ToJSON()
|
||||
capabilitiesJSON := capabilities.ToJSON()
|
||||
|
||||
// Check for disk space warnings
|
||||
checkDiskSpaceWarnings(capabilities)
|
||||
checkDiskSpaceAndCleanup(ctx, capabilities)
|
||||
|
||||
// Send updated capabilities to server
|
||||
_, err := runner.Declare(ctx, labelNames, capabilitiesJson)
|
||||
_, err := runner.Declare(ctx, labelNames, capabilitiesJSON)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failed to update capabilities")
|
||||
} else {
|
||||
|
||||
@@ -264,7 +264,7 @@ func printList(plan *model.Plan) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
func runExecList(_ context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
// plan with filtered jobs - to be used for filtering only
|
||||
var filterPlan *model.Plan
|
||||
|
||||
@@ -286,19 +286,20 @@ func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *e
|
||||
}
|
||||
|
||||
var err error
|
||||
if execArgs.job != "" {
|
||||
switch {
|
||||
case execArgs.job != "":
|
||||
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if filterEventName != "" {
|
||||
case filterEventName != "":
|
||||
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
log.Infof("Preparing plan with all jobs")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
@@ -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 {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -331,18 +332,19 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
// collect all events from loaded workflows
|
||||
events := planner.GetEvents()
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
switch {
|
||||
case len(execArgs.event) > 0:
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
eventName = execArgs.event
|
||||
} else if len(events) == 1 && len(events[0]) > 0 {
|
||||
case len(events) == 1 && len(events[0]) > 0:
|
||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
|
||||
case execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0:
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
} else {
|
||||
default:
|
||||
log.Infof("Using default workflow event: push")
|
||||
eventName = "push"
|
||||
}
|
||||
@@ -388,7 +390,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
execArgs.artifactServerPath = tempDir
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
|
||||
artifactCancel()
|
||||
return nil
|
||||
})
|
||||
@@ -505,7 +507,7 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "docker.gitea.com/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "GitCaddy instance to use.")
|
||||
|
||||
return execCmd
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
return func(_ *cobra.Command, _ []string) error {
|
||||
log.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
@@ -80,6 +80,7 @@ type registerArgs struct {
|
||||
|
||||
type registerStage int8
|
||||
|
||||
// Register stage constants define the steps in the registration workflow.
|
||||
const (
|
||||
StageUnknown registerStage = -1
|
||||
StageOverwriteLocalConfig registerStage = iota + 1
|
||||
@@ -250,7 +251,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("Failed to register runner: %w", err)
|
||||
return fmt.Errorf("failed to register runner: %w", err)
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
@@ -272,7 +273,7 @@ func printStageHelp(stage registerStage) {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
log.Infoln("Enter the GitCaddy instance URL (for example, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
case StageInputRunnerName:
|
||||
@@ -311,7 +312,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
return err
|
||||
}
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("Failed to register runner: %w", err)
|
||||
return fmt.Errorf("failed to register runner: %w", err)
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
@@ -341,7 +342,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
Errorln("Cannot ping the Gitea instance server")
|
||||
Errorln("Cannot ping the GitCaddy instance server")
|
||||
// TODO: if ping failed, retry or exit
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package poll provides task polling functionality for CI runners.
|
||||
package poll
|
||||
|
||||
import (
|
||||
@@ -15,12 +16,14 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/run"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/run"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/cleanup"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/envcheck"
|
||||
)
|
||||
|
||||
// Poller handles task polling from the Gitea server.
|
||||
type Poller struct {
|
||||
client client.Client
|
||||
runner *run.Runner
|
||||
@@ -37,9 +40,10 @@ type Poller struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new Poller instance.
|
||||
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())
|
||||
|
||||
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -64,6 +68,7 @@ func (p *Poller) SetBandwidthManager(bm *envcheck.BandwidthManager) {
|
||||
p.bandwidthManager = bm
|
||||
}
|
||||
|
||||
// Poll starts polling for tasks with the configured capacity.
|
||||
func (p *Poller) Poll() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
@@ -77,6 +82,7 @@ func (p *Poller) Poll() {
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// PollOnce polls for a single task and then exits.
|
||||
func (p *Poller) PollOnce() {
|
||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||
|
||||
@@ -86,18 +92,19 @@ func (p *Poller) PollOnce() {
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the poller.
|
||||
func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownPolling()
|
||||
|
||||
select {
|
||||
// graceful shutdown completed succesfully
|
||||
// graceful shutdown completed successfully
|
||||
case <-p.done:
|
||||
return nil
|
||||
|
||||
// our timeout for shutting down ran out
|
||||
case <-ctx.Done():
|
||||
// when both the timeout fires and the graceful shutdown
|
||||
// completed succsfully, this branch of the select may
|
||||
// completed successfully, this branch of the select may
|
||||
// fire. Do a non-blocking check here against the graceful
|
||||
// shutdown status to avoid sending an error if we don't need to.
|
||||
_, ok := <-p.done
|
||||
@@ -109,7 +116,7 @@ func (p *Poller) Shutdown(ctx context.Context) error {
|
||||
p.shutdownJobs()
|
||||
|
||||
// wait for running jobs to report their status to Gitea
|
||||
_, _ = <-p.done
|
||||
<-p.done
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -165,20 +172,20 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
defer cancel()
|
||||
|
||||
// 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
|
||||
if p.bandwidthManager != nil {
|
||||
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.
|
||||
v := p.tasksVersion.Load()
|
||||
fetchReq := &runnerv1.FetchTaskRequest{
|
||||
TasksVersion: v,
|
||||
CapabilitiesJson: capsJson,
|
||||
CapabilitiesJson: capsJSON,
|
||||
}
|
||||
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(fetchReq))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
@@ -205,6 +212,20 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
||||
}()
|
||||
}
|
||||
|
||||
// Check if server requested a cleanup
|
||||
if resp.Msg.RequestCleanup {
|
||||
log.Info("Server requested cleanup, running now...")
|
||||
go func() {
|
||||
result, err := cleanup.RunCleanup(ctx, p.cfg)
|
||||
if err != nil {
|
||||
log.Errorf("Cleanup failed: %v", err)
|
||||
} else if result != nil {
|
||||
log.Infof("Cleanup completed: freed %d bytes, deleted %d files in %s",
|
||||
result.BytesFreed, result.FilesDeleted, result.Duration)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if resp.Msg.TasksVersion > v {
|
||||
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package run provides the core runner functionality for executing tasks.
|
||||
package run
|
||||
|
||||
import (
|
||||
|
||||
@@ -22,11 +22,11 @@ import (
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/report"
|
||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/labels"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/report"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver"
|
||||
)
|
||||
|
||||
// Runner runs the pipeline.
|
||||
@@ -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 {
|
||||
ls := labels.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 {
|
||||
if _, ok := r.runningTasks.Load(task.Id); ok {
|
||||
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
|
||||
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
|
||||
// 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()
|
||||
if giteaDefaultActionsURL == "https://github.com" && 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
|
||||
}
|
||||
|
||||
if actionsIdTokenRequestUrl := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIdTokenRequestUrl != "" {
|
||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = 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_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
Version: ver.Version(),
|
||||
Labels: labels,
|
||||
CapabilitiesJson: capabilitiesJson,
|
||||
Labels: declareLabels,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
146
internal/pkg/artifact/upload_helper.go
Normal file
146
internal/pkg/artifact/upload_helper.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package artifact provides utilities for handling artifact uploads.
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// UploadHelper handles reliable file uploads with retry logic
|
||||
type UploadHelper struct {
|
||||
MaxRetries int
|
||||
RetryDelay time.Duration
|
||||
ChunkSize int64
|
||||
ConnectTimeout time.Duration
|
||||
MaxTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewUploadHelper creates a new upload helper with sensible defaults
|
||||
func NewUploadHelper() *UploadHelper {
|
||||
return &UploadHelper{
|
||||
MaxRetries: 5,
|
||||
RetryDelay: 10 * time.Second,
|
||||
ChunkSize: 10 * 1024 * 1024, // 10MB
|
||||
ConnectTimeout: 120 * time.Second,
|
||||
MaxTimeout: 3600 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadWithRetry uploads a file with automatic retry on failure
|
||||
func (u *UploadHelper) UploadWithRetry(url, token, filepath string) error {
|
||||
client := &http.Client{
|
||||
Timeout: u.MaxTimeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
MaxIdleConnsPerHost: 5,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableKeepAlives: false, // Keep connections alive
|
||||
ForceAttemptHTTP2: false, // Use HTTP/1.1 for large uploads
|
||||
},
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < u.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
delay := u.RetryDelay * time.Duration(attempt)
|
||||
log.Infof("Upload attempt %d/%d, waiting %v before retry...", attempt+1, u.MaxRetries, delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
// Pre-resolve DNS / warm connection
|
||||
if err := u.prewarmConnection(url); err != nil {
|
||||
lastErr = fmt.Errorf("connection prewarm failed: %w", err)
|
||||
log.Warnf("Prewarm failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt upload
|
||||
if err := u.doUpload(client, url, token, filepath); err != nil {
|
||||
lastErr = err
|
||||
log.Warnf("Upload attempt %d failed: %v", attempt+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Upload succeeded on attempt %d", attempt+1)
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
return fmt.Errorf("upload failed after %d attempts: %w", u.MaxRetries, lastErr)
|
||||
}
|
||||
|
||||
// prewarmConnection establishes a connection to help with DNS and TCP setup
|
||||
func (u *UploadHelper) prewarmConnection(url string) error {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// doUpload performs the actual file upload
|
||||
func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string) error {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Uploading %s (%d bytes) to %s", filepath, stat.Size(), url)
|
||||
|
||||
// Create multipart form
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", stat.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create form file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return fmt.Errorf("failed to copy file to form: %w", err)
|
||||
}
|
||||
_ = writer.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upload request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
log.Infof("Upload completed successfully, status: %d", resp.StatusCode)
|
||||
return nil
|
||||
}
|
||||
390
internal/pkg/cleanup/cleanup.go
Normal file
390
internal/pkg/cleanup/cleanup.go
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cleanup provides disk cleanup utilities for CI runners.
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/config"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Result contains the results of a cleanup operation.
|
||||
type Result struct {
|
||||
BytesFreed int64
|
||||
FilesDeleted int
|
||||
Errors []error
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// RunCleanup performs cleanup operations to free disk space.
|
||||
func RunCleanup(_ context.Context, cfg *config.Config) (*Result, error) {
|
||||
start := time.Now()
|
||||
result := &Result{}
|
||||
|
||||
log.Info("Starting runner cleanup...")
|
||||
|
||||
// 1. Clean old cache directories
|
||||
cacheDir := filepath.Join(cfg.Cache.Dir, "_cache")
|
||||
if cacheDir != "" {
|
||||
if bytes, files, err := cleanOldDir(cacheDir, 24*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("cache cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned cache: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clean old work directories
|
||||
workDir := cfg.Container.WorkdirParent
|
||||
if workDir != "" {
|
||||
if bytes, files, err := cleanOldWorkDirs(workDir, 48*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("workdir cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned work dirs: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clean old artifact staging directories
|
||||
artifactDir := cfg.Cache.Dir
|
||||
if bytes, files, err := cleanOldArtifacts(artifactDir, 72*time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("artifact cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned artifacts: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
// 4. Clean system temp files (older than 24h)
|
||||
if bytes, files, err := cleanTempDir(24 * time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("temp cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned temp: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
// 5. Clean build tool caches (older than 7 days)
|
||||
// These can grow very large from Go, npm, nuget, gradle, maven builds
|
||||
if bytes, files, err := cleanBuildCaches(7 * 24 * time.Hour); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("build cache cleanup: %w", err))
|
||||
} else {
|
||||
result.BytesFreed += bytes
|
||||
result.FilesDeleted += files
|
||||
log.Infof("Cleaned build caches: freed %d bytes, deleted %d files", bytes, files)
|
||||
}
|
||||
|
||||
result.Duration = time.Since(start)
|
||||
log.Infof("Cleanup completed: freed %s in %s", formatBytes(result.BytesFreed), result.Duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// cleanOldDir removes files older than maxAge from a directory
|
||||
func cleanOldDir(dir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := info.Size()
|
||||
if err := os.Remove(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return bytesFreed, filesDeleted, err
|
||||
}
|
||||
|
||||
// cleanOldWorkDirs removes work directories older than maxAge
|
||||
func cleanOldWorkDirs(baseDir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
entries, err := os.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(baseDir, entry.Name())
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := dirSize(path)
|
||||
if err := os.RemoveAll(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
log.Debugf("Removed old work dir: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// cleanOldArtifacts removes artifact staging files older than maxAge
|
||||
func cleanOldArtifacts(baseDir string, maxAge time.Duration) (int64, int, error) {
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
// Look for artifact staging dirs
|
||||
patterns := []string{"artifact-*", "upload-*", "download-*"}
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(baseDir, pattern))
|
||||
for _, path := range matches {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
var size int64
|
||||
if info.IsDir() {
|
||||
size = dirSize(path)
|
||||
err = os.RemoveAll(path)
|
||||
} else {
|
||||
size = info.Size()
|
||||
err = os.Remove(path)
|
||||
}
|
||||
if err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// cleanTempDir removes old files from system temp directory
|
||||
func cleanTempDir(maxAge time.Duration) (int64, int, error) {
|
||||
tmpDir := os.TempDir()
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Only clean files/dirs that look like runner/act artifacts or build tool temp files
|
||||
runnerPatterns := []string{
|
||||
"act-", "runner-", "gitea-", "workflow-",
|
||||
"go-build", "go-link",
|
||||
"node-compile-cache", "npm-", "yarn-", "yarn--", "pnpm-",
|
||||
"ts-node-", "tsx-", "jiti", "v8-compile-cache",
|
||||
"text-diff-expansion-test", "DiagOutputDir",
|
||||
"dugite-native-", "reorderCommitMessage-", "squashCommitMessage-",
|
||||
}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
isRunner := false
|
||||
for _, p := range runnerPatterns {
|
||||
if len(name) >= len(p) && name[:len(p)] == p {
|
||||
isRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isRunner {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(tmpDir, name)
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
var size int64
|
||||
if info.IsDir() {
|
||||
size = dirSize(path)
|
||||
err = os.RemoveAll(path)
|
||||
} else {
|
||||
size = info.Size()
|
||||
err = os.Remove(path)
|
||||
}
|
||||
if err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bytesFreed, filesDeleted, nil
|
||||
}
|
||||
|
||||
// dirSize calculates the total size of a directory.
|
||||
func dirSize(path string) int64 {
|
||||
var size int64
|
||||
_ = filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size
|
||||
}
|
||||
|
||||
// cleanBuildCaches removes old build tool caches that accumulate from CI jobs
|
||||
// These are cleaned more aggressively (files older than 7 days) since they can grow very large
|
||||
func cleanBuildCaches(maxAge time.Duration) (int64, int, error) {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE") // Windows
|
||||
}
|
||||
if home == "" {
|
||||
home = "/root" // fallback for runners typically running as root
|
||||
}
|
||||
|
||||
var totalBytesFreed int64
|
||||
var totalFilesDeleted int
|
||||
|
||||
// Build cache directories to clean
|
||||
// Format: {path, description, maxAge (0 = use default)}
|
||||
// Go build cache cleaned more aggressively (3 days) as it grows very fast
|
||||
goBuildMaxAge := 3 * 24 * time.Hour
|
||||
cacheDirs := []struct {
|
||||
path string
|
||||
desc string
|
||||
maxAge time.Duration
|
||||
}{
|
||||
// Linux paths
|
||||
{filepath.Join(home, ".cache", "go-build"), "Go build cache", goBuildMaxAge},
|
||||
{filepath.Join(home, ".cache", "golangci-lint"), "golangci-lint cache", 0},
|
||||
{filepath.Join(home, ".npm", "_cacache"), "npm cache", 0},
|
||||
{filepath.Join(home, ".cache", "pnpm"), "pnpm cache", 0},
|
||||
{filepath.Join(home, ".cache", "yarn"), "yarn cache", 0},
|
||||
{filepath.Join(home, ".nuget", "packages"), "NuGet cache", 0},
|
||||
{filepath.Join(home, ".gradle", "caches"), "Gradle cache", 0},
|
||||
{filepath.Join(home, ".m2", "repository"), "Maven cache", 0},
|
||||
{filepath.Join(home, ".cache", "pip"), "pip cache", 0},
|
||||
{filepath.Join(home, ".cargo", "registry", "cache"), "Cargo cache", 0},
|
||||
{filepath.Join(home, ".rustup", "tmp"), "Rustup temp", 0},
|
||||
// macOS paths (Library/Caches)
|
||||
{filepath.Join(home, "Library", "Caches", "go-build"), "Go build cache (macOS)", goBuildMaxAge},
|
||||
{filepath.Join(home, "Library", "Caches", "Yarn"), "Yarn cache (macOS)", 0},
|
||||
{filepath.Join(home, "Library", "Caches", "pip"), "pip cache (macOS)", 0},
|
||||
{filepath.Join(home, "Library", "Caches", "Homebrew"), "Homebrew cache (macOS)", 0},
|
||||
// Windows paths (LOCALAPPDATA)
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "go-build"), "Go build cache (Windows)", goBuildMaxAge},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "npm-cache"), "npm cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "pnpm"), "pnpm cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "Yarn", "Cache"), "Yarn cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "NuGet", "v3-cache"), "NuGet cache (Windows)", 0},
|
||||
{filepath.Join(os.Getenv("LOCALAPPDATA"), "pip", "Cache"), "pip cache (Windows)", 0},
|
||||
// Windows custom paths used by some CI setups
|
||||
{"C:\\L\\Yarn", "Yarn global cache (Windows)", 0},
|
||||
{filepath.Join(os.TempDir(), "chocolatey"), "Chocolatey temp cache", 0},
|
||||
}
|
||||
|
||||
for _, cache := range cacheDirs {
|
||||
if _, err := os.Stat(cache.path); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use cache-specific maxAge if set, otherwise use default
|
||||
cacheMaxAge := cache.maxAge
|
||||
if cacheMaxAge == 0 {
|
||||
cacheMaxAge = maxAge
|
||||
}
|
||||
cutoff := time.Now().Add(-cacheMaxAge)
|
||||
|
||||
var bytesFreed int64
|
||||
var filesDeleted int
|
||||
|
||||
err := filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
size := info.Size()
|
||||
if err := os.Remove(path); err == nil {
|
||||
bytesFreed += size
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && (bytesFreed > 0 || filesDeleted > 0) {
|
||||
log.Infof("Cleaned %s: freed %s, deleted %d files", cache.desc, formatBytes(bytesFreed), filesDeleted)
|
||||
totalBytesFreed += bytesFreed
|
||||
totalFilesDeleted += filesDeleted
|
||||
}
|
||||
|
||||
// Also remove empty directories
|
||||
_ = filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || !info.IsDir() || path == cache.path {
|
||||
return nil
|
||||
}
|
||||
entries, _ := os.ReadDir(path)
|
||||
if len(entries) == 0 {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return totalBytesFreed, totalFilesDeleted, nil
|
||||
}
|
||||
|
||||
// formatBytes formats bytes into human readable string
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package client provides the HTTP client for communicating with the runner API.
|
||||
package client
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
package client
|
||||
|
||||
// HTTP header constants for runner authentication and identification.
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
|
||||
@@ -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 {
|
||||
return c.endpoint
|
||||
}
|
||||
|
||||
// Insecure returns whether TLS verification is disabled.
|
||||
func (c *HTTPClient) Insecure() bool {
|
||||
return c.insecure
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package config provides configuration loading and management for the runner.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -137,6 +138,9 @@ func LoadDefault(file string) (*Config, error) {
|
||||
if cfg.Runner.FetchInterval <= 0 {
|
||||
cfg.Runner.FetchInterval = 2 * time.Second
|
||||
}
|
||||
if cfg.Runner.ShutdownTimeout <= 0 {
|
||||
cfg.Runner.ShutdownTimeout = 3 * time.Minute
|
||||
}
|
||||
|
||||
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||
|
||||
@@ -5,5 +5,7 @@ package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// Example contains the example configuration file content.
|
||||
//
|
||||
//go:embed config.example.yaml
|
||||
var Example []byte
|
||||
|
||||
@@ -23,12 +23,13 @@ type Registration struct {
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
}
|
||||
|
||||
// LoadRegistration loads the runner registration from a JSON file.
|
||||
func LoadRegistration(file string) (*Registration, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
var reg Registration
|
||||
if err := json.NewDecoder(f).Decode(®); err != nil {
|
||||
@@ -40,12 +41,13 @@ func LoadRegistration(file string) (*Registration, error) {
|
||||
return ®, nil
|
||||
}
|
||||
|
||||
// SaveRegistration saves the runner registration to a JSON file.
|
||||
func SaveRegistration(file string, reg *Registration) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
reg.Warning = registrationWarning
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func (bm *BandwidthManager) GetLastResult() *BandwidthInfo {
|
||||
return bm.lastResult
|
||||
}
|
||||
|
||||
// TestBandwidth tests network bandwidth to the Gitea server
|
||||
// TestBandwidth tests network bandwidth to the GitCaddy server
|
||||
func TestBandwidth(ctx context.Context, serverURL string) *BandwidthInfo {
|
||||
if serverURL == "" {
|
||||
return nil
|
||||
@@ -121,7 +121,7 @@ func testLatency(ctx context.Context, serverURL string) float64 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
|
||||
latency := time.Since(start).Seconds() * 1000 // Convert to ms
|
||||
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)
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
cancel()
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envcheck
|
||||
@@ -19,13 +19,22 @@ import (
|
||||
|
||||
// DiskInfo holds disk space information
|
||||
type DiskInfo struct {
|
||||
Path string `json:"path,omitempty"` // Path being checked (working directory)
|
||||
Path string `json:"path,omitempty"` // Path being checked (working directory)
|
||||
Total uint64 `json:"total_bytes"`
|
||||
Free uint64 `json:"free_bytes"`
|
||||
Used uint64 `json:"used_bytes"`
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
// CPUInfo holds CPU load information
|
||||
type CPUInfo struct {
|
||||
NumCPU int `json:"num_cpu"` // Number of logical CPUs
|
||||
LoadAvg1m float64 `json:"load_avg_1m"` // 1-minute load average
|
||||
LoadAvg5m float64 `json:"load_avg_5m"` // 5-minute load average
|
||||
LoadAvg15m float64 `json:"load_avg_15m"` // 15-minute load average
|
||||
LoadPercent float64 `json:"load_percent"` // (load_avg_1m / num_cpu) * 100
|
||||
}
|
||||
|
||||
// DistroInfo holds Linux distribution information
|
||||
type DistroInfo struct {
|
||||
ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora"
|
||||
@@ -37,7 +46,7 @@ type DistroInfo struct {
|
||||
type XcodeInfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
SDKs []string `json:"sdks,omitempty"` // e.g., ["iOS 17.0", "macOS 14.0"]
|
||||
SDKs []string `json:"sdks,omitempty"` // e.g., ["iOS 17.0", "macOS 14.0"]
|
||||
Simulators []string `json:"simulators,omitempty"` // Available iOS simulators
|
||||
}
|
||||
|
||||
@@ -52,13 +61,15 @@ type RunnerCapabilities struct {
|
||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||
Shell []string `json:"shell,omitempty"`
|
||||
Tools map[string][]string `json:"tools,omitempty"`
|
||||
BuildTools []string `json:"build_tools,omitempty"` // Available build/installer tools
|
||||
BuildTools []string `json:"build_tools,omitempty"` // Available build/installer tools
|
||||
PackageManagers []string `json:"package_managers,omitempty"`
|
||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Disk *DiskInfo `json:"disk,omitempty"`
|
||||
CPU *CPUInfo `json:"cpu,omitempty"`
|
||||
Bandwidth *BandwidthInfo `json:"bandwidth,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
|
||||
@@ -71,8 +82,9 @@ type CapabilityFeatures struct {
|
||||
|
||||
// DetectCapabilities detects the runner's capabilities
|
||||
// workingDir is the directory where builds will run (for disk space detection)
|
||||
func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string) *RunnerCapabilities {
|
||||
cap := &RunnerCapabilities{
|
||||
func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string, capacity int) *RunnerCapabilities {
|
||||
caps := &RunnerCapabilities{
|
||||
Capacity: capacity,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Tools: make(map[string][]string),
|
||||
@@ -93,37 +105,40 @@ func DetectCapabilities(ctx context.Context, dockerHost string, workingDir strin
|
||||
|
||||
// Detect Linux distribution
|
||||
if runtime.GOOS == "linux" {
|
||||
cap.Distro = detectLinuxDistro()
|
||||
caps.Distro = detectLinuxDistro()
|
||||
}
|
||||
|
||||
// Detect macOS Xcode/iOS
|
||||
if runtime.GOOS == "darwin" {
|
||||
cap.Xcode = detectXcode(ctx)
|
||||
caps.Xcode = detectXcode(ctx)
|
||||
}
|
||||
|
||||
// Detect Docker
|
||||
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
|
||||
if cap.Docker {
|
||||
cap.DockerCompose = detectDockerCompose(ctx)
|
||||
cap.Features.Services = true
|
||||
caps.Docker, caps.ContainerRuntime = detectDocker(ctx, dockerHost)
|
||||
if caps.Docker {
|
||||
caps.DockerCompose = detectDockerCompose(ctx)
|
||||
caps.Features.Services = true
|
||||
}
|
||||
|
||||
// Detect common tools
|
||||
detectTools(ctx, cap)
|
||||
detectTools(ctx, caps)
|
||||
|
||||
// Detect build tools
|
||||
detectBuildTools(ctx, cap)
|
||||
detectBuildTools(ctx, caps)
|
||||
|
||||
// Detect package managers
|
||||
detectPackageManagers(ctx, cap)
|
||||
detectPackageManagers(ctx, caps)
|
||||
|
||||
// Detect disk space on the working directory's filesystem
|
||||
cap.Disk = detectDiskSpace(workingDir)
|
||||
caps.Disk = detectDiskSpace(workingDir)
|
||||
|
||||
// Detect CPU load
|
||||
caps.CPU = detectCPULoad()
|
||||
|
||||
// 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
|
||||
@@ -212,18 +227,19 @@ func detectLinuxDistro() *DistroInfo {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
distro := &DistroInfo{}
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
switch {
|
||||
case strings.HasPrefix(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="), "\"")
|
||||
} else if strings.HasPrefix(line, "PRETTY_NAME=") {
|
||||
case strings.HasPrefix(line, "PRETTY_NAME="):
|
||||
distro.PrettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
|
||||
}
|
||||
}
|
||||
@@ -236,7 +252,7 @@ func detectLinuxDistro() *DistroInfo {
|
||||
}
|
||||
|
||||
// generateSuggestedLabels creates industry-standard labels based on capabilities
|
||||
func generateSuggestedLabels(cap *RunnerCapabilities) []string {
|
||||
func generateSuggestedLabels(caps *RunnerCapabilities) []string {
|
||||
labels := []string{}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
@@ -248,7 +264,7 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
|
||||
}
|
||||
|
||||
// OS labels
|
||||
switch cap.OS {
|
||||
switch caps.OS {
|
||||
case "linux":
|
||||
addLabel("linux")
|
||||
addLabel("linux-latest")
|
||||
@@ -261,17 +277,17 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
|
||||
}
|
||||
|
||||
// Distro labels (Linux only)
|
||||
if cap.Distro != nil && cap.Distro.ID != "" {
|
||||
distro := strings.ToLower(cap.Distro.ID)
|
||||
if caps.Distro != nil && caps.Distro.ID != "" {
|
||||
distro := strings.ToLower(caps.Distro.ID)
|
||||
addLabel(distro)
|
||||
addLabel(distro + "-latest")
|
||||
}
|
||||
|
||||
// Xcode/iOS labels (macOS only)
|
||||
if cap.Xcode != nil {
|
||||
if caps.Xcode != nil {
|
||||
addLabel("xcode")
|
||||
// Check for SDKs
|
||||
for _, sdk := range cap.Xcode.SDKs {
|
||||
for _, sdk := range caps.Xcode.SDKs {
|
||||
sdkLower := strings.ToLower(sdk)
|
||||
if strings.Contains(sdkLower, "ios") {
|
||||
addLabel("ios")
|
||||
@@ -287,24 +303,24 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
|
||||
}
|
||||
}
|
||||
// If simulators available, add simulator label
|
||||
if len(cap.Xcode.Simulators) > 0 {
|
||||
if len(caps.Xcode.Simulators) > 0 {
|
||||
addLabel("ios-simulator")
|
||||
}
|
||||
}
|
||||
|
||||
// Tool-based labels
|
||||
if _, ok := cap.Tools["dotnet"]; ok {
|
||||
if _, ok := caps.Tools["dotnet"]; ok {
|
||||
addLabel("dotnet")
|
||||
}
|
||||
if _, ok := cap.Tools["java"]; ok {
|
||||
if _, ok := caps.Tools["java"]; ok {
|
||||
addLabel("java")
|
||||
}
|
||||
if _, ok := cap.Tools["node"]; ok {
|
||||
if _, ok := caps.Tools["node"]; ok {
|
||||
addLabel("node")
|
||||
}
|
||||
|
||||
// Build tool labels
|
||||
for _, tool := range cap.BuildTools {
|
||||
for _, tool := range caps.BuildTools {
|
||||
switch tool {
|
||||
case "msbuild":
|
||||
addLabel("msbuild")
|
||||
@@ -369,7 +385,7 @@ func detectDocker(ctx context.Context, dockerHost string) (bool, string) {
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
defer cli.Close()
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -405,7 +421,7 @@ func detectDockerCompose(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
func detectTools(ctx context.Context, caps *RunnerCapabilities) {
|
||||
toolDetectors := map[string]func(context.Context) []string{
|
||||
"node": detectNodeVersions,
|
||||
"go": detectGoVersions,
|
||||
@@ -424,7 +440,7 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
|
||||
for tool, detector := range toolDetectors {
|
||||
if versions := detector(ctx); len(versions) > 0 {
|
||||
cap.Tools[tool] = versions
|
||||
caps.Tools[tool] = versions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,23 +461,23 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
|
||||
for name, cmd := range simpleTools {
|
||||
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 {
|
||||
case "windows":
|
||||
detectWindowsBuildTools(ctx, cap)
|
||||
detectWindowsBuildTools(ctx, caps)
|
||||
case "darwin":
|
||||
detectMacOSBuildTools(ctx, cap)
|
||||
detectMacOSBuildTools(caps)
|
||||
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
|
||||
vswherePaths := []string{
|
||||
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`,
|
||||
@@ -471,7 +487,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
if _, err := os.Stat(vswhere); err == nil {
|
||||
cmd := exec.CommandContext(ctx, vswhere, "-latest", "-property", "displayName")
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -488,7 +504,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
}
|
||||
for _, msbuild := range msbuildPaths {
|
||||
if _, err := os.Stat(msbuild); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "msbuild")
|
||||
caps.BuildTools = append(caps.BuildTools, "msbuild")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -502,14 +518,14 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
}
|
||||
for _, iscc := range innoSetupPaths {
|
||||
if _, err := os.Stat(iscc); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||
caps.BuildTools = append(caps.BuildTools, "inno-setup")
|
||||
break
|
||||
}
|
||||
}
|
||||
// Also check PATH
|
||||
if _, err := exec.LookPath("iscc"); err == nil {
|
||||
if !contains(cap.BuildTools, "inno-setup") {
|
||||
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||
if !contains(caps.BuildTools, "inno-setup") {
|
||||
caps.BuildTools = append(caps.BuildTools, "inno-setup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,13 +536,13 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
}
|
||||
for _, nsis := range nsisPaths {
|
||||
if _, err := os.Stat(nsis); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||
caps.BuildTools = append(caps.BuildTools, "nsis")
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, err := exec.LookPath("makensis"); err == nil {
|
||||
if !contains(cap.BuildTools, "nsis") {
|
||||
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||
if !contains(caps.BuildTools, "nsis") {
|
||||
caps.BuildTools = append(caps.BuildTools, "nsis")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +553,7 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
}
|
||||
for _, wix := range wixPaths {
|
||||
if _, err := os.Stat(wix); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "wix")
|
||||
caps.BuildTools = append(caps.BuildTools, "wix")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -545,63 +561,63 @@ func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
// Check for signtool (Windows SDK)
|
||||
signtoolPaths, _ := filepath.Glob(`C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe`)
|
||||
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
|
||||
if _, err := exec.LookPath("xcpretty"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "xcpretty")
|
||||
caps.BuildTools = append(caps.BuildTools, "xcpretty")
|
||||
}
|
||||
|
||||
// Check for fastlane
|
||||
if _, err := exec.LookPath("fastlane"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "fastlane")
|
||||
caps.BuildTools = append(caps.BuildTools, "fastlane")
|
||||
}
|
||||
|
||||
// Check for CocoaPods
|
||||
if _, err := exec.LookPath("pod"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "cocoapods")
|
||||
caps.BuildTools = append(caps.BuildTools, "cocoapods")
|
||||
}
|
||||
|
||||
// Check for Carthage
|
||||
if _, err := exec.LookPath("carthage"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "carthage")
|
||||
caps.BuildTools = append(caps.BuildTools, "carthage")
|
||||
}
|
||||
|
||||
// Check for SwiftLint
|
||||
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
|
||||
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)
|
||||
if _, err := exec.LookPath("packagesbuild"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "packages")
|
||||
caps.BuildTools = append(caps.BuildTools, "packages")
|
||||
}
|
||||
|
||||
// Check for pkgbuild (built-in)
|
||||
if _, err := exec.LookPath("pkgbuild"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "pkgbuild")
|
||||
caps.BuildTools = append(caps.BuildTools, "pkgbuild")
|
||||
}
|
||||
|
||||
// Check for codesign (built-in)
|
||||
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)
|
||||
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
|
||||
tools := []string{
|
||||
"gcc", "g++", "clang", "clang++",
|
||||
@@ -613,54 +629,54 @@ func detectLinuxBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
|
||||
for _, tool := range tools {
|
||||
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 {
|
||||
case "windows":
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "scoop")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "scoop")
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "winget")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "winget")
|
||||
}
|
||||
case "darwin":
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "macports")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "macports")
|
||||
}
|
||||
case "linux":
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "yum")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "yum")
|
||||
}
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "pacman")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "pacman")
|
||||
}
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "apk")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "apk")
|
||||
}
|
||||
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 {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "flatpak")
|
||||
caps.PackageManagers = append(caps.PackageManagers, "flatpak")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,13 +814,8 @@ func detectPwshVersion(ctx context.Context, cmd string) string {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use -Command to get version
|
||||
var c *exec.Cmd
|
||||
if cmd == "pwsh" {
|
||||
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
|
||||
} else {
|
||||
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
|
||||
}
|
||||
// Use -Command to get version (same command works for both pwsh and powershell)
|
||||
c := exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
|
||||
|
||||
output, err := c.Output()
|
||||
if err != nil {
|
||||
@@ -887,3 +898,157 @@ func contains(slice []string, item string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// detectCPULoad detects the current CPU load
|
||||
func detectCPULoad() *CPUInfo {
|
||||
numCPU := runtime.NumCPU()
|
||||
info := &CPUInfo{
|
||||
NumCPU: numCPU,
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// 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")
|
||||
if err != nil {
|
||||
return info
|
||||
}
|
||||
parts := strings.Fields(string(data))
|
||||
if len(parts) >= 3 {
|
||||
if load, err := parseFloat(parts[0]); err == nil {
|
||||
info.LoadAvg1m = load
|
||||
}
|
||||
if load, err := parseFloat(parts[1]); err == nil {
|
||||
info.LoadAvg5m = load
|
||||
}
|
||||
if load, err := parseFloat(parts[2]); err == nil {
|
||||
info.LoadAvg15m = load
|
||||
}
|
||||
}
|
||||
case "darwin":
|
||||
// Use sysctl on macOS
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "sysctl", "-n", "vm.loadavg")
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
// Output format: "{ 1.23 4.56 7.89 }"
|
||||
line := strings.Trim(string(output), "{ }\n")
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
if load, err := parseFloat(parts[0]); err == nil {
|
||||
info.LoadAvg1m = load
|
||||
}
|
||||
if load, err := parseFloat(parts[1]); err == nil {
|
||||
info.LoadAvg5m = load
|
||||
}
|
||||
if load, err := parseFloat(parts[2]); err == nil {
|
||||
info.LoadAvg15m = load
|
||||
}
|
||||
}
|
||||
}
|
||||
case "windows":
|
||||
// Windows doesn't have load average, use PowerShell to get CPU usage
|
||||
// wmic is deprecated, use Get-CimInstance instead
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
||||
"(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average")
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
line := strings.TrimSpace(string(output))
|
||||
if load, err := parseFloat(line); err == nil {
|
||||
info.LoadPercent = load
|
||||
info.LoadAvg1m = load * float64(numCPU) / 100.0
|
||||
return info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate load percent (load_avg_1m / num_cpu * 100)
|
||||
if info.LoadAvg1m > 0 && numCPU > 0 {
|
||||
info.LoadPercent = (info.LoadAvg1m / float64(numCPU)) * 100.0
|
||||
}
|
||||
|
||||
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
|
||||
func parseFloat(s string) (float64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
var f float64
|
||||
err := json.Unmarshal([]byte(s), &f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build unix
|
||||
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build unix
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build windows
|
||||
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// CheckIfDockerRunning verifies that the Docker daemon is running and accessible.
|
||||
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
opts := []client.Opt{
|
||||
client.FromEnv,
|
||||
@@ -23,7 +24,7 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package labels provides utilities for parsing and managing runner labels.
|
||||
package labels
|
||||
|
||||
import (
|
||||
@@ -8,17 +9,20 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Label scheme constants define the execution environments.
|
||||
const (
|
||||
SchemeHost = "host"
|
||||
SchemeDocker = "docker"
|
||||
)
|
||||
|
||||
// Label represents a parsed runner label with name, schema, and optional argument.
|
||||
type Label struct {
|
||||
Name string
|
||||
Schema string
|
||||
Arg string
|
||||
}
|
||||
|
||||
// Parse parses a label string in the format "name:schema:arg" and returns a Label.
|
||||
func Parse(str string) (*Label, error) {
|
||||
splits := strings.SplitN(str, ":", 3)
|
||||
label := &Label{
|
||||
@@ -38,8 +42,10 @@ func Parse(str string) (*Label, error) {
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// Labels is a slice of Label pointers.
|
||||
type Labels []*Label
|
||||
|
||||
// RequireDocker returns true if any label uses the docker schema.
|
||||
func (l Labels) RequireDocker() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeDocker {
|
||||
@@ -49,39 +55,56 @@ func (l Labels) RequireDocker() bool {
|
||||
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 {
|
||||
// Build maps for both platform values and schemas
|
||||
platforms := make(map[string]string, len(l))
|
||||
schemas := make(map[string]string, len(l))
|
||||
for _, label := range l {
|
||||
switch label.Schema {
|
||||
case SchemeDocker:
|
||||
// "//" will be ignored
|
||||
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||
schemas[label.Name] = SchemeDocker
|
||||
case SchemeHost:
|
||||
platforms[label.Name] = "-self-hosted"
|
||||
schemas[label.Name] = SchemeHost
|
||||
default:
|
||||
// It should not happen, because Parse has checked it.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range runsOn {
|
||||
if v, ok := platforms[v]; ok {
|
||||
return v
|
||||
name := 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
|
||||
// like:
|
||||
// ["ubuntu-22.04"] => "ubuntu:22.04"
|
||||
// ["with-gpu"] => "linux:with-gpu"
|
||||
// ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu"
|
||||
|
||||
// return default.
|
||||
// So the runner receives a task with a label that the runner doesn't have,
|
||||
// it happens when the user have edited the label of the runner in the web UI.
|
||||
// TODO: it may be not correct, what if the runner is used as host mode only?
|
||||
return "docker.gitea.com/runner-images:ubuntu-latest"
|
||||
// No matching label found
|
||||
return ""
|
||||
}
|
||||
|
||||
// Names returns the names of all labels.
|
||||
func (l Labels) Names() []string {
|
||||
names := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
@@ -90,6 +113,7 @@ func (l Labels) Names() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// ToStrings converts labels back to their string representation.
|
||||
func (l Labels) ToStrings() []string {
|
||||
ls := make([]string, 0, len(l))
|
||||
for _, label := range l {
|
||||
|
||||
@@ -10,6 +10,94 @@ import (
|
||||
"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) {
|
||||
tests := []struct {
|
||||
args string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package report provides task reporting functionality for communicating with the server.
|
||||
package report
|
||||
|
||||
import (
|
||||
@@ -18,9 +19,10 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client"
|
||||
)
|
||||
|
||||
// Reporter handles logging and state reporting for running tasks.
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -42,6 +44,7 @@ type Reporter struct {
|
||||
stopCommandEndToken string
|
||||
}
|
||||
|
||||
// NewReporter creates a new Reporter for the given task.
|
||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||
var oldnew []string
|
||||
if v := task.Context.Fields["token"].GetStringValue(); v != "" {
|
||||
@@ -72,6 +75,7 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
||||
return rv
|
||||
}
|
||||
|
||||
// ResetSteps initializes the step states with the given number of steps.
|
||||
func (r *Reporter) ResetSteps(l int) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -82,6 +86,7 @@ func (r *Reporter) ResetSteps(l int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Levels returns all log levels that this hook should fire for.
|
||||
func (r *Reporter) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
@@ -93,6 +98,7 @@ func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||
return s
|
||||
}
|
||||
|
||||
// Fire processes a log entry and updates the task state accordingly.
|
||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -175,6 +181,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunDaemon starts the periodic reporting of logs and state.
|
||||
func (r *Reporter) RunDaemon() {
|
||||
if r.closed {
|
||||
return
|
||||
@@ -189,6 +196,7 @@ func (r *Reporter) RunDaemon() {
|
||||
time.AfterFunc(time.Second, r.RunDaemon)
|
||||
}
|
||||
|
||||
// Logf adds a formatted log message to the report.
|
||||
func (r *Reporter) Logf(format string, a ...interface{}) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
@@ -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) {
|
||||
r.stateMu.Lock()
|
||||
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 {
|
||||
r.closed = true
|
||||
|
||||
@@ -260,6 +270,7 @@ func (r *Reporter) Close(lastWords string) error {
|
||||
}, retry.Context(r.ctx))
|
||||
}
|
||||
|
||||
// ReportLog sends accumulated log rows to the server.
|
||||
func (r *Reporter) ReportLog(noMore bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
@@ -295,6 +306,7 @@ func (r *Reporter) ReportLog(noMore bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportState sends the current task state to the server.
|
||||
func (r *Reporter) ReportState() error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
@@ -373,7 +385,7 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||
|
||||
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||
|
||||
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||
func (r *Reporter) handleCommand(originalContent, command, _ /* parameters */, value string) *string {
|
||||
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/pkg/client/mocks"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/client/mocks"
|
||||
)
|
||||
|
||||
func TestReporter_parseLogRow(t *testing.T) {
|
||||
|
||||
27
internal/pkg/service/service_other.go
Normal file
27
internal/pkg/service/service_other.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
|
||||
// Package service provides Windows service integration for the runner.
|
||||
// On non-Windows platforms, these functions are no-ops.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// IsWindowsService returns false on non-Windows platforms.
|
||||
func IsWindowsService() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RunAsService is a no-op on non-Windows platforms.
|
||||
func RunAsService(_ string, _ func(ctx context.Context)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServiceName returns empty on non-Windows platforms.
|
||||
func GetServiceName() string {
|
||||
return ""
|
||||
}
|
||||
103
internal/pkg/service/service_windows.go
Normal file
103
internal/pkg/service/service_windows.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
// Package service provides Windows service integration for the runner.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
)
|
||||
|
||||
// runnerService implements svc.Handler for Windows service management.
|
||||
type runnerService struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Execute is called by the Windows Service Control Manager.
|
||||
func (s *runnerService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
|
||||
log.Info("Windows service started")
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
// Windows wants two responses for interrogate
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
log.Info("Windows service stop/shutdown requested")
|
||||
s.cancel()
|
||||
break loop
|
||||
default:
|
||||
log.Warnf("unexpected control request #%d", c)
|
||||
}
|
||||
case <-s.ctx.Done():
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// IsWindowsService returns true if the process is running as a Windows service.
|
||||
func IsWindowsService() bool {
|
||||
// Check if we're running interactively
|
||||
isInteractive, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failed to detect if running as Windows service")
|
||||
return false
|
||||
}
|
||||
return isInteractive
|
||||
}
|
||||
|
||||
// RunAsService runs the application as a Windows service.
|
||||
func RunAsService(serviceName string, run func(ctx context.Context)) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Start the actual runner in a goroutine
|
||||
go run(ctx)
|
||||
|
||||
// Run the service handler - this blocks until service stops
|
||||
err := svc.Run(serviceName, &runnerService{ctx: ctx, cancel: cancel})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Windows service run failed")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServiceName returns the service name from environment or default.
|
||||
func GetServiceName() string {
|
||||
if name := os.Getenv("GITEA_RUNNER_SERVICE_NAME"); name != "" {
|
||||
return name
|
||||
}
|
||||
// Try to detect from executable name
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
base := strings.TrimSuffix(exe, ".exe")
|
||||
if idx := strings.LastIndex(base, string(os.PathSeparator)); idx >= 0 {
|
||||
return base[idx+1:]
|
||||
}
|
||||
return base
|
||||
}
|
||||
return "GiteaRunnerSvc"
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package ver provides version information for the runner.
|
||||
package ver
|
||||
|
||||
// go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=1.2.3"
|
||||
// go build -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/ver.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
// Version returns the current runner version.
|
||||
func Version() string {
|
||||
return version
|
||||
}
|
||||
|
||||
14
main.go
14
main.go
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// GitCaddy Runner is a CI/CD runner for Gitea Actions.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,10 +9,21 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitea.com/gitea/act_runner/internal/app/cmd"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/app/cmd"
|
||||
"git.marketally.com/gitcaddy/gitcaddy-runner/internal/pkg/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check if running as Windows service
|
||||
if service.IsWindowsService() {
|
||||
// Run as Windows service with proper SCM handling
|
||||
_ = service.RunAsService(service.GetServiceName(), func(ctx context.Context) {
|
||||
cmd.Execute(ctx)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Normal interactive mode with signal handling
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// run the command
|
||||
|
||||
Reference in New Issue
Block a user