mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-09 18:44:23 +02:00
Compare commits
15 Commits
v1.0.4
...
lunny/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d0457615 | ||
|
|
273f6b4247 | ||
|
|
47ee45412a | ||
|
|
38b69bb214 | ||
|
|
1c62c0635f | ||
|
|
0e0c54b272 | ||
|
|
d6fbe75721 | ||
|
|
b30204aa94 | ||
|
|
7b5ebe9618 | ||
|
|
4317662a38 | ||
|
|
2208e7ec63 | ||
|
|
3815aad750 | ||
|
|
fab9714f9a | ||
|
|
10475db58a | ||
|
|
9e738c203c |
27
.gitea/workflows/pull-pr-title.yml
Normal file
27
.gitea/workflows/pull-pr-title.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: pr-title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-pr-title:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- run: make lint-pr-title
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
/gitea-runner
|
||||
.env
|
||||
!/act/runner/testdata/secrets/.env
|
||||
.runner
|
||||
coverage.txt
|
||||
/config.yaml
|
||||
@@ -10,4 +11,4 @@ coverage.txt
|
||||
.vscode
|
||||
__debug_bin
|
||||
# gorelease binary folder
|
||||
dist
|
||||
/dist
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### BUILDER STAGE
|
||||
#
|
||||
#
|
||||
FROM golang:1.26-alpine AS builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
|
||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||
RUN apk add --no-cache make git
|
||||
@@ -17,7 +17,7 @@ RUN make clean && make build
|
||||
### DIND VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29-dind AS dind
|
||||
FROM docker:29.5.2-dind AS dind
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
@@ -37,7 +37,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
### DIND-ROOTLESS VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29-dind-rootless AS dind-rootless
|
||||
FROM docker:29.5.2-dind-rootless AS dind-rootless
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
@@ -63,7 +63,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
### BASIC VARIANT
|
||||
#
|
||||
#
|
||||
FROM alpine AS basic
|
||||
FROM alpine:3.23 AS basic
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -118,6 +118,10 @@ lint-go: ## lint go files
|
||||
lint-go-fix: ## lint go files and fix issues
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
||||
|
||||
.PHONY: lint-pr-title
|
||||
lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...)
|
||||
@node ./tools/lint-pr-title.ts
|
||||
|
||||
.PHONY: security-check
|
||||
security-check: deps-tools
|
||||
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||
|
||||
@@ -243,47 +243,50 @@ type NewGitCloneExecutorInput struct {
|
||||
InsecureSkipTLS bool
|
||||
}
|
||||
|
||||
// CloneIfRequired ...
|
||||
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) {
|
||||
// CloneIfRequired returns the repository and a boolean indicating whether an existing local clone was reused.
|
||||
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) {
|
||||
r, err := git.PlainOpen(input.Dir)
|
||||
if err != nil {
|
||||
var progressWriter io.Writer
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
if entry, ok := logger.(*log.Entry); ok {
|
||||
progressWriter = entry.WriterLevel(log.DebugLevel)
|
||||
} else if lgr, ok := logger.(*log.Logger); ok {
|
||||
progressWriter = lgr.WriterLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.Errorf("Unable to get writer from logger (type=%T)", logger)
|
||||
progressWriter = os.Stdout
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
// Reuse existing clone
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
cloneOptions := git.CloneOptions{
|
||||
URL: input.URL,
|
||||
Progress: progressWriter,
|
||||
|
||||
InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea
|
||||
}
|
||||
if input.Token != "" {
|
||||
cloneOptions.Auth = &http.BasicAuth{
|
||||
Username: "token",
|
||||
Password: input.Token,
|
||||
}
|
||||
}
|
||||
|
||||
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
||||
if err != nil {
|
||||
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
||||
return nil, err
|
||||
var progressWriter io.Writer
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
if entry, ok := logger.(*log.Entry); ok {
|
||||
progressWriter = entry.WriterLevel(log.DebugLevel)
|
||||
} else if lgr, ok := logger.(*log.Logger); ok {
|
||||
progressWriter = lgr.WriterLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.Errorf("Unable to get writer from logger (type=%T)", logger)
|
||||
progressWriter = os.Stdout
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
cloneOptions := git.CloneOptions{
|
||||
URL: input.URL,
|
||||
Progress: progressWriter,
|
||||
|
||||
InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea
|
||||
}
|
||||
if input.Token != "" {
|
||||
cloneOptions.Auth = &http.BasicAuth{
|
||||
Username: "token",
|
||||
Password: input.Token,
|
||||
}
|
||||
}
|
||||
|
||||
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
||||
if err != nil {
|
||||
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return r, false, nil
|
||||
}
|
||||
|
||||
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
||||
@@ -313,7 +316,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||
defer AcquireCloneLock(input.Dir)()
|
||||
|
||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
||||
r, reused, err := CloneIfRequired(ctx, refName, input, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,10 +341,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||
var hash *plumbing.Hash
|
||||
rev := plumbing.Revision(input.Ref)
|
||||
if hash, err = r.ResolveRevision(rev); err != nil {
|
||||
// ResolveRevision returns a nil hash on error, and a branch ref legitimately fails
|
||||
// here (no local refs/heads/<ref>); the duck-typing below resolves it.
|
||||
logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
||||
}
|
||||
|
||||
if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
||||
} else if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
||||
return &Error{
|
||||
err: ErrShortRef,
|
||||
commit: hash.String(),
|
||||
@@ -392,12 +395,18 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reusedMsg := ""
|
||||
|
||||
if !isOfflineMode {
|
||||
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
|
||||
logger.Debugf("Unable to pull %s: %v", refName, err)
|
||||
}
|
||||
} else if reused {
|
||||
reusedMsg = " (reused in offline mode)"
|
||||
}
|
||||
logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
|
||||
|
||||
logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg)
|
||||
|
||||
if hash.String() != input.Ref && refType == "branch" {
|
||||
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
|
||||
|
||||
@@ -279,6 +279,54 @@ func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
|
||||
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
||||
}
|
||||
|
||||
func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||
gitConfig()
|
||||
|
||||
// Build a local "remote" with a single commit on main.
|
||||
remoteDir := t.TempDir()
|
||||
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
|
||||
workDir := t.TempDir()
|
||||
require.NoError(t, gitCmd("clone", remoteDir, workDir))
|
||||
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
|
||||
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
|
||||
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
|
||||
|
||||
// Prime the cache with an online clone of main.
|
||||
cacheDir := t.TempDir()
|
||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||
URL: remoteDir,
|
||||
Ref: "main",
|
||||
Dir: cacheDir,
|
||||
})(context.Background()))
|
||||
|
||||
t.Run("cached branch resolves without fetching", func(t *testing.T) {
|
||||
// Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref)
|
||||
// finds no local refs/heads/<ref>.
|
||||
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||
URL: remoteDir,
|
||||
Ref: "main",
|
||||
Dir: cacheDir,
|
||||
OfflineMode: true,
|
||||
})(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "initial", strings.TrimSpace(string(out)))
|
||||
})
|
||||
|
||||
t.Run("unresolvable cached ref returns error", func(t *testing.T) {
|
||||
// The ref was never cached; offline mode cannot resolve it and must return an error.
|
||||
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||
URL: remoteDir,
|
||||
Ref: "never-fetched",
|
||||
Dir: cacheDir,
|
||||
OfflineMode: true,
|
||||
})(context.Background())
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func gitConfig() {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
var err error
|
||||
|
||||
@@ -8,12 +8,24 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerNetworkRemoveRetryInterval = 200 * time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type dockerNetworkClient interface {
|
||||
NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
NetworkInspect(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error)
|
||||
NetworkRemove(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error)
|
||||
}
|
||||
|
||||
func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
cli, err := GetDockerClient(ctx)
|
||||
@@ -56,31 +68,64 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
// Make sure that all network of the specified name are removed
|
||||
// cli.NetworkRemove refuses to remove a network if there are duplicates
|
||||
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||
return removeDockerNetworks(ctx, cli, name)
|
||||
}
|
||||
}
|
||||
|
||||
func removeDockerNetworks(ctx context.Context, cli dockerNetworkClient, name string) error {
|
||||
cleanupCtx, cancel := context.WithTimeout(ctx, dockerNetworkRemoveTimeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
pendingRemoval, err := removeDockerNetworksOnce(cleanupCtx, cli, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// common.Logger(ctx).Debugf("%v", networks)
|
||||
for _, n := range networks.Items {
|
||||
if n.Name == name {
|
||||
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(result.Network.Containers) == 0 {
|
||||
if _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
|
||||
common.Logger(ctx).Debugf("%v", err)
|
||||
}
|
||||
} else {
|
||||
common.Logger(ctx).Debugf("Refusing to remove network %v because it still has active endpoints", name)
|
||||
}
|
||||
}
|
||||
if !pendingRemoval {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
select {
|
||||
case <-cleanupCtx.Done():
|
||||
common.Logger(ctx).Warnf("Timed out waiting for Docker network %v endpoints to detach; leaving network behind", name)
|
||||
return nil
|
||||
case <-time.After(dockerNetworkRemoveRetryInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeDockerNetworksOnce(ctx context.Context, cli dockerNetworkClient, name string) (bool, error) {
|
||||
// Make sure that all network of the specified name are removed.
|
||||
// cli.NetworkRemove refuses to remove a network if there are duplicates.
|
||||
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// common.Logger(ctx).Debugf("%v", networks)
|
||||
|
||||
pendingRemoval := false
|
||||
for _, n := range networks.Items {
|
||||
if n.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(result.Network.Containers) != 0 {
|
||||
pendingRemoval = true
|
||||
common.Logger(ctx).Debugf("Waiting to remove network %v because it still has active endpoints", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
|
||||
pendingRemoval = true
|
||||
common.Logger(ctx).Debugf("Retrying Docker network removal for %v: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return pendingRemoval, nil
|
||||
}
|
||||
|
||||
115
act/container/docker_network_test.go
Normal file
115
act/container/docker_network_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
containernetwork "github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeDockerNetworkClient struct {
|
||||
listResult client.NetworkListResult
|
||||
inspectByID map[string][]client.NetworkInspectResult
|
||||
inspectCalls map[string]int
|
||||
removeCalls []string
|
||||
removeErrs map[string][]error
|
||||
removeIdx map[string]int
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkList(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
return f.listResult, nil
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkInspect(_ context.Context, networkID string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
|
||||
idx := f.inspectCalls[networkID]
|
||||
f.inspectCalls[networkID] = idx + 1
|
||||
results := f.inspectByID[networkID]
|
||||
if len(results) == 0 {
|
||||
return client.NetworkInspectResult{}, nil
|
||||
}
|
||||
if idx >= len(results) {
|
||||
return results[len(results)-1], nil
|
||||
}
|
||||
return results[idx], nil
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkRemove(_ context.Context, networkID string, _ client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
|
||||
f.removeCalls = append(f.removeCalls, networkID)
|
||||
idx := f.removeIdx[networkID]
|
||||
f.removeIdx[networkID] = idx + 1
|
||||
if errs := f.removeErrs[networkID]; idx < len(errs) {
|
||||
return client.NetworkRemoveResult{}, errs[idx]
|
||||
}
|
||||
return client.NetworkRemoveResult{}, nil
|
||||
}
|
||||
|
||||
func TestRemoveDockerNetworksRetriesUntilEndpointsDetach(t *testing.T) {
|
||||
originalInterval := dockerNetworkRemoveRetryInterval
|
||||
originalTimeout := dockerNetworkRemoveTimeout
|
||||
dockerNetworkRemoveRetryInterval = time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 50 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
dockerNetworkRemoveRetryInterval = originalInterval
|
||||
dockerNetworkRemoveTimeout = originalTimeout
|
||||
})
|
||||
|
||||
cli := &fakeDockerNetworkClient{
|
||||
listResult: client.NetworkListResult{
|
||||
Items: []containernetwork.Summary{{Network: containernetwork.Network{ID: "n1", Name: "test"}}},
|
||||
},
|
||||
inspectByID: map[string][]client.NetworkInspectResult{
|
||||
"n1": {
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{"c1": {}}}},
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{}}},
|
||||
},
|
||||
},
|
||||
inspectCalls: map[string]int{},
|
||||
removeErrs: map[string][]error{},
|
||||
removeIdx: map[string]int{},
|
||||
}
|
||||
|
||||
err := removeDockerNetworks(context.Background(), cli, "test")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"n1"}, cli.removeCalls)
|
||||
assert.GreaterOrEqual(t, cli.inspectCalls["n1"], 2)
|
||||
}
|
||||
|
||||
func TestRemoveDockerNetworksStopsRetryingAfterTimeout(t *testing.T) {
|
||||
originalInterval := dockerNetworkRemoveRetryInterval
|
||||
originalTimeout := dockerNetworkRemoveTimeout
|
||||
dockerNetworkRemoveRetryInterval = time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 5 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
dockerNetworkRemoveRetryInterval = originalInterval
|
||||
dockerNetworkRemoveTimeout = originalTimeout
|
||||
})
|
||||
|
||||
cli := &fakeDockerNetworkClient{
|
||||
listResult: client.NetworkListResult{
|
||||
Items: []containernetwork.Summary{{Network: containernetwork.Network{ID: "n1", Name: "test"}}},
|
||||
},
|
||||
inspectByID: map[string][]client.NetworkInspectResult{
|
||||
"n1": {
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{"c1": {}}}},
|
||||
},
|
||||
},
|
||||
inspectCalls: map[string]int{},
|
||||
removeErrs: map[string][]error{},
|
||||
removeIdx: map[string]int{},
|
||||
}
|
||||
|
||||
err := removeDockerNetworks(context.Background(), cli, "test")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, cli.removeCalls)
|
||||
assert.Positive(t, cli.inspectCalls["n1"])
|
||||
}
|
||||
@@ -35,6 +35,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
steps := make([]common.Executor, 0)
|
||||
preSteps := make([]common.Executor, 0)
|
||||
var postExecutor common.Executor
|
||||
var startErr error
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
@@ -165,7 +166,12 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
pipeline = append(pipeline, preSteps...)
|
||||
pipeline = append(pipeline, steps...)
|
||||
|
||||
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
|
||||
startContainer := func(ctx context.Context) error {
|
||||
startErr = info.startContainer()(ctx)
|
||||
return startErr
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(startContainer, common.NewPipelineExecutor(pipeline...).
|
||||
Finally(func(ctx context.Context) error {
|
||||
var cancel context.CancelFunc
|
||||
if ctx.Err() == context.Canceled {
|
||||
@@ -176,8 +182,23 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
}
|
||||
return postExecutor(ctx)
|
||||
}).
|
||||
Finally(info.interpolateOutputs()).
|
||||
Finally(info.closeContainer()))
|
||||
Finally(info.interpolateOutputs())).
|
||||
Finally(func(ctx context.Context) error {
|
||||
if startErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
logger := common.Logger(cleanupCtx)
|
||||
logger.Infof("Cleaning up container for failed startup of job %s", rc.JobName)
|
||||
if err := info.stopContainer()(cleanupCtx); err != nil {
|
||||
logger.Errorf("Error while cleaning up failed job startup: %v", err)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Finally(info.closeContainer())
|
||||
}
|
||||
|
||||
func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJobExecutor(t *testing.T) {
|
||||
@@ -341,3 +342,64 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJobExecutorCleansUpAfterStartContainerFailure(t *testing.T) {
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
rc := &RunContext{
|
||||
JobName: "test",
|
||||
JobContainer: &jobContainerMock{},
|
||||
Run: &model.Run{
|
||||
JobID: "test",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &Config{},
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
|
||||
executorOrder := make([]string, 0)
|
||||
startErr := errors.New("failed to start container")
|
||||
stepModel := &model.Step{ID: "1"}
|
||||
sm := &stepMock{}
|
||||
|
||||
jim.On("steps").Return([]*model.Step{stepModel})
|
||||
jim.On("startContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "startContainer")
|
||||
return startErr
|
||||
})
|
||||
jim.On("stopContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "stopContainer")
|
||||
return nil
|
||||
})
|
||||
jim.On("closeContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "closeContainer")
|
||||
return nil
|
||||
})
|
||||
jim.On("interpolateOutputs").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sfm.On("newStep", stepModel, rc).Return(sm, nil)
|
||||
sm.On("pre").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sm.On("main").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sm.On("post").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
executor := newJobExecutor(jim, sfm, rc)
|
||||
err := executor(ctx)
|
||||
require.ErrorIs(t, err, startErr)
|
||||
assert.Equal(t, []string{"startContainer", "stopContainer", "closeContainer"}, executorOrder)
|
||||
|
||||
jim.AssertExpectations(t)
|
||||
sfm.AssertExpectations(t)
|
||||
sm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -601,10 +601,34 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
|
||||
|
||||
func (rc *RunContext) startContainer() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
var err error
|
||||
if rc.IsHostEnv(ctx) {
|
||||
return rc.startHostEnvironment()(ctx)
|
||||
err = rc.startHostEnvironment()(ctx)
|
||||
} else {
|
||||
err = rc.startJobContainer()(ctx)
|
||||
}
|
||||
return rc.startJobContainer()(ctx)
|
||||
if err != nil {
|
||||
// The job executor's teardown only runs after a successful start, so a failed
|
||||
// start would otherwise leak the per-job network and container.
|
||||
rc.cleanupFailedStart(ctx)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) cleanupFailedStart(ctx context.Context) {
|
||||
if rc.cleanUpJobContainer == nil {
|
||||
return
|
||||
}
|
||||
cleanCtx := ctx
|
||||
if ctx.Err() != nil {
|
||||
// the start likely failed because ctx was cancelled, detach so teardown still runs
|
||||
var cancel context.CancelFunc
|
||||
cleanCtx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
defer cancel()
|
||||
}
|
||||
if err := rc.cleanUpJobContainer(cleanCtx); err != nil {
|
||||
common.Logger(ctx).Errorf("Error while cleaning up after failed container start for job %s: %v", rc.JobName, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
assert "github.com/stretchr/testify/assert"
|
||||
require "github.com/stretchr/testify/require"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -659,3 +660,53 @@ func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||
}, "\n")
|
||||
assert.Equal(t, want, buf.String())
|
||||
}
|
||||
|
||||
func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
type ctxKey string
|
||||
const sentinel = ctxKey("sentinel")
|
||||
|
||||
// the fresh context is cancelled via defer on return, so capture state inside the stub
|
||||
type capture struct {
|
||||
calls int
|
||||
err error
|
||||
sentinel any
|
||||
}
|
||||
newRC := func(c *capture) *RunContext {
|
||||
return &RunContext{
|
||||
JobName: "job",
|
||||
cleanUpJobContainer: func(ctx context.Context) error {
|
||||
c.calls++
|
||||
c.err = ctx.Err()
|
||||
c.sentinel = ctx.Value(sentinel)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("runs teardown on the live context", func(t *testing.T) {
|
||||
var c capture
|
||||
ctx := context.WithValue(context.Background(), sentinel, "v")
|
||||
|
||||
newRC(&c).cleanupFailedStart(ctx)
|
||||
|
||||
assert.Equal(t, 1, c.calls)
|
||||
require.NoError(t, c.err)
|
||||
assert.Equal(t, "v", c.sentinel)
|
||||
})
|
||||
|
||||
t.Run("falls back to a fresh context when the input is done", func(t *testing.T) {
|
||||
var c capture
|
||||
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), sentinel, "v"))
|
||||
cancel()
|
||||
|
||||
newRC(&c).cleanupFailedStart(ctx)
|
||||
|
||||
assert.Equal(t, 1, c.calls)
|
||||
require.NoError(t, c.err)
|
||||
assert.Nil(t, c.sentinel)
|
||||
})
|
||||
|
||||
t.Run("no-op when there is nothing to clean up", func(t *testing.T) {
|
||||
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type Config struct {
|
||||
Actor string // the user that triggered the event
|
||||
Workdir string // path to working directory
|
||||
ActionCacheDir string // path used for caching action contents
|
||||
ActionOfflineMode bool // when offline, use caching action contents
|
||||
ActionOfflineMode bool // when offline, use cached action contents
|
||||
BindWorkdir bool // bind the workdir to the job container
|
||||
EventName string // name of event to run
|
||||
EventPath string // path to JSON file to use for event.json in containers
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
@@ -192,6 +193,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
Inputs: cfg.Inputs,
|
||||
GitHubInstance: "github.com",
|
||||
ContainerArchitecture: cfg.ContainerArchitecture,
|
||||
ContainerMaxLifetime: time.Hour,
|
||||
Matrix: cfg.Matrix,
|
||||
ActionCache: cfg.ActionCache,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3
|
||||
FROM alpine:3.23
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ outputs:
|
||||
description: 'The time we greeted you'
|
||||
runs:
|
||||
using: 'node24'
|
||||
main: 'dist/index.js'
|
||||
main: 'index.js'
|
||||
|
||||
21
act/runner/testdata/actions/node24/index.js
vendored
21
act/runner/testdata/actions/node24/index.js
vendored
@@ -1,11 +1,14 @@
|
||||
import {getInput, setOutput, setFailed} from '@actions/core';
|
||||
import {context} from '@actions/github';
|
||||
import {appendFileSync, readFileSync} from 'node:fs';
|
||||
|
||||
try {
|
||||
const nameToGreet = getInput('who-to-greet');
|
||||
console.log(`Hello ${nameToGreet}!`);
|
||||
setOutput('time', (new Date()).toTimeString());
|
||||
console.log(`The event payload: ${JSON.stringify(context.payload, undefined, 2)}`);
|
||||
} catch (error) {
|
||||
setFailed(error.message);
|
||||
const nameToGreet = process.env['INPUT_WHO-TO-GREET'] || 'World';
|
||||
console.log(`Hello ${nameToGreet}!`);
|
||||
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `time=${new Date().toTimeString()}\n`);
|
||||
}
|
||||
|
||||
let payload = {};
|
||||
if (process.env.GITHUB_EVENT_PATH) {
|
||||
payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
||||
}
|
||||
console.log(`The event payload: ${JSON.stringify(payload, undefined, 2)}`);
|
||||
|
||||
20
act/runner/testdata/actions/node24/package.json
vendored
20
act/runner/testdata/actions/node24/package.json
vendored
@@ -1,21 +1,5 @@
|
||||
{
|
||||
"name": "node24",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "ncc build index.js"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/github": "^9.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.38.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
}
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
2
act/runner/testdata/secrets/.env
vendored
Normal file
2
act/runner/testdata/secrets/.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
HELLO=WORLD
|
||||
MULTILINE_ENV="foo\nbar\nbaz"
|
||||
8
go.mod
8
go.mod
@@ -4,17 +4,17 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.4.1
|
||||
connectrpc.com/connect v1.19.2
|
||||
connectrpc.com/connect v1.20.0
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/avast/retry-go/v5 v5.0.0
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v29.5.0+incompatible
|
||||
github.com/docker/cli v29.5.2+incompatible
|
||||
github.com/docker/go-connections v0.7.0
|
||||
github.com/go-git/go-billy/v5 v5.9.0
|
||||
github.com/go-git/go-git/v5 v5.19.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/opencontainers/selinux v1.14.1
|
||||
github.com/opencontainers/selinux v1.15.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rhysd/actionlint v1.7.12
|
||||
|
||||
14
go.sum
14
go.sum
@@ -2,6 +2,8 @@ code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLr
|
||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
||||
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
@@ -47,10 +49,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU=
|
||||
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4=
|
||||
github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
|
||||
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
||||
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
@@ -73,8 +73,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
||||
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -151,6 +151,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
|
||||
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||
github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M=
|
||||
github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
||||
@@ -132,7 +132,6 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
cfg.Runner.Insecure,
|
||||
reg.UUID,
|
||||
reg.Token,
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
|
||||
@@ -325,7 +325,6 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
cfg.Runner.Insecure,
|
||||
"",
|
||||
"",
|
||||
ver.Version(),
|
||||
)
|
||||
|
||||
for {
|
||||
@@ -366,12 +365,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
}
|
||||
// register new runner.
|
||||
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: reg.Name,
|
||||
Token: reg.Token,
|
||||
Version: ver.Version(),
|
||||
AgentLabels: ls, // Could be removed after Gitea 1.20
|
||||
Labels: ls,
|
||||
Ephemeral: reg.Ephemeral,
|
||||
Name: reg.Name,
|
||||
Token: reg.Token,
|
||||
Version: ver.Version(),
|
||||
Labels: ls,
|
||||
Ephemeral: reg.Ephemeral,
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
|
||||
@@ -344,10 +344,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
runnerConfig := &runner.Config{
|
||||
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||
Workdir: workdir,
|
||||
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||
Workdir: workdir,
|
||||
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||
ActionOfflineMode: r.cfg.Cache.OfflineMode,
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: r.cfg.Container.ForcePull,
|
||||
|
||||
@@ -6,6 +6,4 @@ package client
|
||||
const (
|
||||
UUIDHeader = "x-runner-uuid"
|
||||
TokenHeader = "x-runner-token"
|
||||
// Deprecated: could be removed after Gitea 1.20 released
|
||||
VersionHeader = "x-runner-version"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||
}
|
||||
|
||||
// New returns a new runner client.
|
||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
||||
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
|
||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||
|
||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
@@ -42,10 +42,6 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
||||
if token != "" {
|
||||
req.Header().Set(TokenHeader, token)
|
||||
}
|
||||
// TODO: version will be removed from request header after Gitea 1.20 released.
|
||||
if version != "" {
|
||||
req.Header().Set(VersionHeader, version)
|
||||
}
|
||||
return next(ctx, req)
|
||||
}
|
||||
})))
|
||||
|
||||
@@ -102,6 +102,9 @@ cache:
|
||||
# (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the
|
||||
# cache-server, and the cache-server enforces bearer auth + per-repo cache isolation.
|
||||
external_secret: ""
|
||||
# When true, reuse a cached action instead of fetching from the remote on every job. Note: a moved tag
|
||||
# (e.g. a re-tagged "v6") or an updated branch stays at the cached commit until its cache entry is removed.
|
||||
offline_mode: false
|
||||
|
||||
container:
|
||||
# Specifies the network to which the container will connect.
|
||||
|
||||
@@ -52,6 +52,7 @@ type Cache struct {
|
||||
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
|
||||
ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior.
|
||||
OfflineMode bool `yaml:"offline_mode"` // OfflineMode reuses a cached action without fetching from the remote; a moved tag or branch stays at the cached commit until the cache entry is removed.
|
||||
}
|
||||
|
||||
// Container represents the configuration for the container.
|
||||
@@ -109,7 +110,6 @@ func LoadDefault(file string) (*Config, error) {
|
||||
return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err)
|
||||
}
|
||||
}
|
||||
compatibleWithOldEnvs(file != "", cfg)
|
||||
|
||||
if cfg.Runner.EnvFile != "" {
|
||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
||||
// Be compatible with old envs.
|
||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
||||
handleEnv := func(key string) (string, bool) {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if fileUsed {
|
||||
log.Warnf("env %s has been ignored because config file is used", key)
|
||||
return "", false
|
||||
}
|
||||
log.Warnf("env %s will be deprecated, please use config file instead", key)
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
if v, ok := handleEnv("GITEA_DEBUG"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "debug"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_TRACE"); ok {
|
||||
if b, _ := strconv.ParseBool(v); b {
|
||||
cfg.Log.Level = "trace"
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
|
||||
if i, _ := strconv.Atoi(v); i > 0 {
|
||||
cfg.Runner.Capacity = i
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
|
||||
cfg.Runner.File = v
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
|
||||
splits := strings.Split(v, ",")
|
||||
if cfg.Runner.Envs == nil {
|
||||
cfg.Runner.Envs = map[string]string{}
|
||||
}
|
||||
for _, split := range splits {
|
||||
kv := strings.SplitN(split, ":", 2)
|
||||
if len(kv) == 2 && kv[0] != "" {
|
||||
cfg.Runner.Envs[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
|
||||
cfg.Runner.EnvFile = v
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
urgentState = true
|
||||
}
|
||||
}
|
||||
if !r.duringSteps() {
|
||||
if r.shouldAppendLogRow(entry) {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
r.unlockAndNotify(urgentState)
|
||||
@@ -219,7 +219,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
}
|
||||
}
|
||||
if step == nil {
|
||||
if !r.duringSteps() {
|
||||
if r.shouldAppendLogRow(entry) {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
r.unlockAndNotify(false)
|
||||
@@ -246,7 +246,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||
r.logRows = append(r.logRows, row)
|
||||
}
|
||||
}
|
||||
} else if !r.duringSteps() {
|
||||
} else if r.shouldAppendLogRow(entry) {
|
||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||
}
|
||||
if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) {
|
||||
@@ -576,6 +576,13 @@ func (r *Reporter) duringSteps() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// shouldAppendLogRow reports whether a non-raw_output entry should be written
|
||||
// to the job log: only when we are between steps and the entry's level is
|
||||
// within the globally configured log level.
|
||||
func (r *Reporter) shouldAppendLogRow(entry *log.Entry) bool {
|
||||
return !r.duringSteps() && entry.Level <= log.GetLevel()
|
||||
}
|
||||
|
||||
var stringToResult = map[string]runnerv1.Result{
|
||||
"success": runnerv1.Result_RESULT_SUCCESS,
|
||||
"failure": runnerv1.Result_RESULT_FAILURE,
|
||||
@@ -639,7 +646,7 @@ func (r *Reporter) handleCommand(originalContent, command, value string) *string
|
||||
}
|
||||
|
||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
content := strings.TrimRight(entry.Message, "\r\n")
|
||||
|
||||
matches := cmdRegex.FindStringSubmatch(content)
|
||||
if matches != nil {
|
||||
|
||||
@@ -219,6 +219,59 @@ func TestReporter_Fire(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||
// Set global level to Info so Debug entries should be filtered.
|
||||
origLevel := log.GetLevel()
|
||||
log.SetLevel(log.InfoLevel)
|
||||
defer log.SetLevel(origLevel)
|
||||
|
||||
client := mocks.NewClient(t)
|
||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||
}), nil
|
||||
})
|
||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
require.NoError(t, err)
|
||||
cfg, _ := config.LoadDefault("")
|
||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||
reporter.RunDaemon()
|
||||
defer func() {
|
||||
require.NoError(t, reporter.Close(""))
|
||||
}()
|
||||
reporter.ResetSteps(2)
|
||||
|
||||
dataStep0 := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||
dataStep0Internal := log.Fields{"stage": "Main", "stepNumber": 0}
|
||||
|
||||
// raw_output entries always appear in job log regardless of level.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step output", Data: dataStep0, Level: log.InfoLevel}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step debug output", Data: dataStep0, Level: log.DebugLevel}))
|
||||
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded")
|
||||
|
||||
// Non-raw_output entries during steps are not added to logRows regardless of level.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal info", Data: dataStep0Internal, Level: log.InfoLevel}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal debug", Data: dataStep0Internal, Level: log.DebugLevel}))
|
||||
|
||||
// stepResult at DebugLevel (skipped step) must still update state even when filtered from log.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
Message: "Skipping step",
|
||||
Data: log.Fields{
|
||||
"stage": "Main",
|
||||
"stepNumber": 1,
|
||||
"stepResult": "skipped",
|
||||
},
|
||||
Level: log.DebugLevel,
|
||||
}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result,
|
||||
"stepResult at DebugLevel must update step state even when log entry is filtered from job log output")
|
||||
}
|
||||
|
||||
// TestReporter_EphemeralRunnerDeletion reproduces the exact scenario from
|
||||
// https://gitea.com/gitea/runner/issues/793:
|
||||
//
|
||||
|
||||
19
tools/lint-pr-title.ts
Normal file
19
tools/lint-pr-title.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
import {env, exit} from 'node:process';
|
||||
|
||||
const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test';
|
||||
const title = env.PR_TITLE;
|
||||
|
||||
if (!title) {
|
||||
console.error('Missing PR_TITLE');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`);
|
||||
|
||||
if (!validTitlePattern.test(title)) {
|
||||
console.error(`Invalid PR title: ${title}`);
|
||||
console.error('Expected format: type(scope): subject');
|
||||
console.error(`Allowed types: ${allowedTypes}`);
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user