Skip to content

docs: changelog for v0.7.3-beta.6 #36

docs: changelog for v0.7.3-beta.6

docs: changelog for v0.7.3-beta.6 #36

Workflow file for this run

name: Release
# Tag a version to ship it: `git tag v0.7.0 && git push origin v0.7.0`.
# A tag with a pre-release suffix (e.g. v0.7.0-beta.1) publishes to the Early Access channel; a plain
# tag (v0.7.0) publishes to everyone. The one-time secret/Pages setup is documented in the
# release-swift skill under .agents/skills/release-swift/.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to build (e.g. v0.7.0)"
required: true
type: string
permissions:
contents: write
# Serialize releases: each run reads appcast.xml from gh-pages, regenerates it with generate_appcast,
# and force-publishes the branch. Without this, two tags pushed close together could race and drop a
# feed entry.
concurrency:
group: release-appcast
cancel-in-progress: false
jobs:
release:
runs-on: macos-26
steps:
- uses: actions/checkout@v7
with:
# Build the requested tag. On a tag push `inputs.tag` is empty and checkout defaults to the
# triggering tag; on workflow_dispatch this pins the tag instead of the default branch.
ref: ${{ inputs.tag }}
fetch-depth: 0 # full history so the CFBundleVersion (commit count) is correct
persist-credentials: true
- name: Select Xcode
# The app icon is committed prebuilt (see script/compile_icon.sh) because no actool on GitHub's
# runners can compile its Liquid Glass `.icon` (Apple regression FB20183399), so the default
# runner Xcode is fine for the build/sign/notarize steps.
run: sudo xcode-select -s /Applications/Xcode.app
- name: Resolve tag, version, and channel
id: meta
run: |
TAG="${{ inputs.tag }}"
[ -z "$TAG" ] && TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
if [[ "$TAG" == *-* ]]; then CHANNEL=beta; PRERELEASE=true; else CHANNEL=stable; PRERELEASE=false; fi
{
echo "tag=$TAG"
echo "version=$VERSION"
echo "channel=$CHANNEL"
echo "prerelease=$PRERELEASE"
} >> "$GITHUB_OUTPUT"
# Imports the Developer ID cert into a temporary keychain and adds it to the search list (cleaned up
# automatically in a post step). Replaces a hand-rolled `security create-keychain`/`import` block.
- name: Import Developer ID certificate
uses: apple-actions/import-codesign-certs@v7
with:
# Developer ID Application cert (.p12, base64) used to sign the app and DMG.
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Resolve signing identity
run: |
# The cert is now in the search list; codesign needs the identity's display name.
IDENTITY="$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/ {print $2; exit}')"
[ -n "$IDENTITY" ] || { echo "No Developer ID Application identity found in the imported cert" >&2; exit 1; }
echo "CODESIGN_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
- name: Write Sparkle private key
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
printf '%s' "$SPARKLE_PRIVATE_KEY" > "$RUNNER_TEMP/sparkle_ed_private_key"
echo "SPARKLE_PRIVATE_KEY_FILE=$RUNNER_TEMP/sparkle_ed_private_key" >> "$GITHUB_ENV"
- name: Carry forward the existing appcast
run: |
# generate_appcast works on a folder: it reads the existing appcast.xml there and the new DMG,
# then writes a merged appcast. "feed/" is that folder. Seed it with the live appcast so prior
# versions and the OTHER channel's items are preserved (verified: items whose archive isn't in
# the folder keep their original enclosure URLs untouched).
mkdir -p feed
# Distinguish "no gh-pages yet" (first release -> fresh feed) from "couldn't read it"
# (transient failure). Only start fresh when the branch genuinely doesn't exist; otherwise
# abort so a network blip can't replace the live feed's full history with a single item.
set +e
git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1
rc=$?
set -e
if [ "$rc" -eq 0 ]; then
git fetch origin gh-pages
git show origin/gh-pages:appcast.xml > feed/appcast.xml \
|| { echo "gh-pages exists but appcast.xml is unreadable — aborting to protect the feed." >&2; exit 1; }
echo "Loaded existing appcast from gh-pages."
elif [ "$rc" -eq 2 ]; then
echo "No gh-pages branch yet — starting a fresh feed."
else
echo "Could not reach origin to check for gh-pages — aborting to protect the feed." >&2
exit 1
fi
- name: Build, sign, and notarize the DMG
env:
SPARKLE_PUBLIC_KEY: ${{ secrets.SPARKLE_PUBLIC_KEY }}
# Apple ID + app-specific password + team ID for notarytool.
NOTARY_APPLE_ID: ${{ secrets.APPLE_ID }}
NOTARY_APP_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
NOTARY_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
OPENUSAGE_VERSION: ${{ steps.meta.outputs.version }}
run: ./script/release.sh
# Upload the dSYM produced by release.sh so PostHog can symbolicate crash reports (the in-app
# crash autocapture is anonymous and honors the Share-Anonymous-Usage opt-out; see docs/privacy.md).
# posthog-cli is a CI-only build tool (npm), never shipped in the app bundle. Skips with a loud
# warning when POSTHOG_CLI_API_KEY is unset (e.g. forks), so a missing key never blocks a release;
# when the key IS set, an upload failure fails the step so we never silently ship unsymbolicated.
- name: Upload dSYMs to PostHog
env:
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }}
# US region, matching TelemetryConfig.host in the app.
POSTHOG_CLI_HOST: https://us.i.posthog.com
VERSION: ${{ steps.meta.outputs.version }}
run: |
if [ -z "${POSTHOG_CLI_API_KEY}" ]; then
echo "::warning::POSTHOG_CLI_API_KEY not set — skipping dSYM upload; crashes for $VERSION will NOT symbolicate."
exit 0
fi
[ -d dist/dSYMs ] || { echo "dist/dSYMs missing — release.sh did not produce a dSYM." >&2; exit 1; }
npm install -g @posthog/cli
# Symbolication matches a crash report to its symbols by the binary's Mach-O UUID (carried in
# the dSYM), so `--directory` is all that's required — no per-release flags to keep in sync.
posthog-cli dsym upload --directory dist/dSYMs
- name: Publish DMG to GitHub Releases
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: OpenUsage ${{ steps.meta.outputs.version }}
prerelease: ${{ steps.meta.outputs.prerelease }}
files: dist/OpenUsage-${{ steps.meta.outputs.version }}.dmg
- name: Preserve Tauri updater manifest on stable releases
if: steps.meta.outputs.channel == 'stable'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
run: |
# Older Tauri builds fetch releases/latest/download/latest.json. Every stable Swift release
# becomes GitHub "Latest", so carry the final Tauri manifest forward on each stable release.
tmpdir="$(mktemp -d)"
gh release download v0.6.28 --pattern latest.json --dir "$tmpdir"
gh release upload "$TAG" "$tmpdir/latest.json" --clobber
- name: Generate appcast with Sparkle
env:
DMG: OpenUsage-${{ steps.meta.outputs.version }}.dmg
CHANNEL: ${{ steps.meta.outputs.channel }}
# Per-tag GitHub release asset base. generate_appcast applies this prefix only to the new DMG
# in feed/; preserved items keep their own (older-tag) URLs.
DOWNLOAD_PREFIX: https://github.com/${{ github.repository }}/releases/download/${{ steps.meta.outputs.tag }}/
run: |
# Locate generate_appcast from the Sparkle SwiftPM artifact that release.sh already fetched.
GA="$(find .build/artifacts -type f -name generate_appcast 2>/dev/null | head -n1)"
[ -x "$GA" ] || { echo "generate_appcast not found under .build/artifacts" >&2; exit 1; }
# How many feed items existed before (to confirm nothing is dropped). 0 on first release.
# Sparkle normally keeps only a few versions per branch; this release feed is intentionally
# append-only so older installs and the other channel's items remain available.
before=0
[ -f feed/appcast.xml ] && before=$(grep -c "<item>" feed/appcast.xml || true)
cp "dist/$DMG" "feed/$DMG"
# Pre-release tags publish to the Early Access (beta) channel; plain tags publish to everyone.
channel_arg=()
[ "$CHANNEL" = "beta" ] && channel_arg=(--channel beta)
# The key file is base64 (it can contain '/'); pass it via stdin so it's never an argv/glob.
cat "$SPARKLE_PRIVATE_KEY_FILE" | "$GA" \
--ed-key-file - \
--download-url-prefix "$DOWNLOAD_PREFIX" \
--maximum-versions 0 \
"${channel_arg[@]}" \
feed
# Fail loudly if the merge went wrong. The new DMG's enclosure must exist AND carry an EdDSA
# signature on the same line (generate_appcast silently omits it if SUPublicEDKey doesn't match
# the signing key), and the item count must not shrink (a dropped preserved/other-channel entry).
after=$(grep -c "<item>" feed/appcast.xml || true)
grep -Eq "$DMG\"[^>]*sparkle:edSignature" feed/appcast.xml \
|| { echo "New item for $DMG is missing or unsigned (SUPublicEDKey/private-key mismatch?)." >&2; exit 1; }
[ "$after" -ge "$before" ] || { echo "Appcast item count dropped ($before -> $after) — aborting." >&2; exit 1; }
echo "Appcast OK: $before -> $after items."
- name: Assemble Pages directory
run: |
mkdir -p public
cp feed/appcast.xml public/appcast.xml
- name: Publish appcast to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: ./public
# gh-pages also hosts pricing_supplement.json (see pricing-supplement.yml). Without
# keep_files, publishing the appcast would delete it from the branch.
keep_files: true