From 213216baf33a5693d39dd7385a9237692cfe4915 Mon Sep 17 00:00:00 2001 From: Fair Bear Labs Date: Fri, 25 Sep 2026 23:53:41 -0400 Subject: [PATCH 1/2] ci: add release workflow for tagged builds Triggered on v* tags (validated as 3-part semver; Go tooling here rejects 4-part versions). Builds the universal Go engine binary and Swift release app via `make bundle`, zips the .app, generates SHA256 checksums, and publishes them to a GitHub Release with generated notes. Mirrors ci.yml's SHA-pinned actions and least-privilege permissions (contents: write scoped to the release job only). The app is unsigned/un-notarized for now (no signing setup exists in this repo); code signing and notarization are tracked as a follow-up. Closes the "Add GitHub Actions release workflow" TODO. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 5 +++ TODOS.md | 16 ++------ 3 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..93e4f01 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +# Release: builds the universal Go engine binary + Swift app, assembles the +# .app bundle exactly as `make bundle` does, zips it, and publishes it (with +# checksums) to a GitHub Release with auto-generated notes. +# +# Triggered by pushing a 3-part semver tag (vX.Y.Z). The Go toolchain used by +# this project rejects 4-part versions, so tags with a 4th segment (or any +# pre-release/build suffix) are rejected before any build work happens. +# +# The resulting app is unsigned/un-notarized (no Apple Developer ID is +# configured in this repo yet) — see the release PR for the follow-up. + +name: release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + build: + runs-on: macos-latest + permissions: + contents: write # to publish the GitHub Release + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: engine/go.mod + + - name: Derive and validate version from tag + id: version + run: | + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Tag '$tag' is not a 3-part semver (vX.Y.Z). Go tooling in this repo rejects 4-part versions; retag with e.g. v${version%%.*}.0.0." >&2 + exit 1 + fi + if [[ -f VERSION && "$(cat VERSION)" != "$version" ]]; then + echo "::warning::Tag version ($version) does not match VERSION file ($(cat VERSION))." + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Bundle (engine + app) + run: make bundle + + - name: Package artifacts + id: package + run: | + set -euo pipefail + cd build + zip_name="ChoosePaste-${{ steps.version.outputs.tag }}-macos.zip" + zip -r -X -q "$zip_name" ChoosePaste.app + shasum -a 256 "$zip_name" > SHA256SUMS.txt + echo "zip_name=$zip_name" >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + "build/${{ steps.package.outputs.zip_name }}" \ + "build/SHA256SUMS.txt" \ + --repo "${{ github.repository }}" \ + --title "${{ steps.version.outputs.tag }}" \ + --generate-notes diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c34e4..65ec577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- CI: release workflow (`.github/workflows/release.yml`) — on a `vX.Y.Z` tag push, + builds the universal Go engine + Swift release app, zips the .app bundle, and + publishes it with SHA256 checksums to a GitHub Release with generated notes + ### Security - Bump `golang.org/x/net` to v0.58.0 and Go to 1.26.6 (fixes reachable HTML-parser DoS/correctness issues; the engine parses arbitrary clipboard HTML) diff --git a/TODOS.md b/TODOS.md index 1c70777..1a29aeb 100644 --- a/TODOS.md +++ b/TODOS.md @@ -14,18 +14,6 @@ **Priority:** P1 **Depends on:** Alpha build available with benchmarkable transform flow -### Add GitHub Actions release workflow - -**What:** Create `.github/workflows/release.yml` to build the universal Go engine binary and assemble the .app bundle on push to main or tag creation. - -**Why:** Currently the Makefile handles local builds, but there's no CI/CD pipeline to produce release artifacts automatically. Users and contributors can't download a pre-built binary after merge. - -**Context:** Deferred from /ship pre-flight check. The Makefile already handles the full build process (Go universal binary + Swift app + bundle assembly), so the workflow mainly needs to replicate those steps on GitHub-hosted macOS runners. - -**Effort:** S -**Priority:** P2 -**Depends on:** None - ## Design Review ### Design first-run onboarding flow @@ -54,6 +42,10 @@ ## Completed +### Add GitHub Actions release workflow + +**Completed:** 2026-09-25. `.github/workflows/release.yml` added: triggers on `v*` tags, validates 3-part semver (rejects 4-part), builds the universal Go engine binary + Swift release app via `make bundle`, zips the .app, generates SHA256 checksums, and publishes a GitHub Release with auto-generated notes. Unsigned/un-notarized for now — code signing and notarization tracked as a follow-up. + ### Create Markdown fixture corpus from real-world sources **Completed:** v0.1.0 (2026-04-08). 5 fixtures created in engine/testdata/fixtures/ covering Claude Code, ChatGPT, browser articles, nested lists/tables, and rich text mixed content. From b3ca33caccb37f424fbcf29efb360b8f39c65bd2 Mon Sep 17 00:00:00 2001 From: Fair Bear Labs Date: Sat, 26 Sep 2026 00:59:44 -0400 Subject: [PATCH 2/2] ci(release): fail on version mismatch, ad-hoc sign bundle, rerun-safe publish - Tag must match VERSION and Info.plist CFBundleShortVersionString; a mismatch previously only warned and would ship an app reporting the old version. - Ad-hoc sign and verify the .app: the unsigned bundle fails `codesign -v` ("code has no resources but signature indicates they must be present"), which Gatekeeper reports as "damaged" on download. - Publish via `gh release upload --clobber` when the release already exists so re-runs after a partial upload don't fail. - Pass step outputs through env instead of inline expressions. - Add a TODO for Developer ID signing + notarization (the PR said it was tracked but no entry existed); update CHANGELOG/TODOS notes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 46 ++++++++++++++++++++++++++--------- CHANGELOG.md | 4 ++- TODOS.md | 14 ++++++++++- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93e4f01..2f9617c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ # this project rejects 4-part versions, so tags with a 4th segment (or any # pre-release/build suffix) are rejected before any build work happens. # -# The resulting app is unsigned/un-notarized (no Apple Developer ID is +# The resulting app is ad-hoc signed only, not notarized (no Apple Developer ID is # configured in this repo yet) — see the release PR for the follow-up. name: release @@ -40,8 +40,13 @@ jobs: echo "::error::Tag '$tag' is not a 3-part semver (vX.Y.Z). Go tooling in this repo rejects 4-part versions; retag with e.g. v${version%%.*}.0.0." >&2 exit 1 fi - if [[ -f VERSION && "$(cat VERSION)" != "$version" ]]; then - echo "::warning::Tag version ($version) does not match VERSION file ($(cat VERSION))." + # The bundle ships VERSION/Info.plist as-is, so a mismatched tag would + # publish an app that reports the wrong version. Fail instead. + file_version="$(tr -d '[:space:]' < VERSION)" + plist_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' ChoosePaste/Resources/Info.plist)" + if [[ "$file_version" != "$version" || "$plist_version" != "$version" ]]; then + echo "::error::Tag version ($version) must match VERSION ($file_version) and Info.plist CFBundleShortVersionString ($plist_version). Bump them before tagging." >&2 + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" @@ -49,23 +54,42 @@ jobs: - name: Bundle (engine + app) run: make bundle + # Without a bundle-level signature the .app fails `codesign -v` ("code has + # no resources but signature indicates they must be present"), which + # Gatekeeper reports as "damaged" for downloaded copies. Ad-hoc sign it + # until Developer ID signing + notarization exist. + - name: Ad-hoc sign bundle + run: | + codesign --force --deep --sign - build/ChoosePaste.app + codesign --verify --strict --deep build/ChoosePaste.app + - name: Package artifacts id: package + env: + TAG: ${{ steps.version.outputs.tag }} run: | set -euo pipefail cd build - zip_name="ChoosePaste-${{ steps.version.outputs.tag }}-macos.zip" + zip_name="ChoosePaste-${TAG}-macos.zip" zip -r -X -q "$zip_name" ChoosePaste.app shasum -a 256 "$zip_name" > SHA256SUMS.txt echo "zip_name=$zip_name" >> "$GITHUB_OUTPUT" - - name: Create GitHub Release + # Re-run safe: if the release already exists (earlier run, or a partial + # upload), replace its assets instead of failing on `gh release create`. + - name: Publish GitHub Release env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.version.outputs.tag }} + ZIP_NAME: ${{ steps.package.outputs.zip_name }} run: | - gh release create "${{ steps.version.outputs.tag }}" \ - "build/${{ steps.package.outputs.zip_name }}" \ - "build/SHA256SUMS.txt" \ - --repo "${{ github.repository }}" \ - --title "${{ steps.version.outputs.tag }}" \ - --generate-notes + set -euo pipefail + assets=("build/$ZIP_NAME" "build/SHA256SUMS.txt") + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber + else + gh release create "$TAG" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$TAG" \ + --generate-notes + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ec577..03f9fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ All notable changes to this project will be documented in this file. ### Added - CI: release workflow (`.github/workflows/release.yml`) — on a `vX.Y.Z` tag push, builds the universal Go engine + Swift release app, zips the .app bundle, and - publishes it with SHA256 checksums to a GitHub Release with generated notes + publishes it with SHA256 checksums to a GitHub Release with generated notes. + The tag must match `VERSION` and Info.plist; the bundle is ad-hoc signed (not + notarized) and re-running the workflow replaces the release assets ### Security - Bump `golang.org/x/net` to v0.58.0 and Go to 1.26.6 (fixes reachable HTML-parser diff --git a/TODOS.md b/TODOS.md index 1a29aeb..8c06fe8 100644 --- a/TODOS.md +++ b/TODOS.md @@ -14,6 +14,18 @@ **Priority:** P1 **Depends on:** Alpha build available with benchmarkable transform flow +### Developer ID signing and notarization for release builds + +**What:** Extend `.github/workflows/release.yml` to sign the app (and the embedded `choosepaste-engine`) with a Developer ID Application certificate, notarize with `notarytool`, and staple the ticket before zipping. + +**Why:** Releases are currently ad-hoc signed only. Downloaded copies pass `codesign --verify` but Gatekeeper still blocks first launch ("Apple could not verify..."), so users must approve the app in System Settings. + +**Context:** Split out of the release-workflow PR (#12): no Apple Developer ID or signing secrets exist in this repo yet. Needs an Apple Developer account, repository secrets for the certificate/notary credentials, and a hardened-runtime entitlements check for the `Process()` launch of the engine. + +**Effort:** M +**Priority:** P2 +**Depends on:** Apple Developer Program membership and signing secrets configured + ## Design Review ### Design first-run onboarding flow @@ -44,7 +56,7 @@ ### Add GitHub Actions release workflow -**Completed:** 2026-09-25. `.github/workflows/release.yml` added: triggers on `v*` tags, validates 3-part semver (rejects 4-part), builds the universal Go engine binary + Swift release app via `make bundle`, zips the .app, generates SHA256 checksums, and publishes a GitHub Release with auto-generated notes. Unsigned/un-notarized for now — code signing and notarization tracked as a follow-up. +**Completed:** 2026-09-25. `.github/workflows/release.yml` added: triggers on `v*` tags, validates 3-part semver (rejects 4-part), builds the universal Go engine binary + Swift release app via `make bundle`, zips the .app, generates SHA256 checksums, and publishes a GitHub Release with auto-generated notes. Hardened in review: tag must match VERSION and Info.plist, the bundle is ad-hoc signed and verified, and publishing is re-run safe. Developer ID signing + notarization tracked above. ### Create Markdown fixture corpus from real-world sources