Compare commits

...

3 Commits

Author SHA1 Message Date
Lunny Xiao c0a07cfb72 enhance: download each action repository once per job (#1178)
A job downloads each action repository once, keyed on the clone URL and ref, so repeated `uses:` and different paths of one repository share a checkout. The download is reported once as `{org}/{repo}@{ref}`, the way actions/runner reports it.

The action itself is still read per step, because a repository without an action file gets a synthetic action built from that step's `with.args`.

Fixes https://gitea.com/gitea/runner/issues/1159

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1178
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-08-22 17:01:39 +00:00
Lunny Xiao 2fa5fe7121 fix: follow the act move in the paths outside Go code
`make test-dind` runs a hardcoded package path, and the `.gitignore`
negation for the test secrets fixture stopped matching.

Assisted-by: Codet:GPT-5.1-Codex
2026-08-07 21:49:42 -07:00
Lunny Xiao 825c6af07c refactor: move act under internal
The runner is an application, not a library. `act/model` and
`act/exprparser` were the last packages anything outside this repository
consumed and they now live in actionslib, so nothing needs the rest of
`act` to be importable, and keeping it importable invites the coupling
that was just removed.

Import paths only, the files are unchanged.

Assisted-by: Codet:GPT-5.1-Codex
2026-08-07 21:40:48 -07:00
267 changed files with 342 additions and 247 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
check-latest: true
- name: prepare anonymous docker config
run: mkdir -p "$DOCKER_CONFIG" && echo '{}' > "$DOCKER_CONFIG/config.json"
# Pre-pull act/runner's two largest base images so a slow pull can't dominate `make test`;
# Pre-pull internal/act/runner's two largest base images so a slow pull can't dominate `make test`;
# the rest (alpine/ubuntu) pull on demand, absorbed by the make-test -timeout. The host
# daemon retains them between runs, so this is usually a fast manifest re-check.
- name: pre-pull test images
@@ -48,4 +48,4 @@ jobs:
- name: coverage report
run: |
make coverage-report
cat .tmp/coverage.md >> "$GITHUB_STEP_SUMMARY"
cat .tmp/coverage.md >> "$GITHUB_STEP_SUMMARY"
+2 -2
View File
@@ -1,6 +1,6 @@
/gitea-runner
.env
!/act/runner/testdata/secrets/.env
!/internal/act/runner/testdata/secrets/.env
.runner
.runner.lock
coverage.txt
@@ -14,4 +14,4 @@ coverage.txt
__debug_bin
# gorelease binary folder
/dist
.DS_Store
.DS_Store
@@ -26,7 +26,7 @@ import (
"sync/atomic"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
@@ -17,7 +17,7 @@ import (
"strings"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/julienschmidt/httprouter"
)
@@ -15,7 +15,7 @@ import (
"strings"
"sync"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/pkg/lock"
"github.com/go-git/go-git/v5"
@@ -458,10 +458,14 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
logger.Debugf("Unable to pull %s: %v", refName, err)
}
case isOfflineMode && reused:
reusedMsg = " (reused in offline mode)"
reusedMsg = " (offline mode)"
}
logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg)
if reused {
logger.Debugf("Reused %s at %s%s", input.URL, input.Dir, reusedMsg)
} else {
logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
}
if hash.String() != input.Ref && refType == "branch" {
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
@@ -16,7 +16,7 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
log "github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
@@ -373,22 +373,28 @@ func TestGitCloneExecutorOfflineMode(t *testing.T) {
// Prime the cache with an online clone of main.
cacheDir := t.TempDir()
logger, hook := logrustest.NewNullLogger()
logger.SetLevel(log.DebugLevel)
ctx := common.WithLogger(context.Background(), logger.WithField("job", "j1"))
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: cacheDir,
})(context.Background()))
})(ctx))
assert.Contains(t, logMessages(hook), "Cloned "+remoteDir+" to "+cacheDir)
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>.
hook.Reset()
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: cacheDir,
OfflineMode: true,
})(context.Background())
})(ctx)
require.NoError(t, err)
assert.Contains(t, logMessages(hook), "Reused "+remoteDir+" at "+cacheDir+" (offline mode)")
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
require.NoError(t, err)
@@ -445,6 +451,14 @@ func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
}
}
func logMessages(hook *logrustest.Hook) []string {
messages := []string{}
for _, entry := range hook.AllEntries() {
messages = append(messages, entry.Message)
}
return messages
}
func TestGitCloneExecutorShallow(t *testing.T) {
// Build a local "remote" with several commits on main plus a tag, so a full clone would pull noticeably more history than a shallow one.
remoteDir := t.TempDir()
@@ -10,7 +10,7 @@ import (
"fmt"
"io"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
@@ -9,7 +9,7 @@ package container
import (
"context"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config"
@@ -12,7 +12,7 @@ import (
"os"
"path/filepath"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
@@ -13,7 +13,7 @@ import (
"strings"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/moby/moby/client"
)
@@ -11,7 +11,7 @@ import (
"fmt"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/distribution/reference"
"github.com/moby/moby/api/pkg/authconfig"
@@ -22,8 +22,8 @@ import (
"strings"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/filecollector"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/filecollector"
"dario.cat/mergo"
cerrdefs "github.com/containerd/errdefs"
@@ -19,7 +19,7 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/pkg/stdcopy"
@@ -12,7 +12,7 @@ import (
"runtime"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/moby/moby/api/types/system"
)
@@ -9,7 +9,7 @@ package container
import (
"context"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/moby/moby/client"
)
@@ -21,9 +21,9 @@ import (
"sync/atomic"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/filecollector"
"gitea.com/gitea/runner/act/lookpath"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/filecollector"
"gitea.com/gitea/runner/internal/act/lookpath"
"gitea.com/gitea/runner/internal/pkg/process"
"github.com/go-git/go-billy/v5/helper/polyfill"
@@ -17,7 +17,7 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -12,7 +12,7 @@ import (
"io"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
@@ -13,8 +13,8 @@ import (
"fmt"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/common/git"
"gitea.dev/actionslib/pkg/model"
)
@@ -20,9 +20,9 @@ import (
"runtime"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/common/git"
"gitea.com/gitea/runner/internal/act/container"
"gitea.dev/actionslib/pkg/model"
"github.com/kballard/go-shellquote"
@@ -293,7 +293,9 @@ func removeGitIgnore(ctx context.Context, directory string) error {
// same `act-dockeraction:latest` image on a shared docker daemon. A subsequent
// repository would then silently run the image built for an earlier one.
// Including the repository keeps the tag stable for caching within a repository
// while preventing cross-repository collisions.
// while preventing cross-repository collisions. A remote action needs the same
// treatment, because its actionName is the shared checkout of its repository and
// ref plus the action's path inside it.
// See https://gitea.com/gitea/runner/issues/1039.
func dockerActionImageTag(repository, actionName string, localAction bool) string {
name := actionName
@@ -302,11 +304,10 @@ func dockerActionImageTag(repository, actionName string, localAction bool) strin
}
// The human-readable name is sanitized by collapsing every non-alphanumeric character to "-".
sanitized := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(name, "-")
if localAction {
// For local actions a short hash of the raw repository and action path is appended so the tag stays unique per repository.
sum := sha256.Sum256([]byte(repository + "\x00" + actionName))
sanitized += "-" + hex.EncodeToString(sum[:])[:12]
}
// Sanitizing is lossy, so a short hash of the raw repository and action path is appended, keeping
// the tag unique per repository and per action inside it.
sum := sha256.Sum256([]byte(repository + "\x00" + actionName))
sanitized += "-" + hex.EncodeToString(sum[:])[:12]
// "-dockeraction" ensures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
image := fmt.Sprintf("%s-dockeraction:%s", sanitized, "latest")
image = "act-" + strings.TrimLeft(image, "-")
@@ -17,7 +17,7 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.dev/actionslib/pkg/model"
"github.com/stretchr/testify/assert"
@@ -31,7 +31,7 @@ func runGit(t *testing.T, dir string, args ...string) {
}
cmd := exec.Command("git", args...)
// Fixed identity and host-config isolation so commits succeed offline regardless of the
// host's git config (mirrors gitCmd in act/common/git).
// host's git config (mirrors gitCmd in internal/act/common/git).
cmd.Env = append(os.Environ(),
"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.com",
"GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.com",
@@ -12,7 +12,7 @@ import (
"strconv"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.dev/actionslib/pkg/model"
)
@@ -13,9 +13,9 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/common/git"
"gitea.com/gitea/runner/internal/act/container"
"gitea.dev/actionslib/pkg/model"
"github.com/stretchr/testify/assert"
@@ -496,11 +496,11 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
}
func TestDockerActionImageTag(t *testing.T) {
// Remote actions already carry a unique, ref-scoped actionName (the uses
// hash), so the tag must be left untouched for backwards compatibility.
assert.Equal(t,
"act-abc123-dockeraction:latest",
dockerActionImageTag("owner/repo", "abc123", false),
// A remote action's actionName is the checkout of its repository and ref plus its path inside it,
// and paths that sanitize alike share the readable prefix, so siblings must stay apart.
assert.NotEqual(t,
dockerActionImageTag("owner/repo", "abc123/a-b", false),
dockerActionImageTag("owner/repo", "abc123/a_b", false),
)
// Local actions keep a human-readable, repository-namespaced prefix and gain a short hash suffix that makes the tag unique per (repository, actionName).
@@ -8,7 +8,7 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model"
@@ -10,7 +10,7 @@ import (
"regexp"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
)
var commandPatternGA *regexp.Regexp
@@ -11,7 +11,7 @@ import (
"os"
"testing"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.dev/actionslib/pkg/model"
"github.com/sirupsen/logrus/hooks/test"
@@ -8,8 +8,8 @@ import (
"context"
"io"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/container"
"github.com/stretchr/testify/mock"
)
@@ -15,8 +15,8 @@ import (
"strings"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/container"
_ "embed"
@@ -9,7 +9,7 @@ import (
"runtime"
"testing"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/container"
mobyclient "github.com/moby/moby/client"
)
@@ -21,8 +21,8 @@ import (
"time"
"unicode"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/container"
"gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model"
@@ -20,8 +20,8 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/container"
"gitea.dev/actionslib/pkg/model"
log "github.com/sirupsen/logrus"
@@ -146,7 +146,8 @@ func TestPrintPrepareActionsGolden(t *testing.T) {
&actionPreparerMock{reference: "actions/checkout@v7", sha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", ok: true},
// A resolved commit is best effort; the ref alone is reported when it is unknown.
&actionPreparerMock{reference: "actions/setup-go@v6", ok: true},
// A step that downloads nothing, such as the checkout of the workflow's own repository.
// A step that downloads nothing, such as the checkout of the workflow's own repository or a
// second step on an action the job already downloaded.
&actionPreparerMock{ok: false},
}
require.NoError(t, printPrepareActions(&RunContext{}, preparers)(ctx))
@@ -11,8 +11,8 @@ import (
"path"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/act/common"
"gitea.com/gitea/runner/internal/act/container"
)
// GitHub's job-hook variables, read as a fallback when the settings are unset.
@@ -11,7 +11,7 @@ import (
"maps"
"testing"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"gitea.dev/actionslib/pkg/model"
"github.com/sirupsen/logrus/hooks/test"
@@ -16,7 +16,7 @@ import (
"path/filepath"
"strings"
"gitea.com/gitea/runner/act/filecollector"
"gitea.com/gitea/runner/internal/act/filecollector"
)
type LocalRepositoryCache struct {
@@ -17,7 +17,7 @@ import (
"strings"
"sync"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/internal/act/common"
"github.com/sirupsen/logrus"
"golang.org/x/term"

Some files were not shown because too many files have changed in this diff Show More