Skip to content

Commit 0d0da96

Browse files
PangMo5claude
andcommitted
ci: publish the site and drive releases + Sparkle notes from CHANGELOG
- Add a deploy-site workflow: publishes web/* to Pages on push (or manual dispatch), re-publishing the live appcast.xml so a site deploy never wipes the Sparkle feed. - Release workflow: build the GitHub release body from the matching CHANGELOG.md section (### promoted to ##) plus Install / Update, and embed that section as sanitized inline HTML in the appcast <description> so Sparkle shows clean notes. Assemble web/* into the Pages artifact with a cache-busted stylesheet. Bump pinned action versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 686e61d commit 0d0da96

2 files changed

Lines changed: 161 additions & 22 deletions

File tree

.github/workflows/deploy-site.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Deploy site
2+
3+
# Publishes the marketing/docs site to GitHub Pages independently of releases.
4+
# The release workflow owns appcast.xml (it signs + regenerates it); this
5+
# workflow re-publishes the currently live appcast alongside the site so a site
6+
# deploy never wipes the Sparkle feed.
7+
8+
on:
9+
push:
10+
branches: [main]
11+
paths:
12+
- "web/**"
13+
- "Resources/Marketing/app-icon.png"
14+
- ".github/workflows/deploy-site.yml"
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
pages: write
20+
id-token: write
21+
22+
concurrency:
23+
group: pages
24+
cancel-in-progress: false
25+
26+
jobs:
27+
build:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v6
31+
32+
- name: Assemble site
33+
env:
34+
GH_TOKEN: ${{ github.token }}
35+
REPO: ${{ github.repository }}
36+
OWNER: ${{ github.repository_owner }}
37+
run: |
38+
set -euo pipefail
39+
SITE="$RUNNER_TEMP/site"
40+
mkdir -p "$SITE"
41+
42+
# Latest release version for the no-JS download/version fallback in
43+
# index.html (the page also resolves this live via the API).
44+
VERSION="$(gh release view --repo "$REPO" --json tagName -q .tagName 2>/dev/null | sed 's/^v//' || true)"
45+
echo "Latest version: ${VERSION:-<none>}"
46+
47+
cp web/*.html web/*.css "$SITE/"
48+
if [ -n "$VERSION" ]; then
49+
sed -i "s/__VERSION__/${VERSION}/g" "$SITE/index.html"
50+
fi
51+
cp Resources/Marketing/app-icon.png "$SITE/icon.png"
52+
53+
# Cache-bust the stylesheet so a CSS change is never masked by a
54+
# stale browser cache (HTML + CSS stay in lockstep per deploy).
55+
BUST="${GITHUB_SHA:0:8}"
56+
sed -i "s#\"./style.css\"#\"./style.css?v=${BUST}\"#g" "$SITE"/*.html
57+
58+
# Preserve the Sparkle feed. GitHub Pages replaces the whole site on
59+
# deploy, so re-publish the live appcast.xml. Fail rather than deploy
60+
# without it (deploying without it would break in-app updates).
61+
OWNER_LC="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')"
62+
NAME="${REPO##*/}"
63+
curl -fsSL "https://${OWNER_LC}.github.io/${NAME}/appcast.xml" -o "$SITE/appcast.xml"
64+
echo "Bundled appcast.xml ($(wc -c < "$SITE/appcast.xml") bytes)"
65+
66+
- uses: actions/upload-pages-artifact@v5
67+
with:
68+
path: ${{ runner.temp }}/site
69+
70+
deploy:
71+
needs: build
72+
runs-on: ubuntu-latest
73+
environment:
74+
name: github-pages
75+
url: ${{ steps.deployment.outputs.page_url }}
76+
steps:
77+
- id: deployment
78+
uses: actions/deploy-pages@v5

.github/workflows/release.yml

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
TUIST_DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
2525
TUIST_SPARKLE_PUBLIC_ED_KEY: ${{ secrets.SPARKLE_PUBLIC_ED_KEY }}
2626
steps:
27-
- uses: actions/checkout@v4
27+
- uses: actions/checkout@v6
2828

2929
- name: Resolve version
3030
id: version
@@ -38,7 +38,7 @@ jobs:
3838
fi
3939
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
4040
41-
- uses: jdx/mise-action@v2
41+
- uses: jdx/mise-action@v4
4242

