-
Notifications
You must be signed in to change notification settings - Fork 2
233 lines (207 loc) · 8.78 KB
/
Copy pathrelease.yml
File metadata and controls
233 lines (207 loc) · 8.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
name: Release
# Trunk-based continuous deployment
# Every push to main with feat:/fix:/perf: triggers a release
# Staging deploys immediately, Production requires approval
on:
workflow_run:
workflows: ["CI/CD"]
types: [completed]
branches: [main]
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions: {}
jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write # semantic-release pushes the version tag
issues: write # semantic-release closes resolved issues
pull-requests: write # semantic-release comments on merged PRs
outputs:
new_version: ${{ steps.release.outputs.new_release_version }}
released: ${{ steps.release.outputs.new_release_published }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.GH_PAT }}
persist-credentials: false
fetch-depth: 0
- name: Create Release
id: release
uses: cycjimmy/semantic-release-action@16ca923e6ccbb50770c415a0ccd43709a8c5f7a4 # v4.2.2
with:
extra_plugins: |
@semantic-release/commit-analyzer
@semantic-release/release-notes-generator
@semantic-release/github
conventional-changelog-conventionalcommits
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
- name: Summary
if: steps.release.outputs.new_release_published == 'true'
run: |
echo "## 🚀 Released v${{ steps.release.outputs.new_release_version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Staging**: Deploying automatically" >> $GITHUB_STEP_SUMMARY
echo "- **Production**: Waiting for approval" >> $GITHUB_STEP_SUMMARY
tag-images:
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: write # commit the digest pin back to main
packages: write # retag images in GHCR
outputs:
agent-pi-digest: ${{ steps.retag.outputs.agent-pi-digest }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
token: ${{ secrets.GH_PAT }}
persist-credentials: true
ref: main
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Retag images and capture digests
id: retag
env:
VERSION: ${{ needs.release.outputs.new_version }}
SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
manifest_digest() {
printf 'sha256:%s\n' "$(docker buildx imagetools inspect --raw "$1" | sha256sum | awk '{print $1}')"
}
IMAGES=("webapp" "application-server" "webhook-ingest" "agent-pi")
for img in "${IMAGES[@]}"; do
FULL_IMAGE="ghcr.io/ls1intum/hephaestus/$img"
echo "::group::$img"
for i in $(seq 1 24); do
if docker buildx imagetools inspect "$FULL_IMAGE:$SHA" > /dev/null 2>&1; then break; fi
if [ "$i" -ge 24 ]; then
echo "::error::Image $FULL_IMAGE:$SHA not found after 120 s"
exit 1
fi
sleep 5
done
SRC_DIGEST=$(manifest_digest "$FULL_IMAGE:$SHA")
[[ "$SRC_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]] || {
echo "::error::$img source digest is malformed: ${SRC_DIGEST:-<empty>}"; exit 1; }
echo "$img source digest: $SRC_DIGEST"
docker buildx imagetools create -t "$FULL_IMAGE:$VERSION" "$FULL_IMAGE:$SHA"
DST_DIGEST=$(manifest_digest "$FULL_IMAGE:$VERSION")
if [[ "$SRC_DIGEST" != "$DST_DIGEST" ]]; then
echo "::error::Retag changed the digest for $img — refusing to publish."
echo "::error:: source ($FULL_IMAGE:$SHA): $SRC_DIGEST"
echo "::error:: retagged ($FULL_IMAGE:$VERSION): $DST_DIGEST"
exit 1
fi
if [ "$img" = "agent-pi" ]; then
echo "agent-pi-digest=$DST_DIGEST" >> "$GITHUB_OUTPUT"
fi
echo "::endgroup::"
done
- name: Smoke-test agent-pi
env:
IMAGE: ghcr.io/ls1intum/hephaestus/agent-pi@${{ steps.retag.outputs.agent-pi-digest }}
run: |
set -euo pipefail
docker pull "$IMAGE"
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'bun --version && node --version'
- name: Update docker/agent-image-pin.env
env:
DIGEST: ${{ steps.retag.outputs.agent-pi-digest }}
VERSION: ${{ needs.release.outputs.new_version }}
run: |
set -euo pipefail
if [ -z "$DIGEST" ]; then
echo "::error::agent-pi digest is empty — refusing to commit."
exit 1
fi
REF="ghcr.io/ls1intum/hephaestus/agent-pi@${DIGEST}"
PIN_FILE="docker/agent-image-pin.env"
EXPECTED_LINE="HEPHAESTUS_AGENT_IMAGE_REFERENCE=${REF}"
tmp=$(mktemp)
awk -v ref="$EXPECTED_LINE" '
/^HEPHAESTUS_AGENT_IMAGE_REFERENCE=/ { print ref; found=1; next }
{ print }
END { exit (found ? 0 : 1) }
' "$PIN_FILE" > "$tmp" || {
echo "::error::$PIN_FILE is missing the HEPHAESTUS_AGENT_IMAGE_REFERENCE= line — refusing to silently no-op."
exit 1
}
mv "$tmp" "$PIN_FILE"
if git diff --quiet -- "$PIN_FILE"; then
echo "agent-pi digest unchanged — skipping commit."
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add "$PIN_FILE"
# [skip ci] keeps cicd.yml from re-firing on this commit.
git commit -m "chore(release): pin agent-pi to ${DIGEST} for v${VERSION} [skip ci]"
for attempt in 1 2 3; do
if git push origin HEAD:main; then exit 0; fi
echo "push attempt $attempt failed — fetching + rebasing"
git fetch origin main
git rebase origin/main || {
echo "::error::rebase produced conflicts on $PIN_FILE — investigate manually."
git rebase --abort
exit 1
}
if ! grep -qxF "$EXPECTED_LINE" "$PIN_FILE"; then
echo "::error::pin line was lost during rebase; refusing to push a no-op."
exit 1
fi
sleep $((attempt * 5))
done
echo "::error::push to main failed after 3 attempts"
exit 1
# Deploy to staging automatically after images are tagged
deploy-staging:
needs: [release, tag-images]
if: needs.release.outputs.released == 'true'
uses: ./.github/workflows/deploy-staging.yml
with:
image-tag: ${{ needs.release.outputs.new_version }}
deploy-app: true
deploy-core: false
deploy-proxy: false
secrets: inherit
# Trigger production deployment (approval happens in deploy-prod.yml)
deploy-production:
needs: [release, deploy-staging]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger Production Deploy
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ secrets.GH_PAT }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'deploy-prod.yml',
ref: 'main',
inputs: {
'image-tag': '${{ needs.release.outputs.new_version }}',
'deploy-app': 'true',
'deploy-core': 'false',
'deploy-proxy': 'false'
}
});
console.log('✅ Production deployment triggered for v${{ needs.release.outputs.new_version }}');
- name: Summary
run: |
echo "## 🚀 Production Deployment Triggered" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Version **v${{ needs.release.outputs.new_version }}** deployment to production has been triggered." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⏳ Waiting for approval in the [Deploy to Production](https://github.com/${{ github.repository }}/actions/workflows/deploy-prod.yml) workflow." >> $GITHUB_STEP_SUMMARY