diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index f568314..d381734 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -66,6 +66,14 @@ on: tag it would release). type: string default: "" + build-contexts-artifact: + description: >- + Name of an artifact of this run (uploaded by an earlier job of the + caller) whose top-level directories become named build contexts + (`COPY --from=`), e.g. data prepared with secrets the + image build must not see. Empty = none. + type: string + default: "" platforms: description: >- JSON list of {"platform": ..., "runner": ...}. Native runners per @@ -239,6 +247,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + if: inputs.build-contexts-artifact != '' + with: + name: ${{ inputs.build-contexts-artifact }} + path: ${{ runner.temp }}/build-contexts - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - id: auth @@ -273,6 +286,11 @@ jobs: while IFS= read -r arg; do [ -n "$arg" ] && args+=(--build-arg "${arg//\{tag\}/$VERSION}") done <<<"$BUILD_ARGS" + if [ -d "$RUNNER_TEMP/build-contexts" ]; then + for dir in "$RUNNER_TEMP"/build-contexts/*/; do + [ -d "$dir" ] && args+=(--build-context "$(basename "$dir")=$dir") + done + fi if [ "$PUSH" = true ]; then echo "$TOKEN" | docker login -u oauth2accesstoken --password-stdin "https://$REGISTRY" args+=(--sbom=true --provenance=mode=max diff --git a/.github/workflows/selftest.yml b/.github/workflows/selftest.yml index e01e8ea..e15e1af 100644 --- a/.github/workflows/selftest.yml +++ b/.github/workflows/selftest.yml @@ -283,6 +283,35 @@ jobs: artifact-prefix: package-json- harness-ref: ${{ github.sha }} + # A named build context from an artifact of the same run. + build-context-artifact: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - run: mkdir -p ctx/extra && echo "from the artifact" > ctx/extra/marker.txt + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: selftest-build-contexts + path: ctx + retention-days: 1 + + release-dry-run-build-context: + needs: [build-context-artifact] + uses: ./.github/workflows/release-image.yml + permissions: + contents: read + id-token: write + with: + image: example.invalid/selftest/release + version-file: tests/fixtures/release/version.txt + dockerfile: tests/fixtures/release/Dockerfile.context + context: tests/fixtures/release + build-contexts-artifact: selftest-build-contexts + workload-identity-provider: dry-run-unused + service-account: dry-run-unused + artifact-prefix: build-context- + harness-ref: ${{ github.sha }} + assert-planted: needs: [security-planted, lint-planted] # Also when a scanner job broke: the counts then show what is missing. @@ -313,7 +342,7 @@ jobs: # above succeeded (skipped or cancelled counts as failed). selftest-ok: if: ${{ always() }} - needs: [lint-self, lint-templates, lint-planted, scripts, gate-pass, gate-fail, node-gate-npm, node-gate-npm-tools, node-gate-bun, node-gate-fail, python-gate-pass, python-gate-fail, security-clean, security-clean-node, security-planted, release-dry-run, release-dry-run-package-json, assert-planted] + needs: [lint-self, lint-templates, lint-planted, scripts, gate-pass, gate-fail, node-gate-npm, node-gate-npm-tools, node-gate-bun, node-gate-fail, python-gate-pass, python-gate-fail, security-clean, security-clean-node, security-planted, release-dry-run, release-dry-run-package-json, build-context-artifact, release-dry-run-build-context, assert-planted] runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/README.md b/README.md index 780b5a5..ee8bcdb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The inputs of each workflow are documented in its `on.workflow_call` block. | [security.yml](.github/workflows/security.yml) | `gitleaks`, `deps`, `semgrep`, `sbom`, `config`, `licenses`, `image`, `report` | security screening; `report` applies `fail-on` | | [workflows-lint.yml](.github/workflows/workflows-lint.yml) | `lint` | actionlint, zizmor, pinact on the caller's workflows; always blocking | | [release-intent.yml](.github/workflows/release-intent.yml) | `intent` | the version file decides: a rise makes a release PR (title `release ` required), an unchanged version a no-release PR; output `kind` gates the release dry run; listed in the `needs` of `ci-ok` | -| [release-image.yml](.github/workflows/release-image.yml) | `verify`, `build`, `publish` | release of a container image on the merge of a release PR into the protected `main`; native multi-arch build, SBOM + provenance attached, keyless push; on a release PR a dry run (every platform build, no push); re-runs are safe; read rights only | +| [release-image.yml](.github/workflows/release-image.yml) | `verify`, `build`, `publish` | release of a container image on the merge of a release PR into the protected `main`; native multi-arch build, SBOM + provenance attached, keyless push; on a release PR a dry run (every platform build, no push); re-runs are safe; read rights only; `build-contexts-artifact` hands data an earlier job prepared (with secrets the build must not see) to the build as named contexts | | [release-tag.yml](.github/workflows/release-tag.yml) | `tag` | the annotated git tag of a release, after every image job of the caller; a tag always means "in the registry"; written with a token of the release GitHub App from the caller's `release` environment | | [selftest.yml](.github/workflows/selftest.yml) | — | proves the harness against [tests/fixtures](tests/fixtures) | diff --git a/tests/fixtures/release/Dockerfile.context b/tests/fixtures/release/Dockerfile.context new file mode 100644 index 0000000..94efdc1 --- /dev/null +++ b/tests/fixtures/release/Dockerfile.context @@ -0,0 +1,8 @@ +# The release workflow's dry run with a named build context from an artifact +# (build-contexts-artifact): the build fails unless `extra` holds the file the +# self-test uploaded. +FROM alpine:3.24 +COPY --from=extra marker.txt /marker.txt +RUN test "$(cat /marker.txt)" = "from the artifact" +USER 65534:65534 +CMD ["cat", "/marker.txt"]