Skip to content

Commit 5a63e79

Browse files
fix(ci): stop pull request container builds from writing shared tags
Under pull_request_target, container builds use the PR's Dockerfile with a packages: write token. They pushed the same tags that every other run consumed (`:<hash>-<arch>`, `:latest`, and `:develop`, because GITHUB_REF is the base branch), so one run could replace the image another run used, including the image the Guix job runs with --privileged, and could seed the inline cache that trusted builds read. - Pull request builds now only write `untrusted` scratch tags that nothing reads. Consumers use the digest this run produced. - Trusted (push, and the Guix schedule) builds read and write cache only under a new `trusted-` prefix, so tags previously written by pull requests are never reused as cache. They still publish the existing `:<hash>`, `:<branch>` and `:latest` tags. - Only the image-producer jobs keep packages: write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f35a24c commit 5a63e79

3 files changed

Lines changed: 48 additions & 28 deletions

File tree

‎.github/workflows/build-container.yml‎

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ on:
2525
type: string
2626
outputs:
2727
path:
28-
description: "Path to built container"
29-
value: ghcr.io/${{ jobs.build-amd64.outputs.repo }}/${{ inputs.name }}:${{ jobs.build-amd64.outputs.tag }}
28+
description: "Digest reference to the container built by this run"
29+
value: ${{ jobs.create-manifest.outputs.path }}
3030

3131
jobs:
3232
build-amd64:
@@ -70,11 +70,11 @@ jobs:
7070
file: ${{ inputs.file }}
7171
push: true
7272
platforms: linux/amd64
73+
# Only trusted events may write tags that other runs read
7374
tags: |
74-
ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ hashFiles(inputs.file) }}-amd64
75+
ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ github.event_name == 'push' && format('trusted-{0}', hashFiles(inputs.file)) || 'untrusted' }}-amd64
7576
cache-from: |
76-
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ hashFiles(inputs.file) }}-amd64
77-
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ steps.prepare.outputs.tag }}
77+
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:trusted-${{ hashFiles(inputs.file) }}-amd64
7878
cache-to: type=inline
7979

8080
build-arm64:
@@ -116,17 +116,19 @@ jobs:
116116
file: ${{ inputs.file }}
117117
push: true
118118
platforms: linux/arm64
119+
# Only trusted events may write tags that other runs read
119120
tags: |
120-
ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ hashFiles(inputs.file) }}-arm64
121+
ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ github.event_name == 'push' && format('trusted-{0}', hashFiles(inputs.file)) || 'untrusted' }}-arm64
121122
cache-from: |
122-
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ hashFiles(inputs.file) }}-arm64
123-
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:${{ steps.prepare.outputs.tag }}
123+
type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo }}/${{ inputs.name }}:trusted-${{ hashFiles(inputs.file) }}-arm64
124124
cache-to: type=inline
125125

126126
create-manifest:
127127
name: Create multi-arch manifest
128128
runs-on: ${{ inputs.runs-on-arm64 }}
129129
needs: [build-amd64, build-arm64]
130+
outputs:
131+
path: ${{ steps.manifest.outputs.path }}
130132
steps:
131133
- name: Checkout code
132134
uses: actions/checkout@v6
@@ -146,20 +148,24 @@ jobs:
146148
password: ${{ secrets.GITHUB_TOKEN }}
147149

148150
- name: Create and push multi-arch manifest
151+
id: manifest
152+
env:
153+
TRUSTED: ${{ github.event_name == 'push' }}
149154
run: |
150155
REPO="ghcr.io/${{ needs.build-amd64.outputs.repo }}/${{ inputs.name }}"
151156
TAG="${{ needs.build-amd64.outputs.tag }}"
152157
HASH_TAG="${{ hashFiles(inputs.file) }}"
153158
154-
# Create manifest from arch-specific images
155-
docker buildx imagetools create -t "${REPO}:${HASH_TAG}" \
156-
"${REPO}:${HASH_TAG}-amd64" \
157-
"${REPO}:${HASH_TAG}-arm64"
159+
if [[ "$TRUSTED" == "true" ]]; then
160+
TAGS=(-t "${REPO}:${HASH_TAG}" -t "${REPO}:${TAG}" -t "${REPO}:latest")
161+
else
162+
TAGS=(-t "${REPO}:untrusted")
163+
fi
158164
159-
docker buildx imagetools create -t "${REPO}:${TAG}" \
160-
"${REPO}:${HASH_TAG}-amd64" \
161-
"${REPO}:${HASH_TAG}-arm64"
165+
# Use this run's digests: tags can be moved by concurrent runs
166+
docker buildx imagetools create --metadata-file "${RUNNER_TEMP}/manifest.json" "${TAGS[@]}" \
167+
"${REPO}@${{ needs.build-amd64.outputs.digest }}" \
168+
"${REPO}@${{ needs.build-arm64.outputs.digest }}"
162169
163-
docker buildx imagetools create -t "${REPO}:latest" \
164-
"${REPO}:${HASH_TAG}-amd64" \
165-
"${REPO}:${HASH_TAG}-arm64"
170+
DIGEST=$(jq -er '."containerimage.descriptor".digest' "${RUNNER_TEMP}/manifest.json")
171+
echo "path=${REPO}@${DIGEST}" >> "$GITHUB_OUTPUT"

