mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 07:33:47 +00:00
fix: keep dry runs off the docker daemon and speed up the tests (#1349)
Without a docker daemon six `act/runner` tests failed instead of passing. A dry run still created and removed job networks, the job teardown dropped the dry-run flag along with every other context value, and the container mocks asked the daemon for the runner arch. This also speeds up the tests and CI without dropping assertions. Most of the time went to git and docker round trips, which are now pinned, batched or coalesced, and the dind and e2e steps cache or overlap more of their setup. On CI, `act/runner` went from 77s to 55s and `test-dind` from 108s to 55s. Locally, `act/runner` without docker went from 53s to 7s. Split out of https://gitea.com/gitea/runner/pulls/1348, which adds Windows CI on top. The go cache changes moved to https://gitea.com/gitea/runner/pulls/1350. Written by Claude Code. --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1349 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
1 parent
8ae48ae033
commit
dafe8d8d0c
50 files changed
+1066
-600
No files matched your search
@@ -45,8 +45,13 @@ cpu.out
|
|||||||
|
|
||||||
/bin
|
/bin
|
||||||
/dist
|
/dist
|
||||||
|
/.tmp
|
||||||
|
/.git/hooks
|
||||||
|
/.git/logs
|
||||||
|
/.git/rr-cache
|
||||||
/.env
|
/.env
|
||||||
/.runner
|
/.runner
|
||||||
|
/.runner.lock
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -22,17 +22,19 @@ jobs:
|
|||||||
run: mkdir -p "$DOCKER_CONFIG" && echo '{}' > "$DOCKER_CONFIG/config.json"
|
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 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
|
# 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.
|
# daemon retains them between runs, and their digest pins skip the registry when it has them.
|
||||||
- name: pre-pull test images
|
- name: pre-pull test images
|
||||||
env:
|
env:
|
||||||
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:55fb5f5a527ea2c5c4a95f963e48ef90aa38c7b346054fdc8ee9957a2b55d205 # renovate: datasource=docker
|
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:55fb5f5a527ea2c5c4a95f963e48ef90aa38c7b346054fdc8ee9957a2b55d205 # renovate: datasource=docker
|
||||||
TEST_SERVICE_IMAGE: nginx:alpine@sha256:a3554c0d0b48177b1413c26e01f695334428020d9df01243fbae3e11cfd17417 # renovate: datasource=docker
|
TEST_SERVICE_IMAGE: nginx:alpine@sha256:a3554c0d0b48177b1413c26e01f695334428020d9df01243fbae3e11cfd17417 # renovate: datasource=docker
|
||||||
run: |
|
run: |
|
||||||
for image in "$TEST_JOB_IMAGE" "$TEST_SERVICE_IMAGE"; do
|
for image in "$TEST_JOB_IMAGE" "$TEST_SERVICE_IMAGE"; do
|
||||||
for attempt in 1 2 3; do
|
if [ "$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null)" != linux/amd64 ]; then
|
||||||
docker pull "$image" && break
|
for attempt in 1 2 3; do
|
||||||
[ "$attempt" = 3 ] || sleep 5
|
docker pull "$image" && break
|
||||||
done
|
[ "$attempt" = 3 ] || sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
docker tag "$image" "${image%@*}"
|
docker tag "$image" "${image%@*}"
|
||||||
done
|
done
|
||||||
- name: lint
|
- name: lint
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,9 @@ ENV GOPROXY=${GOPROXY:-}
|
|||||||
COPY . /opt/src/runner
|
COPY . /opt/src/runner
|
||||||
WORKDIR /opt/src/runner
|
WORKDIR /opt/src/runner
|
||||||
|
|
||||||
RUN make clean && make build
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
make clean && make build
|
||||||
|
|
||||||
### DIND VARIANT
|
### DIND VARIANT
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ func signArtifactURL(h *Handler, id int64) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_ExternalURL(t *testing.T) {
|
func TestHandler_ExternalURL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
assert.Equal(t, "http://[2001:db8::1]:8080", (&Handler{outboundIP: "2001:db8::1", port: 8080}).ExternalURL())
|
assert.Equal(t, "http://[2001:db8::1]:8080", (&Handler{outboundIP: "2001:db8::1", port: 8080}).ExternalURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_Upstream(t *testing.T) {
|
func TestHandler_Upstream(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
var upstream *httptest.Server
|
var upstream *httptest.Server
|
||||||
upstream = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, http.MethodPut, r.Method)
|
assert.Equal(t, http.MethodPut, r.Method)
|
||||||
@@ -101,6 +103,7 @@ func TestHandler_Upstream(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
const version = "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
const version = "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
@@ -470,6 +473,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_gcCache(t *testing.T) {
|
func TestHandler_gcCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir, Policy: Policy{Retention: testRetention}})
|
handler, err := StartHandler(Options{Dir: dir, Policy: Policy{Retention: testRetention}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -572,6 +576,7 @@ func TestHandler_gcCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_evictPolicy(t *testing.T) {
|
func TestHandler_evictPolicy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
stale := func(d time.Duration) int64 { return now.Add(-d).Unix() }
|
stale := func(d time.Duration) int64 { return now.Add(-d).Unix() }
|
||||||
mib := func(n int64) int64 { return n * miB }
|
mib := func(n int64) int64 { return n * miB }
|
||||||
@@ -669,6 +674,7 @@ func TestHandler_evictPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_evictForFreeSpace(t *testing.T) {
|
func TestHandler_evictForFreeSpace(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
free := func(n int64) func(string) (uint64, error) {
|
free := func(n int64) func(string) (uint64, error) {
|
||||||
return func(string) (uint64, error) { return uint64(n), nil }
|
return func(string) (uint64, error) { return uint64(n), nil }
|
||||||
}
|
}
|
||||||
@@ -708,6 +714,7 @@ func TestHandler_evictForFreeSpace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_SweepKeepsEntryWhenBlobSurvives(t *testing.T) {
|
func TestHandler_SweepKeepsEntryWhenBlobSurvives(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
cache := &Cache{Key: "stuck", Complete: true, UsedAt: time.Now().Add(-(testRetention + time.Hour)).Unix()}
|
cache := &Cache{Key: "stuck", Complete: true, UsedAt: time.Now().Add(-(testRetention + time.Hour)).Unix()}
|
||||||
handler := newTestHandler(t, Policy{Retention: testRetention}, cache)
|
handler := newTestHandler(t, Policy{Retention: testRetention}, cache)
|
||||||
|
|
||||||
@@ -722,6 +729,7 @@ func TestHandler_SweepKeepsEntryWhenBlobSurvives(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_FindProtectsFromEviction(t *testing.T) {
|
func TestHandler_FindProtectsFromEviction(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
wanted := &Cache{Repo: testRepo, Key: "wanted", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-3 * time.Hour).Unix()}
|
wanted := &Cache{Repo: testRepo, Key: "wanted", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-3 * time.Hour).Unix()}
|
||||||
other := &Cache{Repo: testRepo, Key: "other", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-2 * time.Hour).Unix()}
|
other := &Cache{Repo: testRepo, Key: "other", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-2 * time.Hour).Unix()}
|
||||||
newest := &Cache{Repo: testRepo, Key: "newest", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-time.Hour).Unix()}
|
newest := &Cache{Repo: testRepo, Key: "newest", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-time.Hour).Unix()}
|
||||||
@@ -742,6 +750,7 @@ func TestHandler_FindProtectsFromEviction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_PrefixUsesNewestReservationWithinSecond(t *testing.T) {
|
func TestHandler_PrefixUsesNewestReservationWithinSecond(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
created := time.Now().Add(-time.Hour).Unix()
|
created := time.Now().Add(-time.Hour).Unix()
|
||||||
older := &Cache{Repo: testRepo, Key: "deps-old", Version: "v", Complete: true, CreatedAt: created}
|
older := &Cache{Repo: testRepo, Key: "deps-old", Version: "v", Complete: true, CreatedAt: created}
|
||||||
newer := &Cache{Repo: testRepo, Key: "deps-new", Version: "v", Complete: true, CreatedAt: created}
|
newer := &Cache{Repo: testRepo, Key: "deps-new", Version: "v", Complete: true, CreatedAt: created}
|
||||||
@@ -756,6 +765,7 @@ func TestHandler_PrefixUsesNewestReservationWithinSecond(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_evictOnCommit(t *testing.T) {
|
func TestHandler_evictOnCommit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
full := &Cache{Repo: testRepo, Key: "full", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-time.Hour).Unix()}
|
full := &Cache{Repo: testRepo, Key: "full", Version: "v", Complete: true, Size: 4 * miB, UsedAt: time.Now().Add(-time.Hour).Unix()}
|
||||||
handler := newTestHandler(t, Policy{RepoSizeLimit: 4 * miB}, full)
|
handler := newTestHandler(t, Policy{RepoSizeLimit: 4 * miB}, full)
|
||||||
|
|
||||||
@@ -765,6 +775,7 @@ func TestHandler_evictOnCommit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_gcCacheInterval(t *testing.T) {
|
func TestHandler_gcCacheInterval(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
cache := &Cache{Key: "temp", UsedAt: time.Now().Add(-time.Hour).Unix()}
|
cache := &Cache{Key: "temp", UsedAt: time.Now().Add(-time.Hour).Unix()}
|
||||||
handler := newTestHandler(t, Policy{SweepInterval: 30 * time.Minute}, cache)
|
handler := newTestHandler(t, Policy{SweepInterval: 30 * time.Minute}, cache)
|
||||||
|
|
||||||
@@ -774,6 +785,7 @@ func TestHandler_gcCacheInterval(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_touchCacheSkipsFreshCompletedEntries(t *testing.T) {
|
func TestHandler_touchCacheSkipsFreshCompletedEntries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
fresh := &Cache{Repo: testRepo, Key: "fresh", Version: "v", Complete: true, UsedAt: time.Now().Unix()}
|
fresh := &Cache{Repo: testRepo, Key: "fresh", Version: "v", Complete: true, UsedAt: time.Now().Unix()}
|
||||||
pending := &Cache{Repo: testRepo, Key: "pending", Version: "v", UsedAt: time.Now().Add(-time.Second).Unix()}
|
pending := &Cache{Repo: testRepo, Key: "pending", Version: "v", UsedAt: time.Now().Add(-time.Second).Unix()}
|
||||||
handler := newTestHandler(t, Policy{}, fresh, pending)
|
handler := newTestHandler(t, Policy{}, fresh, pending)
|
||||||
@@ -840,6 +852,7 @@ func writeBlob(t *testing.T, handler *Handler, id uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_RejectsBearer(t *testing.T) {
|
func TestHandler_RejectsBearer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -876,6 +889,7 @@ func TestHandler_RejectsBearer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_UnregisterRevokes(t *testing.T) {
|
func TestHandler_UnregisterRevokes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
@@ -917,6 +931,7 @@ func TestHandler_UnregisterRevokes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_CrossRepoIsolation(t *testing.T) {
|
func TestHandler_CrossRepoIsolation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -967,6 +982,7 @@ func TestHandler_CrossRepoIsolation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_ArtifactSignature(t *testing.T) {
|
func TestHandler_ArtifactSignature(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -998,6 +1014,7 @@ func TestHandler_ArtifactSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
|
func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
|
||||||
first, err := StartHandler(Options{Dir: dir, OutboundIP: "127.0.0.1"})
|
first, err := StartHandler(Options{Dir: dir, OutboundIP: "127.0.0.1"})
|
||||||
@@ -1014,6 +1031,7 @@ func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_SeesWritesOfOtherProcesses(t *testing.T) {
|
func TestHandler_SeesWritesOfOtherProcesses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
insert := func(by *Handler, cache *Cache) {
|
insert := func(by *Handler, cache *Cache) {
|
||||||
db, err := by.openDB()
|
db, err := by.openDB()
|
||||||
@@ -1031,6 +1049,7 @@ func TestHandler_SeesWritesOfOtherProcesses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_ArtifactSignatureDownload(t *testing.T) {
|
func TestHandler_ArtifactSignatureDownload(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1065,6 +1084,7 @@ func TestHandler_ArtifactSignatureDownload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_GC_PerRepoDedup(t *testing.T) {
|
func TestHandler_GC_PerRepoDedup(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1109,6 +1129,7 @@ func TestHandler_GC_PerRepoDedup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_InternalAPI_Disabled(t *testing.T) {
|
func TestHandler_InternalAPI_Disabled(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(Options{Dir: dir})
|
handler, err := StartHandler(Options{Dir: dir})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1123,6 +1144,7 @@ func TestHandler_InternalAPI_Disabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_InternalAPI_AuthAndUsage(t *testing.T) {
|
func TestHandler_InternalAPI_AuthAndUsage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
const secret = "internal-secret"
|
const secret = "internal-secret"
|
||||||
handler, err := StartHandler(Options{Dir: dir, InternalSecret: secret})
|
handler, err := StartHandler(Options{Dir: dir, InternalSecret: secret})
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ func saveV2(t *testing.T, handler *Handler, key, version string, content []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2RoundTrip(t *testing.T) {
|
func TestCacheServiceV2RoundTrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
content := []byte("the cached archive")
|
content := []byte("the cached archive")
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ func TestCacheServiceV2RoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2BlockUpload(t *testing.T) {
|
func TestCacheServiceV2BlockUpload(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
|
|
||||||
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "blocks", "version": "v1"})
|
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "blocks", "version": "v1"})
|
||||||
@@ -150,6 +152,7 @@ func TestCacheServiceV2BlockUpload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2Lookups(t *testing.T) {
|
func TestCacheServiceV2Lookups(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
saved, _ := saveV2(t, handler, "deps-abc", "v1", []byte("x"))
|
saved, _ := saveV2(t, handler, "deps-abc", "v1", []byte("x"))
|
||||||
require.Equal(t, true, saved["ok"])
|
require.Equal(t, true, saved["ok"])
|
||||||
@@ -231,6 +234,7 @@ func TestCacheServiceV2Lookups(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2RefusalsAreTwirp(t *testing.T) {
|
func TestCacheServiceV2RefusalsAreTwirp(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -267,6 +271,7 @@ func TestCacheServiceV2RefusalsAreTwirp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2ConcurrentSaveAndRetries(t *testing.T) {
|
func TestCacheServiceV2ConcurrentSaveAndRetries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
handler := newTestHandler(t, Policy{})
|
handler := newTestHandler(t, Policy{})
|
||||||
content := []byte("the cached archive")
|
content := []byte("the cached archive")
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFrontResultsService(t *testing.T) {
|
func TestFrontResultsService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
var gotHost, gotPath, gotProto string
|
var gotHost, gotPath, gotProto string
|
||||||
gitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
gitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
gotHost, gotPath, gotProto = r.Host, r.URL.Path, r.Header.Get("X-Forwarded-Proto")
|
gotHost, gotPath, gotProto = r.Host, r.URL.Path, r.Header.Get("X-Forwarded-Proto")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestHandler_S3SharesEntriesAndDropsExpiredOnes(t *testing.T) {
|
func TestHandler_S3SharesEntriesAndDropsExpiredOnes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
var objects sync.Map
|
var objects sync.Map
|
||||||
var uploads atomic.Int32
|
var uploads atomic.Int32
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@@ -91,6 +92,7 @@ func (transport s3TestTransport) RoundTrip(req *http.Request) (*http.Response, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestS3StorageUploadsLargeObjectsInPartsAndAbortsOnFailure(t *testing.T) {
|
func TestS3StorageUploadsLargeObjectsInPartsAndAbortsOnFailure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
for _, failingPart := range []string{"", "2"} {
|
for _, failingPart := range []string{"", "2"} {
|
||||||
var parts, aborts atomic.Int32
|
var parts, aborts atomic.Int32
|
||||||
var completion struct {
|
var completion struct {
|
||||||
@@ -130,6 +132,7 @@ func TestS3StorageUploadsLargeObjectsInPartsAndAbortsOnFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestS3StorageSignsLikeAWS(t *testing.T) {
|
func TestS3StorageSignsLikeAWS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
storage := &s3Storage{opts: S3Options{Region: "us-east-1", AccessKey: "AKIAIOSFODNN7EXAMPLE", SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}}
|
storage := &s3Storage{opts: S3Options{Region: "us-east-1", AccessKey: "AKIAIOSFODNN7EXAMPLE", SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}}
|
||||||
req := httptest.NewRequest(http.MethodGet, "https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J", nil)
|
req := httptest.NewRequest(http.MethodGet, "https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J", nil)
|
||||||
req.Header.Set("X-Amz-Content-Sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
req.Header.Set("X-Amz-Content-Sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
require.NoError(t, gitCmd("-C", remote, "add", "."))
|
require.NoError(t, gitCmd("-C", remote, "add", "."))
|
||||||
require.NoError(t, gitCmd("-C", remote, "commit", "-m", "initial"))
|
require.NoError(t, gitCmd("-C", remote, "commit", "-m", "initial"))
|
||||||
require.NoError(t, gitCmd("-C", remote, "tag", "v1"))
|
require.NoError(t, gitCmd("-C", remote, "tag", "v1"))
|
||||||
|
expectedSha := gitRevParse(t, remote, "main")
|
||||||
|
|
||||||
gitPath, err := exec.LookPath("git")
|
gitPath, err := exec.LookPath("git")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -49,6 +50,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var archiveRequests []http.Header
|
var archiveRequests []http.Header
|
||||||
var mirror *httptest.Server
|
var mirror *httptest.Server
|
||||||
|
archiveCache := map[string][]byte{}
|
||||||
requests, archives, failures := 0, "", 0
|
requests, archives, failures := 0, "", 0
|
||||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -85,7 +87,12 @@ func TestFetchAction(t *testing.T) {
|
|||||||
if archives != "unprefixed" {
|
if archives != "unprefixed" {
|
||||||
args = append(args, "--prefix=action/")
|
args = append(args, "--prefix=action/")
|
||||||
}
|
}
|
||||||
archive, _ := runGit(request.Context(), remote, nil, args...)
|
archiveKey := strings.Join(args, "\x00")
|
||||||
|
archive, ok := archiveCache[archiveKey]
|
||||||
|
if !ok {
|
||||||
|
archive, _ = runGit(request.Context(), remote, nil, args...)
|
||||||
|
archiveCache[archiveKey] = archive
|
||||||
|
}
|
||||||
writer.Header().Set("ETag", etag)
|
writer.Header().Set("ETag", etag)
|
||||||
_, _ = writer.Write(archive)
|
_, _ = writer.Write(archive)
|
||||||
})
|
})
|
||||||
@@ -108,7 +115,6 @@ func TestFetchAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("shares identical fetches and isolates cancellation, panics and differing inputs", func(t *testing.T) {
|
t.Run("shares identical fetches and isolates cancellation, panics and differing inputs", func(t *testing.T) {
|
||||||
expectedSha := gitRevParse(t, remote, "main")
|
|
||||||
for _, scenario := range []string{"shared", "leader cancelled", "leader panics", "different token"} {
|
for _, scenario := range []string{"shared", "leader cancelled", "leader panics", "different token"} {
|
||||||
t.Run(scenario, func(t *testing.T) {
|
t.Run(scenario, func(t *testing.T) {
|
||||||
serve("prefixed", 0)
|
serve("prefixed", 0)
|
||||||
@@ -181,7 +187,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
input := newInput("v1")
|
input := newInput("v1")
|
||||||
tree, sha, err := FetchAction(t.Context(), input)
|
tree, sha, err := FetchAction(t.Context(), input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, gitRevParse(t, remote, "v1"), sha)
|
assert.Equal(t, expectedSha, sha)
|
||||||
assert.Equal(t, filepath.Join(input.Dir, sha), tree)
|
assert.Equal(t, filepath.Join(input.Dir, sha), tree)
|
||||||
assert.NoDirExists(t, filepath.Join(tree, ".git"))
|
assert.NoDirExists(t, filepath.Join(tree, ".git"))
|
||||||
info, err := os.Stat(filepath.Join(tree, "run.sh"))
|
info, err := os.Stat(filepath.Join(tree, "run.sh"))
|
||||||
@@ -197,9 +203,10 @@ func TestFetchAction(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, gitCmd("-C", remote, "commit", "--allow-empty", "-m", "moved"))
|
require.NoError(t, gitCmd("-C", remote, "commit", "--allow-empty", "-m", "moved"))
|
||||||
require.NoError(t, gitCmd("-C", remote, "tag", "--force", "v1"))
|
require.NoError(t, gitCmd("-C", remote, "tag", "--force", "v1"))
|
||||||
|
expectedSha = gitRevParse(t, remote, "v1")
|
||||||
moved, movedSha, err := FetchAction(t.Context(), input)
|
moved, movedSha, err := FetchAction(t.Context(), input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, gitRevParse(t, remote, "v1"), movedSha)
|
assert.Equal(t, expectedSha, movedSha)
|
||||||
assert.NotEqual(t, tree, moved)
|
assert.NotEqual(t, tree, moved)
|
||||||
assert.FileExists(t, filepath.Join(tree, "action.yml"))
|
assert.FileExists(t, filepath.Join(tree, "action.yml"))
|
||||||
|
|
||||||
@@ -226,7 +233,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("downloads a pinned commit once and rejects a short one", func(t *testing.T) {
|
t.Run("downloads a pinned commit once and rejects a short one", func(t *testing.T) {
|
||||||
serve("prefixed", 0)
|
serve("prefixed", 0)
|
||||||
sha := gitRevParse(t, remote, "main")
|
sha := expectedSha
|
||||||
input := newInput(sha)
|
input := newInput(sha)
|
||||||
for range 2 {
|
for range 2 {
|
||||||
tree, got, err := FetchAction(t.Context(), input)
|
tree, got, err := FetchAction(t.Context(), input)
|
||||||
@@ -308,7 +315,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
input := newInput("main")
|
input := newInput("main")
|
||||||
_, sha, err := FetchAction(t.Context(), input)
|
_, sha, err := FetchAction(t.Context(), input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, gitRevParse(t, remote, "main"), sha)
|
assert.Equal(t, expectedSha, sha)
|
||||||
assert.NoDirExists(t, filepath.Join(input.Dir, "git"))
|
assert.NoDirExists(t, filepath.Join(input.Dir, "git"))
|
||||||
_, headers := served()
|
_, headers := served()
|
||||||
require.Len(t, headers, 4)
|
require.Len(t, headers, 4)
|
||||||
@@ -321,7 +328,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
serve(mode, 0)
|
serve(mode, 0)
|
||||||
tree, sha, err := FetchAction(t.Context(), newInput("main"))
|
tree, sha, err := FetchAction(t.Context(), newInput("main"))
|
||||||
require.NoError(t, err, mode)
|
require.NoError(t, err, mode)
|
||||||
assert.Equal(t, gitRevParse(t, remote, "main"), sha, mode)
|
assert.Equal(t, expectedSha, sha, mode)
|
||||||
assert.FileExists(t, filepath.Join(tree, "action.yml"), mode)
|
assert.FileExists(t, filepath.Join(tree, "action.yml"), mode)
|
||||||
assert.NoDirExists(t, filepath.Join(tree, ".git"), mode)
|
assert.NoDirExists(t, filepath.Join(tree, ".git"), mode)
|
||||||
_, headers := served()
|
_, headers := served()
|
||||||
@@ -336,7 +343,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
input.URL = "https://example.invalid/org/action"
|
input.URL = "https://example.invalid/org/action"
|
||||||
_, sha, err := FetchAction(t.Context(), input)
|
_, sha, err := FetchAction(t.Context(), input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, gitRevParse(t, remote, "main"), sha)
|
assert.Equal(t, expectedSha, sha)
|
||||||
_, headers := served()
|
_, headers := served()
|
||||||
assert.Len(t, headers, 1)
|
assert.Len(t, headers, 1)
|
||||||
assert.NoDirExists(t, filepath.Join(input.Dir, "git"))
|
assert.NoDirExists(t, filepath.Join(input.Dir, "git"))
|
||||||
|
|||||||
+18
-3
@@ -10,6 +10,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -320,9 +321,18 @@ func cachedRef(ref string) string {
|
|||||||
|
|
||||||
func resolveRef(ctx context.Context, dir, ref string) (string, error) {
|
func resolveRef(ctx context.Context, dir, ref string) (string, error) {
|
||||||
if !isFullObjectID(ref) {
|
if !isFullObjectID(ref) {
|
||||||
for _, name := range []string{cachedRef(ref), "refs/tags/" + ref, "refs/remotes/origin/" + ref} {
|
revisions := []string{cachedRef(ref) + "^{commit}", "refs/tags/" + ref + "^{commit}", "refs/remotes/origin/" + ref + "^{commit}"}
|
||||||
if sha, err := gitOutput(ctx, dir, "rev-parse", "--verify", name+"^{commit}"); err == nil {
|
if strings.ContainsAny(ref, "\x00\n") {
|
||||||
return sha, nil
|
for _, revision := range revisions {
|
||||||
|
if sha, err := gitOutput(ctx, dir, "rev-parse", "--verify", revision); err == nil {
|
||||||
|
return sha, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if output, err := runGitWithStdin(ctx, dir, nil, strings.NewReader(strings.Join(revisions, "\n")+"\n"), "cat-file", "--batch-check=%(objectname)"); err == nil {
|
||||||
|
for sha := range strings.SplitSeq(string(output), "\n") {
|
||||||
|
if isFullObjectID(sha) {
|
||||||
|
return sha, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,6 +437,10 @@ func ListFiles(ctx context.Context, dir string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runGit(ctx context.Context, dir string, input *NewGitCloneExecutorInput, args ...string) ([]byte, error) {
|
func runGit(ctx context.Context, dir string, input *NewGitCloneExecutorInput, args ...string) ([]byte, error) {
|
||||||
|
return runGitWithStdin(ctx, dir, input, nil, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGitWithStdin(ctx context.Context, dir string, input *NewGitCloneExecutorInput, stdin io.Reader, args ...string) ([]byte, error) {
|
||||||
config := []string{"core.autocrlf=false", "core.eol=lf"} // keep action files LF
|
config := []string{"core.autocrlf=false", "core.eol=lf"} // keep action files LF
|
||||||
if input != nil {
|
if input != nil {
|
||||||
config = append(config, RemoteConfig(input.Token, input.InsecureSkipTLS)...)
|
config = append(config, RemoteConfig(input.Token, input.InsecureSkipTLS)...)
|
||||||
@@ -438,6 +452,7 @@ func runGit(ctx context.Context, dir string, input *NewGitCloneExecutorInput, ar
|
|||||||
args = append([]string{"-C", dir}, args...)
|
args = append([]string{"-C", dir}, args...)
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, "git", args...)
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
cmd.Stdin = stdin
|
||||||
cmd.Env = slices.DeleteFunc(os.Environ(), func(entry string) bool {
|
cmd.Env = slices.DeleteFunc(os.Environ(), func(entry string) bool {
|
||||||
name, _, _ := strings.Cut(entry, "=")
|
name, _, _ := strings.Cut(entry, "=")
|
||||||
return slices.Contains(LocalEnvVars, name)
|
return slices.Contains(LocalEnvVars, name)
|
||||||
|
|||||||
+59
-28
@@ -29,6 +29,31 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var gitFixtureDir string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
gitFixtureDir, err = os.MkdirTemp("", "runner-git-fixture-")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
code := 1
|
||||||
|
if err = gitCmd("init", "--quiet", "--template=", "--bare", "--initial-branch=main", filepath.Join(gitFixtureDir, "remote")); err == nil {
|
||||||
|
err = gitCmd("init", "--quiet", "--template=", "--initial-branch=main", filepath.Join(gitFixtureDir, "work"))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
code = m.Run()
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(gitFixtureDir); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
code = 1
|
||||||
|
}
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFindGitSlug(t *testing.T) {
|
func TestFindGitSlug(t *testing.T) {
|
||||||
for _, testcase := range []struct {
|
for _, testcase := range []struct {
|
||||||
remoteURL string
|
remoteURL string
|
||||||
@@ -141,9 +166,8 @@ func TestGitFindRef(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
dir := filepath.Join(basedir, name)
|
dir := filepath.Join(basedir, name)
|
||||||
require.NoError(t, os.MkdirAll(dir, 0o755))
|
require.NoError(t, os.CopyFS(dir, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
|
||||||
require.NoError(t, gitCmd("-C", dir, "init", "--initial-branch=master"))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, ".git", "HEAD"), []byte("ref: refs/heads/master\n"), 0o644))
|
||||||
require.NoError(t, os.RemoveAll(filepath.Join(dir, ".git", "hooks")))
|
|
||||||
for _, command := range tt.commands {
|
for _, command := range tt.commands {
|
||||||
require.NoError(t, gitCmd(append([]string{"-C", dir}, command...)...))
|
require.NoError(t, gitCmd(append([]string{"-C", dir}, command...)...))
|
||||||
}
|
}
|
||||||
@@ -180,6 +204,7 @@ func TestGitCloneExecutor(t *testing.T) {
|
|||||||
t.Setenv("GIT_CONFIG_GLOBAL", hostConfig)
|
t.Setenv("GIT_CONFIG_GLOBAL", hostConfig)
|
||||||
t.Setenv("GIT_DIR", t.TempDir())
|
t.Setenv("GIT_DIR", t.TempDir())
|
||||||
|
|
||||||
|
var clones sync.WaitGroup
|
||||||
for name, tt := range map[string]struct {
|
for name, tt := range map[string]struct {
|
||||||
Err error
|
Err error
|
||||||
Ref string
|
Ref string
|
||||||
@@ -207,31 +232,34 @@ func TestGitCloneExecutor(t *testing.T) {
|
|||||||
Ref: fullSha[:4],
|
Ref: fullSha[:4],
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
clones.Go(func() {
|
||||||
dir := t.TempDir()
|
t.Run(name, func(t *testing.T) {
|
||||||
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
dir := t.TempDir()
|
||||||
URL: "https://example.invalid/action",
|
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
Ref: tt.Ref,
|
URL: "https://example.invalid/action",
|
||||||
Dir: dir,
|
Ref: tt.Ref,
|
||||||
})
|
Dir: dir,
|
||||||
|
})
|
||||||
|
|
||||||
err := clone(t.Context())
|
err := clone(t.Context())
|
||||||
if tt.Err != nil {
|
if tt.Err != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Equal(t, tt.Err, err)
|
assert.Equal(t, tt.Err, err)
|
||||||
assert.Equal(t, tt.Err, clone(t.Context()), "a retry on the cache keeps the error")
|
assert.Equal(t, tt.Err, clone(t.Context()), "a retry on the cache keeps the error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
marker := filepath.Join(dir, "marker")
|
marker := filepath.Join(dir, "marker")
|
||||||
require.NoError(t, os.WriteFile(marker, nil, 0o644))
|
require.NoError(t, os.WriteFile(marker, nil, 0o644))
|
||||||
require.NoError(t, clone(t.Context()))
|
require.NoError(t, clone(t.Context()))
|
||||||
assert.FileExists(t, marker, "an insteadOf rewrite must not evict the cache")
|
assert.FileExists(t, marker, "an insteadOf rewrite must not evict the cache")
|
||||||
script, err := os.ReadFile(filepath.Join(dir, "action.sh"))
|
script, err := os.ReadFile(filepath.Join(dir, "action.sh"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "#!/bin/sh\necho hi\n", string(script), "host core.autocrlf must not convert action files")
|
assert.Equal(t, "#!/bin/sh\necho hi\n", string(script), "host core.autocrlf must not convert action files")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
clones.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
|
func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
|
||||||
@@ -476,9 +504,12 @@ func TestGitCloneExecutorPinnedHashIgnoresShadowingRef(t *testing.T) {
|
|||||||
func newGitRemote(t *testing.T, messages ...string) (string, string) {
|
func newGitRemote(t *testing.T, messages ...string) (string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
remoteDir, workDir := t.TempDir(), t.TempDir()
|
remoteDir, workDir := t.TempDir(), t.TempDir()
|
||||||
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
|
require.NoError(t, os.CopyFS(remoteDir, os.DirFS(filepath.Join(gitFixtureDir, "remote"))))
|
||||||
require.NoError(t, gitCmd("clone", remoteDir, workDir))
|
require.NoError(t, os.CopyFS(workDir, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
|
||||||
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
|
config, err := os.ReadFile(filepath.Join(workDir, ".git", "config"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(workDir, ".git", "config"),
|
||||||
|
fmt.Appendf(config, "\n[remote \"origin\"]\n\turl = %s\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n", strconv.Quote(remoteDir)), 0o644))
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", message))
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,17 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/common/logging"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
|
mobyclient "github.com/moby/moby/client"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -21,48 +24,47 @@ func init() {
|
|||||||
logging.SetLevel(logging.LevelDebug)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildScratchImage builds a tiny empty image for the given platform locally (FROM scratch, no
|
func loadScratchImage(t *testing.T, platform string) string {
|
||||||
// network or emulation since there is nothing to run) and returns its tag, removing it after
|
|
||||||
// the test.
|
|
||||||
func buildScratchImage(t *testing.T, platform string) string {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
tag := fmt.Sprintf("act-test-exists-%s:latest", strings.TrimPrefix(platform, "linux/"))
|
tag := fmt.Sprintf("act-test-exists-%s-%d:latest", strings.TrimPrefix(platform, "linux/"), time.Now().UnixNano())
|
||||||
cmd := exec.Command("docker", "build", "--platform", platform, "-t", tag, "-")
|
var archive bytes.Buffer
|
||||||
cmd.Stdin = strings.NewReader("FROM scratch\nLABEL act-test=1\n")
|
require.NoError(t, writeFilesTar(t.Context(), &archive, 0, 0,
|
||||||
// Force BuildKit: it records the requested architecture in the image config for a
|
&FileEntry{Name: "config.json", Mode: 0o600, Body: fmt.Sprintf(`{"architecture":%q,"os":"linux","rootfs":{"type":"layers","diff_ids":[]}}`, strings.TrimPrefix(platform, "linux/"))},
|
||||||
// FROM-scratch build, whereas the classic builder ignores --platform and tags it with the
|
&FileEntry{Name: "manifest.json", Mode: 0o600, Body: fmt.Sprintf(`[{"Config":"config.json","RepoTags":[%q],"Layers":[]}]`, tag)},
|
||||||
// host arch, which would break the per-platform existence assertions below.
|
))
|
||||||
cmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=1")
|
cli, err := GetDockerClient(t.Context())
|
||||||
out, err := cmd.CombinedOutput()
|
require.NoError(t, err)
|
||||||
require.NoError(t, err, string(out))
|
t.Cleanup(func() { _ = cli.Close() })
|
||||||
t.Cleanup(func() { _ = exec.Command("docker", "rmi", "-f", tag).Run() })
|
response, err := cli.ImageLoad(t.Context(), &archive)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer response.Close()
|
||||||
|
require.NoError(t, logDockerResponse(common.Logger(t.Context()), response, false))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = cli.ImageRemove(context.WithoutCancel(t.Context()), tag, mobyclient.ImageRemoveOptions{Force: true})
|
||||||
|
})
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageExistsLocally(t *testing.T) {
|
func TestImageExistsLocally(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireDocker(t)
|
requireDocker(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
// a non-existent image is reported absent
|
|
||||||
missing, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
missing, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.False(t, missing)
|
assert.False(t, missing)
|
||||||
|
|
||||||
// Build tiny images for two architectures locally so per-platform existence can be checked
|
amd64Ref := loadScratchImage(t, "linux/amd64")
|
||||||
// offline (formerly pulled node:24-bookworm-slim for amd64 and arm64 over the network).
|
arm64Ref := loadScratchImage(t, "linux/arm64")
|
||||||
amd64Ref := buildScratchImage(t, "linux/amd64")
|
|
||||||
arm64Ref := buildScratchImage(t, "linux/arm64")
|
|
||||||
|
|
||||||
amd64Exists, err := ImageExistsLocally(ctx, amd64Ref, "linux/amd64")
|
amd64Exists, err := ImageExistsLocally(ctx, amd64Ref, "linux/amd64")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.True(t, amd64Exists)
|
assert.True(t, amd64Exists)
|
||||||
|
|
||||||
// a non-host architecture image is detected for its own architecture
|
|
||||||
arm64Exists, err := ImageExistsLocally(ctx, arm64Ref, "linux/arm64")
|
arm64Exists, err := ImageExistsLocally(ctx, arm64Ref, "linux/arm64")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.True(t, arm64Exists)
|
assert.True(t, arm64Exists)
|
||||||
|
|
||||||
// a present image is reported absent for a different platform
|
|
||||||
wrongPlatform, err := ImageExistsLocally(ctx, amd64Ref, "linux/arm64")
|
wrongPlatform, err := ImageExistsLocally(ctx, amd64Ref, "linux/arm64")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.False(t, wrongPlatform)
|
assert.False(t, wrongPlatform)
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ networks:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerNetworkCreateExecutor(name string, opts NewDockerNetworkCreateExecutorInput) common.Executor {
|
func NewDockerNetworkCreateExecutor(name string, opts NewDockerNetworkCreateExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return common.Executor(func(ctx context.Context) error {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
return createDockerNetwork(ctx, cli, name, opts)
|
return createDockerNetwork(ctx, cli, name, opts)
|
||||||
}
|
}).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDockerNetwork(ctx context.Context, cli client.APIClient, name string, opts NewDockerNetworkCreateExecutorInput) error {
|
func createDockerNetwork(ctx context.Context, cli client.APIClient, name string, opts NewDockerNetworkCreateExecutorInput) error {
|
||||||
@@ -157,7 +157,7 @@ func isAddressPoolExhausted(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerNetworkConnectExecutor(networkName, container string) common.Executor {
|
func NewDockerNetworkConnectExecutor(networkName, container string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return common.Executor(func(ctx context.Context) error {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -167,7 +167,7 @@ func NewDockerNetworkConnectExecutor(networkName, container string) common.Execu
|
|||||||
common.Logger(ctx).Warnf("cannot connect %s to network %s, so jobs on it cannot reach the cache: %v", container, networkName, err)
|
common.Logger(ctx).Warnf("cannot connect %s to network %s, so jobs on it cannot reach the cache: %v", container, networkName, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectNetwork(ctx context.Context, cli client.APIClient, networkName, container string) error {
|
func connectNetwork(ctx context.Context, cli client.APIClient, networkName, container string) error {
|
||||||
@@ -182,14 +182,14 @@ func connectNetwork(ctx context.Context, cli client.APIClient, networkName, cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerNetworkDisconnectExecutor(networkName, container string) common.Executor {
|
func NewDockerNetworkDisconnectExecutor(networkName, container string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return common.Executor(func(ctx context.Context) error {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
return disconnectNetwork(ctx, cli, networkName, container)
|
return disconnectNetwork(ctx, cli, networkName, container)
|
||||||
}
|
}).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnectNetwork(ctx context.Context, cli client.APIClient, networkName, container string) error {
|
func disconnectNetwork(ctx context.Context, cli client.APIClient, networkName, container string) error {
|
||||||
@@ -201,14 +201,14 @@ func disconnectNetwork(ctx context.Context, cli client.APIClient, networkName, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return common.Executor(func(ctx context.Context) error {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
return removeNetwork(ctx, cli, name)
|
return removeNetwork(ctx, cli, name)
|
||||||
}
|
}).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeNetwork(ctx context.Context, cli client.APIClient, name string) error {
|
func removeNetwork(ctx context.Context, cli client.APIClient, name string) error {
|
||||||
|
|||||||
@@ -48,13 +48,20 @@ var (
|
|||||||
rawStreamPath = regexp.MustCompile(`^(/v[0-9.]+)?/(containers/[^/]+/attach|exec/[^/]+/start)$`)
|
rawStreamPath = regexp.MustCompile(`^(/v[0-9.]+)?/(containers/[^/]+/attach|exec/[^/]+/start)$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var dockerProxyDirectories sync.Map
|
var dockerProxyDirectories, dockerProxyProbes sync.Map
|
||||||
|
|
||||||
type dockerProxyDirectory struct {
|
type dockerProxyDirectory struct {
|
||||||
dir, daemonDir string
|
dir, daemonDir string
|
||||||
socket os.FileInfo
|
socket os.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dockerProxyProbe struct {
|
||||||
|
directory dockerProxyDirectory
|
||||||
|
done chan struct{}
|
||||||
|
seen bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
||||||
if host := os.Getenv("DOCKER_HOST"); runtime.GOOS != "linux" || host != "" && !strings.HasPrefix(host, "unix://") {
|
if host := os.Getenv("DOCKER_HOST"); runtime.GOOS != "linux" || host != "" && !strings.HasPrefix(host, "unix://") {
|
||||||
return nil
|
return nil
|
||||||
@@ -79,16 +86,8 @@ func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
|||||||
cached, _ := dockerProxyDirectories.Load(key)
|
cached, _ := dockerProxyDirectories.Load(key)
|
||||||
directory, seen := cached.(dockerProxyDirectory)
|
directory, seen := cached.(dockerProxyDirectory)
|
||||||
seen = seen && os.SameFile(directory.socket, socket) // daemon replacement invalidates cached paths
|
seen = seen && os.SameFile(directory.socket, socket) // daemon replacement invalidates cached paths
|
||||||
dir, daemonDir := directory.dir, directory.daemonDir
|
|
||||||
if !seen {
|
if !seen {
|
||||||
dir, daemonDir = runnerContainerWorkdir(probeCtx, cli)
|
directory, seen, err = probeDockerProxyDirectory(probeCtx, cli, key, socket)
|
||||||
seen = daemonDir != ""
|
|
||||||
if !seen {
|
|
||||||
if dir, err = filepath.Abs(os.TempDir()); err == nil {
|
|
||||||
daemonDir = dir
|
|
||||||
seen, err = daemonSeesDir(probeCtx, cli, dir, daemonDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Infof("docker proxy probe failed, jobs get the daemon socket directly: %v", err)
|
common.Logger(ctx).Infof("docker proxy probe failed, jobs get the daemon socket directly: %v", err)
|
||||||
@@ -101,17 +100,51 @@ func NewDockerProxy(ctx context.Context, job string) *DockerProxy {
|
|||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
proxy, err := StartDockerProxy(daemonSocket, dir, job)
|
proxy, err := StartDockerProxy(daemonSocket, directory.dir, job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dockerProxyDirectories.Delete(key)
|
dockerProxyDirectories.Delete(key)
|
||||||
common.Logger(ctx).Warnf("docker proxy not started, the job gets the daemon socket directly: %v", err)
|
common.Logger(ctx).Warnf("docker proxy not started, the job gets the daemon socket directly: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dockerProxyDirectories.Store(key, dockerProxyDirectory{dir, daemonDir, socket})
|
dockerProxyDirectories.Store(key, directory)
|
||||||
proxy.Socket = daemonDir + strings.TrimPrefix(proxy.Socket, dir)
|
proxy.Socket = directory.daemonDir + strings.TrimPrefix(proxy.Socket, directory.dir)
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func probeDockerProxyDirectory(ctx context.Context, cli client.APIClient, key [3]string, socket os.FileInfo) (dockerProxyDirectory, bool, error) {
|
||||||
|
for {
|
||||||
|
probe := &dockerProxyProbe{directory: dockerProxyDirectory{socket: socket}, done: make(chan struct{})}
|
||||||
|
actual, loaded := dockerProxyProbes.LoadOrStore(key, probe)
|
||||||
|
if loaded {
|
||||||
|
probe, _ = actual.(*dockerProxyProbe)
|
||||||
|
if !os.SameFile(probe.directory.socket, socket) {
|
||||||
|
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return dockerProxyDirectory{}, false, ctx.Err()
|
||||||
|
case <-probe.done:
|
||||||
|
}
|
||||||
|
if ctx.Err() == nil && (errors.Is(probe.err, context.Canceled) || errors.Is(probe.err, context.DeadlineExceeded)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
probe.directory.dir, probe.directory.daemonDir = runnerContainerWorkdir(ctx, cli)
|
||||||
|
probe.seen = probe.directory.daemonDir != ""
|
||||||
|
if !probe.seen {
|
||||||
|
if probe.directory.dir, probe.err = filepath.Abs(key[2]); probe.err == nil {
|
||||||
|
probe.directory.daemonDir = probe.directory.dir
|
||||||
|
probe.seen, probe.err = daemonSeesDir(ctx, cli, probe.directory.dir, probe.directory.daemonDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dockerProxyProbes.CompareAndDelete(key, probe)
|
||||||
|
close(probe.done)
|
||||||
|
}
|
||||||
|
return probe.directory, probe.seen, probe.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runnerContainerWorkdir looks the runner's container up by hostname to find the daemon's path to its working directory.
|
// runnerContainerWorkdir looks the runner's container up by hostname to find the daemon's path to its working directory.
|
||||||
func runnerContainerWorkdir(ctx context.Context, cli client.APIClient) (workdir, daemonDir string) {
|
func runnerContainerWorkdir(ctx context.Context, cli client.APIClient) (workdir, daemonDir string) {
|
||||||
workdir, err := os.Getwd()
|
workdir, err := os.Getwd()
|
||||||
@@ -162,12 +195,18 @@ func daemonSeesDir(ctx context.Context, cli client.APIClient, dir, daemonDir str
|
|||||||
if len(images.Items) == 0 {
|
if len(images.Items) == 0 {
|
||||||
return false, errors.New("no image available for the docker proxy probe")
|
return false, errors.New("no image available for the docker proxy probe")
|
||||||
}
|
}
|
||||||
created, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
var created client.ContainerCreateResult
|
||||||
Config: &container.Config{Image: images.Items[0].ID, Cmd: []string{"true"}},
|
for _, image := range images.Items {
|
||||||
HostConfig: &container.HostConfig{Mounts: []mount.Mount{
|
created, err = cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||||
{Type: mount.TypeBind, Source: daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}, // not the marker itself, podman creates a missing bind source where docker rejects it
|
Config: &container.Config{Image: image.ID, Cmd: []string{"true"}},
|
||||||
}},
|
HostConfig: &container.HostConfig{Mounts: []mount.Mount{
|
||||||
})
|
{Type: mount.TypeBind, Source: daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}, // not the marker itself, podman creates a missing bind source where docker rejects it
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if !cerrdefs.IsNotFound(err) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if cerrdefs.IsInvalidArgument(err) {
|
if cerrdefs.IsInvalidArgument(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
@@ -486,7 +487,7 @@ func (c *probeClient) ContainerInspect(context.Context, string, mobyclient.Conta
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *probeClient) ImageList(context.Context, mobyclient.ImageListOptions) (mobyclient.ImageListResult, error) {
|
func (c *probeClient) ImageList(context.Context, mobyclient.ImageListOptions) (mobyclient.ImageListResult, error) {
|
||||||
return mobyclient.ImageListResult{Items: []image.Summary{{ID: "probe-image"}}}, nil
|
return mobyclient.ImageListResult{Items: []image.Summary{{ID: "removed-image"}, {ID: "probe-image"}}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *probeClient) ContainerCreate(_ context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
func (c *probeClient) ContainerCreate(_ context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||||
@@ -501,6 +502,139 @@ func (c *probeClient) ContainerStatPath(_ context.Context, _ string, opts mobycl
|
|||||||
return mobyclient.ContainerStatPathResult{}, c.stat(opts.Path)
|
return mobyclient.ContainerStatPathResult{}, c.stat(opts.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDockerProxyDirectoryProbes(t *testing.T) {
|
||||||
|
type result struct {
|
||||||
|
seen bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
for name, createErr := range map[string]error{"unavailable directory": nil, "daemon error": errors.New("probe failed")} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
socket, err := os.Stat(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
key := [3]string{"daemon", dir, dir}
|
||||||
|
release := make(chan struct{})
|
||||||
|
var calls, removals atomic.Int32
|
||||||
|
visible := false
|
||||||
|
cli := &probeClient{
|
||||||
|
create: func(_opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||||
|
calls.Add(1)
|
||||||
|
<-release
|
||||||
|
return mobyclient.ContainerCreateResult{ID: "probe"}, createErr
|
||||||
|
},
|
||||||
|
stat: func(_path string) error {
|
||||||
|
if visible {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return cerrdefs.ErrNotFound
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: func(_ctx context.Context, _id string, _opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||||
|
removals.Add(1)
|
||||||
|
return mobyclient.ContainerRemoveResult{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
results := make(chan result, 8)
|
||||||
|
for range cap(results) {
|
||||||
|
go func() {
|
||||||
|
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||||
|
results <- result{seen, err}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
synctest.Wait()
|
||||||
|
assert.EqualValues(t, 1, calls.Load())
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
cancel()
|
||||||
|
_, seen, err := probeDockerProxyDirectory(ctx, cli, key, socket)
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
assert.False(t, seen)
|
||||||
|
replacement, err := os.Stat(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
replaced := make(chan result, 1)
|
||||||
|
go func() {
|
||||||
|
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, replacement)
|
||||||
|
replaced <- result{seen, err}
|
||||||
|
}()
|
||||||
|
synctest.Wait()
|
||||||
|
assert.EqualValues(t, 2, calls.Load())
|
||||||
|
close(release)
|
||||||
|
for range cap(results) {
|
||||||
|
result := <-results
|
||||||
|
require.ErrorIs(t, result.err, createErr)
|
||||||
|
assert.False(t, result.seen)
|
||||||
|
}
|
||||||
|
replacementResult := <-replaced
|
||||||
|
require.ErrorIs(t, replacementResult.err, createErr)
|
||||||
|
assert.False(t, replacementResult.seen)
|
||||||
|
assert.EqualValues(t, 2, calls.Load())
|
||||||
|
if createErr == nil {
|
||||||
|
assert.EqualValues(t, 2, removals.Load())
|
||||||
|
} else {
|
||||||
|
assert.Zero(t, removals.Load())
|
||||||
|
}
|
||||||
|
createErr, visible = nil, true
|
||||||
|
directory, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, seen)
|
||||||
|
assert.Equal(t, dir, directory.dir)
|
||||||
|
assert.Equal(t, dir, directory.daemonDir)
|
||||||
|
assert.EqualValues(t, 3, calls.Load())
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, entries)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("cancelled probe does not cancel another caller", func(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
socket, err := os.Stat(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
key := [3]string{"daemon", dir, dir}
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
release := make(chan struct{})
|
||||||
|
calls := 0
|
||||||
|
cli := &probeClient{
|
||||||
|
create: func(_opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||||
|
calls++
|
||||||
|
if calls == 1 {
|
||||||
|
<-release
|
||||||
|
return mobyclient.ContainerCreateResult{}, ctx.Err()
|
||||||
|
} else {
|
||||||
|
return mobyclient.ContainerCreateResult{ID: "probe"}, nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stat: func(_path string) error { return nil },
|
||||||
|
remove: func(_ctx context.Context, _id string, _opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
||||||
|
return mobyclient.ContainerRemoveResult{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
leader, follower := make(chan result, 1), make(chan result, 1)
|
||||||
|
go func() {
|
||||||
|
_, seen, err := probeDockerProxyDirectory(ctx, cli, key, socket)
|
||||||
|
leader <- result{seen, err}
|
||||||
|
}()
|
||||||
|
synctest.Wait()
|
||||||
|
go func() {
|
||||||
|
_, seen, err := probeDockerProxyDirectory(t.Context(), cli, key, socket)
|
||||||
|
follower <- result{seen, err}
|
||||||
|
}()
|
||||||
|
synctest.Wait()
|
||||||
|
assert.Equal(t, 1, calls)
|
||||||
|
cancel()
|
||||||
|
close(release)
|
||||||
|
leaderResult, followerResult := <-leader, <-follower
|
||||||
|
require.ErrorIs(t, leaderResult.err, context.Canceled)
|
||||||
|
assert.False(t, leaderResult.seen)
|
||||||
|
require.NoError(t, followerResult.err)
|
||||||
|
assert.True(t, followerResult.seen)
|
||||||
|
assert.Equal(t, 2, calls)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDaemonSeesDir(t *testing.T) {
|
func TestDaemonSeesDir(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
markers := make(map[string]bool)
|
markers := make(map[string]bool)
|
||||||
@@ -520,9 +654,14 @@ func TestDaemonSeesDir(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
removed := false
|
removed := false
|
||||||
|
var attemptedImages []string
|
||||||
cli := &probeClient{
|
cli := &probeClient{
|
||||||
create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||||
assert.Equal(t, []mount.Mount{{Type: mount.TypeBind, Source: testCase.daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}}, opts.HostConfig.Mounts)
|
assert.Equal(t, []mount.Mount{{Type: mount.TypeBind, Source: testCase.daemonDir, Target: "/gitea-runner-probe", ReadOnly: true}}, opts.HostConfig.Mounts)
|
||||||
|
attemptedImages = append(attemptedImages, opts.Config.Image)
|
||||||
|
if opts.Config.Image == "removed-image" {
|
||||||
|
return mobyclient.ContainerCreateResult{}, cerrdefs.ErrNotFound
|
||||||
|
}
|
||||||
cancel()
|
cancel()
|
||||||
if _, err := os.Stat(testCase.daemonDir); err != nil {
|
if _, err := os.Stat(testCase.daemonDir); err != nil {
|
||||||
return mobyclient.ContainerCreateResult{}, cerrdefs.ErrInvalidArgument
|
return mobyclient.ContainerCreateResult{}, cerrdefs.ErrInvalidArgument
|
||||||
@@ -550,6 +689,7 @@ func TestDaemonSeesDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
seen, err := daemonSeesDir(ctx, cli, dir, testCase.daemonDir)
|
seen, err := daemonSeesDir(ctx, cli, dir, testCase.daemonDir)
|
||||||
require.ErrorIs(t, err, testCase.removeErr)
|
require.ErrorIs(t, err, testCase.removeErr)
|
||||||
|
assert.Equal(t, []string{"removed-image", "probe-image"}, attemptedImages)
|
||||||
assert.Equal(t, testCase.seen, seen)
|
assert.Equal(t, testCase.seen, seen)
|
||||||
assert.Equal(t, testCase.removed, removed)
|
assert.Equal(t, testCase.removed, removed)
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func TestSSHArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDialCommandStreamsStdioAndReportsExit(t *testing.T) {
|
func TestDialCommandStreamsStdioAndReportsExit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if _, err := exec.LookPath("sh"); err != nil {
|
if _, err := exec.LookPath("sh"); err != nil {
|
||||||
t.Skip("needs a POSIX shell")
|
t.Skip("needs a POSIX shell")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,12 +225,16 @@ func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) {
|
|||||||
return ppty, tty, nil
|
return ppty, tty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeKeepAlive(ppty io.Writer) {
|
func writeKeepAlive(ctx context.Context, ppty io.Writer) {
|
||||||
c := 1
|
for ctx.Err() == nil {
|
||||||
var err error
|
if count, err := ppty.Write([]byte{4}); count != 1 || err != nil {
|
||||||
for c == 1 && err == nil {
|
return
|
||||||
c, err = ppty.Write([]byte{4})
|
}
|
||||||
<-time.After(time.Second)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +306,7 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
defer finishLog()
|
defer finishLog()
|
||||||
_, _ = io.Copy(writer, ppty)
|
_, _ = io.Copy(writer, ppty)
|
||||||
}()
|
}()
|
||||||
go writeKeepAlive(ppty)
|
go writeKeepAlive(logctx, ppty)
|
||||||
}
|
}
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -79,42 +80,48 @@ func TestGetContainerArchive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentExecExitCode(t *testing.T) {
|
func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("uses POSIX shell")
|
t.Skip("uses POSIX shell")
|
||||||
}
|
}
|
||||||
for name, allocatePTY := range map[string]bool{"without PTY": false, "with PTY": true} {
|
for name, allocatePTY := range map[string]bool{"without PTY": false, "with PTY": true} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
reader, writer := io.Pipe()
|
t.Parallel()
|
||||||
defer reader.Close()
|
synctest.Test(t, func(t *testing.T) {
|
||||||
e := &HostEnvironment{Path: t.TempDir(), StdOut: writer, AllocatePTY: allocatePTY}
|
reader, writer := io.Pipe()
|
||||||
t.Cleanup(func() { assert.NoError(t, e.Remove()(t.Context())) })
|
defer reader.Close()
|
||||||
|
e := &HostEnvironment{Path: t.TempDir(), StdOut: writer, AllocatePTY: allocatePTY}
|
||||||
|
t.Cleanup(func() { assert.NoError(t, e.Remove()(t.Context())) })
|
||||||
|
|
||||||
result := make(chan error, 1)
|
result := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
result <- e.Exec([]string{"sh", "-c", "printf 'failed\\n'; exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(t.Context())
|
result <- e.Exec([]string{"sh", "-c", "printf 'failed\\n'; exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(t.Context())
|
||||||
}()
|
}()
|
||||||
first := make([]byte, 1)
|
first := make([]byte, 1)
|
||||||
_, err := io.ReadFull(reader, first)
|
_, err := io.ReadFull(reader, first)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
select {
|
synctest.Wait()
|
||||||
case err := <-result:
|
select {
|
||||||
t.Fatalf("Exec returned before its output was consumed: %v", err)
|
case err := <-result:
|
||||||
case <-time.After(100 * time.Millisecond):
|
t.Fatalf("Exec returned before its output was consumed: %v", err)
|
||||||
}
|
default:
|
||||||
remaining, err := io.ReadAll(reader)
|
}
|
||||||
require.NoError(t, err)
|
remaining, err := io.ReadAll(reader)
|
||||||
err = <-result
|
require.NoError(t, err)
|
||||||
var exitErr ExitCodeError
|
err = <-result
|
||||||
require.ErrorAs(t, err, &exitErr)
|
var exitErr ExitCodeError
|
||||||
assert.Equal(t, ExitCodeError(3), exitErr)
|
require.ErrorAs(t, err, &exitErr)
|
||||||
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
assert.Equal(t, ExitCodeError(3), exitErr)
|
||||||
assert.Equal(t, "failed\n", string(first)+string(remaining))
|
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||||
|
assert.Equal(t, "failed\n", string(first)+string(remaining))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("uses POSIX shell")
|
t.Skip("uses POSIX shell")
|
||||||
}
|
}
|
||||||
@@ -127,6 +134,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
|||||||
{name: "on", allocPTY: true, expect: "TTY"},
|
{name: "on", allocPTY: true, expect: "TTY"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
e := &HostEnvironment{
|
e := &HostEnvironment{
|
||||||
Path: t.TempDir(),
|
Path: t.TempDir(),
|
||||||
@@ -168,63 +176,67 @@ func TestHostEnvironmentRemoveWorkdir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveAllWithContextDoesNotHangOnStuckDelete(t *testing.T) {
|
func TestRemoveAllWithContextDoesNotHangOnStuckDelete(t *testing.T) {
|
||||||
release := make(chan struct{})
|
synctest.Test(t, func(t *testing.T) {
|
||||||
stubDone := make(chan struct{})
|
release := make(chan struct{})
|
||||||
|
stubDone := make(chan struct{})
|
||||||
|
|
||||||
orig := removeAll
|
orig := removeAll
|
||||||
removeAll = func(string) error {
|
removeAll = func(string) error {
|
||||||
defer close(stubDone)
|
defer close(stubDone)
|
||||||
<-release
|
<-release
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
close(release)
|
close(release)
|
||||||
<-stubDone
|
<-stubDone
|
||||||
removeAll = orig
|
removeAll = orig
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := removeAllWithContext(ctx, t.TempDir())
|
||||||
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := removeAllWithContext(ctx, t.TempDir())
|
|
||||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveDoesNotHang(t *testing.T) {
|
func TestHostEnvironmentRemoveDoesNotHang(t *testing.T) {
|
||||||
for _, phase := range []string{"CleanUp callback", "path removal"} {
|
for _, phase := range []string{"CleanUp callback", "path removal"} {
|
||||||
t.Run(phase, func(t *testing.T) {
|
t.Run(phase, func(t *testing.T) {
|
||||||
origTimeout := hostCleanupTimeout
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hostCleanupTimeout = 100 * time.Millisecond
|
origTimeout := hostCleanupTimeout
|
||||||
t.Cleanup(func() { hostCleanupTimeout = origTimeout })
|
hostCleanupTimeout = 100 * time.Millisecond
|
||||||
release, stubDone := make(chan struct{}), make(chan struct{})
|
t.Cleanup(func() { hostCleanupTimeout = origTimeout })
|
||||||
block := func() {
|
release, stubDone := make(chan struct{}), make(chan struct{})
|
||||||
defer close(stubDone)
|
block := func() {
|
||||||
<-release
|
defer close(stubDone)
|
||||||
}
|
<-release
|
||||||
e := &HostEnvironment{Path: filepath.Join(t.TempDir(), "misc", "hostexecutor"), StdOut: os.Stdout}
|
}
|
||||||
if phase == "CleanUp callback" {
|
e := &HostEnvironment{Path: filepath.Join(t.TempDir(), "misc", "hostexecutor"), StdOut: os.Stdout}
|
||||||
e.CleanUp = block
|
if phase == "CleanUp callback" {
|
||||||
} else {
|
e.CleanUp = block
|
||||||
origRemoveAll := removeAll
|
} else {
|
||||||
removeAll = func(string) error {
|
origRemoveAll := removeAll
|
||||||
block()
|
removeAll = func(string) error {
|
||||||
return nil
|
block()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { removeAll = origRemoveAll })
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
close(release)
|
||||||
|
<-stubDone
|
||||||
|
})
|
||||||
|
require.NoError(t, os.MkdirAll(e.Path, 0o700))
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- e.Remove()(common.WithLogger(t.Context(), logging.Default())) }()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.NoError(t, err)
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatalf("Remove() hung on a stuck %s", phase)
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { removeAll = origRemoveAll })
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
close(release)
|
|
||||||
<-stubDone
|
|
||||||
})
|
})
|
||||||
require.NoError(t, os.MkdirAll(e.Path, 0o700))
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() { done <- e.Remove()(common.WithLogger(t.Context(), logging.Default())) }()
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
require.NoError(t, err)
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
t.Fatalf("Remove() hung on a stuck %s", phase)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func execFailure(t *testing.T, code int, status string) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fakeKubernetes(t *testing.T, podStatus string) (string, *fakeCluster) {
|
func fakeKubernetes(t *testing.T, podStatus string, env ...string) (string, *fakeCluster) {
|
||||||
cluster := &fakeCluster{requests: map[string][]string{}}
|
cluster := &fakeCluster{requests: map[string][]string{}}
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get("Authorization") != "Bearer test-token" {
|
if r.Header.Get("Authorization") != "Bearer test-token" {
|
||||||
@@ -109,7 +109,7 @@ func fakeKubernetes(t *testing.T, podStatus string) (string, *fakeCluster) {
|
|||||||
if failure := cluster.nextExecFailure(); failure != nil {
|
if failure := cluster.nextExecFailure(); failure != nil {
|
||||||
failure(w, r)
|
failure(w, r)
|
||||||
} else {
|
} else {
|
||||||
serveLocalExec(t, w, r)
|
serveLocalExec(t, w, r, env)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(r.URL.Path, "/log"):
|
case strings.HasSuffix(r.URL.Path, "/log"):
|
||||||
_, _ = io.WriteString(w, "log of "+r.URL.Query().Get("container"))
|
_, _ = io.WriteString(w, "log of "+r.URL.Query().Get("container"))
|
||||||
@@ -132,7 +132,7 @@ users: [{name: test, user: {tokenFile: token}}]
|
|||||||
return kubeconfig, cluster
|
return kubeconfig, cluster
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLocalExec(t *testing.T, w http.ResponseWriter, r *http.Request) {
|
func serveLocalExec(t *testing.T, w http.ResponseWriter, r *http.Request, env []string) {
|
||||||
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{execProtocol}})
|
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{execProtocol}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
@@ -154,7 +154,7 @@ func serveLocalExec(t *testing.T, w http.ResponseWriter, r *http.Request) {
|
|||||||
stdin.Write(frame[1:])
|
stdin.Write(frame[1:])
|
||||||
}
|
}
|
||||||
cmd := exec.Command(command[0], command[1:]...)
|
cmd := exec.Command(command[0], command[1:]...)
|
||||||
cmd.Env = append(os.Environ(), "COPYFILE_DISABLE=1")
|
cmd.Env = append(append(os.Environ(), "COPYFILE_DISABLE=1"), env...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = &stdin, &stdout, &stderr
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = &stdin, &stdout, &stderr
|
||||||
status := `{"status":"Success"}`
|
status := `{"status":"Success"}`
|
||||||
@@ -183,15 +183,17 @@ func TestKubernetesServiceHostnames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("uses POSIX shell")
|
t.Skip("uses POSIX shell")
|
||||||
}
|
}
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
kubeconfig, cluster := fakeKubernetes(t, `{"spec":{"containers":[{},{}]},"status":{"phase":"Running","containerStatuses":[
|
|
||||||
{"name":"job","ready":true,"state":{"running":{}}},
|
|
||||||
{"name":"my-db","ready":true,"state":{"running":{}}}]}}`)
|
|
||||||
workdir, err := filepath.EvalSymlinks(t.TempDir())
|
workdir, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
path := workdir + ":" + os.Getenv("PATH")
|
||||||
|
kubeconfig, cluster := fakeKubernetes(t, `{"spec":{"containers":[{},{}]},"status":{"phase":"Running","containerStatuses":[
|
||||||
|
{"name":"job","ready":true,"state":{"running":{}}},
|
||||||
|
{"name":"my-db","ready":true,"state":{"running":{}}}]}}`, "PATH="+path)
|
||||||
options := KubernetesOptions{Kubeconfig: kubeconfig, RunnerUUID: "runner-uuid", PodTemplates: []map[string]any{
|
options := KubernetesOptions{Kubeconfig: kubeconfig, RunnerUUID: "runner-uuid", PodTemplates: []map[string]any{
|
||||||
{"spec": map[string]any{
|
{"spec": map[string]any{
|
||||||
"automountServiceAccountToken": true,
|
"automountServiceAccountToken": true,
|
||||||
@@ -252,7 +254,6 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
|||||||
assert.Contains(t, cluster.requests["POST /api/v1/namespaces/ci/pods"][0], `"args":["postgres","-D","$$(PGDATA)"]`)
|
assert.Contains(t, cluster.requests["POST /api/v1/namespaces/ci/pods"][0], `"args":["postgres","-D","$$(PGDATA)"]`)
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(workdir, "env"), []byte("#!/bin/sh\necho \"$@\" > \"$0-args\"\nexec /usr/bin/env \"$@\"\n"), 0o755))
|
require.NoError(t, os.WriteFile(filepath.Join(workdir, "env"), []byte("#!/bin/sh\necho \"$@\" > \"$0-args\"\nexec /usr/bin/env \"$@\"\n"), 0o755))
|
||||||
t.Setenv("PATH", workdir+":"+os.Getenv("PATH"))
|
|
||||||
cluster.failExec(
|
cluster.failExec(
|
||||||
execFailure(t, http.StatusInternalServerError, `{"message":"error dialing backend: remote error: tls: internal error"}`),
|
execFailure(t, http.StatusInternalServerError, `{"message":"error dialing backend: remote error: tls: internal error"}`),
|
||||||
execFailure(t, 0, `{"status":"Failure","reason":"InternalError","message":"Internal error occurred: error sending request: remote error: tls: internal error"}`),
|
execFailure(t, 0, `{"status":"Failure","reason":"InternalError","message":"Internal error occurred: error sending request: remote error: tls: internal error"}`),
|
||||||
@@ -300,7 +301,7 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
|||||||
|
|
||||||
pathEnv := map[string]string{}
|
pathEnv := map[string]string{}
|
||||||
require.NoError(t, job.UpdateFromImageEnv(&pathEnv)(ctx))
|
require.NoError(t, job.UpdateFromImageEnv(&pathEnv)(ctx))
|
||||||
assert.Equal(t, os.Getenv("PATH"), pathEnv["PATH"])
|
assert.Equal(t, path, pathEnv["PATH"])
|
||||||
|
|
||||||
info, err := service.Inspect(ctx)
|
info, err := service.Inspect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package container
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -39,12 +40,19 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string, windows b
|
|||||||
s := bufio.NewScanner(decoded)
|
s := bufio.NewScanner(decoded)
|
||||||
// Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB.
|
// Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB.
|
||||||
s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||||
|
scanned := 0
|
||||||
s.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
s.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
||||||
advance, token, err := bufio.ScanLines(data, atEOF)
|
if index := bytes.IndexByte(data[scanned:], '\n'); index >= 0 {
|
||||||
if token != nil {
|
advance := scanned + index + 1
|
||||||
return advance, data[:advance], err
|
scanned = 0
|
||||||
|
return advance, data[:advance], nil
|
||||||
}
|
}
|
||||||
return advance, token, err
|
scanned = len(data)
|
||||||
|
if atEOF && len(data) > 0 {
|
||||||
|
scanned = 0
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
})
|
})
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
line := trimEnvLineEnding(s.Text(), windows)
|
line := trimEnvLineEnding(s.Text(), windows)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func newTestHostEnv(t *testing.T) (*HostEnvironment, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEnvFile(t *testing.T) {
|
func TestParseEnvFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
largeValue := strings.Repeat("x", 2*1024*1024)
|
largeValue := strings.Repeat("x", 2*1024*1024)
|
||||||
encodeUTF16 := func(endianness unicode.Endianness) string {
|
encodeUTF16 := func(endianness unicode.Endianness) string {
|
||||||
content, err := unicode.UTF16(endianness, unicode.UseBOM).NewEncoder().Bytes([]byte("FOO=bar\r\nMULTI<<EOF\r\nline1\r\nEOF\r\n"))
|
content, err := unicode.UTF16(endianness, unicode.UseBOM).NewEncoder().Bytes([]byte("FOO=bar\r\nMULTI<<EOF\r\nline1\r\nEOF\r\n"))
|
||||||
@@ -66,6 +67,7 @@ func TestParseEnvFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
e, envPath := newTestHostEnv(t)
|
e, envPath := newTestHostEnv(t)
|
||||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\n"+strings.Repeat("x", 17*1024*1024)+"\nEOF\n"), 0o600))
|
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\n"+strings.Repeat("x", 17*1024*1024)+"\nEOF\n"), 0o600))
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ var (
|
|||||||
|
|
||||||
type pinnedKey struct{}
|
type pinnedKey struct{}
|
||||||
|
|
||||||
// WithPinnedCheckout keeps a run's sha and ref fixed when HEAD moves.
|
// WithPinnedCheckout keeps a run's sha, ref and repository fixed when HEAD moves, in pins unless ctx has some.
|
||||||
func WithPinnedCheckout(ctx context.Context) context.Context {
|
func WithPinnedCheckout(ctx context.Context, pins *sync.Map) context.Context {
|
||||||
if ctx.Value(pinnedKey{}) != nil {
|
if ctx.Value(pinnedKey{}) != nil {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
return context.WithValue(ctx, pinnedKey{}, &sync.Map{})
|
return context.WithValue(ctx, pinnedKey{}, pins)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pinned(ctx context.Context, key string, lookup func() (string, error)) (string, error) {
|
func pinned(ctx context.Context, key string, lookup func() (string, error)) (string, error) {
|
||||||
@@ -148,7 +148,7 @@ func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
|||||||
// remote in repoPath when it is not set yet, and derives its owner.
|
// remote in repoPath when it is not set yet, and derives its owner.
|
||||||
func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
||||||
if ghc.Repository == "" {
|
if ghc.Repository == "" {
|
||||||
repo, err := findGithubRepo(ctx, repoPath)
|
repo, err := pinned(ctx, "repo:"+repoPath, func() (string, error) { return findGithubRepo(ctx, repoPath) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Warnf("unable to get git repo (repoPath: %v): %v", repoPath, err)
|
common.Logger(ctx).Warnf("unable to get git repo (repoPath: %v): %v", repoPath, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package ghcontext
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
@@ -164,8 +165,10 @@ func TestSetRef(t *testing.T) {
|
|||||||
func TestSetSha(t *testing.T) {
|
func TestSetSha(t *testing.T) {
|
||||||
oldFindGitRef := findGitRef
|
oldFindGitRef := findGitRef
|
||||||
oldFindGitRevision := findGitRevision
|
oldFindGitRevision := findGitRevision
|
||||||
|
oldFindGithubRepo := findGithubRepo
|
||||||
defer func() { findGitRef = oldFindGitRef }()
|
defer func() { findGitRef = oldFindGitRef }()
|
||||||
defer func() { findGitRevision = oldFindGitRevision }()
|
defer func() { findGitRevision = oldFindGitRevision }()
|
||||||
|
defer func() { findGithubRepo = oldFindGithubRepo }()
|
||||||
|
|
||||||
findGitRevision = func(_ctx context.Context, _file string) (string, error) {
|
findGitRevision = func(_ctx context.Context, _file string) (string, error) {
|
||||||
return "1234fakesha", nil
|
return "1234fakesha", nil
|
||||||
@@ -231,16 +234,19 @@ func TestSetSha(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("pinned checkout keeps the first sha and ref", func(t *testing.T) {
|
t.Run("pinned checkout keeps the first sha, ref and repository", func(t *testing.T) {
|
||||||
ctx := WithPinnedCheckout(t.Context())
|
ctx := WithPinnedCheckout(t.Context(), &sync.Map{})
|
||||||
for _, head := range []string{"first", "moved"} {
|
for _, head := range []string{"first", "moved"} {
|
||||||
findGitRevision = func(context.Context, string) (string, error) { return head, nil }
|
findGitRevision = func(context.Context, string) (string, error) { return head, nil }
|
||||||
findGitRef = func(context.Context, string) (string, error) { return "refs/heads/" + head, nil }
|
findGitRef = func(context.Context, string) (string, error) { return "refs/heads/" + head, nil }
|
||||||
|
findGithubRepo = func(context.Context, string) (string, error) { return "owner/" + head, nil }
|
||||||
ghc := &model.GithubContext{Event: map[string]any{}}
|
ghc := &model.GithubContext{Event: map[string]any{}}
|
||||||
SetSha(ctx, ghc, "/some/dir")
|
SetSha(ctx, ghc, "/some/dir")
|
||||||
SetRef(ctx, ghc, "/some/dir")
|
SetRef(ctx, ghc, "/some/dir")
|
||||||
|
SetRepositoryAndOwner(ctx, ghc, "/some/dir")
|
||||||
assert.Equal(t, "first", ghc.Sha)
|
assert.Equal(t, "first", ghc.Sha)
|
||||||
assert.Equal(t, "refs/heads/first", ghc.Ref)
|
assert.Equal(t, "refs/heads/first", ghc.Ref)
|
||||||
|
assert.Equal(t, "owner/first", ghc.Repository)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -315,9 +315,12 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
correctArchExists, err := ContainerImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
correctArchExists := anyArchExists
|
||||||
if err != nil {
|
if anyArchExists && rc.Config.ContainerArchitecture != "" && rc.Config.ContainerArchitecture != "any" {
|
||||||
return err
|
correctArchExists, err = ContainerImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if anyArchExists && !correctArchExists {
|
if anyArchExists && !correctArchExists {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func TestCompositeActionParity(t *testing.T) {
|
|||||||
return "", "", assert.AnError
|
return "", "", assert.AnError
|
||||||
})
|
})
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{GitHubInstance: "github.com", ActionCacheDir: t.TempDir()},
|
Config: &Config{GitHubInstance: "github.com", ActionCacheDir: t.TempDir(), PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
JobContainer: &jobContainerMock{},
|
JobContainer: &jobContainerMock{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -427,7 +428,7 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
|||||||
step := &stepActionRemote{
|
step := &stepActionRemote{
|
||||||
Step: &model.Step{ID: "1", Uses: "remote/action@v1", With: map[string]string{}},
|
Step: &model.Step{ID: "1", Uses: "remote/action@v1", With: map[string]string{}},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{Env: testCheckoutEnv()},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
@@ -468,6 +469,67 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
|||||||
assert.ErrorIs(t, <-done, context.Canceled)
|
assert.ErrorIs(t, <-done, context.Canceled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecAsDockerImageChecks(t *testing.T) {
|
||||||
|
originalExists, originalBuild, originalContainer := ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer = originalExists, originalBuild, originalContainer
|
||||||
|
})
|
||||||
|
ContainerNewContainer = func(*container.NewContainerInput) container.ExecutionsEnvironment { return fakeContainer{} }
|
||||||
|
|
||||||
|
for _, testcase := range []struct {
|
||||||
|
name, platform string
|
||||||
|
exists, matches, rebuild bool
|
||||||
|
wantBuild, wantRemove bool
|
||||||
|
wantChecks []string
|
||||||
|
}{
|
||||||
|
{name: "missing image", platform: "linux/arm64", wantBuild: true, wantChecks: []string{"any"}},
|
||||||
|
{name: "unspecified platform", exists: true, matches: true, wantChecks: []string{"any"}},
|
||||||
|
{name: "any platform", platform: "any", exists: true, matches: true, wantChecks: []string{"any"}},
|
||||||
|
{name: "matching platform", platform: "linux/arm64", exists: true, matches: true, wantChecks: []string{"any", "linux/arm64"}},
|
||||||
|
{name: "different platform", platform: "linux/arm64", exists: true, wantBuild: true, wantRemove: true, wantChecks: []string{"any", "linux/arm64"}},
|
||||||
|
{name: "forced rebuild", exists: true, matches: true, rebuild: true, wantBuild: true, wantChecks: []string{"any"}},
|
||||||
|
} {
|
||||||
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
var checks []string
|
||||||
|
ContainerImageExistsLocally = func(_ctx context.Context, _image, platform string) (bool, error) {
|
||||||
|
checks = append(checks, platform)
|
||||||
|
return testcase.exists && (platform == "any" || testcase.matches), nil
|
||||||
|
}
|
||||||
|
var built, removed bool
|
||||||
|
ContainerNewDockerBuildExecutor = func(container.NewDockerBuildExecutorInput) common.Executor {
|
||||||
|
return func(context.Context) error { built = true; return nil }
|
||||||
|
}
|
||||||
|
fakeDockerDaemon(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method == http.MethodDelete {
|
||||||
|
removed = true
|
||||||
|
_, _ = io.WriteString(writer, `[]`)
|
||||||
|
} else {
|
||||||
|
_, _ = io.WriteString(writer, `{"Architecture":"aarch64"}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{ID: "action"},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{
|
||||||
|
PresetGitHubContext: &model.GithubContext{},
|
||||||
|
ContainerArchitecture: testcase.platform,
|
||||||
|
ForceRebuild: testcase.rebuild,
|
||||||
|
},
|
||||||
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
|
JobContainer: &jobContainerMock{},
|
||||||
|
},
|
||||||
|
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
||||||
|
env: map[string]string{},
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
require.NoError(t, execAsDocker(t.Context(), step, "action", dir, dir, false, stepStageMain))
|
||||||
|
assert.Equal(t, testcase.wantChecks, checks)
|
||||||
|
assert.Equal(t, testcase.wantBuild, built)
|
||||||
|
assert.Equal(t, testcase.wantRemove, removed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDockerActionImageTag(t *testing.T) {
|
func TestDockerActionImageTag(t *testing.T) {
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
"act-abc123-dockeraction:latest",
|
"act-abc123-dockeraction:latest",
|
||||||
@@ -519,6 +581,9 @@ func TestDockerActionImageTag(t *testing.T) {
|
|||||||
func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||||
orig := ContainerNewContainer
|
orig := ContainerNewContainer
|
||||||
defer func() { ContainerNewContainer = orig }()
|
defer func() { ContainerNewContainer = orig }()
|
||||||
|
fakeDockerDaemon(t, func(writer http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = io.WriteString(writer, `{"Architecture":"x86_64"}`)
|
||||||
|
})
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -16,10 +16,18 @@ import (
|
|||||||
|
|
||||||
var noopExecutor = func(context.Context) error { return nil }
|
var noopExecutor = func(context.Context) error { return nil }
|
||||||
|
|
||||||
|
type linuxEnvironment struct {
|
||||||
|
container.LinuxContainerEnvironmentExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (linuxEnvironment) GetRunnerContext(context.Context) map[string]any {
|
||||||
|
return map[string]any{"os": "Linux", "arch": "X64", "temp": "/tmp", "tool_cache": container.DefaultToolCache}
|
||||||
|
}
|
||||||
|
|
||||||
type containerMock struct {
|
type containerMock struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
container.Container
|
container.Container
|
||||||
container.LinuxContainerEnvironmentExtensions
|
linuxEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *containerMock) Create(capAdd, capDrop []string) common.Executor {
|
func (cm *containerMock) Create(capAdd, capDrop []string) common.Executor {
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ func createRunContext(t *testing.T) *RunContext {
|
|||||||
"os": {"Linux", "Windows"},
|
"os": {"Linux", "Windows"},
|
||||||
"foo": {"bar", "baz"},
|
"foo": {"bar", "baz"},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return &RunContext{
|
return &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
Workdir: ".",
|
Workdir: ".",
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
"CASE_INSENSITIVE_SECRET": "value",
|
"CASE_INSENSITIVE_SECRET": "value",
|
||||||
@@ -279,6 +280,7 @@ func TestEvaluateStep(t *testing.T) {
|
|||||||
func TestInterpolate(t *testing.T) {
|
func TestInterpolate(t *testing.T) {
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
Workdir: ".",
|
Workdir: ".",
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
"CASE_INSENSITIVE_SECRET": "value",
|
"CASE_INSENSITIVE_SECRET": "value",
|
||||||
@@ -444,9 +446,9 @@ jobs:
|
|||||||
for _, value := range []string{"true", "false"} {
|
for _, value := range []string{"true", "false"} {
|
||||||
t.Run(value, func(t *testing.T) {
|
t.Run(value, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
parent, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"${{ 'runner' }}"}`})
|
parent, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"${{ 'runner' }}"}`})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
child, err := (&runnerImpl{config: &Config{}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
child, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, map[string]any{"flag": value == "true", "name": "${{ 'runner' }}", "unset": false}, child.workflowCallInputs)
|
assert.Equal(t, map[string]any{"flag": value == "true", "name": "${{ 'runner' }}", "unset": false}, child.workflowCallInputs)
|
||||||
assert.Equal(t, rawWith, job.RawWith)
|
assert.Equal(t, rawWith, job.RawWith)
|
||||||
@@ -525,7 +527,7 @@ jobs:
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
notCallable, err := model.ReadWorkflow(strings.NewReader("on: push\njobs:\n test: {}\n"))
|
notCallable, err := model.ReadWorkflow(strings.NewReader("on: push\njobs:\n test: {}\n"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
config := &Config{Secrets: map[string]string{"token": "${{ github.token }}"}}
|
config := &Config{Secrets: map[string]string{"token": "${{ github.token }}"}, PresetGitHubContext: &model.GithubContext{}}
|
||||||
resolve := func(callerJobID string, workflow *model.Workflow) *RunContext {
|
resolve := func(callerJobID string, workflow *model.Workflow) *RunContext {
|
||||||
parent, err := (&runnerImpl{config: config}).newRunContext(t.Context(), &model.Run{Workflow: callerWorkflow, JobID: callerJobID}, nil)
|
parent, err := (&runnerImpl{config: config}).newRunContext(t.Context(), &model.Run{Workflow: callerWorkflow, JobID: callerJobID}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -552,7 +554,7 @@ jobs:
|
|||||||
`))
|
`))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
runner := &runnerImpl{config: &Config{Secrets: map[string]string{"A": "s3cr3t-a", "B": "s3cr3t-b"}}}
|
runner := &runnerImpl{config: &Config{Secrets: map[string]string{"A": "s3cr3t-a", "B": "s3cr3t-b"}, PresetGitHubContext: &model.GithubContext{}}}
|
||||||
containerName := func(jobID string) string {
|
containerName := func(jobID string) string {
|
||||||
rc, err := runner.newRunContext(t.Context(), &model.Run{JobID: jobID, Workflow: workflow}, nil)
|
rc, err := runner.newRunContext(t.Context(), &model.Run{JobID: jobID, Workflow: workflow}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -60,3 +60,7 @@ func requireHostTools(t *testing.T, tools ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCheckoutEnv() map[string]string {
|
||||||
|
return map[string]string{"GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "refs/heads/main", "SHA_REF": "1234567890"}
|
||||||
|
}
|
||||||
@@ -285,7 +285,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
jobError := common.JobError(ctx)
|
jobError := common.JobError(ctx)
|
||||||
var err error
|
var err error
|
||||||
// always allow 1 min for stopping and removing the runner, even if we were cancelled
|
// always allow 1 min for stopping and removing the runner, even if we were cancelled
|
||||||
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), jobCleanupTimeout)
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), jobCleanupTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (jim *jobInfoMock) result(result string) {
|
|||||||
|
|
||||||
type jobContainerMock struct {
|
type jobContainerMock struct {
|
||||||
container.Container
|
container.Container
|
||||||
container.LinuxContainerEnvironmentExtensions
|
linuxEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jcm *jobContainerMock) ReplaceLogWriter(_, _ io.Writer) (io.Writer, io.Writer) {
|
func (jcm *jobContainerMock) ReplaceLogWriter(_, _ io.Writer) (io.Writer, io.Writer) {
|
||||||
@@ -290,7 +290,7 @@ func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
|
|||||||
Jobs: map[string]*model.Job{"test": {}},
|
Jobs: map[string]*model.Job{"test": {}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
}
|
}
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ func TestNewJobExecutor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
}
|
}
|
||||||
if tt.output != "" {
|
if tt.output != "" {
|
||||||
require.NoError(t, rc.Run.Job().RawOutputs.Encode(map[string]string{"bad": tt.output}))
|
require.NoError(t, rc.Run.Job().RawOutputs.Encode(map[string]string{"bad": tt.output}))
|
||||||
@@ -695,6 +695,7 @@ func TestSetJobResultMatrixContinueOnError(t *testing.T) {
|
|||||||
workflow := &model.Workflow{Name: "workflow1", Jobs: map[string]*model.Job{"job1": job}}
|
workflow := &model.Workflow{Name: "workflow1", Jobs: map[string]*model.Job{"job1": job}}
|
||||||
for _, experimental := range []bool{true, tolerated} {
|
for _, experimental := range []bool{true, tolerated} {
|
||||||
rc := newTestRC(workflow, map[string]any{"experimental": experimental})
|
rc := newTestRC(workflow, map[string]any{"experimental": experimental})
|
||||||
|
rc.Config.PresetGitHubContext = &model.GithubContext{}
|
||||||
setJobResult(ctx, rc, rc, false)
|
setJobResult(ctx, rc, rc, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1023,7 +1024,7 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
|
|||||||
|
|
||||||
func newTestRC(wf *model.Workflow, matrix map[string]any) *RunContext {
|
func newTestRC(wf *model.Workflow, matrix map[string]any) *RunContext {
|
||||||
return &RunContext{
|
return &RunContext{
|
||||||
Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
Config: &Config{Workdir: ".", Env: testCheckoutEnv(), PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
Env: map[string]string{},
|
Env: map[string]string{},
|
||||||
Matrix: matrix,
|
Matrix: matrix,
|
||||||
@@ -1039,6 +1040,7 @@ func makeTestRC(t *testing.T, jobYAML string) *RunContext {
|
|||||||
Name: "workflow1",
|
Name: "workflow1",
|
||||||
Jobs: map[string]*model.Job{"job1": job},
|
Jobs: map[string]*model.Job{"job1": job},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
rc.Config.PresetGitHubContext = &model.GithubContext{}
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (c *hookContainer) GetContainerArchive(context.Context, string) (io.ReadClo
|
|||||||
// newHookRunContext returns a RunContext and the context to run a hook with, whose logger is
|
// newHookRunContext returns a RunContext and the context to run a hook with, whose logger is
|
||||||
// silenced so the hook's job-log output does not reach the test output.
|
// silenced so the hook's job-log output does not reach the test output.
|
||||||
func newHookRunContext(jobContainer *hookContainer, config *Config) (*RunContext, context.Context) {
|
func newHookRunContext(jobContainer *hookContainer, config *Config) (*RunContext, context.Context) {
|
||||||
// Env is left nil so that it is built from the config, as it is for a real job.
|
config.PresetGitHubContext = &model.GithubContext{}
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: config,
|
Config: config,
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
job := workflow.Jobs["test"]
|
job := workflow.Jobs["test"]
|
||||||
rawStrategy := model.CloneYamlNode(job.RawStrategy)
|
rawStrategy := model.CloneYamlNode(job.RawStrategy)
|
||||||
require.NoError(t, (&runnerImpl{config: &Config{}}).NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "test"}}}}})(t.Context()))
|
require.NoError(t, (&runnerImpl{config: &Config{Env: testCheckoutEnv()}}).NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "test"}}}}})(t.Context()))
|
||||||
require.NotNil(t, job.Strategy)
|
require.NotNil(t, job.Strategy)
|
||||||
assert.False(t, job.Strategy.GetFailFast())
|
assert.False(t, job.Strategy.GetFailFast())
|
||||||
assert.Equal(t, 2, maxParallelFor(job.Strategy, 5))
|
assert.Equal(t, 2, maxParallelFor(job.Strategy, 5))
|
||||||
@@ -70,7 +70,7 @@ func TestNewPlanExecutorInvalidMatrix(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
JobID: "test",
|
JobID: "test",
|
||||||
}}}}}
|
}}}}}
|
||||||
runner := &runnerImpl{config: &Config{}}
|
runner := &runnerImpl{config: &Config{Env: testCheckoutEnv()}}
|
||||||
|
|
||||||
require.ErrorContains(t, runner.NewPlanExecutor(plan)(t.Context()), "could not get job matrix:")
|
require.ErrorContains(t, runner.NewPlanExecutor(plan)(t.Context()), "could not get job matrix:")
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func tempDirPath(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2EndToEnd(t *testing.T) {
|
func TestCacheServiceV2EndToEnd(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireHostTools(t, "node")
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
restore := patchedAction(t, "actions/cache", actionsCacheRef, "dist/restore/index.js")
|
restore := patchedAction(t, "actions/cache", actionsCacheRef, "dist/restore/index.js")
|
||||||
@@ -222,6 +223,7 @@ func TestPatchedBundleAcrossActions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUploadArtifactThroughTheResultsService(t *testing.T) {
|
func TestUploadArtifactThroughTheResultsService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireHostTools(t, "node")
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
var called []string
|
var called []string
|
||||||
@@ -290,6 +292,7 @@ func TestUploadArtifactThroughTheResultsService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetupActionFindsTheCacheService(t *testing.T) {
|
func TestSetupActionFindsTheCacheService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireHostTools(t, "node", "npm")
|
requireHostTools(t, "node", "npm")
|
||||||
|
|
||||||
setup := patchedAction(t, "actions/setup-node", "v7.1.0", "dist/setup/index.js")
|
setup := patchedAction(t, "actions/setup-node", "v7.1.0", "dist/setup/index.js")
|
||||||
|
|||||||
@@ -135,39 +135,36 @@ func gateOpened(body string) bool {
|
|||||||
// an ordinary Gitea host, the cache server for the service URL, and the results URL when there is
|
// an ordinary Gitea host, the cache server for the service URL, and the results URL when there is
|
||||||
// no cache server. Unpatched, the same bundle must still choose v1, or the patch proves nothing.
|
// no cache server. Unpatched, the same bundle must still choose v1, or the patch proves nothing.
|
||||||
func TestPatchedBundleBehavesInNode(t *testing.T) {
|
func TestPatchedBundleBehavesInNode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireHostTools(t, "node")
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
eval := func(t *testing.T, bundle, prelude, cacheURL string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
script := prelude + bundle + "\nprocess.stdout.write(getCacheServiceVersion()+' '+getCacheServiceURL())"
|
|
||||||
cmd := exec.CommandContext(t.Context(), "node", "-e", script)
|
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
"ACTIONS_CACHE_SERVICE_V2=true",
|
|
||||||
"ACTIONS_CACHE_URL="+cacheURL,
|
|
||||||
"ACTIONS_RESULTS_URL=https://gitea.example",
|
|
||||||
"GITHUB_SERVER_URL=https://gitea.example",
|
|
||||||
)
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
require.NoError(t, err, "%s", out)
|
|
||||||
return string(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct{ name, bundle, prelude string }{
|
for _, tc := range []struct{ name, bundle, prelude string }{
|
||||||
{"tsc", gateTSC, ""},
|
{"tsc", gateTSC, ""},
|
||||||
{"webpack", gateWebpack, "const getCacheServiceVersion=()=>config_getCacheServiceVersion();"},
|
{"webpack", gateWebpack, "const getCacheServiceVersion=()=>config_getCacheServiceVersion();"},
|
||||||
{"esbuild", gateEsbuild, "var vu={};const getCacheServiceVersion=()=>XK(),getCacheServiceURL=()=>YK();"},
|
{"esbuild", gateEsbuild, "var vu={};const getCacheServiceVersion=()=>XK(),getCacheServiceURL=()=>YK();"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// Unpatched, a Gitea host is taken for GHES: v1, whose branch already reads the
|
|
||||||
// cache URL. The patch has to move the version without moving that.
|
|
||||||
assert.Equal(t, "v1 http://cache:8088/", eval(t, tc.bundle, tc.prelude, "http://cache:8088/"))
|
|
||||||
|
|
||||||
patched, ok := patchedBundle([]byte(tc.bundle))
|
patched, ok := patchedBundle([]byte(tc.bundle))
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
assert.Equal(t, "v2 http://cache:8088/", eval(t, string(patched), tc.prelude, "http://cache:8088/"))
|
evaluate := func(bundle string) string {
|
||||||
assert.Equal(t, "v2 https://gitea.example", eval(t, string(patched), tc.prelude, ""),
|
return "(() => {" + tc.prelude + bundle + "\nconsole.log(getCacheServiceVersion()+' '+getCacheServiceURL());})();\n"
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(t.Context(), "node", "-e", evaluate(tc.bundle)+evaluate(string(patched))+
|
||||||
|
"process.env.ACTIONS_CACHE_URL='';\n"+evaluate(string(patched)))
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"ACTIONS_CACHE_SERVICE_V2=true",
|
||||||
|
"ACTIONS_CACHE_URL=http://cache:8088/",
|
||||||
|
"ACTIONS_RESULTS_URL=https://gitea.example",
|
||||||
|
"GITHUB_SERVER_URL=https://gitea.example",
|
||||||
|
)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "%s", out)
|
||||||
|
results := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
|
||||||
|
require.Len(t, results, 3)
|
||||||
|
assert.Equal(t, "v1 http://cache:8088/", results[0])
|
||||||
|
assert.Equal(t, "v2 http://cache:8088/", results[1])
|
||||||
|
assert.Equal(t, "v2 https://gitea.example", results[2],
|
||||||
"with no cache server the results URL is still the fallback")
|
"with no cache server the results URL is still the fallback")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -177,6 +174,7 @@ func TestPatchedBundleBehavesInNode(t *testing.T) {
|
|||||||
// the gate inside a double-quoted literal. Rewriting the call rather than emptying its argument
|
// the gate inside a double-quoted literal. Rewriting the call rather than emptying its argument
|
||||||
// would end that string early and leave the bundle unparseable.
|
// would end that string early and leave the bundle unparseable.
|
||||||
func TestPatchedBundleSurvivesInsideAStringLiteral(t *testing.T) {
|
func TestPatchedBundleSurvivesInsideAStringLiteral(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
requireHostTools(t, "node")
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
escaped := strings.ReplaceAll(gateTSC, `"`, `\"`)
|
escaped := strings.ReplaceAll(gateTSC, `"`, `\"`)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
// ref (branch/tag) must reflect the new tip on every invocation, not stay pinned
|
// ref (branch/tag) must reflect the new tip on every invocation, not stay pinned
|
||||||
// to the cache populated on the first run.
|
// to the cache populated on the first run.
|
||||||
func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if _, err := exec.LookPath("git"); err != nil {
|
if _, err := exec.LookPath("git"); err != nil {
|
||||||
t.Skip("git not available in PATH")
|
t.Skip("git not available in PATH")
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|||||||
gitMust(t, workDir, "push", "-u", "origin", "master")
|
gitMust(t, workDir, "push", "-u", "origin", "master")
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{Env: testCheckoutEnv()},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "j1",
|
JobID: "j1",
|
||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
@@ -149,6 +150,7 @@ func TestCloneRemoteReusableWorkflowSendsInstanceTokenAndClientCert(t *testing.T
|
|||||||
require.NoError(t, os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600))
|
require.NoError(t, os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600))
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
GitHubInstance: server.URL,
|
GitHubInstance: server.URL,
|
||||||
Secrets: map[string]string{"GITEA_TOKEN": "token"},
|
Secrets: map[string]string{"GITEA_TOKEN": "token"},
|
||||||
InsecureSkipTLS: true,
|
InsecureSkipTLS: true,
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ type RunContext struct {
|
|||||||
stepEnv map[string]string
|
stepEnv map[string]string
|
||||||
unsecureCommandErr error // refused ::set-env::/::add-path::, turned into a step failure
|
unsecureCommandErr error // refused ::set-env::/::add-path::, turned into a step failure
|
||||||
unsecureCommandMu sync.Mutex
|
unsecureCommandMu sync.Mutex
|
||||||
|
checkoutPins sync.Map // git lookups of contexts no run has pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
// serviceContainer pairs a service container with the workflow id that keys job.services.
|
// serviceContainer pairs a service container with the workflow id that keys job.services.
|
||||||
@@ -1539,6 +1540,7 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
|||||||
|
|
||||||
ghc.SetBaseAndHeadRef()
|
ghc.SetBaseAndHeadRef()
|
||||||
repoPath := rc.Config.Workdir
|
repoPath := rc.Config.Workdir
|
||||||
|
ctx = ghcontext.WithPinnedCheckout(ctx, &rc.checkoutPins)
|
||||||
ghcontext.SetRepositoryAndOwner(ctx, ghc, repoPath)
|
ghcontext.SetRepositoryAndOwner(ctx, ghc, repoPath)
|
||||||
if ghc.Ref == "" {
|
if ghc.Ref == "" {
|
||||||
ghcontext.SetRef(ctx, ghc, repoPath)
|
ghcontext.SetRef(ctx, ghc, repoPath)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func TestRunContext_EvalBool(t *testing.T) {
|
|||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
Workdir: ".",
|
Workdir: ".",
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
},
|
},
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
"SOMETHING_TRUE": "true",
|
"SOMETHING_TRUE": "true",
|
||||||
@@ -200,7 +201,7 @@ func startJobContainerInputs(t *testing.T, workflowYAML string, cfg *Config) []*
|
|||||||
|
|
||||||
cfg.Workdir = "/tmp"
|
cfg.Workdir = "/tmp"
|
||||||
cfg.ContainerNetworkMode = "host"
|
cfg.ContainerNetworkMode = "host"
|
||||||
cfg.Env = map[string]string{}
|
cfg.Env = testCheckoutEnv()
|
||||||
cfg.Secrets = map[string]string{}
|
cfg.Secrets = map[string]string{}
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
@@ -481,6 +482,7 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rc := newRunContext(&Config{
|
rc := newRunContext(&Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
ValidVolumes: []string{"**"},
|
ValidVolumes: []string{"**"},
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
"MAME": "/host/mame/roms",
|
"MAME": "/host/mame/roms",
|
||||||
@@ -777,7 +779,7 @@ func TestInterpolateOutputsIsPerMatrixComboKeepingNeedsOutputs(t *testing.T) {
|
|||||||
require.NoError(t, job.RawOutputs.Encode(job.Outputs))
|
require.NoError(t, job.RawOutputs.Encode(job.Outputs))
|
||||||
needed := &model.Job{Outputs: map[string]string{"o": "from-gitea"}}
|
needed := &model.Job{Outputs: map[string]string{"o": "from-gitea"}}
|
||||||
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job, "needed": needed}}}
|
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job, "needed": needed}}}
|
||||||
r := &runnerImpl{config: &Config{}}
|
r := &runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
_, err := r.newRunContext(ctx, &model.Run{JobID: "needed", Workflow: run.Workflow}, nil)
|
_, err := r.newRunContext(ctx, &model.Run{JobID: "needed", Workflow: run.Workflow}, nil)
|
||||||
@@ -804,7 +806,7 @@ func TestInterpolateOutputsFromExpression(t *testing.T) {
|
|||||||
|
|
||||||
job := &model.Job{RawOutputs: rawOutputs}
|
job := &model.Job{RawOutputs: rawOutputs}
|
||||||
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job}}}
|
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job}}}
|
||||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), run, nil)
|
rc, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}).newRunContext(t.Context(), run, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, rc.interpolateOutputs()(t.Context()))
|
require.NoError(t, rc.interpolateOutputs()(t.Context()))
|
||||||
@@ -956,6 +958,7 @@ func TestGetGithubContextRef(t *testing.T) {
|
|||||||
Config: &Config{
|
Config: &Config{
|
||||||
EventName: data.event,
|
EventName: data.event,
|
||||||
Workdir: "",
|
Workdir: "",
|
||||||
|
Env: map[string]string{"GITHUB_REPOSITORY": "owner/repo", "SHA_REF": "1234567890"},
|
||||||
},
|
},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
@@ -973,7 +976,7 @@ func TestGetGithubContextRef(t *testing.T) {
|
|||||||
|
|
||||||
func createIfTestRunContext(jobs map[string]*model.Job) *RunContext {
|
func createIfTestRunContext(jobs map[string]*model.Job) *RunContext {
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
Config: &Config{Workdir: ".", PresetGitHubContext: &model.GithubContext{}, PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "job1",
|
JobID: "job1",
|
||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
@@ -1210,7 +1213,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
workflow, err := model.ReadWorkflow(strings.NewReader("jobs: {job: {services: {postgres: {image: postgres:latest}}}}"))
|
workflow, err := model.ReadWorkflow(strings.NewReader("jobs: {job: {services: {postgres: {image: postgres:latest}}}}"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{ForcePull: true, ContainerNetworkMode: "host", Workdir: "/workspace"},
|
Config: &Config{ForcePull: true, ContainerNetworkMode: "host", Workdir: "/workspace", Env: testCheckoutEnv()},
|
||||||
Run: &model.Run{JobID: "job", Workflow: workflow},
|
Run: &model.Run{JobID: "job", Workflow: workflow},
|
||||||
platformImage: "node:20",
|
platformImage: "node:20",
|
||||||
}
|
}
|
||||||
@@ -1562,6 +1565,7 @@ func TestRunContextGetRunnerContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunContextUpdateExtraPath(t *testing.T) {
|
func TestRunContextUpdateExtraPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
longPath := strings.Repeat("x", 64*1024+1)
|
longPath := strings.Repeat("x", 64*1024+1)
|
||||||
for _, testcase := range []struct {
|
for _, testcase := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
|||||||
|
|
||||||
executor := common.NewPipelineExecutor(stagePipeline...).Then(handleFailure(plan))
|
executor := common.NewPipelineExecutor(stagePipeline...).Then(handleFailure(plan))
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
return executor(container.WithRunnerArch(ghcontext.WithPinnedCheckout(ctx)))
|
return executor(container.WithRunnerArch(ghcontext.WithPinnedCheckout(ctx, &sync.Map{})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/common/logging"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/ghcontext"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
@@ -26,12 +28,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
baseImage = "node:24-bookworm-slim"
|
baseImage = "node:24-bookworm-slim"
|
||||||
platforms map[string]string
|
platforms map[string]string
|
||||||
logLevel = logging.LevelDebug
|
logLevel = logging.LevelDebug
|
||||||
workdir = "testdata"
|
workdir = "testdata"
|
||||||
secrets map[string]string
|
secrets map[string]string
|
||||||
testRunID = strings.ToLower(rand.Text()[:8]) // keeps docker names apart from concurrent test processes on one daemon
|
testRunID = strings.ToLower(rand.Text()[:8]) // keeps docker names apart from concurrent test processes on one daemon
|
||||||
|
testCheckoutPins sync.Map
|
||||||
)
|
)
|
||||||
|
|
||||||
func mapPlatformPicker(platforms map[string]string) func([]string) string {
|
func mapPlatformPicker(platforms map[string]string) func([]string) string {
|
||||||
@@ -158,12 +161,12 @@ func TestNewPlanExecutorRequiresRunsOnBeforeRunningAnyJob(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob("test")
|
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob("test")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.ErrorContains(t, (&runnerImpl{config: &Config{}}).NewPlanExecutor(plan)(t.Context()), `job "test" requires a non-empty 'runs-on'`, runsOn)
|
require.ErrorContains(t, (&runnerImpl{config: &Config{Env: testCheckoutEnv()}}).NewPlanExecutor(plan)(t.Context()), `job "test" requires a non-empty 'runs-on'`, runsOn)
|
||||||
assert.Empty(t, workflow.Jobs["first"].Result, runsOn)
|
assert.Empty(t, workflow.Jobs["first"].Result, runsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
workflow := &model.Workflow{Jobs: map[string]*model.Job{"build": {Result: "success"}}}
|
workflow := &model.Workflow{Jobs: map[string]*model.Job{"build": {Result: "success"}}}
|
||||||
require.NoError(t, (&runnerImpl{config: &Config{}}).NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "build"}}}}})(t.Context()))
|
require.NoError(t, (&runnerImpl{config: &Config{Env: testCheckoutEnv()}}).NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "build"}}}}})(t.Context()))
|
||||||
assert.Equal(t, "success", workflow.Jobs["build"].Result)
|
assert.Equal(t, "success", workflow.Jobs["build"].Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +222,7 @@ type TestJobFileInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) {
|
func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) {
|
||||||
|
ctx = ghcontext.WithPinnedCheckout(ctx, &testCheckoutPins)
|
||||||
fmt.Printf("::group::%s\n", j.workflowPath) //nolint:forbidigo // pre-existing issue from nektos/act
|
fmt.Printf("::group::%s\n", j.workflowPath) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||||
|
|
||||||
logging.SetLevel(logLevel)
|
logging.SetLevel(logLevel)
|
||||||
@@ -481,7 +485,7 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDryrunEvent(t *testing.T) {
|
func TestDryrunEvent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Setenv("DOCKER_HOST", "://")
|
||||||
// Dryrun plans without containers or network (shells and local actions only).
|
// Dryrun plans without containers or network (shells and local actions only).
|
||||||
ctx := common.WithDryrun(t.Context(), true)
|
ctx := common.WithDryrun(t.Context(), true)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func TestStepActionLocalTest(t *testing.T) {
|
|||||||
ExprEval: &expressionEvaluator{},
|
ExprEval: &expressionEvaluator{},
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
Workdir: "/tmp",
|
Workdir: "/tmp",
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
},
|
},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
@@ -109,7 +110,7 @@ func TestStepActionPost(t *testing.T) {
|
|||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{GitHubInstance: "https://github.com", ActionCacheDir: t.TempDir()},
|
Config: &Config{GitHubInstance: "https://github.com", ActionCacheDir: t.TempDir(), PresetGitHubContext: &model.GithubContext{}},
|
||||||
JobContainer: cm,
|
JobContainer: cm,
|
||||||
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
||||||
StepResults: map[string]*model.StepResult{"step": {Conclusion: test.status}},
|
StepResults: map[string]*model.StepResult{"step": {Conclusion: test.status}},
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ func TestShortSHAActionRejected(t *testing.T) {
|
|||||||
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "push.yml"), []byte(workflow), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "push.yml"), []byte(workflow), 0o644))
|
||||||
|
|
||||||
runner, err := New(&Config{
|
runner, err := New(&Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
Workdir: workflowDir,
|
Workdir: workflowDir,
|
||||||
EventName: "push",
|
EventName: "push",
|
||||||
GitHubInstance: "github.com",
|
GitHubInstance: "github.com",
|
||||||
DefaultActionInstance: actionRoot,
|
DefaultActionInstance: actionRoot,
|
||||||
|
ActionCacheDir: t.TempDir(),
|
||||||
ContainerMaxLifetime: time.Hour,
|
ContainerMaxLifetime: time.Hour,
|
||||||
PlatformPicker: func([]string) string { return baseImage },
|
PlatformPicker: func([]string) string { return baseImage },
|
||||||
})
|
})
|
||||||
@@ -94,8 +96,9 @@ func TestStepActionRemote(t *testing.T) {
|
|||||||
sar := &stepActionRemote{
|
sar := &stepActionRemote{
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
GitHubInstance: "github.com",
|
GitHubInstance: "github.com",
|
||||||
ActionCacheDir: t.TempDir(),
|
ActionCacheDir: t.TempDir(),
|
||||||
|
PresetGitHubContext: &model.GithubContext{},
|
||||||
},
|
},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
@@ -134,7 +137,7 @@ func TestStepActionRemote(t *testing.T) {
|
|||||||
t.Run("refreshes composite inputs without leaking environment", func(t *testing.T) {
|
t.Run("refreshes composite inputs without leaking environment", func(t *testing.T) {
|
||||||
step := &stepActionRemote{
|
step := &stepActionRemote{
|
||||||
Step: &model.Step{Uses: "org/composite@v1", With: map[string]string{"SHARED": "first"}},
|
Step: &model.Step{Uses: "org/composite@v1", With: map[string]string{"SHARED": "first"}},
|
||||||
RunContext: &RunContext{Config: &Config{ActionCacheDir: t.TempDir()}, Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}}, JobContainer: &jobContainerMock{}},
|
RunContext: &RunContext{Config: &Config{ActionCacheDir: t.TempDir(), PresetGitHubContext: &model.GithubContext{}}, Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}}, JobContainer: &jobContainerMock{}},
|
||||||
action: &model.Action{Inputs: map[string]model.Input{"shared": {}}},
|
action: &model.Action{Inputs: map[string]model.Input{"shared": {}}},
|
||||||
env: map[string]string{},
|
env: map[string]string{},
|
||||||
remoteAction: mustNewRemoteAction(t, "org/composite@v1"),
|
remoteAction: mustNewRemoteAction(t, "org/composite@v1"),
|
||||||
@@ -161,7 +164,7 @@ func TestStepActionRemotePrepare(t *testing.T) {
|
|||||||
})
|
})
|
||||||
newJob := func(instance string) *RunContext {
|
newJob := func(instance string) *RunContext {
|
||||||
return &RunContext{
|
return &RunContext{
|
||||||
Config: &Config{GitHubInstance: instance, ActionCacheDir: t.TempDir()},
|
Config: &Config{GitHubInstance: instance, ActionCacheDir: t.TempDir(), PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
||||||
JobContainer: &jobContainerMock{},
|
JobContainer: &jobContainerMock{},
|
||||||
}
|
}
|
||||||
@@ -222,7 +225,7 @@ func TestStepActionRemotePreCleanup(t *testing.T) {
|
|||||||
sar := &stepActionRemote{
|
sar := &stepActionRemote{
|
||||||
Step: &model.Step{ID: "step", Uses: "remote/action@v1", If: yaml.Node{Value: "false"}},
|
Step: &model.Step{ID: "step", Uses: "remote/action@v1", If: yaml.Node{Value: "false"}},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
JobContainer: cm,
|
JobContainer: cm,
|
||||||
@@ -405,6 +408,7 @@ func TestStepActionRemotePreResolvesDownloadedCommit(t *testing.T) {
|
|||||||
Step: &model.Step{Uses: "actions/setup-go@main"},
|
Step: &model.Step{Uses: "actions/setup-go@main"},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
GitHubInstance: "https://gitea.example.com",
|
GitHubInstance: "https://gitea.example.com",
|
||||||
DefaultActionInstance: instance,
|
DefaultActionInstance: instance,
|
||||||
ActionCacheDir: t.TempDir(),
|
ActionCacheDir: t.TempDir(),
|
||||||
@@ -484,6 +488,7 @@ func TestStepActionRemoteCloneTokenSurvivesNilSecrets(t *testing.T) {
|
|||||||
Step: &model.Step{Uses: "org/repo@v1"},
|
Step: &model.Step{Uses: "org/repo@v1"},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
Env: testCheckoutEnv(),
|
||||||
GitHubInstance: tt.gitHubInstance,
|
GitHubInstance: tt.gitHubInstance,
|
||||||
DefaultActionInstance: tt.defaultActionInstance,
|
DefaultActionInstance: tt.defaultActionInstance,
|
||||||
ActionCacheDir: t.TempDir(),
|
ActionCacheDir: t.TempDir(),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func TestStepDockerMain(t *testing.T) {
|
|||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
|
PresetGitHubContext: &model.GithubContext{},
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
"DOCKER_USERNAME": "docker-user",
|
"DOCKER_USERNAME": "docker-user",
|
||||||
"DOCKER_PASSWORD": "docker-password",
|
"DOCKER_PASSWORD": "docker-password",
|
||||||
@@ -116,7 +117,8 @@ func TestStepDockerNewStepContainer(t *testing.T) {
|
|||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
AllocatePTY: test.allocPTY,
|
PresetGitHubContext: &model.GithubContext{},
|
||||||
|
AllocatePTY: test.allocPTY,
|
||||||
PlatformPicker: func(_platforms []string) string {
|
PlatformPicker: func(_platforms []string) string {
|
||||||
return test.platform
|
return test.platform
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func TestStepRun(t *testing.T) {
|
|||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
ExprEval: &expressionEvaluator{},
|
ExprEval: &expressionEvaluator{},
|
||||||
Config: &Config{},
|
Config: &Config{Env: testCheckoutEnv()},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
@@ -167,7 +167,7 @@ func TestStepRunShellParity(t *testing.T) {
|
|||||||
|
|
||||||
sr := &stepRun{
|
sr := &stepRun{
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
||||||
JobContainer: jobContainer,
|
JobContainer: jobContainer,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -128,9 +128,7 @@ func (sm *stepMock) getEnv() *map[string]string {
|
|||||||
func TestSetupEnv(t *testing.T) {
|
func TestSetupEnv(t *testing.T) {
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
Env: map[string]string{
|
Env: mergeMaps(testCheckoutEnv(), map[string]string{"GITHUB_RUN_ID": "runId"}),
|
||||||
"GITHUB_RUN_ID": "runId",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
@@ -224,7 +222,7 @@ jobs:
|
|||||||
for _, value := range []string{"first", "second"} {
|
for _, value := range []string{"first", "second"} {
|
||||||
t.Run(value, func(t *testing.T) {
|
t.Run(value, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, map[string]any{"env": `{"WORKFLOW":"` + value + `${{ github.job }}","PRIORITY":"workflow"}`, "priority": "${{ github.job }}", "defaults": `{"shell":"` + value + `"}`})
|
rc, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, map[string]any{"env": `{"WORKFLOW":"` + value + `${{ github.job }}","PRIORITY":"workflow"}`, "priority": "${{ github.job }}", "defaults": `{"shell":"` + value + `"}`})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rc.workflowCallInputs = map[string]any{"args": `{"name":"` + value + `${{ github.job }}","fetch-depth":1234567}`}
|
rc.workflowCallInputs = map[string]any{"args": `{"name":"` + value + `${{ github.job }}","fetch-depth":1234567}`}
|
||||||
enabled, err := rc.isEnabled(t.Context())
|
enabled, err := rc.isEnabled(t.Context())
|
||||||
@@ -252,7 +250,7 @@ jobs:
|
|||||||
t.Run("rejects inputs that still contain an expression", func(t *testing.T) {
|
t.Run("rejects inputs that still contain an expression", func(t *testing.T) {
|
||||||
workflow, err := model.ReadWorkflow(strings.NewReader("jobs:\n test:\n steps:\n - uses: ./act\n with: ${{ inputs.args }}\n"))
|
workflow, err := model.ReadWorkflow(strings.NewReader("jobs:\n test:\n steps:\n - uses: ./act\n with: ${{ inputs.args }}\n"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
rc, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rc.workflowCallInputs = map[string]any{"args": "${{ inputs.unresolved }}"}
|
rc.workflowCallInputs = map[string]any{"args": "${{ inputs.unresolved }}"}
|
||||||
require.ErrorContains(t, setupInputs(t.Context(), &stepRun{RunContext: rc, Step: workflow.Jobs["test"].Steps[0].Clone(), env: map[string]string{}}), "with:")
|
require.ErrorContains(t, setupInputs(t.Context(), &stepRun{RunContext: rc, Step: workflow.Jobs["test"].Steps[0].Clone(), env: map[string]string{}}), "with:")
|
||||||
@@ -288,7 +286,7 @@ func TestIsStepEnabled(t *testing.T) {
|
|||||||
t.Run(condition.expression, func(t *testing.T) {
|
t.Run(condition.expression, func(t *testing.T) {
|
||||||
step := &stepRun{
|
step := &stepRun{
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
},
|
},
|
||||||
@@ -308,7 +306,7 @@ func TestIsStepEnabled(t *testing.T) {
|
|||||||
t.Run("env and step inputs are unavailable at every stage", func(t *testing.T) {
|
t.Run("env and step inputs are unavailable at every stage", func(t *testing.T) {
|
||||||
step := &stepRun{
|
step := &stepRun{
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
Env: map[string]string{"INPUT_FORGED": "leaked"},
|
Env: map[string]string{"INPUT_FORGED": "leaked"},
|
||||||
},
|
},
|
||||||
@@ -340,7 +338,7 @@ func TestIsContinueOnError(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
step := &stepRun{
|
step := &stepRun{
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
},
|
},
|
||||||
Step: &model.Step{},
|
Step: &model.Step{},
|
||||||
@@ -363,7 +361,7 @@ func TestRunStepExecutorDoesNotLeakRefusalToNextStep(t *testing.T) {
|
|||||||
cm.On("UpdateFromEnv", mock.Anything, mock.Anything).Return(noopExecutor)
|
cm.On("UpdateFromEnv", mock.Anything, mock.Anything).Return(noopExecutor)
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{Env: map[string]string{}},
|
Config: &Config{Env: testCheckoutEnv()},
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
JobID: "1",
|
JobID: "1",
|
||||||
Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}},
|
Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}},
|
||||||
|
|||||||
@@ -53,33 +53,18 @@ runs:
|
|||||||
if [ "${{ inputs.test_input_required }}" != "test_input_required_value" ]; then
|
if [ "${{ inputs.test_input_required }}" != "test_input_required_value" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
if [ "${{ inputs.test_input_optional }}" != "test_input_optional_value" ]; then
|
if [ "${{ inputs.test_input_optional }}" != "test_input_optional_value" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
if [ "${{ inputs.test_input_optional_with_default_overriden }}" != "test_input_optional_with_default_overriden" ]; then
|
if [ "${{ inputs.test_input_optional_with_default_overriden }}" != "test_input_optional_with_default_overriden" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
if [ "${{ inputs.test_input_required_with_default }}" != "test_input_optional_value" ]; then
|
if [ "${{ inputs.test_input_required_with_default }}" != "test_input_optional_value" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
if [ "${{ inputs.test_input_required_with_default_overriden }}" != "test_input_required_with_default_overriden" ]; then
|
if [ "${{ inputs.test_input_required_with_default_overriden }}" != "test_input_required_with_default_overriden" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
if [ -z "$GITHUB_ACTION_PATH" ]; then
|
if [ -z "$GITHUB_ACTION_PATH" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
+38
-5
@@ -6,9 +6,11 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -30,9 +32,7 @@ import (
|
|||||||
func testKubernetes(t *testing.T) {
|
func testKubernetes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
k3sID, kubernetes := startK3s(t)
|
k3sID, kubernetes, images := startK3s(t)
|
||||||
|
|
||||||
images := preloadImages(t, k3sID, os.Getenv("E2E_JOB_IMAGE"), os.Getenv("SERVICE_IMAGE"))
|
|
||||||
|
|
||||||
api, repo := newScenario(t)
|
api, repo := newScenario(t)
|
||||||
if err := api.CreateVariable(ctx, repo, "E2E_SERVICE_IMAGE", images[1]); err != nil {
|
if err := api.CreateVariable(ctx, repo, "E2E_SERVICE_IMAGE", images[1]); err != nil {
|
||||||
@@ -51,7 +51,7 @@ func testKubernetes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startK3s(t *testing.T) (string, *config.Kubernetes) {
|
func startK3s(t *testing.T) (string, *config.Kubernetes, []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
cli := fixture.cli
|
cli := fixture.cli
|
||||||
@@ -93,6 +93,13 @@ func startK3s(t *testing.T) (string, *config.Kubernetes) {
|
|||||||
t.Fatalf("start k3s container: %v", err)
|
t.Fatalf("start k3s container: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containerdCtx, stopWaiting := context.WithTimeout(ctx, 2*time.Minute)
|
||||||
|
defer stopWaiting()
|
||||||
|
if err := waitForContainerLog(containerdCtx, created.ID, "containerd is now running"); err != nil {
|
||||||
|
t.Fatalf("wait for k3s containerd: %v", err)
|
||||||
|
}
|
||||||
|
images := preloadImages(t, created.ID, os.Getenv("E2E_JOB_IMAGE"), os.Getenv("SERVICE_IMAGE"))
|
||||||
|
|
||||||
waitCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
waitCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
for {
|
for {
|
||||||
@@ -139,7 +146,33 @@ func startK3s(t *testing.T) (string, *config.Kubernetes) {
|
|||||||
"hostnames": []any{giteaURL.Hostname()},
|
"hostnames": []any{giteaURL.Hostname()},
|
||||||
}}}}
|
}}}}
|
||||||
}
|
}
|
||||||
return created.ID, kubernetes
|
return created.ID, kubernetes, images
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForContainerLog(ctx context.Context, id, message string) error {
|
||||||
|
logs, err := fixture.cli.ContainerLogs(ctx, id, mobyclient.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer logs.Close()
|
||||||
|
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
defer reader.Close()
|
||||||
|
go func() {
|
||||||
|
_, err := stdcopy.StdCopy(writer, writer, logs)
|
||||||
|
_ = writer.CloseWithError(err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if strings.Contains(scanner.Text(), message) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("container exited before logging %q", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func preloadImages(t *testing.T, k3sID string, images ...string) []string {
|
func preloadImages(t *testing.T, k3sID string, images ...string) []string {
|
||||||
|
|||||||
+143
-134
@@ -9,6 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
@@ -216,33 +217,35 @@ func TestPollerReady(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPollerPausesAndResumesForLocalAvailability(t *testing.T) {
|
func TestPollerPausesAndResumesForLocalAvailability(t *testing.T) {
|
||||||
var fetches atomic.Int64
|
synctest.Test(t, func(t *testing.T) {
|
||||||
cli := mocks.NewClient(t)
|
var fetches atomic.Int64
|
||||||
cli.On("FetchTask", mock.Anything, mock.Anything).Maybe().Run(func(mock.Arguments) {
|
cli := mocks.NewClient(t)
|
||||||
fetches.Add(1)
|
cli.On("FetchTask", mock.Anything, mock.Anything).Maybe().Run(func(mock.Arguments) {
|
||||||
}).Return(&runnerv1.FetchTaskResponse{}, nil)
|
fetches.Add(1)
|
||||||
|
}).Return(&runnerv1.FetchTaskResponse{}, nil)
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
cfg, err := config.LoadDefault("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
||||||
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
||||||
runner := &availabilityRunner{reason: "low disk space"}
|
runner := &availabilityRunner{reason: "low disk space"}
|
||||||
poller := New(cfg, cli, runner)
|
poller := New(cfg, cli, runner)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Go(poller.Poll)
|
wg.Go(poller.Poll)
|
||||||
time.Sleep(40 * time.Millisecond)
|
<-time.After(40 * time.Millisecond)
|
||||||
assert.Zero(t, fetches.Load(), "an unavailable runner must not fetch a task")
|
assert.Zero(t, fetches.Load(), "an unavailable runner must not fetch a task")
|
||||||
|
|
||||||
runner.ready.Store(true)
|
runner.ready.Store(true)
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
return fetches.Load() > 0
|
return fetches.Load() > 0
|
||||||
}, time.Second, 10*time.Millisecond, "polling should resume after local recovery")
|
}, time.Second, 10*time.Millisecond, "polling should resume after local recovery")
|
||||||
|
|
||||||
shutdownCtx, cancel := context.WithTimeout(t.Context(), time.Second)
|
shutdownCtx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
require.NoError(t, poller.Shutdown(shutdownCtx))
|
require.NoError(t, poller.Shutdown(shutdownCtx))
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockRunner) Run(ctx context.Context, _ *runnerv1.Task) error {
|
func (m *mockRunner) Run(ctx context.Context, _ *runnerv1.Task) error {
|
||||||
@@ -276,36 +279,38 @@ func TestPollerCallsOnIdle(t *testing.T) {
|
|||||||
name = "PollOnce"
|
name = "PollOnce"
|
||||||
}
|
}
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
cli := mocks.NewClient(t)
|
synctest.Test(t, func(t *testing.T) {
|
||||||
cli.On("FetchTask", mock.Anything, mock.Anything).Return(&runnerv1.FetchTaskResponse{}, nil)
|
cli := mocks.NewClient(t)
|
||||||
|
cli.On("FetchTask", mock.Anything, mock.Anything).Return(&runnerv1.FetchTaskResponse{}, nil)
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
cfg, err := config.LoadDefault("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if !pollOnce {
|
if !pollOnce {
|
||||||
cfg.Runner.Capacity = 1
|
cfg.Runner.Capacity = 1
|
||||||
}
|
|
||||||
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
|
||||||
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
|
||||||
|
|
||||||
runner := &idleAwareRunner{}
|
|
||||||
poller := New(cfg, cli, runner)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Go(func() {
|
|
||||||
if pollOnce {
|
|
||||||
poller.PollOnce(func() {})
|
|
||||||
} else {
|
|
||||||
poller.Poll()
|
|
||||||
}
|
}
|
||||||
|
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
||||||
|
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
||||||
|
|
||||||
|
runner := &idleAwareRunner{}
|
||||||
|
poller := New(cfg, cli, runner)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Go(func() {
|
||||||
|
if pollOnce {
|
||||||
|
poller.PollOnce(func() {})
|
||||||
|
} else {
|
||||||
|
poller.Poll()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return runner.idleCalls.Load() > 0
|
||||||
|
}, time.Second, 10*time.Millisecond)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
require.NoError(t, poller.Shutdown(ctx))
|
||||||
|
wg.Wait()
|
||||||
})
|
})
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
return runner.idleCalls.Load() > 0
|
|
||||||
}, time.Second, 10*time.Millisecond)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
|
||||||
defer cancel()
|
|
||||||
require.NoError(t, poller.Shutdown(ctx))
|
|
||||||
wg.Wait()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,109 +328,113 @@ func TestPollerPollOnceCallsOnTaskFetchedBeforeRunningIt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPoller_ConcurrencyLimitedByCapacity(t *testing.T) {
|
func TestPoller_ConcurrencyLimitedByCapacity(t *testing.T) {
|
||||||
const (
|
synctest.Test(t, func(t *testing.T) {
|
||||||
capacity = 3
|
const (
|
||||||
totalTasks = 6
|
capacity = 3
|
||||||
taskDelay = 50 * time.Millisecond
|
totalTasks = 6
|
||||||
)
|
taskDelay = 50 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tasksReturned atomic.Int64
|
tasksReturned atomic.Int64
|
||||||
fetchConcur atomic.Int64
|
fetchConcur atomic.Int64
|
||||||
maxFetchConcur atomic.Int64
|
maxFetchConcur atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
cli := mocks.NewClient(t)
|
cli := mocks.NewClient(t)
|
||||||
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
|
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
|
||||||
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
|
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
|
||||||
atomicMax(&maxFetchConcur, fetchConcur.Add(1))
|
atomicMax(&maxFetchConcur, fetchConcur.Add(1))
|
||||||
defer fetchConcur.Add(-1)
|
defer fetchConcur.Add(-1)
|
||||||
|
|
||||||
n := tasksReturned.Add(1)
|
n := tasksReturned.Add(1)
|
||||||
if n <= totalTasks {
|
if n <= totalTasks {
|
||||||
return &runnerv1.FetchTaskResponse{
|
return &runnerv1.FetchTaskResponse{
|
||||||
Task: &runnerv1.Task{Id: n},
|
Task: &runnerv1.Task{Id: n},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return &runnerv1.FetchTaskResponse{}, nil
|
return &runnerv1.FetchTaskResponse{}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
runner := &mockRunner{delay: taskDelay}
|
runner := &mockRunner{delay: taskDelay}
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
cfg, err := config.LoadDefault("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cfg.Runner.Capacity = capacity
|
cfg.Runner.Capacity = capacity
|
||||||
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
||||||
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
||||||
|
|
||||||
poller := New(cfg, cli, runner)
|
poller := New(cfg, cli, runner)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Go(poller.Poll)
|
wg.Go(poller.Poll)
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
return runner.totalCompleted.Load() >= totalTasks
|
return runner.totalCompleted.Load() >= totalTasks
|
||||||
}, 2*time.Second, 10*time.Millisecond, "all tasks should complete")
|
}, 2*time.Second, 10*time.Millisecond, "all tasks should complete")
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = poller.Shutdown(ctx)
|
err = poller.Shutdown(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
assert.LessOrEqual(t, runner.maxConcurrent.Load(), int64(capacity),
|
assert.LessOrEqual(t, runner.maxConcurrent.Load(), int64(capacity),
|
||||||
"concurrent running tasks must not exceed capacity")
|
"concurrent running tasks must not exceed capacity")
|
||||||
assert.GreaterOrEqual(t, runner.maxConcurrent.Load(), int64(2),
|
assert.GreaterOrEqual(t, runner.maxConcurrent.Load(), int64(2),
|
||||||
"with 6 tasks and capacity 3, at least 2 should overlap")
|
"with 6 tasks and capacity 3, at least 2 should overlap")
|
||||||
assert.Equal(t, int64(1), maxFetchConcur.Load(),
|
assert.Equal(t, int64(1), maxFetchConcur.Load(),
|
||||||
"FetchTask must never be called concurrently (single poller)")
|
"FetchTask must never be called concurrently (single poller)")
|
||||||
assert.Equal(t, int64(totalTasks), runner.totalCompleted.Load(),
|
assert.Equal(t, int64(totalTasks), runner.totalCompleted.Load(),
|
||||||
"all tasks should have been executed")
|
"all tasks should have been executed")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPoller_ShutdownForcesJobsOnTimeout(t *testing.T) {
|
func TestPoller_ShutdownForcesJobsOnTimeout(t *testing.T) {
|
||||||
var served atomic.Bool
|
synctest.Test(t, func(t *testing.T) {
|
||||||
cli := mocks.NewClient(t)
|
var served atomic.Bool
|
||||||
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
|
cli := mocks.NewClient(t)
|
||||||
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
|
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
|
||||||
if served.CompareAndSwap(false, true) {
|
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
|
||||||
return &runnerv1.FetchTaskResponse{
|
if served.CompareAndSwap(false, true) {
|
||||||
Task: &runnerv1.Task{Id: 1},
|
return &runnerv1.FetchTaskResponse{
|
||||||
}, nil
|
Task: &runnerv1.Task{Id: 1},
|
||||||
}
|
}, nil
|
||||||
return &runnerv1.FetchTaskResponse{}, nil
|
}
|
||||||
},
|
return &runnerv1.FetchTaskResponse{}, nil
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
runner := &mockRunner{delay: 30 * time.Second}
|
runner := &mockRunner{delay: 30 * time.Second}
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
cfg, err := config.LoadDefault("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cfg.Runner.Capacity = 1
|
cfg.Runner.Capacity = 1
|
||||||
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
cfg.Runner.FetchInterval = 10 * time.Millisecond
|
||||||
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
cfg.Runner.FetchIntervalMax = 10 * time.Millisecond
|
||||||
|
|
||||||
poller := New(cfg, cli, runner)
|
poller := New(cfg, cli, runner)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Go(poller.Poll)
|
wg.Go(poller.Poll)
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
return runner.running.Load() == 1
|
return runner.running.Load() == 1
|
||||||
}, time.Second, 10*time.Millisecond, "task should start running")
|
}, time.Second, 10*time.Millisecond, "task should start running")
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err = poller.Shutdown(ctx)
|
err = poller.Shutdown(ctx)
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||||
assert.Less(t, elapsed, 5*time.Second,
|
assert.Less(t, elapsed, 5*time.Second,
|
||||||
"Shutdown must not block on the parked task; shutdownJobs() must run on timeout")
|
"Shutdown must not block on the parked task; shutdownJobs() must run on timeout")
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Equal(t, int64(1), runner.totalCompleted.Load(),
|
assert.Equal(t, int64(1), runner.totalCompleted.Load(),
|
||||||
"the parked task must be cancelled and unwound")
|
"the parked task must be cancelled and unwound")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/common/logging"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
@@ -198,34 +199,36 @@ func TestReporter_LogFlush(t *testing.T) {
|
|||||||
{"batch size", 10 * time.Second, 5, []string{"log line 0", "log line 1", "log line 2", "log line 3", "log line 4"}},
|
{"batch size", 10 * time.Second, 5, []string{"log line 0", "log line 1", "log line 2", "log line 3", "log line 4"}},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var updateLogCalls atomic.Int64
|
synctest.Test(t, func(t *testing.T) {
|
||||||
client := mocks.NewClient(t)
|
var updateLogCalls atomic.Int64
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(
|
client := mocks.NewClient(t)
|
||||||
func(_ctx context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(
|
||||||
updateLogCalls.Add(1)
|
func(_ctx context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
||||||
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
|
updateLogCalls.Add(1)
|
||||||
},
|
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
|
||||||
)
|
},
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(&runnerv1.UpdateTaskResponse{}, nil)
|
)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(&runnerv1.UpdateTaskResponse{}, nil)
|
||||||
|
|
||||||
cfg, _ := config.LoadDefault("")
|
cfg, _ := config.LoadDefault("")
|
||||||
cfg.Runner.LogReportInterval = 10 * time.Second
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
cfg.Runner.LogReportMaxLatency = tc.maxLatency
|
cfg.Runner.LogReportMaxLatency = tc.maxLatency
|
||||||
cfg.Runner.LogReportBatchSize = tc.batchSize
|
cfg.Runner.LogReportBatchSize = tc.batchSize
|
||||||
reporter := newTestReporter(t, client, cfg)
|
reporter := newTestReporter(t, client, cfg)
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
defer func() { require.NoError(t, reporter.Close("")) }()
|
defer func() { require.NoError(t, reporter.Close("")) }()
|
||||||
|
|
||||||
for _, message := range tc.messages {
|
for _, message := range tc.messages {
|
||||||
require.NoError(t, fire(reporter, logEntry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: message,
|
Message: message,
|
||||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return updateLogCalls.Load() > 0
|
return updateLogCalls.Load() > 0
|
||||||
}, 500*time.Millisecond, 10*time.Millisecond, "logs must flush before the periodic timers")
|
}, 500*time.Millisecond, 10*time.Millisecond, "logs must flush before the periodic timers")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,43 +316,45 @@ func TestReporter_StateChangedRestoredOnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_StateNotifyFlush(t *testing.T) {
|
func TestReporter_StateNotifyFlush(t *testing.T) {
|
||||||
var updateTaskCalls atomic.Int64
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Maybe().Return(
|
client.On("UpdateLog", mock.Anything, mock.Anything).Maybe().Return(
|
||||||
func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
||||||
return &runnerv1.UpdateLogResponse{
|
return &runnerv1.UpdateLogResponse{
|
||||||
AckIndex: req.Index + int64(len(req.Rows)),
|
AckIndex: req.Index + int64(len(req.Rows)),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
||||||
updateTaskCalls.Add(1)
|
updateTaskCalls.Add(1)
|
||||||
return &runnerv1.UpdateTaskResponse{}, nil
|
return &runnerv1.UpdateTaskResponse{}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg, _ := config.LoadDefault("")
|
cfg, _ := config.LoadDefault("")
|
||||||
cfg.Runner.StateReportInterval = 10 * time.Second
|
cfg.Runner.StateReportInterval = 10 * time.Second
|
||||||
cfg.Runner.LogReportInterval = 10 * time.Second
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
|
|
||||||
reporter := newTestReporter(t, client, cfg)
|
reporter := newTestReporter(t, client, cfg)
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = reporter.Close("")
|
_ = reporter.Close("")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.NoError(t, fire(reporter, logEntry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "step starting",
|
Message: "step starting",
|
||||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return updateTaskCalls.Load() > 0
|
return updateTaskCalls.Load() > 0
|
||||||
}, 500*time.Millisecond, 10*time.Millisecond,
|
}, 500*time.Millisecond, 10*time.Millisecond,
|
||||||
"step transition should have triggered immediate state flush via stateNotify")
|
"step transition should have triggered immediate state flush via stateNotify")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_CloseAlwaysSendsRowsWithNoMore(t *testing.T) {
|
func TestReporter_CloseAlwaysSendsRowsWithNoMore(t *testing.T) {
|
||||||
@@ -401,33 +406,35 @@ func TestReporter_CloseAlwaysSendsRowsWithNoMore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_StateHeartbeat(t *testing.T) {
|
func TestReporter_StateHeartbeat(t *testing.T) {
|
||||||
var updateTaskCalls atomic.Int64
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
||||||
updateTaskCalls.Add(1)
|
updateTaskCalls.Add(1)
|
||||||
return &runnerv1.UpdateTaskResponse{}, nil
|
return &runnerv1.UpdateTaskResponse{}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg, _ := config.LoadDefault("")
|
cfg, _ := config.LoadDefault("")
|
||||||
cfg.Runner.StateReportInterval = 50 * time.Millisecond
|
cfg.Runner.StateReportInterval = 50 * time.Millisecond
|
||||||
reporter := newTestReporter(t, client, cfg)
|
reporter := newTestReporter(t, client, cfg)
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
|
|
||||||
reporter.stateMu.Lock()
|
reporter.stateMu.Lock()
|
||||||
reporter.stateChanged = true
|
reporter.stateChanged = true
|
||||||
reporter.stateMu.Unlock()
|
reporter.stateMu.Unlock()
|
||||||
require.NoError(t, reporter.ReportState(false))
|
require.NoError(t, reporter.ReportState(false))
|
||||||
require.Equal(t, int64(1), updateTaskCalls.Load())
|
require.Equal(t, int64(1), updateTaskCalls.Load())
|
||||||
|
|
||||||
require.NoError(t, reporter.ReportState(false))
|
require.NoError(t, reporter.ReportState(false))
|
||||||
assert.Equal(t, int64(1), updateTaskCalls.Load(), "no-op ReportState within stateReportInterval must skip")
|
assert.Equal(t, int64(1), updateTaskCalls.Load(), "no-op ReportState within stateReportInterval must skip")
|
||||||
|
|
||||||
time.Sleep(2 * cfg.Runner.StateReportInterval)
|
<-time.After(2 * cfg.Runner.StateReportInterval)
|
||||||
require.NoError(t, reporter.ReportState(false))
|
require.NoError(t, reporter.ReportState(false))
|
||||||
assert.Equal(t, int64(2), updateTaskCalls.Load(), "ReportState must heartbeat after stateReportInterval even with no state change")
|
assert.Equal(t, int64(2), updateTaskCalls.Load(), "ReportState must heartbeat after stateReportInterval even with no state change")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_ServerCancelStillFlushesFinal(t *testing.T) {
|
func TestReporter_ServerCancelStillFlushesFinal(t *testing.T) {
|
||||||
@@ -556,58 +563,60 @@ func TestReporter_CloseReportsCancelledOnCanceledCtx(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_StopHeartbeats(t *testing.T) {
|
func TestReporter_StopHeartbeats(t *testing.T) {
|
||||||
var updateTaskCalls atomic.Int64
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
var updateTaskCalls atomic.Int64
|
||||||
|
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Maybe().Return(
|
client.On("UpdateLog", mock.Anything, mock.Anything).Maybe().Return(
|
||||||
func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
||||||
return &runnerv1.UpdateLogResponse{
|
return &runnerv1.UpdateLogResponse{
|
||||||
AckIndex: req.Index + int64(len(req.Rows)),
|
AckIndex: req.Index + int64(len(req.Rows)),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
||||||
updateTaskCalls.Add(1)
|
updateTaskCalls.Add(1)
|
||||||
return &runnerv1.UpdateTaskResponse{}, nil
|
return &runnerv1.UpdateTaskResponse{}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
cfg, err := config.LoadDefault("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cfg.Runner.StateReportInterval = 20 * time.Millisecond
|
cfg.Runner.StateReportInterval = 20 * time.Millisecond
|
||||||
cfg.Runner.LogReportInterval = time.Hour
|
cfg.Runner.LogReportInterval = time.Hour
|
||||||
|
|
||||||
reporter := newTestReporter(t, client, cfg)
|
reporter := newTestReporter(t, client, cfg)
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
|
|
||||||
reporter.stateMu.Lock()
|
reporter.stateMu.Lock()
|
||||||
reporter.stateChanged = true
|
reporter.stateChanged = true
|
||||||
reporter.state.Result = runnerv1.Result_RESULT_SUCCESS
|
reporter.state.Result = runnerv1.Result_RESULT_SUCCESS
|
||||||
reporter.state.StoppedAt = timestamppb.Now()
|
reporter.state.StoppedAt = timestamppb.Now()
|
||||||
reporter.stateMu.Unlock()
|
reporter.stateMu.Unlock()
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
return updateTaskCalls.Load() >= 1
|
return updateTaskCalls.Load() >= 1
|
||||||
}, time.Second, 5*time.Millisecond, "daemon must send at least one UpdateTask before StopHeartbeats")
|
}, time.Second, 5*time.Millisecond, "daemon must send at least one UpdateTask before StopHeartbeats")
|
||||||
|
|
||||||
beforeStop := updateTaskCalls.Load()
|
beforeStop := updateTaskCalls.Load()
|
||||||
reporter.StopHeartbeats()
|
reporter.StopHeartbeats()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-reporter.daemon:
|
case <-reporter.daemon:
|
||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
t.Fatal("StopHeartbeats must stop the daemon loop")
|
t.Fatal("StopHeartbeats must stop the daemon loop")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(3 * cfg.Runner.StateReportInterval)
|
<-time.After(3 * cfg.Runner.StateReportInterval)
|
||||||
assert.Equal(t, beforeStop, updateTaskCalls.Load(),
|
assert.Equal(t, beforeStop, updateTaskCalls.Load(),
|
||||||
"UpdateTask must not be called after StopHeartbeats")
|
"UpdateTask must not be called after StopHeartbeats")
|
||||||
|
|
||||||
require.NoError(t, reporter.Close(""))
|
require.NoError(t, reporter.Close(""))
|
||||||
assert.Greater(t, updateTaskCalls.Load(), beforeStop,
|
assert.Greater(t, updateTaskCalls.Load(), beforeStop,
|
||||||
"Close() must still send a final UpdateTask after StopHeartbeats")
|
"Close() must still send a final UpdateTask after StopHeartbeats")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendLogRow(t *testing.T) {
|
func TestAppendLogRow(t *testing.T) {
|
||||||
@@ -786,48 +795,53 @@ func TestReporter_AckIndexBeyondBuffer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_CloseWithStuckDaemon(t *testing.T) {
|
func TestReporter_CloseWithStuckDaemon(t *testing.T) {
|
||||||
release := make(chan struct{})
|
synctest.Test(t, func(t *testing.T) {
|
||||||
entered := make(chan struct{})
|
release := make(chan struct{})
|
||||||
var once sync.Once
|
entered := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
output := logging.Output()
|
||||||
|
logging.SetOutput(common.NewLineWriter(func(line string) bool {
|
||||||
|
if strings.Contains(line, "No Response from RunDaemon") {
|
||||||
|
close(release)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() { logging.SetOutput(output) })
|
||||||
|
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
close(entered)
|
close(entered)
|
||||||
<-release
|
<-release
|
||||||
|
})
|
||||||
|
return &runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Index + int64(len(req.Rows)),
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
return &runnerv1.UpdateLogResponse{
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
||||||
AckIndex: req.Index + int64(len(req.Rows)),
|
return &runnerv1.UpdateTaskResponse{}, nil
|
||||||
}, nil
|
}).Maybe()
|
||||||
|
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
cfg.Runner.LogReportInterval = 10 * time.Millisecond
|
||||||
|
reporter := newTestReporter(t, client, cfg)
|
||||||
|
reporter.daemonWait = time.Millisecond
|
||||||
|
reporter.RunDaemon()
|
||||||
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
|
|
||||||
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
|
Message: "hello",
|
||||||
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
|
Level: logging.LevelInfo,
|
||||||
|
}))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-entered:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("daemon never reached UpdateLog")
|
||||||
|
}
|
||||||
|
require.NoError(t, reporter.Close(""))
|
||||||
})
|
})
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, _ *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
|
|
||||||
return &runnerv1.UpdateTaskResponse{}, nil
|
|
||||||
}).Maybe()
|
|
||||||
|
|
||||||
cfg, _ := config.LoadDefault("")
|
|
||||||
cfg.Runner.LogReportInterval = 10 * time.Millisecond
|
|
||||||
reporter := newTestReporter(t, client, cfg)
|
|
||||||
reporter.daemonWait = time.Millisecond
|
|
||||||
reporter.RunDaemon()
|
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
|
||||||
|
|
||||||
require.NoError(t, fire(reporter, logEntry{
|
|
||||||
Message: "hello",
|
|
||||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
|
||||||
Level: logging.LevelInfo,
|
|
||||||
}))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-entered:
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("daemon never reached UpdateLog")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * reporter.daemonWait)
|
|
||||||
close(release)
|
|
||||||
}()
|
|
||||||
|
|
||||||
require.NoError(t, reporter.Close(""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_ServerFailureResultIsReportedAsFailure(t *testing.T) {
|
func TestReporter_ServerFailureResultIsReportedAsFailure(t *testing.T) {
|
||||||
|
|||||||
+13
-10
@@ -33,7 +33,7 @@ host_docker="${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.dock
|
|||||||
test_dir=""
|
test_dir=""
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker -H "$host_docker" rm -fv "$name" "$name-s6" >/dev/null 2>&1 || true
|
docker -H "$host_docker" rm -fv "$name" "$name-s6-failure" "$name-s6-stop" >/dev/null 2>&1 || true
|
||||||
if [ -n "$test_dir" ]; then
|
if [ -n "$test_dir" ]; then
|
||||||
rm -rf "$test_dir"
|
rm -rf "$test_dir"
|
||||||
fi
|
fi
|
||||||
@@ -52,7 +52,7 @@ else
|
|||||||
fi
|
fi
|
||||||
# Override the image entrypoint (s6) and run only dockerd, exposed over insecure TCP.
|
# Override the image entrypoint (s6) and run only dockerd, exposed over insecure TCP.
|
||||||
# We are testing the daemon the image ships, not the runner supervision tree.
|
# We are testing the daemon the image ships, not the runner supervision tree.
|
||||||
daemon_args=(-e DOCKER_TLS_CERTDIR= --entrypoint dockerd-entrypoint.sh "$image" --host=tcp://0.0.0.0:2375)
|
daemon_args=(-e DOCKER_TLS_CERTDIR= --entrypoint dockerd-entrypoint.sh "$image" --host=tcp://0.0.0.0:2375 --tls=false)
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
default_tests=true
|
default_tests=true
|
||||||
set -- -count=1 -race -run '^TestImageExistsLocally$' ./act/container/
|
set -- -count=1 -race -run '^TestImageExistsLocally$' ./act/container/
|
||||||
@@ -87,6 +87,11 @@ fi
|
|||||||
# Create the dind container on the host daemon first, then repoint DOCKER_HOST at it: exporting
|
# Create the dind container on the host daemon first, then repoint DOCKER_HOST at it: exporting
|
||||||
# DOCKER_HOST before `docker run` would make this `docker run` target the not-yet-existent dind.
|
# DOCKER_HOST before `docker run` would make this `docker run` target the not-yet-existent dind.
|
||||||
docker -H "$host_docker" run -d --privileged --name "$name" "${run_args[@]}" "${daemon_args[@]}" >/dev/null
|
docker -H "$host_docker" run -d --privileged --name "$name" "${run_args[@]}" "${daemon_args[@]}" >/dev/null
|
||||||
|
if [ "$default_tests" = true ]; then
|
||||||
|
echo "==> Starting ${target} supervision checks"
|
||||||
|
docker -H "$host_docker" run -d --privileged --name "$name-s6-failure" -e DOCKER_TLS_CERTDIR= -e RUNNER_STATE_FILE=/etc/hostname "$image" >/dev/null
|
||||||
|
docker -H "$host_docker" run -d --privileged --name "$name-s6-stop" -e DOCKER_TLS_CERTDIR= "$image" >/dev/null
|
||||||
|
fi
|
||||||
export DOCKER_HOST="$daemon_host"
|
export DOCKER_HOST="$daemon_host"
|
||||||
|
|
||||||
echo "==> Waiting for daemon"
|
echo "==> Waiting for daemon"
|
||||||
@@ -148,7 +153,6 @@ if [ "$default_tests" = true ]; then
|
|||||||
docker -H "$host_docker" exec -e DOCKER_HOST="$socket" "$name" docker run --rm -v "${socket#unix://}:/var/run/docker.sock" -v /tmp/gitea-runner-proxy-test:/data -w /data \
|
docker -H "$host_docker" exec -e DOCKER_HOST="$socket" "$name" docker run --rm -v "${socket#unix://}:/var/run/docker.sock" -v /tmp/gitea-runner-proxy-test:/data -w /data \
|
||||||
-e ACT_TEST_DOCKER_PROXY=proxy -e ACT_TEST_IMAGE="$job_image" "$job_image" ./runner.test -test.v -test.run '^TestDockerProxyMountedJob$' -test.timeout 3m
|
-e ACT_TEST_DOCKER_PROXY=proxy -e ACT_TEST_IMAGE="$job_image" "$job_image" ./runner.test -test.v -test.run '^TestDockerProxyMountedJob$' -test.timeout 3m
|
||||||
|
|
||||||
supervised="$name-s6"
|
|
||||||
supervised_exit_is() {
|
supervised_exit_is() {
|
||||||
if [ "$(docker -H "$host_docker" inspect -f '{{.State.ExitCode}}' "$supervised")" != "$1" ] ||
|
if [ "$(docker -H "$host_docker" inspect -f '{{.State.ExitCode}}' "$supervised")" != "$1" ] ||
|
||||||
! docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F "$2" >/dev/null; then
|
! docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F "$2" >/dev/null; then
|
||||||
@@ -158,15 +162,14 @@ if [ "$default_tests" = true ]; then
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
echo "==> Checking that a runner failure sets the ${target} container exit code"
|
echo "==> Checking that a runner failure sets the ${target} container exit code"
|
||||||
docker -H "$host_docker" run -d --privileged --name "$supervised" -e DOCKER_TLS_CERTDIR= -e RUNNER_STATE_FILE=/etc/hostname "$image" >/dev/null
|
supervised="$name-s6-failure"
|
||||||
for _ in $(seq 1 120); do
|
if ! timeout 120 docker -H "$host_docker" wait "$supervised" >/dev/null; then
|
||||||
[ "$(docker -H "$host_docker" inspect -f '{{.State.Running}}' "$supervised")" = false ] && break
|
docker -H "$host_docker" logs "$supervised" >&2
|
||||||
sleep 1
|
exit 1
|
||||||
done
|
fi
|
||||||
supervised_exit_is 1 'registration file not found'
|
supervised_exit_is 1 'registration file not found'
|
||||||
docker -H "$host_docker" rm -fv "$supervised" >/dev/null
|
|
||||||
echo "==> Checking that stopping a started ${target} runner exits 0"
|
echo "==> Checking that stopping a started ${target} runner exits 0"
|
||||||
docker -H "$host_docker" run -d --privileged --name "$supervised" -e DOCKER_TLS_CERTDIR= "$image" >/dev/null
|
supervised="$name-s6-stop"
|
||||||
for _ in $(seq 1 120); do
|
for _ in $(seq 1 120); do
|
||||||
docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F 'is missing or not a regular file' >/dev/null && break
|
docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F 'is missing or not a regular file' >/dev/null && break
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|||||||
+15
-4
@@ -7,9 +7,20 @@ set -euo pipefail
|
|||||||
: "${E2E_K3S_IMAGE:?E2E_K3S_IMAGE is required}"
|
: "${E2E_K3S_IMAGE:?E2E_K3S_IMAGE is required}"
|
||||||
: "${SERVICE_IMAGE:?SERVICE_IMAGE is required}"
|
: "${SERVICE_IMAGE:?SERVICE_IMAGE is required}"
|
||||||
|
|
||||||
docker pull "$E2E_JOB_IMAGE"
|
platform=$(docker version --format '{{.Server.Os}}/{{.Server.Arch}}')
|
||||||
docker pull "$SERVICE_IMAGE"
|
pulls=()
|
||||||
docker pull "$E2E_GITEA_IMAGE"
|
for image in "$E2E_JOB_IMAGE" "$SERVICE_IMAGE" "$E2E_GITEA_IMAGE" "$E2E_K3S_IMAGE"; do
|
||||||
docker pull "$E2E_K3S_IMAGE"
|
if [[ "$image" == *@sha256:* ]] && [[ "$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null)" == "$platform" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
docker pull "$image" &
|
||||||
|
pulls+=("$!")
|
||||||
|
done
|
||||||
|
|
||||||
|
result=0
|
||||||
|
for pull in "${pulls[@]}"; do
|
||||||
|
wait "$pull" || result=$?
|
||||||
|
done
|
||||||
|
((result == 0)) || exit "$result"
|
||||||
|
|
||||||
exec "${GO:-go}" test -tags e2e -count=1 -parallel "$E2E_CONCURRENCY" -timeout 20m -v ./e2e/...
|
exec "${GO:-go}" test -tags e2e -count=1 -parallel "$E2E_CONCURRENCY" -timeout 20m -v ./e2e/...
|
||||||
Reference in new issue
Block a user