Repository navigation
Expand file tree
/
Copy pathrelease-version-client-sync.yml
More file actions
253 lines (236 loc) · 10.8 KB
/
Copy pathrelease-version-client-sync.yml
File metadata and controls
253 lines (236 loc) · 10.8 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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
name: Regenerate versioned clients
on:
pull_request:
types: [opened, synchronize, reopened, closed]
branches: [main]
permissions:
contents: write
actions: write
concurrency:
group: release-version-client-sync-${{ github.ref }}
cancel-in-progress: false
env:
RIVET_PIN: 288c81f547eb64ec043546acc4df07efc19aceec
KEEL_PIN: 0740fdfbdab7bf5dab32232c82f07dd98aa6a196
jobs:
regenerate:
if: >-
github.event_name == 'pull_request' &&
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/version-')
name: Regenerate and validate versioned clients
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Set up Racket CS 9.3
uses: Bogdanp/setup-racket@v1.15
with:
architecture: x64
distribution: full
variant: CS
version: '9.3'
- name: Install native build dependencies
run: |
sudo apt-get update
sudo apt-get install --yes cmake ninja-build pkg-config \
libgtk-4-dev libadwaita-1-dev libjson-glib-dev \
libsecret-1-dev libglib2.0-dev
- name: Set up embeddable Racket CS
uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@288c81f547eb64ec043546acc4df07efc19aceec
- name: Install pinned Racket dependencies
run: |
raco pkg install --auto --no-docs "https://github.com/turinglambdaai/keel.git#${KEEL_PIN}"
raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}"
raco pkg install --auto --no-docs db crypto
- name: Regenerate clients from the canonical RPC contract
run: |
set -euo pipefail
raco rivet build
bash scripts/check-release-version.sh
- name: Commit generated client version identities
env:
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add \
macos-host/Sources/RivetHost/GeneratedBackend.swift \
windows/GeneratedBackend.hpp \
linux/GeneratedBackend.hpp
if git diff --cached --quiet; then
echo "Generated client files are already aligned."
else
git commit -m "chore(release): regenerate versioned clients"
git push origin "HEAD:${RELEASE_BRANCH}"
fi
- name: Dispatch full CI and product acceptance on generated commit
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
gh workflow run ci.yml --ref "${RELEASE_BRANCH}"
gh workflow run acceptance.yml --ref "${RELEASE_BRANCH}"
publish-verified-release:
name: Tag and publish after merged version PR
if: >-
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/version-')
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0
- name: Verify version, release notes, and signing secret
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
UPDATE_ED25519_PRIVATE_KEY: ${{ secrets.UPDATE_ED25519_PRIVATE_KEY }}
run: |
set -euo pipefail
VERSION="$(tr -d '[:space:]' < VERSION)"
TAG="v${VERSION}"
[[ "$RELEASE_BRANCH" == "release/version-v$VERSION" ]] || {
echo "::error::Release branch $RELEASE_BRANCH does not match VERSION $VERSION"
exit 1
}
bash scripts/check-release-version.sh
NOTES="$(awk -v header="## [${VERSION}]" '
index($0, header) == 1 {on=1; next}
on && /^## / {exit}
on {print}
' CHANGELOG.md)"
[[ -n "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]] || {
echo "::error::Missing release notes for $VERSION"
exit 1
}
[[ -n "${UPDATE_ED25519_PRIVATE_KEY:-}" ]] || {
echo "::error::UPDATE_ED25519_PRIVATE_KEY is not configured; no tag will be created"
exit 1
}
# Confirm the configured private key matches both embedded public
# keys before creating an immutable tag. Never print the private key.
KEY_PEM="$(mktemp)"
trap 'rm -f "$KEY_PEM"' EXIT
printf '%s\n' "$UPDATE_ED25519_PRIVATE_KEY" > "$KEY_PEM"
DER_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | base64 -w0)"
RAW_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 -w0)"
EXPECTED_DER="$(sed -n '/(define update-public-key-b64/{n;s/^[[:space:]]*"\([^"]*\)".*/\1/p;}' racket/taskly/updater.rkt)"
EXPECTED_RAW="$(sed -n 's/.*public static let publicKeyBase64 = "\([^"]*\)".*/\1/p' macos-host/Sources/RivetHost/UpdateService.swift)"
[[ -n "$EXPECTED_DER" && "$DER_PUBLIC" == "$EXPECTED_DER" ]] || {
echo "::error::Signing secret does not match the backend Ed25519 public key"
exit 1
}
[[ -n "$EXPECTED_RAW" && "$RAW_PUBLIC" == "$EXPECTED_RAW" ]] || {
echo "::error::Signing secret does not match the macOS Ed25519 public key"
exit 1
}
if [[ -n "$(git ls-remote --tags origin "refs/tags/$TAG")" ]]; then
# A failed publish can be retried without moving the immutable tag,
# but only if it is annotated and points at this exact merge commit.
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
[[ "$(git cat-file -t "$TAG")" == "tag" ]] || {
echo "::error::Existing tag $TAG is lightweight; refusing to use it"
exit 1
}
[[ "$(git rev-parse "$TAG^{commit}")" == "$(git rev-parse HEAD)" ]] || {
echo "::error::Existing tag $TAG points at a different commit"
exit 1
}
echo "Reusing existing annotated tag $TAG for a safe release retry"
fi
echo "Verified release candidate $TAG at $(git rev-parse HEAD)"
- name: Wait for CI and product acceptance on the merge commit
env:
GH_TOKEN: ${{ github.token }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
set -euo pipefail
ci_ok=false
acceptance_ok=false
for attempt in $(seq 1 180); do
runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${MERGE_SHA}&per_page=100")"
ci="$(jq -c --arg sha "$MERGE_SHA" '[.workflow_runs[] | select(.name == "CI" and .event == "push" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
acceptance="$(jq -c --arg sha "$MERGE_SHA" '[.workflow_runs[] | select(.name == "Product Acceptance" and .event == "push" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
if [[ -n "$ci" ]]; then
status="$(jq -r '.status' <<< "$ci")"
conclusion="$(jq -r '.conclusion // ""' <<< "$ci")"
if [[ "$status" == "completed" && "$conclusion" != "success" ]]; then
echo "::error::CI did not pass for merge commit $MERGE_SHA: $conclusion"
exit 1
fi
[[ "$status" == "completed" && "$conclusion" == "success" ]] && ci_ok=true
fi
if [[ -n "$acceptance" ]]; then
status="$(jq -r '.status' <<< "$acceptance")"
conclusion="$(jq -r '.conclusion // ""' <<< "$acceptance")"
if [[ "$status" == "completed" && "$conclusion" != "success" ]]; then
echo "::error::Product Acceptance did not pass for merge commit $MERGE_SHA: $conclusion"
exit 1
fi
[[ "$status" == "completed" && "$conclusion" == "success" ]] && acceptance_ok=true
fi
if [[ "$ci_ok" == true && "$acceptance_ok" == true ]]; then
echo "CI and Product Acceptance passed for $MERGE_SHA"
break
fi
if [[ "$attempt" -eq 180 ]]; then
echo "::error::Timed out waiting for CI and Product Acceptance on $MERGE_SHA"
exit 1
fi
sleep 15
done
- name: Create immutable annotated tag and dispatch release build
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
VERSION="$(tr -d '[:space:]' < VERSION)"
TAG="v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git show-ref --verify --quiet "refs/tags/$TAG"; then
echo "Reusing immutable tag $TAG"
else
git tag -a "$TAG" -m "Taskly $TAG"
git push origin "$TAG"
fi
gh workflow run release.yml --ref "$TAG"
- name: Wait for the release workflow
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
set -euo pipefail
for attempt in $(seq 1 360); do
runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${RELEASE_SHA}&per_page=100")"
release="$(jq -c --arg sha "$RELEASE_SHA" '[.workflow_runs[] | select(.name == "Release" and .event == "workflow_dispatch" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
if [[ -n "$release" ]]; then
status="$(jq -r '.status' <<< "$release")"
conclusion="$(jq -r '.conclusion // ""' <<< "$release")"
url="$(jq -r '.html_url' <<< "$release")"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" != "success" ]]; then
echo "::error::Release workflow failed: $url"
exit 1
fi
echo "Release completed successfully: $url"
exit 0
fi
fi
if [[ "$attempt" -eq 360 ]]; then
echo "::error::Timed out waiting for release workflow"
exit 1
fi
sleep 15
done