2525 required : false
2626 type : string
2727 default : " true"
28+ release_pin_fetcher_changed :
29+ description : " Whether release-pin-fetcher image inputs changed"
30+ required : false
31+ type : string
32+ default : " true"
33+ postgres_image_changed :
34+ description : " Whether the PostgreSQL image inputs changed"
35+ required : false
36+ type : string
37+ default : " true"
2838
2939 outputs :
3040 application-server-published :
3141 description : " Whether an application-server image exists at this commit"
32- value : ${{ jobs.application-server-build.result == 'success ' }}
42+ value : ${{ jobs.application-server-build.outputs.manifest-digest != '' || jobs.tag-unchanged-images.outputs.application-server-published == 'true ' }}
3343
3444jobs :
3545 webapp-build :
5161 tags : |
5262 ${{ github.ref_name }}
5363 ${{ github.sha }}
64+ ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
5465 ci-${{ github.run_number }}
5566 ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || '' }}
5667 labels : |
6273
6374 application-server-build :
6475 name : " App Server"
65- # Every code PR receives an immutable app-server tag. If server code did not change, this is
66- # intentionally a cached rebuild of main; previews can then use SOURCE_COMMIT uniformly instead
67- # of racing on a global IMAGE_TAG or failing because the commit tag does not exist.
6876 if : >-
6977 inputs.should_skip != 'true' &&
70- (inputs.application_server_changed == 'true' || github.event_name = = 'pull_request')
78+ (inputs.application_server_changed == 'true' || github.event_name ! = 'pull_request')
7179 uses : ./.github/workflows/reusable-docker-build.yml
7280 with :
7381 image-name : " ls1intum/hephaestus/application-server"
@@ -127,7 +135,9 @@ jobs:
127135
128136 release-pin-fetcher-build :
129137 name : " Release Pin Fetcher"
130- if : inputs.should_skip != 'true'
138+ if : >-
139+ inputs.should_skip != 'true' &&
140+ (inputs.release_pin_fetcher_changed == 'true' || github.event_name != 'pull_request')
131141 uses : ./.github/workflows/reusable-docker-build.yml
132142 with :
133143 image-name : " ls1intum/hephaestus/release-pin-fetcher"
@@ -139,6 +149,7 @@ jobs:
139149 tags : |
140150 ${{ github.ref_name }}
141151 ${{ github.sha }}
152+ ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
142153 ci-${{ github.run_number }}
143154 ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || '' }}
144155 labels : |
@@ -150,7 +161,9 @@ jobs:
150161
151162 postgres-build :
152163 name : " Postgres (pg_partman)"
153- if : inputs.should_skip != 'true'
164+ if : >-
165+ inputs.should_skip != 'true' &&
166+ (inputs.postgres_image_changed == 'true' || github.event_name != 'pull_request')
154167 uses : ./.github/workflows/reusable-docker-build.yml
155168 with :
156169 image-name : " ls1intum/hephaestus/postgres"
@@ -162,6 +175,7 @@ jobs:
162175 tags : |
163176 ${{ github.ref_name }}
164177 ${{ github.sha }}
178+ ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
165179 ci-${{ github.run_number }}
166180 ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || '' }}
167181 labels : |
@@ -170,3 +184,71 @@ jobs:
170184 org.opencontainers.image.vendor=AET TUM
171185 org.opencontainers.image.licenses=MIT
172186 hephaestus.component=postgres
187+
188+ # Preview stacks use one commit tag for every component. Point unchanged components at their
189+ # verified base-commit digest instead of rebuilding identical content under a new name.
190+ tag-unchanged-images :
191+ name : " Tag unchanged images"
192+ if : >-
193+ inputs.should_skip != 'true' && github.event_name == 'pull_request' &&
194+ (inputs.webapp_changed != 'true' || inputs.application_server_changed != 'true' ||
195+ inputs.agent_images_changed != 'true' || inputs.release_pin_fetcher_changed != 'true' ||
196+ inputs.postgres_image_changed != 'true')
197+ runs-on : ubuntu-latest
198+ outputs :
199+ application-server-published : ${{ steps.tag.outputs.application-server-published }}
200+ permissions :
201+ packages : write
202+ attestations : read
203+ contents : read
204+ steps :
205+ - name : Log in to Container Registry
206+ uses : docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
207+ with :
208+ registry : ghcr.io
209+ username : ${{ github.actor }}
210+ password : ${{ secrets.GITHUB_TOKEN }}
211+
212+ - name : Verify and tag unchanged images
213+ id : tag
214+ env :
215+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
216+ WEBAPP_CHANGED : ${{ inputs.webapp_changed }}
217+ APPLICATION_SERVER_CHANGED : ${{ inputs.application_server_changed }}
218+ AGENT_IMAGES_CHANGED : ${{ inputs.agent_images_changed }}
219+ RELEASE_PIN_FETCHER_CHANGED : ${{ inputs.release_pin_fetcher_changed }}
220+ POSTGRES_IMAGE_CHANGED : ${{ inputs.postgres_image_changed }}
221+ PR_NUMBER : ${{ github.event.number }}
222+ BASE_SHA : ${{ github.event.pull_request.base.sha }}
223+ run : |
224+ set -euo pipefail
225+
226+ tag_image() {
227+ local image="ghcr.io/ls1intum/hephaestus/$1"
228+ local source="$image:$BASE_SHA"
229+ local ref_tag="${GITHUB_REF_NAME//\//-}"
230+ local digest
231+ digest=$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$source")
232+ if [[ ! "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]; then
233+ echo "::error::Invalid digest for $source: ${digest:-<empty>}"
234+ return 1
235+ fi
236+ local verified="$image@$digest"
237+ gh attestation verify "oci://$verified" --owner "${{ github.repository_owner }}"
238+ docker buildx imagetools create \
239+ --tag "$image:$ref_tag" \
240+ --tag "$image:${{ github.sha }}" \
241+ --tag "$image:${{ github.event.pull_request.head.sha }}" \
242+ --tag "$image:ci-${{ github.run_number }}" \
243+ --tag "$image:pr-$PR_NUMBER" \
244+ "$verified"
245+ }
246+
247+ [ "$WEBAPP_CHANGED" = "true" ] || tag_image webapp
248+ if [ "$APPLICATION_SERVER_CHANGED" != "true" ]; then
249+ tag_image application-server
250+ echo "application-server-published=true" >> "$GITHUB_OUTPUT"
251+ fi
252+ [ "$AGENT_IMAGES_CHANGED" = "true" ] || tag_image agent-pi
253+ [ "$RELEASE_PIN_FETCHER_CHANGED" = "true" ] || tag_image release-pin-fetcher
254+ [ "$POSTGRES_IMAGE_CHANGED" = "true" ] || tag_image postgres
0 commit comments