From 80deaeb80f0e3dcecd8d3cab7f6a27cfa575b9fa Mon Sep 17 00:00:00 2001 From: arzafran Date: Tue, 7 Jul 2026 17:26:47 -0300 Subject: [PATCH] chore: collapse stable + nightly into one rolling auto-ship lane Delete the nightly channel entirely (no users) and make `release.yml` the single lane: every commit on `main` that passes the `CI` workflow auto-builds, signs, notarizes, and publishes to a single reused `rolling` GitHub release that is overwritten each ship and marked "latest". Fix-forward on `main`. - `workflow_run` trigger on `CI` success (branches: main), gated on conclusion == success; checkout pins the tested head_sha. - Build number (CFBundleVersion / sparkle:version) is now ALWAYS the monotonic GitHub run ID for both auto-ship and milestone `v*` tags, so a milestone tag can never fail the monotonic guard behind an auto-ship's larger build number. - Auto-ship skips the immutable-asset guard and overwrites the rolling release; the guard still protects milestone `v*` tags from clobbering signed artifacts. - `workflow_dispatch` stays a dry-run (artifact only, no publish, no tag move). - Remove nightly Sparkle feed detection from UpdateDelegate, the nightly bug report dropdown, the CLI's dead nightly.yml signer reference, and stale nightly refs across CI guard tests and daemon docs. --- .claude/commands/release-local.md | 106 ---- .claude/commands/release-nightly.md | 107 ---- .claude/commands/release.md | 85 +-- .github/ISSUE_TEMPLATE/bug_report.yml | 12 - .github/workflows/nightly.yml | 553 ------------------ .github/workflows/release.yml | 105 +++- CLAUDE.md | 35 +- CLI/programa.swift | 4 +- PROJECTS.md | 1 + README.md | 8 +- Sources/Update/UpdateDelegate.swift | 11 +- daemon/remote/README.md | 4 +- docs/remote-daemon-spec.md | 4 +- .../ShortcutAndCommandPaletteTests.swift | 14 +- tests/test_ci_create_dmg_pinned.sh | 1 - tests/test_ci_ghosttykit_checksum_present.sh | 2 +- ...est_ci_ghosttykit_checksum_verification.sh | 1 - tests/test_nightly_universal_build.sh | 99 ---- 18 files changed, 177 insertions(+), 975 deletions(-) delete mode 100644 .claude/commands/release-local.md delete mode 100644 .claude/commands/release-nightly.md delete mode 100644 .github/workflows/nightly.yml delete mode 100644 tests/test_nightly_universal_build.sh diff --git a/.claude/commands/release-local.md b/.claude/commands/release-local.md deleted file mode 100644 index 1e8dcff9..00000000 --- a/.claude/commands/release-local.md +++ /dev/null @@ -1,106 +0,0 @@ -# Release Local - -Full end-to-end release built locally. Bumps version, updates changelog, tags, then builds/signs/notarizes/uploads via `scripts/build-sign-upload.sh`. - -## Steps - -### 1. Determine the new version number - -- Get the current version from `GhosttyTabs.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`) -- Bump the minor version unless the user specifies otherwise (e.g., 0.54.0 → 0.55.0) - -### 2. Gather changes and contributors since the last release - -- Find the most recent git tag: `git describe --tags --abbrev=0` -- Get commits since that tag: `git log --oneline ..HEAD --no-merges` -- **Filter for end-user visible changes only** — ignore developer tooling, CI, docs, tests -- Categorize changes into: Added, Changed, Fixed, Removed -- If there are no user-facing changes, ask the user if they still want to release -- **Collect contributors:** For each PR referenced in the commits, get the author: - ```bash - gh pr view --repo manaflow-ai/cmux --json author --jq '.author.login' - ``` -- Also check for linked issue reporters (the person who filed the bug): - ```bash - gh issue view --repo manaflow-ai/cmux --json author --jq '.author.login' - ``` -- Build a deduplicated list of all contributor `@handle`s for the release - -### 3. Update the changelog - -- Add a new section at the top of `CHANGELOG.md` with the new version and today's date -- **Only include changes that affect the end-user experience** -- Write clear, user-facing descriptions (not raw commit messages) -- **Credit contributors inline** (see Contributor Credits below) -- Also update `docs-site/content/docs/changelog.mdx` if it exists - -### 4. Bump the version - -- Run: `./scripts/bump-version.sh` (bumps minor by default) - -### 5. Commit, tag, and push - -- Stage: `CHANGELOG.md`, `GhosttyTabs.xcodeproj/project.pbxproj` -- Commit message: `Bump version to X.Y.Z` -- Create tag: `git tag vX.Y.Z` -- Push: `git push origin main && git push origin vX.Y.Z` - -### 6. Build, sign, notarize, and upload - -```bash -./scripts/build-sign-upload.sh vX.Y.Z -``` - -This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, homebrew cask update, and cleanup. - -If the script fails, run `say "cmux release failed"`. - -### 7. Verify homebrew cask - -- Run `bash tests/test_homebrew_sha.sh` to confirm the cask SHA matches the release DMG -- Update the homebrew-cmux submodule pointer: `git add homebrew-cmux && git commit -m "Update homebrew-cmux submodule to latest" && git push origin main` - -## Changelog Guidelines - -**Include only end-user visible changes:** -- New features users can see or interact with -- Bug fixes users would notice (crashes, UI glitches, incorrect behavior) -- Performance improvements users would feel -- UI/UX changes -- Breaking changes or removed features - -**Exclude internal/developer changes:** -- Setup scripts, build scripts, reload scripts -- CI/workflow changes -- Documentation updates (README, CONTRIBUTING, CLAUDE.md) -- Test additions or fixes -- Internal refactoring with no user-visible effect -- Dependency updates (unless they fix a user-facing bug) - -**Writing style:** -- Use present tense ("Add feature" not "Added feature") -- Group by category: Added, Changed, Fixed, Removed -- Be concise but descriptive -- Focus on what the user experiences, not how it was implemented - -## Contributor Credits - -Credit the people who made each release happen. This builds community and encourages contributions. - -**Per-entry attribution** — append contributor credit after each changelog bullet: -- For code contributions (PR author): `— thanks @user!` -- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!` -- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline - -**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release: -```markdown -### Thanks to N contributors! - -- [@user1](https://github.com/user1) -- [@user2](https://github.com/user2) -``` -- List all contributors alphabetically by GitHub handle (including core team) -- Link each handle to their GitHub profile -- Include everyone: PR authors, issue reporters, anyone whose work is in the release - -**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles. diff --git a/.claude/commands/release-nightly.md b/.claude/commands/release-nightly.md deleted file mode 100644 index 36f1a8d2..00000000 --- a/.claude/commands/release-nightly.md +++ /dev/null @@ -1,107 +0,0 @@ -# Release Nightly - -End-to-end release via PR flow: bump version, update changelog, create PR, merge, tag, then build locally via `scripts/build-sign-upload.sh`. - -## Steps - -### Phase 1: Version bump, changelog, PR, merge, tag - -1. **Determine the new version number** - - Get the current version from `GhosttyTabs.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`) - - Bump the minor version unless the user specifies otherwise (e.g., 0.48.0 → 0.49.0) - -2. **Create a release branch** - - Create branch: `git checkout -b release/vX.Y.Z` - -3. **Gather changes and contributors since the last release** - - Find the most recent git tag: `git describe --tags --abbrev=0` - - Get commits since that tag: `git log --oneline ..HEAD --no-merges` - - **Filter for end-user visible changes only** - ignore developer tooling, CI, docs, tests - - Categorize changes into: Added, Changed, Fixed, Removed - - **Collect contributors:** For each PR referenced in the commits, get the author: - ```bash - gh pr view --repo manaflow-ai/cmux --json author --jq '.author.login' - ``` - - Also check for linked issue reporters (the person who filed the bug): - ```bash - gh issue view --repo manaflow-ai/cmux --json author --jq '.author.login' - ``` - - Build a deduplicated list of all contributor `@handle`s for the release - -4. **Update the changelog** - - Add a new section at the top of `CHANGELOG.md` with the new version and today's date - - **Only include changes that affect the end-user experience** - - Write clear, user-facing descriptions (not raw commit messages) - - **Credit contributors inline** (see Contributor Credits below) - - Also update `docs-site/content/docs/changelog.mdx` if it exists - - If there are no user-facing changes, ask the user if they still want to release - -5. **Bump the version** - - Run `./scripts/bump-version.sh` (bumps minor by default) - -6. **Commit and push the release branch** - - Stage: `CHANGELOG.md`, `GhosttyTabs.xcodeproj/project.pbxproj` - - Commit message: `Bump version to X.Y.Z` - - Push: `git push -u origin release/vX.Y.Z` - -7. **Create PR and wait for CI** - - `gh pr create --title "Release vX.Y.Z" --body "...changelog..."` - - `gh pr checks --watch` - -8. **Merge PR** - - `gh pr merge --squash --delete-branch` - - `git checkout main && git pull` - -9. **Create and push the tag** - - `git tag vX.Y.Z && git push origin vX.Y.Z` - -### Phase 2: Local build, sign, notarize, upload - -10. **Run the build script** - -```bash -./scripts/build-sign-upload.sh vX.Y.Z -``` - -This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, and cleanup. - -If the script fails, run `say "cmux release failed"`. - -## Changelog Guidelines - -**Include only end-user visible changes:** -- New features users can see or interact with -- Bug fixes users would notice (crashes, UI glitches, incorrect behavior) -- Performance improvements users would feel -- UI/UX changes -- Breaking changes or removed features - -**Exclude internal/developer changes:** -- Setup scripts, build scripts, reload scripts -- CI/workflow changes -- Documentation updates (README, CONTRIBUTING, CLAUDE.md) -- Test additions or fixes -- Internal refactoring with no user-visible effect -- Dependency updates (unless they fix a user-facing bug) - -## Contributor Credits - -Credit the people who made each release happen. This builds community and encourages contributions. - -**Per-entry attribution** — append contributor credit after each changelog bullet: -- For code contributions (PR author): `— thanks @user!` -- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!` -- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline - -**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release: -```markdown -### Thanks to N contributors! - -- [@user1](https://github.com/user1) -- [@user2](https://github.com/user2) -``` -- List all contributors alphabetically by GitHub handle (including core team) -- Link each handle to their GitHub profile -- Include everyone: PR authors, issue reporters, anyone whose work is in the release - -**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles. diff --git a/.claude/commands/release.md b/.claude/commands/release.md index 9903627d..4fcca787 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -1,12 +1,24 @@ # Release -Prepare a new release for cmux. This command updates the changelog, bumps the version, creates a PR, monitors CI, and then merges and tags. +Programa ships from a single continuously-updating lane: every commit on `main` that passes the +`CI` GitHub Actions workflow is automatically built, signed, notarized, and published as the +latest release by `.github/workflows/release.yml` (triggered via `workflow_run` on `CI` +completing with `conclusion: success`). There is no nightly/beta channel and no manual publish +step for ordinary changes — merge to `main`, let CI go green, and the release ships itself. + +This command is only for **milestone marketing-version bumps** (e.g. `0.15.0` → `0.16.0`), which +are still done manually via a PR + optional `vX.Y.Z` tag marker. + +## When to use this + +- The user wants to bump the marketing version and record a changelog entry for a milestone. +- Not needed for routine fixes/features — those ship automatically on the next green `main` build. ## Steps 1. **Determine the new version number** - Get the current version from `GhosttyTabs.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`) - - Bump the minor version unless the user specifies otherwise (e.g., 0.12.0 → 0.13.0) + - Bump the minor version unless the user specifies otherwise (e.g., 0.15.0 → 0.16.0) 2. **Create a release branch** - Create branch: `git checkout -b release/vX.Y.Z` @@ -18,11 +30,11 @@ Prepare a new release for cmux. This command updates the changelog, bumps the ve - Categorize changes into: Added, Changed, Fixed, Removed - **Collect contributors:** For each PR referenced in the commits, get the author: ```bash - gh pr view --repo manaflow-ai/cmux --json author --jq '.author.login' + gh pr view --repo darkroomengineering/programa --json author --jq '.author.login' ``` - Also check for linked issue reporters (the person who filed the bug): ```bash - gh issue view --repo manaflow-ai/cmux --json author --jq '.author.login' + gh issue view --repo darkroomengineering/programa --json author --jq '.author.login' ``` - Build a deduplicated list of all contributor `@handle`s for the release @@ -31,49 +43,50 @@ Prepare a new release for cmux. This command updates the changelog, bumps the ve - **Only include changes that affect the end-user experience** - things users will see, feel, or interact with - Write clear, user-facing descriptions (not raw commit messages) - **Credit contributors inline** (see Contributor Credits below) - - Also update `docs-site/content/docs/changelog.mdx` with the same content - - If there are no user-facing changes, ask the user if they still want to release + - Also update the docs changelog page at `web/app/docs/changelog/page.tsx` with the same content + - If there are no user-facing changes, ask the user if they still want to bump the version -5. **Bump the version in Xcode project** - - Update all occurrences of `MARKETING_VERSION` in `GhosttyTabs.xcodeproj/project.pbxproj` - - There are typically 4 occurrences (Debug/Release for main app and CLI) +5. **Bump the version** + - Run `./scripts/bump-version.sh` (bumps minor by default; accepts `patch`, `major`, or an explicit version) 6. **Commit and push the release branch** - - Stage: `CHANGELOG.md`, `docs-site/content/docs/changelog.mdx`, `GhosttyTabs.xcodeproj/project.pbxproj` + - Stage: `CHANGELOG.md`, `web/app/docs/changelog/page.tsx`, `GhosttyTabs.xcodeproj/project.pbxproj` - Commit message: `Bump version to X.Y.Z` - Push: `git push -u origin release/vX.Y.Z` 7. **Create a pull request** - - Create PR: `gh pr create --title "Release vX.Y.Z" --body "...changelog summary..."` + - Create PR: `gh pr create --title "Bump version to vX.Y.Z" --body "...changelog summary..."` - Include the changelog entries in the PR body 8. **Monitor CI** - - Watch the CI workflow: `gh pr checks --watch` + - Watch: `gh pr checks --watch` - If CI fails, fix the issues and push again - Wait for all checks to pass before proceeding 9. **Merge the PR** - Merge: `gh pr merge --squash --delete-branch` - Switch back to main: `git checkout main && git pull` - -10. **Create and push the tag** - - Create tag: `git tag vX.Y.Z` - - Push tag: `git push origin vX.Y.Z` + - Once `CI` goes green on this merge commit, `release.yml` auto-ships it as the new latest + release — no further action is required. + +10. **(Optional) Tag the milestone** + - If you want a durable `vX.Y.Z` marker in the release history (in addition to the + auto-shipped release), tag it: + ```bash + git tag vX.Y.Z + git push origin vX.Y.Z + ``` + - This also triggers `release.yml` via its `push: tags: v*` trigger, publishing under that + exact tag. 11. **Monitor the release workflow** - - Watch: `gh run watch --repo manaflow-ai/cmux` - - Verify the release appears at: https://github.com/manaflow-ai/cmux/releases - - Check that the DMG is attached to the release - -12. **Verify homebrew cask update** - - The "Update Homebrew Cask" workflow triggers automatically after the release workflow completes - - Watch: `gh run list --workflow=update-homebrew.yml --limit=1` and `gh run watch` - - Verify: `cd homebrew-cmux && git pull && grep version Casks/cmux.rb` - - Run `bash tests/test_homebrew_sha.sh` to confirm the SHA matches + - Watch: `gh run watch --repo darkroomengineering/programa` + - Verify the release appears at: https://github.com/darkroomengineering/programa/releases + - Check that `programa-macos.dmg` is attached to the release -13. **Notify** - - On success: `say "cmux release complete"` - - On failure: `say "cmux release failed"` +12. **Notify** + - On success: `say "programa release complete"` + - On failure: `say "programa release failed"` ## Changelog Guidelines @@ -106,7 +119,6 @@ Credit the people who made each release happen. This builds community and encour **Per-entry attribution** — append contributor credit after each changelog bullet: - For code contributions (PR author): `— thanks @user!` - For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!` -- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline **Summary section** — add a "Thanks to N contributors!" section at the bottom of each release: ```markdown @@ -115,7 +127,7 @@ Credit the people who made each release happen. This builds community and encour - [@user1](https://github.com/user1) - [@user2](https://github.com/user2) ``` -- List all contributors alphabetically by GitHub handle (including core team) +- List all contributors alphabetically by GitHub handle - Link each handle to their GitHub profile - Include everyone: PR authors, issue reporters, anyone whose work is in the release @@ -124,22 +136,21 @@ Credit the people who made each release happen. This builds community and encour ## Example Changelog Entry ```markdown -## [0.13.0] - 2025-01-30 +## [0.16.0] - 2026-07-07 ### Added -- New keyboard shortcut for quick tab switching ([#42](https://github.com/manaflow-ai/cmux/pull/42)) — thanks @contributor! +- New keyboard shortcut for quick tab switching ([#42](https://github.com/darkroomengineering/programa/pull/42)) — thanks @contributor! ### Fixed -- Memory leak when closing split panes ([#38](https://github.com/manaflow-ai/cmux/pull/38)) — thanks @fixer! -- Notification badges not clearing properly ([#35](https://github.com/manaflow-ai/cmux/pull/35)) — thanks @reporter for the report! +- Memory leak when closing split panes ([#38](https://github.com/darkroomengineering/programa/pull/38)) — thanks @fixer! +- Notification badges not clearing properly ([#35](https://github.com/darkroomengineering/programa/pull/35)) — thanks @reporter for the report! ### Changed -- Improved terminal rendering performance ([#40](https://github.com/manaflow-ai/cmux/pull/40)) +- Improved terminal rendering performance ([#40](https://github.com/darkroomengineering/programa/pull/40)) -### Thanks to 4 contributors! +### Thanks to 3 contributors! - [@contributor](https://github.com/contributor) - [@fixer](https://github.com/fixer) -- [@lawrencechen](https://github.com/lawrencechen) - [@reporter](https://github.com/reporter) ``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 81a47cdc..0b5735eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,18 +46,6 @@ body: validations: required: true - - type: dropdown - id: nightly-repro - attributes: - label: Can you reproduce this on cmux NIGHTLY? - description: "Please test with the latest NIGHTLY build first: https://github.com/manaflow-ai/cmux?tab=readme-ov-file#nightly-builds" - options: - - Yes, it still reproduces on NIGHTLY - - No, it does not reproduce on NIGHTLY - - I could not test NIGHTLY - validations: - required: true - - type: textarea id: description attributes: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index bf596124..00000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,553 +0,0 @@ -name: Nightly macOS build - -on: - push: - branches: [main] - workflow_dispatch: - inputs: - force: - description: Force a nightly build even if main has no new commits - required: false - default: false - type: boolean - -concurrency: - group: nightly-build-${{ github.ref_name }} - # Queue concurrent runs instead of canceling them so no build is lost. - cancel-in-progress: false - -permissions: - contents: write - attestations: write - id-token: write - -env: - CREATE_DMG_VERSION: 8.0.0 - -jobs: - decide: - runs-on: ubuntu-latest - outputs: - should_build: ${{ steps.decide.outputs.should_build }} - head_sha: ${{ steps.decide.outputs.head_sha }} - short_sha: ${{ steps.decide.outputs.short_sha }} - should_publish: ${{ steps.decide.outputs.should_publish }} - steps: - - name: Decide whether a nightly build is needed - id: decide - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - env: - FORCE_BUILD: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' && 'true' || 'false' }} - with: - script: | - const forceBuild = process.env.FORCE_BUILD === 'true'; - const { owner, repo } = context.repo; - const requestedRef = context.ref.startsWith('refs/heads/') - ? context.ref.replace('refs/heads/', '') - : 'main'; - const isMainRef = requestedRef === 'main'; - - let headSha = context.sha; - if (isMainRef) { - const branch = await github.rest.repos.getBranch({ - owner, - repo, - branch: 'main', - }); - headSha = branch.data.commit.sha; - } - - let nightlySha = null; - if (isMainRef) { - try { - const ref = await github.rest.git.getRef({ - owner, - repo, - ref: 'tags/nightly', - }); - if (ref.data.object.type === 'commit') { - nightlySha = ref.data.object.sha; - } else if (ref.data.object.type === 'tag') { - const tagObject = await github.rest.git.getTag({ - owner, - repo, - tag_sha: ref.data.object.sha, - }); - nightlySha = tagObject.data.object.sha; - } - } catch (error) { - if (error.status !== 404) throw error; - } - } - - const shouldBuild = !isMainRef || forceBuild || nightlySha !== headSha; - core.setOutput('should_build', shouldBuild ? 'true' : 'false'); - core.setOutput('head_sha', headSha); - core.setOutput('short_sha', headSha.slice(0, 7)); - core.setOutput('should_publish', isMainRef ? 'true' : 'false'); - core.summary - .addHeading('Nightly build decision') - .addTable([ - [{data: 'requested ref', header: true}, requestedRef], - [{data: 'build HEAD', header: true}, headSha], - [{data: 'nightly tag', header: true}, nightlySha ?? '(missing)'], - [{data: 'force build', header: true}, String(forceBuild)], - [{data: 'should build', header: true}, String(shouldBuild)], - [{data: 'should publish', header: true}, String(isMainRef)], - ]) - .write(); - - build-sign-notarize-nightly: - needs: decide - if: needs.decide.outputs.should_build == 'true' - runs-on: macos-15 - timeout-minutes: 20 - steps: - - name: Checkout build ref - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - ref: ${{ needs.decide.outputs.head_sha }} - submodules: recursive - - - name: Check whether build commit is still current main HEAD before build - if: needs.decide.outputs.should_publish == 'true' - id: current_head_prebuild - run: | - set -euo pipefail - CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" - BUILD_SHA="${{ needs.decide.outputs.head_sha }}" - if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then - STILL_CURRENT=true - else - STILL_CURRENT=false - fi - echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" - { - echo "### Pre-build publish guard" - echo - echo "- build sha: \`$BUILD_SHA\`" - echo "- current main sha: \`$CURRENT_MAIN_SHA\`" - echo "- continue build/sign/publish: \`$STILL_CURRENT\`" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Select Xcode - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - set -euo pipefail - if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then - XCODE_DIR="/Applications/Xcode.app/Contents/Developer" - else - XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)" - if [ -n "$XCODE_APP" ]; then - XCODE_DIR="$XCODE_APP/Contents/Developer" - else - echo "No Xcode.app found under /Applications" >&2 - exit 1 - fi - fi - echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV" - export DEVELOPER_DIR="$XCODE_DIR" - xcodebuild -version - xcrun --sdk macosx --show-sdk-path - - - name: Install build deps - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - ZIG_REQUIRED="0.15.2" - if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then - echo "zig ${ZIG_REQUIRED} already installed" - else - echo "Installing zig ${ZIG_REQUIRED} from tarball" - curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz - tar xf /tmp/zig.tar.xz -C /tmp - sudo mkdir -p /usr/local/bin /usr/local/lib - sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig - sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig - export PATH="/usr/local/bin:$PATH" - zig version - fi - npm install --global "create-dmg@${CREATE_DMG_VERSION}" - - - name: Download pre-built GhosttyKit.xcframework - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - ./scripts/download-prebuilt-ghosttykit.sh - - - name: Cache Swift packages - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: .spm-cache - key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm- - - - name: Setup Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version-file: daemon/remote/go.mod - - - name: Derive Sparkle public key from private key - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - env: - SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} - run: | - if [ -z "$SPARKLE_PRIVATE_KEY" ]; then - echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 - exit 1 - fi - DERIVED_PUBLIC_KEY=$(swift scripts/derive_sparkle_public_key.swift "$SPARKLE_PRIVATE_KEY") - echo "Derived Sparkle public key: $DERIVED_PUBLIC_KEY" - echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV" - - - name: Build universal nightly app (Release) - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - xcodebuild -scheme programa -configuration Release -derivedDataPath build-universal \ - -destination 'generic/platform=macOS' \ - -clonedSourcePackagesDirPath .spm-cache \ - ARCHS="arm64 x86_64" \ - ONLY_ACTIVE_ARCH=NO \ - CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build - - - name: Verify nightly binary architectures - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - set -euo pipefail - APP_BINARY="build-universal/Build/Products/Release/Programa.app/Contents/MacOS/Programa" - CLI_BINARY="build-universal/Build/Products/Release/Programa.app/Contents/Resources/bin/programa" - HELPER_BINARY="build-universal/Build/Products/Release/Programa.app/Contents/Resources/bin/ghostty" - APP_ARCHS="$(lipo -archs "$APP_BINARY")" - CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" - HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")" - echo "App binary architectures: $APP_ARCHS" - echo "CLI binary architectures: $CLI_ARCHS" - echo "Ghostty helper architectures: $HELPER_ARCHS" - [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] - [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] - [[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]] - - - name: Run CLI version memory guard regression - if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' - run: | - set -euo pipefail - CLI_BINARY="build-universal/Build/Products/Release/Programa.app/Contents/Resources/bin/programa" - [ -x "$CLI_BINARY" ] || { echo "programa CLI binary not found at $CLI_BINARY" >&2; exit 1; } - PROGRAMA_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py - - - name: Check whether build commit is still current main HEAD after build - if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' - id: current_head_postbuild - run: | - set -euo pipefail - CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" - BUILD_SHA="${{ needs.decide.outputs.head_sha }}" - if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then - STILL_CURRENT=true - else - STILL_CURRENT=false - fi - echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" - { - echo "### Post-build publish guard" - echo - echo "- build sha: \`$BUILD_SHA\`" - echo "- current main sha: \`$CURRENT_MAIN_SHA\`" - echo "- continue signing/publish: \`$STILL_CURRENT\`" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Inject nightly identities and metadata - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - run: | - set -euo pipefail - SHORT_SHA="${{ needs.decide.outputs.short_sha }}" - APP_DIR="build-universal/Build/Products/Release" - - BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${APP_DIR}/Programa.app/Contents/Info.plist") - NIGHTLY_DATE=$(date -u +%Y%m%d) - - # Build number: unique/monotonic per workflow run attempt so same-day - # nightlies and reruns still compare as newer in Sparkle. - if [ -n "${GITHUB_RUN_ID:-}" ]; then - RUN_ATTEMPT="$(printf '%02d' "${GITHUB_RUN_ATTEMPT:-1}")" - NIGHTLY_BUILD="${GITHUB_RUN_ID}${RUN_ATTEMPT}" - else - NIGHTLY_BUILD="${NIGHTLY_DATE}000000" - fi - NIGHTLY_MARKETING_VERSION="${BASE_MARKETING}-nightly.${NIGHTLY_BUILD}" - echo "NIGHTLY_BUILD=${NIGHTLY_BUILD}" >> "$GITHUB_ENV" - echo "NIGHTLY_MARKETING_VERSION=${NIGHTLY_MARKETING_VERSION}" >> "$GITHUB_ENV" - echo "NIGHTLY_REMOTE_DAEMON_VERSION=${NIGHTLY_MARKETING_VERSION}" >> "$GITHUB_ENV" - - NIGHTLY_DMG_IMMUTABLE="programa-nightly-macos-${NIGHTLY_BUILD}.dmg" - echo "NIGHTLY_DMG_IMMUTABLE=${NIGHTLY_DMG_IMMUTABLE}" >> "$GITHUB_ENV" - - prepare_variant() { - local app_dir="$1" - local bundle_id="$2" - local feed_url="$3" - local app_plist="$app_dir/Programa.app/Contents/Info.plist" - - /usr/libexec/PlistBuddy -c "Set :CFBundleName Programa NIGHTLY" "$app_plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName Programa NIGHTLY" "$app_plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${bundle_id}" "$app_plist" - /usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$app_plist" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$app_plist" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string ${SPARKLE_PUBLIC_KEY}" "$app_plist" - /usr/libexec/PlistBuddy -c "Add :SUFeedURL string ${feed_url}" "$app_plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${NIGHTLY_MARKETING_VERSION}" "$app_plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NIGHTLY_BUILD}" "$app_plist" - /usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$app_plist" >/dev/null 2>&1 || true - /usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$app_plist" - mv "$app_dir/Programa.app" "$app_dir/Programa NIGHTLY.app" - } - - prepare_variant \ - "$APP_DIR" \ - "com.darkroom.programa.nightly" \ - "https://github.com/darkroomengineering/programa/releases/download/nightly/appcast.xml" - - echo "Nightly app name: Programa NIGHTLY" - echo "Nightly bundle ID: com.darkroom.programa.nightly" - echo "Nightly marketing version: ${NIGHTLY_MARKETING_VERSION}" - echo "Nightly build number: ${NIGHTLY_BUILD}" - echo "Nightly immutable DMG: ${NIGHTLY_DMG_IMMUTABLE}" - echo "Commit SHA: ${SHORT_SHA}" - - - name: Build remote daemon nightly assets and inject manifest - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - run: | - set -euo pipefail - # Build with --asset-suffix so manifest download URLs point to - # immutable, build-specific asset names (e.g. programad-remote-darwin-arm64-2362248028801). - # This prevents checksum mismatches when a newer nightly overwrites - # the shared "latest" assets on the release. - ./scripts/build_remote_daemon_release_assets.sh \ - --version "$NIGHTLY_REMOTE_DAEMON_VERSION" \ - --release-tag "nightly" \ - --repo "darkroomengineering/programa" \ - --output-dir "remote-daemon-assets" \ - --asset-suffix "$NIGHTLY_BUILD" - MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' "remote-daemon-assets/programad-remote-manifest-${NIGHTLY_BUILD}.json")" - APP_PLIST="build-universal/Build/Products/Release/Programa NIGHTLY.app/Contents/Info.plist" - if [ ! -f "$APP_PLIST" ]; then - echo "Missing nightly app Info.plist at $APP_PLIST" >&2 - exit 1 - fi - plutil -remove CMUXRemoteDaemonManifestJSON "$APP_PLIST" >/dev/null 2>&1 || true - plutil -insert CMUXRemoteDaemonManifestJSON -string "$MANIFEST_JSON" "$APP_PLIST" - # Also create unsuffixed "latest" copies for the release page and - # any tooling that fetches the generic asset names. The manifest's - # downloadURLs still point to the versioned filenames (intentional: - # the live manifest is used by the client-side checksum fallback - # which only reads sha256, not downloadURL). The unsuffixed copies - # are convenience aliases and don't carry build-provenance - # attestation (attested versioned files are canonical). - for platform in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64; do - cp "remote-daemon-assets/programad-remote-${platform}-${NIGHTLY_BUILD}" \ - "remote-daemon-assets/programad-remote-${platform}" - done - # Regenerate unsuffixed checksums with generic filenames so - # `shasum -c programad-remote-checksums.txt` works against the aliases. - ( - cd remote-daemon-assets - shasum -a 256 \ - programad-remote-darwin-arm64 \ - programad-remote-darwin-amd64 \ - programad-remote-linux-arm64 \ - programad-remote-linux-amd64 \ - > programad-remote-checksums.txt - ) - cp "remote-daemon-assets/programad-remote-manifest-${NIGHTLY_BUILD}.json" \ - "remote-daemon-assets/programad-remote-manifest.json" - - - name: Import signing cert - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - env: - APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - if [ -z "$APPLE_CERTIFICATE_BASE64" ]; then - echo "Missing APPLE_CERTIFICATE_BASE64 secret" >&2 - exit 1 - fi - if [ -z "$APPLE_CERTIFICATE_PASSWORD" ]; then - echo "Missing APPLE_CERTIFICATE_PASSWORD secret" >&2 - exit 1 - fi - KEYCHAIN_PASSWORD="$(uuidgen)" - echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > /tmp/cert.p12 - security delete-keychain build.keychain >/dev/null 2>&1 || true - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security set-keychain-settings -lut 21600 build.keychain - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security import /tmp/cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security - security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain - security list-keychains -d user -s build.keychain - - - name: Codesign apps - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - env: - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: | - if [ -z "$APPLE_SIGNING_IDENTITY" ]; then - echo "Missing APPLE_SIGNING_IDENTITY secret" >&2 - exit 1 - fi - ENTITLEMENTS="programa.entitlements" - for APP_PATH in \ - "build-universal/Build/Products/Release/Programa NIGHTLY.app" - do - CLI_PATH="$APP_PATH/Contents/Resources/bin/programa" - HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" - if [ -f "$CLI_PATH" ]; then - /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" - fi - if [ -f "$HELPER_PATH" ]; then - /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_PATH" - fi - /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" - /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" - done - - - name: Notarize apps and dmgs - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: | - if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then - echo "Missing notarization secrets (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID)" >&2 - exit 1 - fi - notarize_and_package() { - local app_path="$1" - local dmg_release="$2" - local dmg_immutable="$3" - local zip_submit="${dmg_release%.dmg}-notary.zip" - local dmg_tmp_dir - local created_dmg - - ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_submit" - APP_SUBMIT_JSON="$(xcrun notarytool submit "$zip_submit" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" - APP_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$APP_SUBMIT_JSON")" - APP_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$APP_SUBMIT_JSON")" - if [ "$APP_STATUS" != "Accepted" ]; then - echo "App notarization failed for $app_path with status: $APP_STATUS" >&2 - xcrun notarytool log "$APP_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true - exit 1 - fi - xcrun stapler staple "$app_path" - xcrun stapler validate "$app_path" - spctl -a -vv --type execute "$app_path" - rm -f "$zip_submit" - - dmg_tmp_dir="$(mktemp -d)" - create-dmg \ - --identity="$APPLE_SIGNING_IDENTITY" \ - "$app_path" \ - "$dmg_tmp_dir" - created_dmg="$(find "$dmg_tmp_dir" -maxdepth 1 -name '*.dmg' | head -n 1)" - if [ -z "$created_dmg" ]; then - echo "Failed to locate created DMG for $app_path" >&2 - exit 1 - fi - mv "$created_dmg" "$dmg_release" - rm -rf "$dmg_tmp_dir" - - DMG_SUBMIT_JSON="$(xcrun notarytool submit "$dmg_release" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" - DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")" - DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")" - if [ "$DMG_STATUS" != "Accepted" ]; then - echo "DMG notarization failed for $dmg_release with status: $DMG_STATUS" >&2 - xcrun notarytool log "$DMG_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true - exit 1 - fi - xcrun stapler staple "$dmg_release" - xcrun stapler validate "$dmg_release" - cp "$dmg_release" "$dmg_immutable" - } - - notarize_and_package \ - "build-universal/Build/Products/Release/Programa NIGHTLY.app" \ - "programa-nightly-macos.dmg" \ - "$NIGHTLY_DMG_IMMUTABLE" - - - name: Generate Sparkle appcasts (nightly) - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - env: - SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} - run: | - if [ -z "$SPARKLE_PRIVATE_KEY" ]; then - echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 - exit 1 - fi - ./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast.xml - # Keep the legacy universal feed alive long enough for older nightly - # installs to migrate onto the unified nightly appcast. - cp appcast.xml appcast-universal.xml - - - name: Attest remote daemon nightly assets - if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true') - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-path: | - remote-daemon-assets/programad-remote-darwin-arm64-${{ env.NIGHTLY_BUILD }} - remote-daemon-assets/programad-remote-darwin-amd64-${{ env.NIGHTLY_BUILD }} - remote-daemon-assets/programad-remote-linux-arm64-${{ env.NIGHTLY_BUILD }} - remote-daemon-assets/programad-remote-linux-amd64-${{ env.NIGHTLY_BUILD }} - remote-daemon-assets/programad-remote-checksums-${{ env.NIGHTLY_BUILD }}.txt - remote-daemon-assets/programad-remote-manifest-${{ env.NIGHTLY_BUILD }}.json - - - name: Upload branch nightly artifacts - if: needs.decide.outputs.should_publish != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: programa-nightly-${{ needs.decide.outputs.short_sha }} - path: | - programa-nightly-macos*.dmg - appcast.xml - remote-daemon-assets/programad-remote-* - appcast-universal.xml - if-no-files-found: error - - - name: Move nightly tag to built commit - if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true' - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -f nightly "${{ needs.decide.outputs.head_sha }}" - git push origin refs/tags/nightly --force - - - name: Publish nightly release assets - if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true' - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 - with: - tag_name: nightly - name: Nightly - prerelease: true - make_latest: false - body: | - Automated nightly build for `${{ needs.decide.outputs.short_sha }}`. - - **Programa NIGHTLY** is published as a universal app: - - bundle ID `com.darkroom.programa.nightly` - - feed `appcast.xml` - - compatibility feed `appcast-universal.xml` for older universal nightlies - - [Download programa-nightly-macos.dmg](https://github.com/darkroomengineering/programa/releases/download/nightly/programa-nightly-macos.dmg) - files: | - programa-nightly-macos-${{ github.run_id }}*.dmg - programa-nightly-macos.dmg - appcast.xml - remote-daemon-assets/programad-remote-* - appcast-universal.xml - overwrite_files: true - - - name: Cleanup keychain - if: always() - run: | - security delete-keychain build.keychain >/dev/null 2>&1 || true - rm -f /tmp/cert.p12 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b34ce60..6eb5b4a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,18 @@ name: Release macOS app +# Single-lane release pipeline: this is the ONLY macOS release lane. +# - workflow_run: auto-ships from `main` after the "CI" workflow succeeds (green CI on main), +# publishing to a single reused `rolling` release that is overwritten each ship. +# - push tags v*: milestone marketing-version releases (manual `scripts/bump-version.sh` + tag). +# - workflow_dispatch: dry-run build that uploads an artifact instead of publishing. on: push: tags: - "v*" + workflow_run: + workflows: ["CI"] + types: [completed] + branches: [main] workflow_dispatch: permissions: @@ -16,21 +25,74 @@ env: jobs: build-sign-notarize: + # Only build for workflow_run events when the upstream CI run actually succeeded. + # Tag pushes and manual dispatch always run. + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' runs-on: macos-15 timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: + # For workflow_run, github.ref/github.sha resolve to the default branch's + # workflow definition, not the commit that triggered CI. Pin to the exact + # commit CI validated. + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }} submodules: recursive + - name: Determine release tag and effective build number + id: version + run: | + set -euo pipefail + PROJECT_FILE="GhosttyTabs.xcodeproj/project.pbxproj" + MARKETING_VERSION=$(grep -m1 'MARKETING_VERSION = ' "$PROJECT_FILE" | sed 's/.*= //;s/;.*//') + + # Sparkle build number (CFBundleVersion) is ALWAYS derived from the monotonic + # GitHub run ID — for BOTH auto-ship and milestone-tag releases — so there is a + # single strictly-increasing build-number sequence across the one feed. The + # committed CURRENT_PROJECT_VERSION is only a local-dev default and is + # intentionally NOT used for shipping: mixing a small committed build with + # run-ID builds would make the first milestone tag after any auto-ship fail the + # monotonic guard below (and Sparkle would never offer it). Marketing version + # (CFBundleShortVersionString, baked in at build time) carries the human semver. + RUN_ATTEMPT="$(printf '%02d' "${GITHUB_RUN_ATTEMPT:-1}")" + EFFECTIVE_BUILD="${GITHUB_RUN_ID}${RUN_ATTEMPT}" + + if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then + # Milestone tag release: semver comes from the committed MARKETING_VERSION/tag. + RELEASE_TAG="${GITHUB_REF_NAME}" + IS_AUTO_SHIP="false" + else + # Auto-ship from main (workflow_run) or dry-run (workflow_dispatch): publish to a + # single reused "rolling" release that is overwritten each ship, so the releases + # page stays clean (one rolling entry + permanent milestone v* tags) instead of + # accumulating one release per commit. + RELEASE_TAG="rolling" + IS_AUTO_SHIP="true" + fi + + echo "MARKETING_VERSION=${MARKETING_VERSION}" >> "$GITHUB_ENV" + echo "EFFECTIVE_BUILD=${EFFECTIVE_BUILD}" >> "$GITHUB_ENV" + echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_ENV" + echo "IS_AUTO_SHIP=${IS_AUTO_SHIP}" >> "$GITHUB_ENV" + + echo "Marketing version: ${MARKETING_VERSION}" + echo "Effective build: ${EFFECTIVE_BUILD}" + echo "Release tag: ${RELEASE_TAG}" + echo "Auto-ship: ${IS_AUTO_SHIP}" + - name: Guard immutable release assets id: guard_release_assets + # Only milestone v* tag releases are immutable — this guard prevents a re-run from + # clobbering already-signed milestone artifacts. Auto-ship publishes to the reused + # "rolling" release and intentionally overwrites, so it skips this guard entirely + # (when skipped, the empty skip_all/skip_upload outputs let the build/publish run). + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const { evaluateReleaseAssetGuard } = require('./scripts/release_asset_guard'); - const tag = context.ref.replace('refs/tags/', ''); + const tag = process.env.RELEASE_TAG; core.setOutput('skip_all', 'false'); core.setOutput('skip_upload', 'false'); core.setOutput('release_state', 'clear'); @@ -83,20 +145,15 @@ jobs: if: steps.guard_release_assets.outputs.skip_all != 'true' run: | set -euo pipefail - LOCAL_BUILD=$(grep -m1 'CURRENT_PROJECT_VERSION = ' GhosttyTabs.xcodeproj/project.pbxproj | sed 's/.*= //;s/;.*//') - if ! [[ "$LOCAL_BUILD" =~ ^[0-9]+$ ]]; then - echo "Could not parse CURRENT_PROJECT_VERSION (got '$LOCAL_BUILD')" >&2 - exit 1 - fi - echo "Local CURRENT_PROJECT_VERSION=$LOCAL_BUILD" + echo "Effective CURRENT_PROJECT_VERSION=$EFFECTIVE_BUILD" PUBLISHED_BUILD=$(curl -fsSL --max-time 15 \ https://github.com/darkroomengineering/programa/releases/latest/download/appcast.xml 2>/dev/null \ | sed -n 's#.*\([0-9][0-9]*\).*#\1#p' \ | head -n1 || true) if [[ "$PUBLISHED_BUILD" =~ ^[0-9]+$ ]]; then echo "Latest published Sparkle build=$PUBLISHED_BUILD" - if (( LOCAL_BUILD <= PUBLISHED_BUILD )); then - echo "::error::CURRENT_PROJECT_VERSION ($LOCAL_BUILD) must be > latest published Sparkle build ($PUBLISHED_BUILD). Run scripts/bump-version.sh to bump the build number before tagging." >&2 + if (( EFFECTIVE_BUILD <= PUBLISHED_BUILD )); then + echo "::error::Effective build number ($EFFECTIVE_BUILD) must be > latest published Sparkle build ($PUBLISHED_BUILD). Build numbers derive from the monotonic GitHub run ID, so this is unexpected — likely a re-run of an older run or a stale published appcast." >&2 exit 1 fi else @@ -208,7 +265,7 @@ jobs: APP_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PLIST") ./scripts/build_remote_daemon_release_assets.sh \ --version "$APP_VERSION" \ - --release-tag "$GITHUB_REF_NAME" \ + --release-tag "$RELEASE_TAG" \ --repo "darkroomengineering/programa" \ --output-dir "remote-daemon-assets" MANIFEST_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1], encoding="utf-8")), separators=(",",":")))' remote-daemon-assets/programad-remote-manifest.json)" @@ -230,6 +287,16 @@ jobs: HELPER_BINARY="build-universal/Build/Products/Release/Programa.app/Contents/Resources/bin/ghostty" [ -x "$HELPER_BINARY" ] || { echo "Ghostty theme picker helper not found at $HELPER_BINARY" >&2; exit 1; } + - name: Set effective build number in Info.plist + if: steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + APP_PLIST="build-universal/Build/Products/Release/Programa.app/Contents/Info.plist" + # No-op for tag releases (EFFECTIVE_BUILD already matches the committed value). + # For auto-ship runs this stamps the monotonic run-derived build number without + # committing anything back to the repo. + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${EFFECTIVE_BUILD}" "$APP_PLIST" + - name: Inject Sparkle keys into Info.plist if: steps.guard_release_assets.outputs.skip_all != 'true' run: | @@ -347,7 +414,7 @@ jobs: echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 exit 1 fi - ./scripts/sparkle_generate_appcast.sh programa-macos.dmg "$GITHUB_REF_NAME" appcast.xml + ./scripts/sparkle_generate_appcast.sh programa-macos.dmg "$RELEASE_TAG" appcast.xml - name: Attest remote daemon release assets if: steps.guard_release_assets.outputs.skip_all != 'true' @@ -372,10 +439,22 @@ jobs: remote-daemon-assets/programad-remote-* if-no-files-found: error + - name: Move rolling tag to shipped commit + if: github.event_name == 'workflow_run' && steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + # Reuse a single "rolling" release: point its tag at the commit we just shipped so + # the release page reflects the actual build. Assets are overwritten below. + git tag -f rolling "${{ github.event.workflow_run.head_sha }}" + git push origin refs/tags/rolling --force + - name: Upload release asset - if: steps.guard_release_assets.outputs.skip_upload != 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + if: steps.guard_release_assets.outputs.skip_upload != 'true' && (github.event_name == 'workflow_run' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))) uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: + tag_name: ${{ env.RELEASE_TAG }} + name: ${{ env.IS_AUTO_SHIP == 'true' && 'Rolling (latest main)' || env.RELEASE_TAG }} + make_latest: true files: | programa-macos.dmg appcast.xml @@ -386,7 +465,7 @@ jobs: remote-daemon-assets/programad-remote-checksums.txt remote-daemon-assets/programad-remote-manifest.json generate_release_notes: true - overwrite_files: false + overwrite_files: true - name: Cleanup keychain if: always() diff --git a/CLAUDE.md b/CLAUDE.md index 9ca6cdf4..154ad58e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -230,14 +230,18 @@ git commit -m "Update ghostty submodule" ## Release -Use the `/release` command to prepare a new release. This will: -1. Determine the new version (bumps minor by default) -2. Gather commits since the last tag and update the changelog -3. Update `CHANGELOG.md` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it) -4. Run `./scripts/bump-version.sh` to update both versions -5. Commit, tag, and push - -Version bumping: +Single lane: every commit on `main` that passes the `CI` workflow is automatically built, +signed, notarized, and published as the latest GitHub release via `.github/workflows/release.yml` +(triggered by `workflow_run` on `CI` completing with `conclusion: success`, on `branches: [main]`). +There is no nightly/beta channel — if something ships broken, fix it forward on `main` and the +next green CI run auto-ships the fix. Auto-ship builds get a monotonic build number derived from +the run ID (injected into `Info.plist` at build time, never committed) and publish to a single, +reused `rolling` GitHub release that is overwritten each ship and marked "latest" — so the +releases page stays clean (one `rolling` entry plus permanent milestone `v*` tags) and +`releases/latest/download/*` always resolves to the newest green build. + +Milestone marketing-version bumps (e.g. `0.15.0` → `0.16.0`) are still done manually and can +optionally be tagged as a `vX.Y.Z` marker, which the same `release.yml` also builds on tag push: ```bash ./scripts/bump-version.sh # bump minor (0.15.0 → 0.16.0) @@ -246,9 +250,9 @@ Version bumping: ./scripts/bump-version.sh 1.0.0 # set specific version ``` -This updates both `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` (build number). The build number is auto-incremented and is required for Sparkle auto-update to work. - -Manual release steps (if not using the command): +This updates both `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` (build number). Then update +`CHANGELOG.md` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it), commit, +and optionally tag: ```bash git tag vX.Y.Z @@ -256,10 +260,15 @@ git push origin vX.Y.Z gh run watch --repo darkroomengineering/programa ``` +Tagging is now optional — it exists only to mark a milestone version in the release history; it is +not required to ship. Regular pushes to `main` ship automatically. + Notes: - Requires GitHub secrets: `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`. -- The release asset is `programa-macos.dmg` attached to the tag. +- The release asset is `programa-macos.dmg` attached to each release. - README download button points to `releases/latest/download/programa-macos.dmg`. -- Versioning: bump the minor version for updates unless explicitly asked otherwise. +- Versioning: bump the minor version for milestone tags unless explicitly asked otherwise. - Changelog: update `CHANGELOG.md`; docs changelog is rendered from it. +- `workflow_dispatch` on `release.yml` still runs a dry-run build that uploads an artifact instead + of publishing. diff --git a/CLI/programa.swift b/CLI/programa.swift index a0d756c3..31e1135e 100644 --- a/CLI/programa.swift +++ b/CLI/programa.swift @@ -4675,9 +4675,7 @@ struct CMUXCLI { let downloadCommand = "gh release download \(releaseTag) --repo darkroomengineering/programa --pattern \(assetName)" let downloadChecksumsCommand = "gh release download \(releaseTag) --repo darkroomengineering/programa --pattern \(checksumsAssetName)" let checksumVerifyCommand = "shasum -a 256 -c \(checksumsAssetName) --ignore-missing" - let signerWorkflow = releaseTag == "nightly" - ? "darkroomengineering/programa/.github/workflows/nightly.yml" - : "darkroomengineering/programa/.github/workflows/release.yml" + let signerWorkflow = "darkroomengineering/programa/.github/workflows/release.yml" let verifyCommand = "gh attestation verify ./\(assetName) --repo darkroomengineering/programa --signer-workflow \(signerWorkflow)" let payload: [String: Any] = [ diff --git a/PROJECTS.md b/PROJECTS.md index 72b9eefc..51affbca 100644 --- a/PROJECTS.md +++ b/PROJECTS.md @@ -3,6 +3,7 @@ Cross-project tracking (features, bugs, backlog) for Programa. ## Done +- 2026-07-07: Collapsed the stable/nightly release split into a single lane: `.github/workflows/nightly.yml` is deleted, and `.github/workflows/release.yml` now auto-ships from `main` on green CI (via a `workflow_run` trigger on the "CI" workflow) in addition to the existing `v*` tag push for milestone releases. Removed the nightly Sparkle feed detection from `UpdateDelegate.swift`, the nightly dropdown from the bug report template, and stale nightly references across CI test scripts and daemon docs. The nightly channel had no users, so it was deleted outright rather than migrated. - 2026-02-14: Fixed updater release regression path: made `.github/workflows/release.yml` Sparkle Info.plist key injection idempotent (re-running tags no longer fails with "Entry Already Exists"), and hardened `scripts/bump-version.sh` to keep `CURRENT_PROJECT_VERSION` above the latest published Sparkle appcast build number so upgrades from `0.27.0` can be detected. - 2026-02-14: Relicensed the repository to strong copyleft (`AGPL-3.0-or-later`), added canonical `LICENSE` text, and updated project/package metadata to advertise AGPL consistently. - 2026-02-14: Added an opt-in nightly update channel in Settings (`Receive nightly builds`) that routes Sparkle feed selection between stable and nightly appcasts, plus a scheduled GitHub Actions nightly pipeline (`.github/workflows/nightly.yml`) that only rebuilds when `main` has new commits since `nightly` (or when manually forced). diff --git a/README.md b/README.md index f1a1c5e2..20d31908 100644 --- a/README.md +++ b/README.md @@ -228,13 +228,11 @@ Browser developer-tool shortcuts follow Safari defaults and are customizable in | ⌘ ⇧ , | Reload configuration | | ⌘ Q | Quit | -## Nightly Builds +## Releases -[Download Programa NIGHTLY](https://github.com/darkroomengineering/programa/releases/download/nightly/programa-nightly-macos.dmg) +Programa ships from a single continuously-updating lane: every commit on `main` that passes CI is automatically built, signed, notarized, and published as the latest release. There is no separate nightly/beta channel — `main` is always the shipping branch, and the app auto-updates via Sparkle to whatever was most recently published. -Programa NIGHTLY is a separate app with its own bundle ID, so it runs alongside the stable version. Built automatically from the latest `main` commit and auto-updates via its own Sparkle feed. - -Report nightly bugs on [GitHub Issues](https://github.com/darkroomengineering/programa/issues) or in [#nightly-bugs on Discord](https://discord.gg/xsgFEVrWCZ). +Report bugs on [GitHub Issues](https://github.com/darkroomengineering/programa/issues) or on [Discord](https://discord.gg/xsgFEVrWCZ). ## Session restore (current behavior) diff --git a/Sources/Update/UpdateDelegate.swift b/Sources/Update/UpdateDelegate.swift index ac0e4422..5fc2169e 100644 --- a/Sources/Update/UpdateDelegate.swift +++ b/Sources/Update/UpdateDelegate.swift @@ -4,11 +4,11 @@ import Cocoa enum UpdateFeedResolver { static let fallbackFeedURL = "https://github.com/darkroomengineering/programa/releases/latest/download/appcast.xml" - static func resolvedFeedURLString(infoFeedURL: String?) -> (url: String, isNightly: Bool, usedFallback: Bool) { + static func resolvedFeedURLString(infoFeedURL: String?) -> (url: String, usedFallback: Bool) { guard let infoFeedURL, !infoFeedURL.isEmpty else { - return (fallbackFeedURL, false, true) + return (fallbackFeedURL, true) } - return (infoFeedURL, infoFeedURL.contains("/nightly/"), false) + return (infoFeedURL, false) } } @@ -26,12 +26,9 @@ extension UpdateDriver: SPUUpdaterDelegate { return override } #endif - // The feed URL is baked into Info.plist at build time: - // - Stable releases use the stable appcast URL - // - cmux NIGHTLY has the nightly appcast URL injected by CI + // The stable feed URL is baked into Info.plist at build time. let infoFeedURL = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeedURL) - UpdateLogStore.shared.append("update channel: \(resolved.isNightly ? "nightly" : "stable")") recordFeedURLString(resolved.url, usedFallback: resolved.usedFallback) return resolved.url } diff --git a/daemon/remote/README.md b/daemon/remote/README.md index de077c95..0c8eb7d4 100644 --- a/daemon/remote/README.md +++ b/daemon/remote/README.md @@ -40,7 +40,7 @@ Current integration in programa: ## Distribution -Release and nightly builds publish prebuilt `programad-remote` binaries on GitHub Releases for: +Release builds publish prebuilt `programad-remote` binaries on GitHub Releases for: 1. `darwin/arm64` 2. `darwin/amd64` 3. `linux/arm64` @@ -51,7 +51,7 @@ The app embeds a compact manifest in `Info.plist` with: 2. pinned SHA-256 digests 3. release tag and checksums asset URL -Release and nightly apps download and cache the matching binary locally, verify its SHA-256, then upload it to the remote host if needed. Dev builds can opt into a local `go build` fallback with `PROGRAMA_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1`. +Release apps download and cache the matching binary locally, verify its SHA-256, then upload it to the remote host if needed. Dev builds can opt into a local `go build` fallback with `PROGRAMA_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1`. To inspect what a given app build trusts, run: 1. `programa remote-daemon-status` diff --git a/docs/remote-daemon-spec.md b/docs/remote-daemon-spec.md index 99245a62..378eb855 100644 --- a/docs/remote-daemon-spec.md +++ b/docs/remote-daemon-spec.md @@ -56,8 +56,8 @@ This is a **living implementation spec** (also called an **execution spec**): a - `DONE` orphaned relay SSH processes from previous app sessions are cleaned up before starting a new relay. ### 3.6 Artifact Trust -- `DONE` release and nightly workflows publish `programad-remote` assets for `darwin/linux × arm64/amd64`. -- `DONE` release and nightly apps embed a compact `PROGRAMARemoteDaemonManifestJSON` in `Info.plist` with exact asset URLs and SHA-256 digests. +- `DONE` the release workflow publishes `programad-remote` assets for `darwin/linux × arm64/amd64`. +- `DONE` release apps embed a compact `PROGRAMARemoteDaemonManifestJSON` in `Info.plist` with exact asset URLs and SHA-256 digests. - `DONE` `programa remote-daemon-status` exposes the current manifest entry, local cache verification state, release download command, and GitHub attestation verification command. ### 3.3 Error Surfacing diff --git a/programaTests/ShortcutAndCommandPaletteTests.swift b/programaTests/ShortcutAndCommandPaletteTests.swift index e7d77cfd..8fa9e8af 100644 --- a/programaTests/ShortcutAndCommandPaletteTests.swift +++ b/programaTests/ShortcutAndCommandPaletteTests.swift @@ -1118,31 +1118,19 @@ final class UpdateChannelSettingsTests: XCTestCase { func testResolvedFeedFallsBackWhenInfoFeedMissing() { let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: nil) XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL) - XCTAssertFalse(resolved.isNightly) XCTAssertTrue(resolved.usedFallback) } func testResolvedFeedFallsBackWhenInfoFeedEmpty() { let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: "") XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL) - XCTAssertFalse(resolved.isNightly) XCTAssertTrue(resolved.usedFallback) } - func testResolvedFeedUsesInfoFeedForStableChannel() { + func testResolvedFeedUsesInfoFeedURLWhenPresent() { let infoFeed = "https://example.com/custom/appcast.xml" let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed) XCTAssertEqual(resolved.url, infoFeed) - XCTAssertFalse(resolved.isNightly) - XCTAssertFalse(resolved.usedFallback) - } - - func testResolvedFeedDetectsNightlyFromInfoFeedURL() { - let resolved = UpdateFeedResolver.resolvedFeedURLString( - infoFeedURL: "https://example.com/nightly/appcast.xml" - ) - XCTAssertEqual(resolved.url, "https://example.com/nightly/appcast.xml") - XCTAssertTrue(resolved.isNightly) XCTAssertFalse(resolved.usedFallback) } } diff --git a/tests/test_ci_create_dmg_pinned.sh b/tests/test_ci_create_dmg_pinned.sh index 1199f699..b9933c42 100755 --- a/tests/test_ci_create_dmg_pinned.sh +++ b/tests/test_ci_create_dmg_pinned.sh @@ -7,7 +7,6 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" WORKFLOWS=( "$ROOT_DIR/.github/workflows/release.yml" - "$ROOT_DIR/.github/workflows/nightly.yml" ) for workflow in "${WORKFLOWS[@]}"; do diff --git a/tests/test_ci_ghosttykit_checksum_present.sh b/tests/test_ci_ghosttykit_checksum_present.sh index b1d1d577..13ace701 100755 --- a/tests/test_ci_ghosttykit_checksum_present.sh +++ b/tests/test_ci_ghosttykit_checksum_present.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Fails fast when the checked-in ghostty submodule SHA lacks a pinned # GhosttyKit archive checksum. This prevents new ghostty bumps from merging -# without the checksum entry that nightly/release workflows require. +# without the checksum entry that the release workflow requires. set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" diff --git a/tests/test_ci_ghosttykit_checksum_verification.sh b/tests/test_ci_ghosttykit_checksum_verification.sh index 1eba6ecf..9eba92c2 100755 --- a/tests/test_ci_ghosttykit_checksum_verification.sh +++ b/tests/test_ci_ghosttykit_checksum_verification.sh @@ -9,7 +9,6 @@ trap 'rm -rf "$TMP_DIR"' EXIT WORKFLOWS=( "$ROOT_DIR/.github/workflows/ci.yml" - "$ROOT_DIR/.github/workflows/nightly.yml" "$ROOT_DIR/.github/workflows/release.yml" ) diff --git a/tests/test_nightly_universal_build.sh b/tests/test_nightly_universal_build.sh deleted file mode 100644 index e86dbf36..00000000 --- a/tests/test_nightly_universal_build.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# Regression test for dual nightly macOS tracks. -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -WORKFLOW_FILE="$ROOT_DIR/.github/workflows/nightly.yml" - -if ! awk ' - /^ - name: Build Apple Silicon app \(Release\)/ { in_arm=1; next } - /^ - name: Build universal app \(Release\)/ { in_universal=1; next } - in_arm && /^ - name:/ { in_arm=0 } - in_universal && /^ - name:/ { in_universal=0 } - in_arm && /-destination '\''platform=macOS,arch=arm64'\''/ { saw_arm_destination=1 } - in_arm && /ARCHS="arm64"/ { saw_arm_archs=1 } - in_arm && /ONLY_ACTIVE_ARCH=YES/ { saw_arm_only_active_arch=1 } - in_universal && /-destination '\''generic\/platform=macOS'\''/ { saw_universal_destination=1 } - in_universal && /ARCHS="arm64 x86_64"/ { saw_universal_archs=1 } - in_universal && /ONLY_ACTIVE_ARCH=NO/ { saw_universal_only_active_arch=1 } - END { - exit !(saw_arm_destination && saw_arm_archs && saw_arm_only_active_arch && saw_universal_destination && saw_universal_archs && saw_universal_only_active_arch) - } -' "$WORKFLOW_FILE"; then - echo "FAIL: nightly workflow must force Apple Silicon nightly to arm64-only and universal nightly to both slices" - exit 1 -fi - -if ! awk ' - /^ - name: Verify nightly binary architectures/ { in_verify=1; next } - in_verify && /^ - name:/ { in_verify=0 } - in_verify && /lipo -archs "\$ARM_APP_BINARY"/ { saw_arm_app=1 } - in_verify && /lipo -archs "\$ARM_CLI_BINARY"/ { saw_arm_cli=1 } - in_verify && /lipo -archs "\$APP_BINARY"/ { saw_app=1 } - in_verify && /lipo -archs "\$CLI_BINARY"/ { saw_cli=1 } - in_verify && /\[\[ "\$ARM_APP_ARCHS" == "arm64" \]\]/ { saw_arm_app_assert=1 } - in_verify && /\[\[ "\$ARM_CLI_ARCHS" == "arm64" \]\]/ { saw_arm_cli_assert=1 } - END { exit !(saw_arm_app && saw_arm_cli && saw_app && saw_cli && saw_arm_app_assert && saw_arm_cli_assert) } -' "$WORKFLOW_FILE"; then - echo "FAIL: nightly workflow must verify arm-only and universal slices with lipo" - exit 1 -fi - -if ! grep -Fq 'com.cmuxterm.app.nightly.universal' "$WORKFLOW_FILE"; then - echo "FAIL: nightly workflow must set a distinct .universal bundle ID" - exit 1 -fi - -if ! grep -Fq 'https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast-universal.xml' "$WORKFLOW_FILE"; then - echo "FAIL: nightly workflow must publish a separate universal appcast feed" - exit 1 -fi - -if ! grep -Fq './scripts/sparkle_generate_appcast.sh "$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE" nightly appcast-universal.xml' "$WORKFLOW_FILE"; then - echo "FAIL: nightly workflow must generate a separate universal appcast" - exit 1 -fi - -if ! grep -Fq "core.setOutput('should_publish', isMainRef ? 'true' : 'false');" "$WORKFLOW_FILE"; then - echo "FAIL: nightly decide step must expose should_publish based on whether the ref is main" - exit 1 -fi - -if ! awk ' - /^ - name: Upload branch nightly artifacts/ { in_upload=1; next } - in_upload && /^ - name:/ { in_upload=0 } - in_upload && /if: needs\.decide\.outputs\.should_publish != '\''true'\''/ { saw_if=1 } - in_upload && /uses: actions\/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4/ { saw_upload=1 } - in_upload && /cmux-nightly-macos\*\.dmg/ { saw_arm_artifacts=1 } - in_upload && /cmux-nightly-universal-macos\*\.dmg/ { saw_universal_artifacts=1 } - in_upload && /appcast-universal\.xml/ { saw_universal_appcast=1 } - END { exit !(saw_if && saw_upload && saw_arm_artifacts && saw_universal_artifacts && saw_universal_appcast) } -' "$WORKFLOW_FILE"; then - echo "FAIL: non-main nightly runs must upload both nightly variants and both appcasts" - exit 1 -fi - -if ! awk ' - /^ - name: Move nightly tag to built commit/ { in_move=1; next } - in_move && /^ - name:/ { in_move=0 } - in_move && /if: needs\.decide\.outputs\.should_publish == '\''true'\''/ { saw_move_if=1 } - END { exit !saw_move_if } -' "$WORKFLOW_FILE"; then - echo "FAIL: moving the nightly tag must be gated to main nightly publishes" - exit 1 -fi - -if ! awk ' - /^ - name: Publish nightly release assets/ { in_publish=1; next } - in_publish && /^ - name:/ { in_publish=0 } - in_publish && /if: needs\.decide\.outputs\.should_publish == '\''true'\''/ { saw_publish_if=1 } - in_publish && /cmux-nightly-universal-macos-\$\{\{ github\.run_id \}\}\*\.dmg/ { saw_universal_immutable=1 } - in_publish && /cmux-nightly-universal-macos\.dmg/ { saw_universal_stable=1 } - in_publish && /appcast-universal\.xml/ { saw_universal_appcast=1 } - END { exit !(saw_publish_if && saw_universal_immutable && saw_universal_stable && saw_universal_appcast) } -' "$WORKFLOW_FILE"; then - echo "FAIL: main nightly publish must include the universal assets and appcast" - exit 1 -fi - -echo "PASS: nightly workflow keeps separate Apple Silicon and universal nightly tracks"