‎.github/workflows/build.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
permissions:
88
actions: read
99
contents: read
10-
packages: write
10+
packages: read
1111

1212
concurrency:
1313
group: |
@@ -97,6 +97,9 @@ jobs:
9797
runs-on: ${{ needs.check-skip.outputs['runner-arm64'] }}
9898

9999
container:
100+
permissions:
101+
contents: read
102+
packages: write
100103
name: Build container
101104
needs: [check-skip]
102105
if: ${{ needs.check-skip.outputs.skip == 'false' }}
@@ -109,6 +112,9 @@ jobs:
109112
runs-on-arm64: ${{ needs.check-skip.outputs['runner-arm64'] }}
110113

111114
container-slim:
115+
permissions:
116+
contents: read
117+
packages: write
112118
name: Build slim container
113119
needs: [check-skip]
114120
if: ${{ needs.check-skip.outputs.skip == 'false' }}

‎.github/workflows/guix-build.yml‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Guix Build
22

33
permissions:
4-
packages: write
4+
packages: read
55
id-token: write
66
attestations: write
77

@@ -15,14 +15,15 @@ on:
1515

1616
jobs:
1717
build-image:
18+
permissions:
19+
packages: write
1820
runs-on: ubuntu-24.04-arm
1921
if: |
2022
(github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || vars.RUN_GUIX_ON_ALL_PUSH == 'true')) ||
2123
contains(github.event.pull_request.labels.*.name, 'guix-build') ||
2224
github.event_name == 'schedule'
2325
outputs:
24-
image-tag: ${{ steps.prepare.outputs.image-tag }}
25-
repo-name: ${{ steps.prepare.outputs.repo-name }}
26+
image: ghcr.io/${{ steps.prepare.outputs.repo-name }}/dashcore-guix-builder@${{ steps.build.outputs.digest }}
2627
steps:
2728
- name: Checkout
2829
uses: actions/checkout@v6
@@ -38,13 +39,21 @@ jobs:
3839

3940
- name: Commit variables
4041
id: prepare
42+
env:
43+
TRUSTED: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
4144
run: |
4245
echo "hash=$(sha256sum ./dash/contrib/containers/guix/Dockerfile | cut -d ' ' -f1)" >> $GITHUB_OUTPUT
4346
echo "host_user_id=$(id -u)" >> $GITHUB_OUTPUT
4447
echo "host_group_id=$(id -g)" >> $GITHUB_OUTPUT
4548
BRANCH_NAME=$(echo "${GITHUB_REF##*/}" | tr '[:upper:]' '[:lower:]')
4649
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
47-
echo "image-tag=${BRANCH_NAME}" >> $GITHUB_OUTPUT
50+
IMAGE="ghcr.io/${REPO_NAME}/dashcore-guix-builder"
51+
# Only trusted events may write tags that other runs read
52+
if [[ "$TRUSTED" == "true" ]]; then
53+
echo "tags=${IMAGE}:${BRANCH_NAME},${IMAGE}:latest,${IMAGE}:trusted-latest" >> "$GITHUB_OUTPUT"
54+
else
55+
echo "tags=${IMAGE}:untrusted" >> "$GITHUB_OUTPUT"
56+
fi
4857
echo "repo-name=${REPO_NAME}" >> $GITHUB_OUTPUT
4958
5059
- name: Login to GitHub Container Registry
@@ -55,6 +64,7 @@ jobs:
5564
password: ${{ secrets.GITHUB_TOKEN }}
5665

5766
- name: Build Docker image
67+
id: build
5868
uses: docker/build-push-action@v7
5969
with:
6070
context: ${{ github.workspace }}/dash
@@ -65,10 +75,8 @@ jobs:
6575
docker_root=${{ github.workspace }}/dash/contrib/containers/guix
6676
file: ./dash/contrib/containers/guix/Dockerfile
6777
push: true
68-
tags: |
69-
ghcr.io/${{ steps.prepare.outputs.repo-name }}/dashcore-guix-builder:${{ steps.prepare.outputs.image-tag }}
70-
ghcr.io/${{ steps.prepare.outputs.repo-name }}/dashcore-guix-builder:latest
71-
cache-from: type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo-name }}/dashcore-guix-builder:latest
78+
tags: ${{ steps.prepare.outputs.tags }}
79+
cache-from: type=registry,ref=ghcr.io/${{ steps.prepare.outputs.repo-name }}/dashcore-guix-builder:trusted-latest
7280
cache-to: type=inline,mode=max
7381

7482
build:
@@ -128,7 +136,7 @@ jobs:
128136
-v ${{ github.workspace }}/dash:/src/dash \
129137
-v ${{ github.workspace }}/.cache:/home/ubuntu/.cache \
130138
-w /src/dash \
131-
ghcr.io/${{ needs.build-image.outputs.repo-name }}/dashcore-guix-builder:${{ needs.build-image.outputs.image-tag }} && \
139+
${{ needs.build-image.outputs.image }} && \
132140
docker exec guix-daemon bash -c 'HOSTS=${{ matrix.build_target }} /usr/local/bin/guix-start /src/dash'
133141
134142
- name: Ensure build passes

0 commit comments

Comments
 (0)