diff --git a/.github/workflows/publish-server-image.yml b/.github/workflows/publish-server-image.yml index dedea74..791958c 100644 --- a/.github/workflows/publish-server-image.yml +++ b/.github/workflows/publish-server-image.yml @@ -256,9 +256,23 @@ jobs: grep -Fqx "$want" <<< "$platforms" || { echo "$first_tag is missing $want" >&2; exit 1; } done echo "digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')" >> "$GITHUB_OUTPUT" - # GitHub artifact attestations are unavailable for private repos on personal accounts. + # Owner type is not exposed on the github context; resolve it from the repository metadata API. + - name: Resolve registry attestation eligibility + id: attestation + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + owner_type="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.owner.type')" + private="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.private')" + if [[ "$owner_type" == "User" && "$private" == "true" ]]; then + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "Skipping registry attestation: unavailable for private repositories on personal accounts." + else + echo "enabled=true" >> "$GITHUB_OUTPUT" + fi - name: Attest the published image - if: github.event.repository.private != true || github.repository_owner_type != 'User' + if: steps.attestation.outputs.enabled == 'true' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }} diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index f75de81..4c9e292 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -38,12 +38,14 @@ describe("server image publish workflow", () => { expect(publish.needs).toBe("build"); expect(workflowText).toContain("push-by-digest=true"); expect(workflowText).toContain("docker buildx imagetools create"); - expect(workflowText).toContain('if [[ "${#sources[@]}" -ne 1 ]]'); + expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`); expect(workflowText).toContain("for want in linux/amd64; do"); expect(workflowText).toContain("actions/attest-build-provenance@"); - expect(workflowText).toContain( - "if: github.event.repository.private != true || github.repository_owner_type != 'User'", - ); + expect(workflowText).not.toContain("github.repository_owner_type"); + expect(workflowText).toContain(`gh api "repos/\${GITHUB_REPOSITORY}"`); + expect(workflowText).toContain(".owner.type"); + expect(workflowText).toContain("if: steps.attestation.outputs.enabled == 'true'"); + expect(workflowText).toContain('[[ "$owner_type" == "User" && "$private" == "true" ]]'); }); it("stores digest markers under RUNNER_TEMP so self-hosted runners do not leak stale files", () => { @@ -52,11 +54,11 @@ describe("server image publish workflow", () => { }); it("names every image under the lowercase repository namespace", () => { - expect(workflowText).not.toContain("ghcr.io/${{ github.repository }}"); + expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`); const references = workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? []; expect(references.length).toBeGreaterThanOrEqual(6); - expect(workflowText).toContain("repo=${GITHUB_REPOSITORY,,}"); + expect(workflowText).toContain(`repo=\${GITHUB_REPOSITORY,,}`); }); it("keeps pull requests read-only and every action pinned to a commit", () => {