2
0

Compare commits

..

11 Commits

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:17:20 -05:00
50480c989c build(deps): update go version to 1.25.0
Some checks failed
CI / build-and-test (push) Failing after 23s
Remove explicit toolchain directive and update to Go 1.25.0
2026-01-25 12:12:49 -05:00
bf71b55cb7 style(service): improve comments and fix linter warnings
Some checks failed
CI / build-and-test (push) Failing after 24s
Release / build (amd64, darwin) (push) Failing after 38s
Release / build (amd64, linux) (push) Failing after 40s
Release / build (amd64, windows) (push) Failing after 32s
Release / build (arm64, darwin) (push) Failing after 31s
Release / build (arm64, linux) (push) Failing after 39s
Release / release (push) Has been skipped
- Add package documentation comments
- Use blank identifiers for unused parameters
- Add periods to comment sentences for consistency
- Fix naked return statement
2026-01-25 11:44:38 -05:00
26b4e7497f Merge branch 'main' of https://git.marketally.com/gitcaddy/gitcaddy-runner 2026-01-25 11:42:24 -05:00
b2922e332a feat(i18n): add windows service support and graceful shutdown
- Add native Windows service detection and signal handling
- Implement configurable shutdown timeout for graceful job completion
- Improve HTTP client with connection pooling and timeouts
- Propagate context through poller for proper shutdown coordination
- Add documentation for Windows service installation (NSSM and sc.exe)
- Add *.exe to .gitignore for Windows builds
2026-01-25 11:40:30 -05:00
d388ec5519 chore(scanner): add gitsecrets ignore file
Some checks failed
CI / build-and-test (push) Has been cancelled
Initializes .gitsecrets-ignore file to track false positives from secret scanning. Includes documentation header explaining the file format and usage.
2026-01-24 14:42:10 -05:00
cb1c1a3264 Add LICENSE.md (MIT)
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-01-23 00:53:55 +00:00
15 changed files with 288 additions and 21 deletions

View File

@@ -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:

1
.gitignore vendored
View File

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

14
.gitsecrets-ignore Normal file
View File

@@ -0,0 +1,14 @@
# GitSecrets Ignore File
# This file tracks false positives identified by AI evaluation or manually marked.
# Each line is a JSON object with the following fields:
# - contentHash: SHA256 hash prefix of the secret content
# - patternId: The pattern that detected this secret
# - filePath: Relative path where the secret was found
# - reason: Why this was marked as a false positive
# - confidence: AI confidence level (if from AI evaluation)
# - addedAt: Timestamp when this entry was added
#
# You can safely commit this file to share false positive markers with your team.
# To remove an entry, simply delete the corresponding line.
{"contentHash":"5af30500c6463ec4","patternId":"password-assignment","filePath":"..\\gitcaddy\\internal\\app\\cmd\\register.go","reason":"Manually marked as false positive","addedAt":1769249840525}

