-
Notifications
You must be signed in to change notification settings - Fork 0
377 lines (327 loc) · 14.8 KB
/
release-publish.yml
File metadata and controls
377 lines (327 loc) · 14.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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# SPDX-FileCopyrightText: 2026 Inter Fonts App Contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# .github/workflows/release-publish.yml
#
# Phase 2 of 2 in the branch-protection-friendly release flow.
#
# Triggered automatically when a release-prep PR (label: `release`)
# merges into main, OR manually via workflow_dispatch (e.g. to retry
# after a partial failure where the tag exists but the App Store
# upload didn't complete).
#
# What this does (mirrors phase 6+ of the previous single-shot release.yml):
#
# 1. Reads the now-bumped version from main's appinfo/info.xml
# 2. Verifies the corresponding tag does not already exist
# 3. Creates + pushes the annotated git tag — tags are NOT covered
# by the branch ruleset that gates main, so this works without a
# personal access token
# 4. Builds release notes
# 5. Builds the App Store tarball (interfonts/ folder, runtime files only;
# same allowlist as integration.yml's tarball-dry-run job)
# 6. Publishes the GitHub Release with the tarball attached
# 7. Polls GitHub's CDN until the asset is gzip-valid + tar-valid +
# SHA-256 matches the locally built tarball — bridges the
# propagation delay between asset upload and CDN serving
# 8. Signs the (CDN-verified) tarball with APP_PRIVATE_KEY and POSTs
# to the Nextcloud App Store API. Skipped gracefully when
# APPSTORE_TOKEN is not configured.
#
# Why two workflows?
# ------------------
# Branch protection on main requires all changes via PR + 16/16 required
# checks. A direct `git push origin main` from CI is rejected. Splitting
# into prepare-PR + publish-on-merge keeps the existing protections
# intact while preserving zero-touch releases.
name: Release — Publish
on:
pull_request:
types: [closed]
branches: [main]
workflow_dispatch:
inputs:
prerelease:
description: "Mark the GitHub Release as pre-release"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
permissions:
contents: write # push tag + create GitHub Release
jobs:
publish:
name: Tag, build & publish release
runs-on: ubuntu-latest
# Only fire on:
# - manual workflow_dispatch (recovery scenarios), OR
# - a release-prep PR that actually merged (not just closed).
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'release'))
steps:
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# -----------------------------------------------------------------------
# 1. Resolve the version + prerelease flag from main + (PR body | input)
# -----------------------------------------------------------------------
- name: Resolve release metadata
id: meta
env:
PR_BODY: ${{ github.event.pull_request.body }}
INPUT_PRERELEASE: ${{ inputs.prerelease }}
EVENT: ${{ github.event_name }}
run: |
VERSION=$(grep -oP '(?<=<version>)[^<]+' appinfo/info.xml | tr -d '[:space:]')
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::unexpected version in info.xml: '$VERSION'"
exit 1
fi
TAG="v${VERSION}"
# On PR-merge: read prerelease from the body marker that
# release-prepare.yml embedded. On manual dispatch: use the input.
if [ "$EVENT" = "pull_request" ]; then
PRERELEASE=$(printf '%s' "$PR_BODY" \
| grep -oP 'release-publish:prerelease=\K[a-z]+' \
| head -1)
else
PRERELEASE="$INPUT_PRERELEASE"
fi
[ -z "$PRERELEASE" ] && PRERELEASE="false"
echo "Version : $VERSION"
echo "Tag : $TAG"
echo "Prerelease : $PRERELEASE"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
# -----------------------------------------------------------------------
# 2. Verify tag does not already exist (locally OR on origin)
# -----------------------------------------------------------------------
- name: Verify tag does not exist
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::tag $TAG already exists locally — main is at a stale revision?"
exit 1
fi
if git ls-remote --tags origin "$TAG" | grep -q .; then
echo "::error::tag $TAG already exists on origin"
exit 1
fi
# -----------------------------------------------------------------------
# 3. Create + push annotated tag
# -----------------------------------------------------------------------
- name: Create + push annotated tag
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" \
-m "Release ${TAG}" \
-m "Released : $(date -u +'%Y-%m-%d %H:%M UTC')"
git push origin "$TAG"
echo "Pushed tag: $TAG"
# -----------------------------------------------------------------------
# 4. Build release notes
# -----------------------------------------------------------------------
- name: Build release notes
env:
NEW: ${{ steps.meta.outputs.version }}
TAG: ${{ steps.meta.outputs.tag }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
if [[ -f fonts/inter-version.txt ]]; then
INTER=$(cat fonts/inter-version.txt | tr -d '[:space:]')
else
INTER="unknown"
fi
cat > /tmp/release-notes.md << EOF
## Inter Fonts ${NEW}
| | |
|---|---|
| **Version** | \`${NEW}\` |
| **Bundled Inter** | ${INTER} |
| **Released by** | [@${ACTOR}](${REPO_URL%/*}/${ACTOR}) |
## Installation
\`\`\`bash
TAG=${TAG}
curl -fsSL "https://github.com/solracsf/nc-interfonts/releases/download/\${TAG}/interfonts.tar.gz" \
-o /tmp/interfonts.tar.gz
tar -xzf /tmp/interfonts.tar.gz -C /var/www/nextcloud/apps/
chown -R www-data:www-data /var/www/nextcloud/apps/interfonts/
sudo -u www-data php /var/www/nextcloud/occ app:enable interfonts
\`\`\`
See the full [CHANGELOG](${REPO_URL}/blob/${TAG}/CHANGELOG.md) for details.
---
_Published by the [Release — Publish](${REPO_URL}/actions/workflows/release-publish.yml) workflow._
EOF
# -----------------------------------------------------------------------
# 5. Build the App Store tarball
#
# The Nextcloud App Store requires a .tar.gz whose top-level folder
# name matches <id> in info.xml exactly ("interfonts").
# The auto-generated GitHub source archive has folder name
# "nc-interfonts-X.Y.Z/" and includes dev files — unsuitable.
#
# The allowlist below MUST match integration.yml's tarball-dry-run
# job — the dry-run runs on every PR and asserts these contents
# (plus that no dev files leak in), so any drift here would have
# been caught before merge.
# -----------------------------------------------------------------------
- name: Build app store tarball
run: |
APP_DIR=/tmp/build/interfonts
mkdir -p "${APP_DIR}"
cp -r appinfo fonts img lib LICENSES "${APP_DIR}/"
cp COPYING README.md CHANGELOG.md "${APP_DIR}/"
tar -czf /tmp/interfonts.tar.gz -C /tmp/build interfonts
echo "Tarball top level:"
tar -tzf /tmp/interfonts.tar.gz | grep -E '^interfonts/[^/]+/?$' | sort
echo "Tarball size: $(du -sh /tmp/interfonts.tar.gz | cut -f1)"
# -----------------------------------------------------------------------
# 6. Publish GitHub Release with the tarball attached
# -----------------------------------------------------------------------
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
NEW: ${{ steps.meta.outputs.version }}
PRERELEASE: ${{ steps.meta.outputs.prerelease }}
run: |
PRERELEASE_FLAG=""
if [[ "$PRERELEASE" == "true" ]]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "$TAG" \
--title "Inter Fonts ${NEW}" \
--notes-file /tmp/release-notes.md \
--latest \
/tmp/interfonts.tar.gz \
$PRERELEASE_FLAG
echo "Published: $TAG"
echo "URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${TAG}"
# -----------------------------------------------------------------------
# 7. Wait for the release asset to be served correctly by GitHub's CDN
#
# GitHub's CDN can take a few seconds to propagate a newly uploaded
# release asset. The Nextcloud App Store downloads the tarball from
# this same URL right after we POST it. If the CDN isn't ready yet,
# the App Store receives an HTML redirect or empty body and rejects
# with: "interfonts.tar.gz is not a valid tar.gz archive".
#
# This step polls the download URL up to ~60 s and validates:
# - HTTP 200
# - file(1) reports "gzip compressed"
# - tar -tzf lists at least one interfonts/ entry
# - SHA-256 matches the locally built tarball byte-for-byte
# On success the local copy is replaced with the verified CDN copy
# so the signature in step 8 covers the exact bytes the App Store
# will download.
# -----------------------------------------------------------------------
- name: Verify release asset on CDN
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/interfonts.tar.gz"
LOCAL="/tmp/interfonts.tar.gz"
REMOTE="/tmp/interfonts-cdn.tar.gz"
MAX_ATTEMPTS=12
SLEEP_SECS=5
echo "Waiting for CDN: ${DOWNLOAD_URL}"
LOCAL_SHA=$(sha256sum "${LOCAL}" | awk '{print $1}')
echo "Local SHA-256 : ${LOCAL_SHA}"
for attempt in $(seq 1 ${MAX_ATTEMPTS}); do
echo "--- Attempt ${attempt}/${MAX_ATTEMPTS} ---"
HTTP_STATUS=$(curl -sL -o "${REMOTE}" -w "%{http_code}" \
--retry 0 --max-time 30 \
"${DOWNLOAD_URL}")
echo "HTTP status: ${HTTP_STATUS}"
if [[ "${HTTP_STATUS}" != "200" ]]; then
echo "Non-200 response; retrying in ${SLEEP_SECS}s…"
sleep ${SLEEP_SECS}
continue
fi
FILE_TYPE=$(file -b "${REMOTE}")
echo "file(1) says: ${FILE_TYPE}"
if ! echo "${FILE_TYPE}" | grep -qi "gzip compressed"; then
echo "Not a gzip file yet; retrying in ${SLEEP_SECS}s…"
sleep ${SLEEP_SECS}
continue
fi
if ! tar -tzf "${REMOTE}" 2>/dev/null | grep -q '^interfonts/'; then
echo "tar listing invalid or missing interfonts/ prefix; retrying in ${SLEEP_SECS}s…"
sleep ${SLEEP_SECS}
continue
fi
REMOTE_SHA=$(sha256sum "${REMOTE}" | awk '{print $1}')
echo "Remote SHA-256: ${REMOTE_SHA}"
if [[ "${REMOTE_SHA}" != "${LOCAL_SHA}" ]]; then
echo "SHA-256 mismatch — CDN may still be propagating; retrying in ${SLEEP_SECS}s…"
sleep ${SLEEP_SECS}
continue
fi
echo "✓ CDN asset verified: gzip-valid, tar-valid, SHA-256 matches."
cp "${REMOTE}" "${LOCAL}"
exit 0
done
echo "::error::release asset could not be verified after ${MAX_ATTEMPTS} attempts"
exit 1
# -----------------------------------------------------------------------
# 8. Sign tarball and publish to the Nextcloud App Store
#
# Skipped if APPSTORE_TOKEN is not configured (before the app is
# registered on the store for the first time).
# -----------------------------------------------------------------------
- name: Publish to Nextcloud App Store
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
APPSTORE_TOKEN: ${{ secrets.APPSTORE_TOKEN }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
PRERELEASE: ${{ steps.meta.outputs.prerelease }}
run: |
if [[ -z "${APPSTORE_TOKEN}" ]]; then
echo "APPSTORE_TOKEN not configured — skipping App Store publish."
exit 0
fi
if [[ -z "${APP_PRIVATE_KEY}" ]]; then
echo "::error::APP_PRIVATE_KEY secret is not set"
exit 1
fi
echo "${APP_PRIVATE_KEY}" > /tmp/interfonts.key
chmod 600 /tmp/interfonts.key
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/interfonts.tar.gz"
# Sign the CDN-verified copy of the tarball, so the signature
# covers the exact bytes the App Store will download.
SIGNATURE=$(openssl dgst -sha512 \
-sign /tmp/interfonts.key \
/tmp/interfonts.tar.gz \
| openssl base64 -A)
NIGHTLY="false"
if [[ "${PRERELEASE}" == "true" ]]; then
NIGHTLY="true"
fi
HTTP_STATUS=$(curl -s -o /tmp/appstore-response.json -w "%{http_code}" \
-X POST \
-H "Authorization: Token ${APPSTORE_TOKEN}" \
-H "Content-Type: application/json; charset=utf8" \
"https://apps.nextcloud.com/api/v1/apps/releases" \
-d "{\"download\": \"${DOWNLOAD_URL}\", \"signature\": \"${SIGNATURE}\", \"nightly\": ${NIGHTLY}}")
echo "App Store HTTP status: ${HTTP_STATUS}"
cat /tmp/appstore-response.json
# 200 = updated, 201 = created
if [[ "${HTTP_STATUS}" != "200" && "${HTTP_STATUS}" != "201" ]]; then
echo "::error::App Store API returned HTTP ${HTTP_STATUS}"
exit 1
fi
echo "Successfully published to the Nextcloud App Store."