From 7efe25f13de216f68e6e94c40dbec3e55373d7d5 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 28 Aug 2025 16:58:49 +0200 Subject: [PATCH 1/2] chore: local action name collision regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the tag name collide (e.g. v9.1.1), it will fail with: ``` [push.yml/test] [DEBUG] Working directory '/home/earl-warren/software/runner/act/runner/testdata/local-action-dockerfile-tag/example2' [push.yml/test] ❌ Failure - Main [[ "example1 SOMEONE" == "example2 SOMEONE" ]] ``` --- act/runner/runner_test.go | 2 ++ .../testdata/local-action-dockerfile-tag/README.txt | 4 ++++ .../example1/actions/docker-local/Dockerfile | 8 ++++++++ .../example1/actions/docker-local/action.yml | 12 ++++++++++++ .../example1/actions/docker-local/entrypoint.sh | 3 +++ .../local-action-dockerfile-example1/push.yml | 11 +++++++++++ .../example2/actions/docker-local/Dockerfile | 8 ++++++++ .../example2/actions/docker-local/action.yml | 12 ++++++++++++ .../example2/actions/docker-local/entrypoint.sh | 3 +++ .../local-action-dockerfile-example2/push.yml | 11 +++++++++++ 10 files changed, 74 insertions(+) create mode 100644 act/runner/testdata/local-action-dockerfile-tag/README.txt create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/Dockerfile create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/action.yml create mode 100755 act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/entrypoint.sh create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example1/local-action-dockerfile-example1/push.yml create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/Dockerfile create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/action.yml create mode 100755 act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/entrypoint.sh create mode 100644 act/runner/testdata/local-action-dockerfile-tag/example2/local-action-dockerfile-example2/push.yml diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index 83523c1e..7f3b46e9 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -245,6 +245,8 @@ func TestRunner_RunEvent(t *testing.T) { {workdir, "local-action-fails-schema-validation", "push", "Job 'test' failed", platforms, secrets}, {workdir, "local-action-docker-url", "push", "", platforms, secrets}, {workdir, "local-action-dockerfile", "push", "", platforms, secrets}, + {workdir + "/local-action-dockerfile-tag/example1", "local-action-dockerfile-example1", "push", "", platforms, secrets}, + {workdir + "/local-action-dockerfile-tag/example2", "local-action-dockerfile-example2", "push", "", platforms, secrets}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets}, {workdir, "local-action-js", "push", "", platforms, secrets}, diff --git a/act/runner/testdata/local-action-dockerfile-tag/README.txt b/act/runner/testdata/local-action-dockerfile-tag/README.txt new file mode 100644 index 00000000..aba97f3b --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/README.txt @@ -0,0 +1,4 @@ +example1 and example2 eacho use a local actions that have the same +path (actions/docker-local) but do not behave the same. This verifies +that the locally built images have different names and do not collide +despite both being called with `uses: ./actions/docker-local. diff --git a/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/Dockerfile b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/Dockerfile new file mode 100644 index 00000000..b8054873 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/Dockerfile @@ -0,0 +1,8 @@ +# Container image that runs your code +FROM code.forgejo.org/oci/alpine:latest + +# Copies your code file from your action repository to the filesystem path `/` of the container +COPY entrypoint.sh /entrypoint.sh + +# Code file to execute when the docker container starts up (`entrypoint.sh`) +ENTRYPOINT ["/entrypoint.sh"] diff --git a/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/action.yml b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/action.yml new file mode 100644 index 00000000..a6b97460 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/action.yml @@ -0,0 +1,12 @@ +inputs: + who-to-greet: +outputs: + whoami: + description: 'The time we greeted you' +runs: + using: 'docker' + image: 'Dockerfile' + env: + WHOAMI: ${{ inputs.who-to-greet }} + args: + - ${{ inputs.who-to-greet }} diff --git a/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/entrypoint.sh b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/entrypoint.sh new file mode 100755 index 00000000..7047838a --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example1/actions/docker-local/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo ::set-output "name=whoami::example1 $WHOAMI" diff --git a/act/runner/testdata/local-action-dockerfile-tag/example1/local-action-dockerfile-example1/push.yml b/act/runner/testdata/local-action-dockerfile-tag/example1/local-action-dockerfile-example1/push.yml new file mode 100644 index 00000000..fd24e76e --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example1/local-action-dockerfile-example1/push.yml @@ -0,0 +1,11 @@ +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./actions/docker-local + id: dockerlocal + with: + who-to-greet: 'SOMEONE' + - run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "example1 SOMEONE" ]]' diff --git a/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/Dockerfile b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/Dockerfile new file mode 100644 index 00000000..b8054873 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/Dockerfile @@ -0,0 +1,8 @@ +# Container image that runs your code +FROM code.forgejo.org/oci/alpine:latest + +# Copies your code file from your action repository to the filesystem path `/` of the container +COPY entrypoint.sh /entrypoint.sh + +# Code file to execute when the docker container starts up (`entrypoint.sh`) +ENTRYPOINT ["/entrypoint.sh"] diff --git a/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/action.yml b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/action.yml new file mode 100644 index 00000000..a6b97460 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/action.yml @@ -0,0 +1,12 @@ +inputs: + who-to-greet: +outputs: + whoami: + description: 'The time we greeted you' +runs: + using: 'docker' + image: 'Dockerfile' + env: + WHOAMI: ${{ inputs.who-to-greet }} + args: + - ${{ inputs.who-to-greet }} diff --git a/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/entrypoint.sh b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/entrypoint.sh new file mode 100755 index 00000000..c6750948 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example2/actions/docker-local/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo ::set-output "name=whoami::example2 $WHOAMI" diff --git a/act/runner/testdata/local-action-dockerfile-tag/example2/local-action-dockerfile-example2/push.yml b/act/runner/testdata/local-action-dockerfile-tag/example2/local-action-dockerfile-example2/push.yml new file mode 100644 index 00000000..b6f39f47 --- /dev/null +++ b/act/runner/testdata/local-action-dockerfile-tag/example2/local-action-dockerfile-example2/push.yml @@ -0,0 +1,11 @@ +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./actions/docker-local + id: dockerlocal + with: + who-to-greet: 'SOMEONE' + - run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "example2 SOMEONE" ]]' From 11a96bb46274b2cadda71f5d4c901e6e111e1dbf Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 28 Aug 2025 10:17:00 +0200 Subject: [PATCH 2/2] fix(security): ensure unique names for container images created by actions Container images built by the runner are tagged with a unique name: - based on the specified `uses` URL for remote actions. - random for local actions. In the case of local actions, this will create new tags for each run but the images (and their layers) will be shared and not be duplicated. The least recently used tags can be garbage collected by tools such as https://github.com/stepchowfun/docuum. Using a different method for creating the tag name for the remote actions is to help with maintenance by establishing a direct relation with the `uses` field. It was instead relying on a name transformed multiple times which makes it more difficult to verify name collision are not accidentally made possible by one of those transformations. Without this fix, when a workflow ran a local [docker action](https://forgejo.org/docs/next/user/actions/actions/#docker-actions) (e.g. the [example in the end-to-end tests](https://code.forgejo.org/forgejo/end-to-end/src/commit/8f920b4b7adcb920e435db1acd02407b2312ab74/actions/example-force-rebuild/.forgejo/workflows/test.yml)), it used an image tag that could collide with other workflows that happen to use the same name. The workaround for older runner versions is to set [`[container].force_rebuild: true`](https://forgejo.org/docs/next/admin/actions/runner-installation/#configuration) in the runner configuration file. --- act/runner/action.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/act/runner/action.go b/act/runner/action.go index fdfc6373..d27f0a84 100644 --- a/act/runner/action.go +++ b/act/runner/action.go @@ -277,7 +277,6 @@ func removeGitIgnore(ctx context.Context, directory string) error { return nil } -// TODO: break out parts of function to reduce complexicity func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error { logger := common.Logger(ctx) rc := step.getRunContext() @@ -291,10 +290,11 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, basedir stri // Apply forcePull only for prebuild docker images forcePull = rc.Config.ForcePull } else { - // "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names - image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest") - image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-")) - image = strings.ToLower(image) + if localAction { + image = fmt.Sprintf("runner-local-docker-action-%s:latest", common.MustRandName(16)) + } else { + image = fmt.Sprintf("runner-remote-docker-action-%s:latest", common.Sha256(step.getStepModel().Uses)) + } contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image)) anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")