4343
- name: Import Developer ID certificate
4444
env:
@@ -159,33 +159,82 @@ jobs:
159159
--download-url-prefix "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/" \
160160
"$RUNNER_TEMP/release"
161161
162-
- name: Add release-notes links to appcast
162+
- name: Embed release notes in appcast
163163
env:
164164
APPCAST: ${{ runner.temp }}/release/appcast.xml
165-
REPO: ${{ github.repository }}
165+
VERSION: ${{ steps.version.outputs.version }}
166166
run: |
167-
python3 - <<'PY'
168-
import os, xml.etree.ElementTree as ET
169-
sparkle = "http://www.andymatuschak.org/xml-namespaces/sparkle"
170-
ET.register_namespace("sparkle", sparkle)
171-
path, repo = os.environ["APPCAST"], os.environ["REPO"]
172-
tree = ET.parse(path)
173-
for item in tree.getroot().iter("item"):
174-
version = item.find(f"{{{sparkle}}}shortVersionString")
175-
if version is None or item.find(f"{{{sparkle}}}releaseNotesLink") is not None:
176-
continue
177-
link = ET.SubElement(item, f"{{{sparkle}}}releaseNotesLink")
178-
link.text = f"https://github.com/{repo}/releases/tag/v{version.text}"
179-
tree.write(path, xml_declaration=True, encoding="utf-8")
180-
print("appcast: release-notes links added")
167+
set -euo pipefail
168+
# This version's changelog section (### promoted to ##), without the
169+
# install boilerplate — Sparkle shows the actual changes inline.
170+
awk -v h="## ${VERSION} " 'index($0, h) == 1 { g = 1; next } g && /^## / { exit } g { print }' \
171+
CHANGELOG.md | sed 's/^### /## /' > "$RUNNER_TEMP/changelog-section.md"
172+
SECTION="$RUNNER_TEMP/changelog-section.md" python3 - <<'PY'
173+
import os, re, html as H
174+
def inline(s):
175+
s = H.escape(s)
176+
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
177+
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
178+
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
179+
s = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", s)
180+
return s
181+
out, in_ul = [], False
182+
for line in open(os.environ["SECTION"], encoding="utf-8").read().splitlines():
183+
if line.startswith("## "):
184+
if in_ul: out.append("</ul>"); in_ul = False
185+
out.append("<h2>" + inline(line[3:].strip()) + "</h2>")
186+
elif line.startswith("- "):
187+
if not in_ul: out.append("<ul>"); in_ul = True
188+
out.append("<li>" + inline(line[2:].strip()) + "</li>")
189+
elif line.strip() == "":
190+
if in_ul: out.append("</ul>"); in_ul = False
191+
else:
192+
if in_ul: out.append("</ul>"); in_ul = False
193+
out.append("<p>" + inline(line.strip()) + "</p>")
194+
if in_ul: out.append("</ul>")
195+
html = "\n".join(out).strip()
196+
appcast = os.environ["APPCAST"]
197+
xml = open(appcast, encoding="utf-8").read()
198+
if html and "<description>" not in xml:
199+
desc = "<description><![CDATA[\n" + html + "\n]]></description>\n "
200+
xml = xml.replace("</item>", desc + "</item>", 1)
201+
open(appcast, "w", encoding="utf-8").write(xml)
202+
print("appcast: embedded release notes")
203+
else:
204+
print("appcast: skipped (no notes or description already present)")
181205
PY
182206
207+
- name: Build release notes from CHANGELOG
208+
env:
209+
VERSION: ${{ steps.version.outputs.version }}
210+
TAG: ${{ github.ref_name }}
211+
REPO: ${{ github.repository }}
212+
run: |
213+
set -euo pipefail
214+
NOTES="$RUNNER_TEMP/release-notes.md"
215+
# Pull this version's section from CHANGELOG.md (the source of truth),
216+
# promoting its ### subheadings to ## for the GitHub release.
217+
awk -v h="## ${VERSION} " 'index($0, h) == 1 { g = 1; next } g && /^## / { exit } g { print }' \
218+
CHANGELOG.md | sed 's/^### /## /' > "$NOTES"
219+
if [ ! -s "$NOTES" ]; then
220+
echo "See the [changelog](https://github.com/${REPO}/blob/main/CHANGELOG.md)." > "$NOTES"
221+
fi
222+
# Append the install/update boilerplate (kept out of CHANGELOG.md).
223+
{
224+
echo ""
225+
echo "## Install / Update"
226+
echo ""
227+
echo "- Homebrew: \`brew upgrade --cask tatami\`"
228+
echo "- Direct: [Tatami-${VERSION}.dmg](https://github.com/${REPO}/releases/download/${TAG}/Tatami-${VERSION}.dmg)"
229+
echo "- In-app: Sparkle will pick this up on its next check."
230+
} >> "$NOTES"
231+
183232
- name: Publish GitHub Release
184233
run: |
185234
gh release create "${{ github.ref_name }}" \
186235
"$RUNNER_TEMP/Tatami-${{ steps.version.outputs.version }}.dmg" \
187236
--title "${{ github.ref_name }}" \
188-
--generate-notes
237+
--notes-file "$RUNNER_TEMP/release-notes.md"
189238
env:
190239
GH_TOKEN: ${{ github.token }}
191240

@@ -214,13 +263,25 @@ jobs:
214263
git commit -am "tatami ${VERSION}"
215264
git push
216265
217-
- name: Publish appcast to GitHub Pages
266+
- name: Assemble GitHub Pages site (landing page + appcast)
267+
env:
268+
VERSION: ${{ steps.version.outputs.version }}
218269
run: |
219270
set -euo pipefail
220271
mkdir -p "$RUNNER_TEMP/site"
272+
# Static pages (configuration.html / releases.html render their source
273+
# of truth at runtime; index.html gets the release version injected).
274+
cp web/*.html web/*.css "$RUNNER_TEMP/site/"
275+
sed "s/__VERSION__/${VERSION}/g" web/index.html > "$RUNNER_TEMP/site/index.html"
276+
cp Resources/Marketing/app-icon.png "$RUNNER_TEMP/site/icon.png"
277+
# Cache-bust the stylesheet so a CSS change is never masked by a
278+
# stale browser cache.
279+
BUST="${GITHUB_SHA:0:8}"
280+
sed -i "s#\"./style.css\"#\"./style.css?v=${BUST}\"#g" "$RUNNER_TEMP/site"/*.html
281+
# Sparkle feed.
221282
cp "$RUNNER_TEMP/release/appcast.xml" "$RUNNER_TEMP/site/appcast.xml"
222283
223-
- uses: actions/upload-pages-artifact@v3
284+
- uses: actions/upload-pages-artifact@v5
224285
with:
225286
path: ${{ runner.temp }}/site
226287

@@ -232,4 +293,4 @@ jobs:
232293
url: ${{ steps.deployment.outputs.page_url }}
233294
steps:
234295
- id: deployment
235-
uses: actions/deploy-pages@v4
296+
uses: actions/deploy-pages@v5

0 commit comments

Comments
 (0)