18
LICENSE.md Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2026 gitcaddy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -85,6 +85,7 @@ runner:
file: .runner
capacity: 2 # Number of concurrent jobs (default: 1)
timeout: 3h
shutdown_timeout: 3m # Grace period for running jobs on shutdown
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
@@ -229,6 +230,71 @@ 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:
@@ -306,6 +372,7 @@ GitCaddy Runner automatically detects and reports system capabilities:
|--------|------|---------|-------------|
| `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 |

4
go.mod
View File

@@ -1,8 +1,6 @@
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

2
go.sum
View File

@@ -6,8 +6,6 @@ cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
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=

View File

@@ -218,7 +218,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
}()
poller := poll.New(cfg, cli, runner)
poller := poll.New(ctx, cfg, cli, runner)
poller.SetBandwidthManager(bandwidthManager)
if daemArgs.Once || reg.Ephemeral {

View File

@@ -40,11 +40,11 @@ type Poller struct {
done chan struct{}
}
// New creates a new Poller instance.
func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
pollingCtx, shutdownPolling := context.WithCancel(context.Background())
jobsCtx, shutdownJobs := context.WithCancel(context.Background())
// New creates a new Poller instance with the given context for shutdown propagation.
func New(ctx context.Context, cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
// Inherit from parent context so shutdown signals propagate properly
pollingCtx, shutdownPolling := context.WithCancel(ctx)
jobsCtx, shutdownJobs := context.WithCancel(ctx)
done := make(chan struct{})

View File

@@ -8,6 +8,7 @@ import (
"crypto/tls"
"net/http"
"strings"
"time"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
@@ -15,16 +16,24 @@ import (
)
func getHTTPClient(endpoint string, insecure bool) *http.Client {
transport := &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: false,
}
if strings.HasPrefix(endpoint, "https://") && insecure {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
return http.DefaultClient
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}
// New returns a new runner client.

View File

@@ -138,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 == "" {

View File

@@ -190,10 +190,20 @@ func (r *Reporter) RunDaemon() {
return
}
_ = r.ReportLog(false)
_ = r.ReportState()
if err := r.ReportLog(false); err != nil {
log.WithError(err).Warn("failed to report log")
}
if err := r.ReportState(); err != nil {
log.WithError(err).Warn("failed to report state")
}
time.AfterFunc(time.Second, r.RunDaemon)
// Use select with context to allow clean shutdown
select {
case <-r.ctx.Done():
return
case <-time.After(time.Second):
r.RunDaemon()
}
}
// Logf adds a formatted log message to the report.

View File

@@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows
// Package service provides Windows service integration for the runner.
// On non-Windows platforms, these functions are no-ops.
package service
import (
"context"
)
// IsWindowsService returns false on non-Windows platforms.
func IsWindowsService() bool {
return false
}
// RunAsService is a no-op on non-Windows platforms.
func RunAsService(_ string, _ func(ctx context.Context)) error {
return nil
}
// GetServiceName returns empty on non-Windows platforms.
func GetServiceName() string {
return ""
}

View File

@@ -0,0 +1,103 @@
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build windows
// Package service provides Windows service integration for the runner.
package service
import (
"context"
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc"
)
// runnerService implements svc.Handler for Windows service management.
type runnerService struct {
ctx context.Context
cancel context.CancelFunc
}
// Execute is called by the Windows Service Control Manager.
func (s *runnerService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
log.Info("Windows service started")
loop:
for {
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
// Windows wants two responses for interrogate
time.Sleep(100 * time.Millisecond)
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
log.Info("Windows service stop/shutdown requested")
s.cancel()
break loop
default:
log.Warnf("unexpected control request #%d", c)
}
case <-s.ctx.Done():
break loop
}
}
changes <- svc.Status{State: svc.StopPending}
return false, 0
}
// IsWindowsService returns true if the process is running as a Windows service.
func IsWindowsService() bool {
// Check if we're running interactively
isInteractive, err := svc.IsWindowsService()
if err != nil {
log.WithError(err).Debug("failed to detect if running as Windows service")
return false
}
return isInteractive
}
// RunAsService runs the application as a Windows service.
func RunAsService(serviceName string, run func(ctx context.Context)) error {
ctx, cancel := context.WithCancel(context.Background())
// Start the actual runner in a goroutine
go run(ctx)
// Run the service handler - this blocks until service stops
err := svc.Run(serviceName, &runnerService{ctx: ctx, cancel: cancel})
if err != nil {
log.WithError(err).Error("Windows service run failed")
return err
}
return nil
}
// GetServiceName returns the service name from environment or default.
func GetServiceName() string {
if name := os.Getenv("GITEA_RUNNER_SERVICE_NAME"); name != "" {
return name
}
// Try to detect from executable name
exe, err := os.Executable()
if err == nil {
base := strings.TrimSuffix(exe, ".exe")
if idx := strings.LastIndex(base, string(os.PathSeparator)); idx >= 0 {
return base[idx+1:]
}
return base
}
return "GiteaRunnerSvc"
}

11
main.go
View File

@@ -10,9 +10,20 @@ import (
"syscall"
"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