Merge branch 'main' of https://git.marketally.com/gitcaddy/gitcaddy-runner
All checks were successful
CI / build-and-test (push) Successful in 57s
All checks were successful
CI / build-and-test (push) Successful in 57s
This commit is contained in:
@@ -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
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/act_runner
|
||||
*.exe
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
|
||||
67
README.md
67
README.md
@@ -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
4
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -42,8 +42,8 @@ type Poller 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{})
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
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"
|
||||
}
|
||||
11
main.go
11
main.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user