-
Notifications
You must be signed in to change notification settings - Fork 2
222 lines (197 loc) · 7.93 KB
/
Copy pathrelease.yml
File metadata and controls
222 lines (197 loc) · 7.93 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
name: release
# Builds the beeperbox image and publishes it to GHCR — gated on the same
# functional tests the PRs run, so a broken build (or a broken upstream Beeper
# pulled by the weekly rebuild) can't silently become :latest.
#
# Flow for the release path (tag push / weekly cron / manual dispatch):
# prepare -> resolve which ref to build
# verify -> build amd64, run the MCP + VNC guard checks (the gate)
# publish -> roll current :latest to :previous, then build+push multi-arch
# semver tags + :latest (only runs if verify passed)
# If verify fails, publish is skipped: the previous :latest stays live as the
# last-known-good image, :previous is untouched, and notify-failure flags it.
#
# Triggers:
# - tag push (v*.*.*) -> :X.Y.Z, :X.Y, :X, :latest (gated)
# - weekly cron (Mon 06 UTC) -> rebuild newest release tag, refresh Beeper (gated)
# - manual dispatch -> ad-hoc gated rebuild of newest release tag
# - push to master -> :edge (bleeding-edge, ungated by design)
on:
push:
tags:
- 'v*.*.*'
branches:
- master
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
# Resolve the ref to build. Tag push builds the pushed tag; schedule/dispatch
# resolve the newest release tag so :latest tracks released code, not master.
prepare:
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.ref.outputs.ref }}
skip: ${{ steps.ref.outputs.skip }}
steps:
- name: Resolve ref
id: ref
run: |
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
git_ref=$(git ls-remote --tags --sort=-v:refname https://github.com/${{ github.repository }}.git 'v*.*.*' \
| awk '{print $2}' | sed 's|refs/tags/||' | grep -v '\^{}' | head -n1)
if [ -z "$git_ref" ]; then
echo "No release tag found; nothing to rebuild."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "ref=$git_ref" >> "$GITHUB_OUTPUT"
else
echo "ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
fi
# The gate: build the candidate image and run the same checks the PRs run.
# Nothing here pushes to GHCR.
verify:
needs: prepare
if: needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-latest
steps:
# Build context = the release ref (what actually ships). But the gate
# scripts come from the workflow ref (latest), NOT the release ref: the
# weekly cron rebuilds the newest release *tag*, which can predate the
# test tooling (e.g. v0.5.0 was tagged before these scripts existed). The
# scripts are black-box probes of the built image, so taking them from
# latest is correct and avoids "script not found" on older tags.
- name: Checkout build context (release ref)
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
- name: Checkout test tooling (latest workflow ref)
uses: actions/checkout@v6
with:
path: .ci-tools
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build candidate (amd64, load locally)
uses: docker/build-push-action@v7
with:
context: .
load: true
platforms: linux/amd64
tags: beeperbox:candidate
cache-from: type=gha
cache-to: type=gha,mode=max
- name: MCP guard gate
run: bash .ci-tools/scripts/mcp-guard-check.sh beeperbox:candidate
- name: Install Xvfb + x11vnc
run: sudo apt-get update -qq && sudo apt-get install -y -qq xvfb x11vnc
- name: VNC auth gate
run: bash .ci-tools/scripts/vnc-auth-check.sh
# Publish only runs if verify passed. Rolls the current :latest to :previous
# for a known-good fallback, then builds + pushes the multi-arch image.
publish:
needs: [prepare, verify]
if: needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Roll current :latest -> :previous (known-good fallback)
run: |
IMG=ghcr.io/${{ github.repository_owner }}/beeperbox
if docker buildx imagetools inspect "$IMG:latest" >/dev/null 2>&1; then
docker buildx imagetools create -t "$IMG:previous" "$IMG:latest"
echo "rolled $IMG:latest -> $IMG:previous"
else
echo "no existing :latest yet — skipping :previous roll (first publish)"
fi
- name: Compute image tags
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/beeperbox
tags: |
type=semver,pattern={{version}},value=${{ needs.prepare.outputs.ref }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.prepare.outputs.ref }}
type=semver,pattern={{major}},value=${{ needs.prepare.outputs.ref }}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Flags a failed gate/publish loudly. :latest is unchanged on failure, so
# this is "we did NOT ship", not "we shipped something broken".
notify-failure:
needs: [prepare, verify, publish]
if: failure() && needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-latest
steps:
- name: Summarize
run: |
{
echo "## ⚠️ Release gate failed — \`:latest\` was NOT updated"
echo ""
echo "Ref: \`${{ needs.prepare.outputs.ref }}\`"
echo ""
echo "The verify (guard tests) or publish step failed. The previous \`:latest\` remains the live image and \`:previous\` is unchanged — nothing broken was shipped. Investigate the failing job before re-running."
} >> "$GITHUB_STEP_SUMMARY"
# Every push to master publishes :edge — bleeding-edge, may break between
# releases, ungated by design. Users opt in explicitly.
edge:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute OCI labels
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/beeperbox
tags: type=raw,value=edge
- name: Build and push :edge
uses: docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max