diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index 147c32b8e..34eb666a5 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -1,480 +1,339 @@ --- name: release -description: 'Cut a new ADE release: scope desktop/iOS, write changelog, push + tag main, poll release workflow, and ship an iOS build via asc' +description: 'Local-first ADE release conductor: detect whether desktop and/or iOS actually changed, bump desktop patch versions, keep iOS marketing versions fixed while bumping build numbers, publish desktop GitHub Releases from this Mac, and distribute TestFlight builds to all beta users.' --- -# Release Command +# ADE Release Skill + +Use this skill when the user wants to release ADE, automate releases from a +cron/agent, decide whether a release is needed, publish a desktop release, or +ship a TestFlight build. + +This is a **local-first release flow**. GitHub Releases remain the public +artifact host for Electron updater, but this Mac can be the release machine for +Apple signing, notarization, GitHub asset upload, and TestFlight upload. + +## Hard Rules + +- **No-op is valid.** If no relevant product code changed, do not create a + release just to bump numbers. +- **Desktop and iOS are independent.** Release desktop without iOS when only + desktop changed; release iOS without desktop when only iOS changed. +- **Desktop version bumps the patch segment only** unless the user explicitly + asks otherwise: `v1.2.14 -> v1.2.15`. +- **iOS marketing version does not change** unless the user explicitly asks. + Only bump the TestFlight build number by one, using App Store Connect as the + source of truth. +- **Do not omit App Clip in normal releases.** The v1.1.10 build 16 omission was + an emergency unblock. Normal mobile releases must include the app, widgets, + and App Clip after signing is fixed. +- **Do not publish broken updater metadata.** Before making a desktop release + public/latest, verify `latest-mac.yml` references assets that exist. +- **Do not wait forever on Apple.** If notarization or TestFlight processing + exceeds its normal window by a lot, preserve state, retry only the failed + phase, or stop with a clear recovery command. + +## Machine Notes + +This release lane runs on an Apple Silicon Mac (`arm64`). Rosetta is available +on the intended release machine, so x64 desktop builds are plausible through +Electron Builder/Rosetta, and ADE already has scripts for x64/per-arch/local +mac release builds. + +Still verify the output instead of trusting architecture assumptions: + +- Desktop updater correctness requires `latest-mac.yml` plus the referenced mac + ZIP assets. +- If supporting Intel users, the feed must include an Intel/x64 ZIP as well as + arm64. Do not silently publish arm64-only unless the user explicitly accepts + dropping Intel updates for that release. +- If local x64 build fails, use a fallback: reuse a known-good x64 app/ZIP input + with `release:mac:local -- --x64-app=... --x64-zip=...`, or run only the x64 + build on a remote runner and publish locally after verification. + +## State and Locking + +Create a state file before mutating release state: -Drive a full ADE release end-to-end: figure out what needs to ship (desktop, iOS, or both), generate the Mintlify changelog page, push + tag on `main` to kick the release workflow, poll it to completion, and (when iOS is in scope) drive the TestFlight build through `asc`. - -**Usage:** -- `/release` — interactive. Agent will ask for the new version number and the iOS build number when it needs them. -- `/release ` — e.g. `/release v1.1.3`. Skip the version prompt; agent will still ask for the iOS build number if iOS is in scope. -- `/release ` — e.g. `/release v1.1.3 42`. Fully unattended. - -**Arguments:** $ARGUMENTS - ---- - -## Execution mode - -Mostly autonomous, but **pause for explicit user input** on: -- The new version number (if not passed in `$ARGUMENTS`). -- The iOS build number (if iOS is in scope and not passed in `$ARGUMENTS`). -- **The iOS target TestFlight group(s)** (always — enumerate groups + tester counts first; never assume a default). See Phase 7a. -- Any step that would force-push `main`, bypass a ruleset in a surprising way, or publish a release that is still in `draft=false`. +```bash +mkdir -p .ade/release +``` -Do NOT publish the GitHub draft release automatically. Leave it as a draft for a human to flip. +Use a path like: ---- +```text +.ade/release/local-release-YYYYMMDD-HHMMSS.json +``` -## Pipeline overview +Track: -``` -Phase 0: Verify repo state and find last release -Phase 1: Scope — desktop, iOS, or both (surface-level path scan) -Phase 2: Version number (ask user if not provided) -Phase 3: Generate changelog MDX + register in docs.json -Phase 4: Commit + push changelog to main -Phase 5: Tag the release commit, push tag, confirm workflow started -Phase 6: Poll release workflow every 5 minutes until done (scheduled wake-ups) -Phase 7: iOS build via asc (only if iOS is in scope) -Phase 8: Final summary — draft release link, changelog link, TestFlight status +```json +{ + "desktop": { "needed": false, "version": null, "tag": null, "lastTag": null }, + "ios": { "needed": false, "marketingVersion": null, "buildNumber": null, "lastTag": null }, + "phase": "detect|docs|desktop|ios|verify|done|blocked", + "notes": [] +} ``` ---- +For cron mode, also use a lock file under `.ade/release/` so two releases do not +overlap. If the lock is held by a live process, exit cleanly. -## Phase 0 — Verify repo state and find last release +## Phase 0: Preflight -1. Confirm you are on `main` locally and clean, OR that the branch you are on already contains the commits that will be released (if working in an ADE worktree, fetch and reason about `origin/main`). +1. Sync repository state: ```bash git fetch origin --tags --prune - git log origin/main --oneline -1 + git status --short + git rev-parse --abbrev-ref HEAD ``` -2. Find the last release tag: +2. Release from `main`. If not on `main`, switch only after confirming the + worktree is clean. - ```bash - git describe --tags --abbrev=0 origin/main 2>/dev/null || \ - git tag --list 'v*' --sort=-v:refname | head -n 1 - ``` +3. Do not proceed with uncommitted changes unless they are the release docs + changes created by this skill. -3. Count commits since that tag on `origin/main`: +4. Verify tools: ```bash - LAST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n 1) - git log --oneline "$LAST_TAG..origin/main" + gh auth status + asc doctor + security find-identity -v -p codesigning ``` - If the list is empty → nothing to release. Exit with a clear message. Do not proceed to any later phase. - -4. Sanity-check GitHub state: +5. Verify this Mac can sign desktop releases: ```bash - gh release view "$LAST_TAG" --json tagName,isDraft,isLatest - gh run list --workflow release.yml --limit 3 + test -f apps/desktop/scripts/release-mac-local.mjs + test -f apps/desktop/scripts/require-macos-release-secrets.cjs ``` - If there is already an in-flight release workflow, stop and surface it to the user before doing anything else. - -Record `LAST_TAG` and the ordered commit list in your working notes — Phases 1 and 3 both need them. + `release:mac:local` reads `.env.local` itself. Do not print secret values + while checking signing/notarization setup. ---- +## Phase 1: Detect Release Scope -## Phase 1 — Release scope (surface-level) +Do this separately for desktop and iOS. -Decide whether desktop, iOS, or both need to ship. **This is intentionally shallow** — path-based signals only, no deep diff review. +### Desktop scope -Produce the changed-file list: +Find the latest public desktop release tag: ```bash -git diff --name-only "$LAST_TAG..origin/main" -``` - -Classification rules: - -- **Desktop in scope** if any file matches: - - `apps/desktop/**` - - `apps/ade-cli/**` (ships with desktop) - - shared packages that desktop imports (e.g. root-level shared types used by desktop — check `apps/desktop/package.json` imports if unsure) - - `.github/workflows/release.yml`, `release-core.yml`, `prepare-release.yml` -- **iOS in scope** if any file matches: - - `apps/ios/**` - - iOS-specific shared code (Swift files anywhere) -- **Both** if both sets are non-empty. -- If only doc-only or changelog-only files changed → warn and ask the user whether they really want to cut a release (usually no). - -Output a one-line scope decision: - -``` -Scope: desktop= ios= commits since +DESKTOP_LAST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n 1) +git diff --name-only "$DESKTOP_LAST_TAG..origin/main" ``` -Store `scope.desktop` and `scope.ios` as booleans for later phases. +Desktop release is needed if any changed file matches: ---- - -## Phase 2 — Version number - -If `$ARGUMENTS` provided a version (first positional arg that matches `v?\d+\.\d+\.\d+`), use it. Strip or add the leading `v` so you have both `v1.1.3` (tag form) and `1.1.3` (bare form) available. +- `apps/desktop/**` +- `apps/ade-cli/**` +- desktop/runtime release scripts under `apps/desktop/scripts/**` +- `.github/workflows/release*.yml`, `.github/workflows/update-brew-tap.yml` +- shared package files that desktop imports +- root package/build files that affect desktop packaging -Otherwise, ask the user: +Do **not** count these as product changes by themselves: -> What version should this release be cut as? Last tagged release was ``. Reply with e.g. `v1.1.3`. +- `changelog/**` +- `docs/**` +- `docs.json` +- `CHANGELOG.md` +- pure website/docs assets -Validation: -- Must be strictly greater than `LAST_TAG` under semver. Reject otherwise. -- Must not already exist as a tag: `git rev-parse "v$VERSION" >/dev/null 2>&1 && exit 1`. -- Must not already exist on GitHub: `gh release view "v$VERSION" 2>/dev/null` should fail. - ---- +### iOS scope -## Phase 3 — Generate changelog MDX +iOS needs its own shipped marker. Prefer tags of this shape: -The Mintlify site renders `changelog/vX.Y.Z.mdx` at `https://www.ade-app.dev/docs/changelog/vX.Y.Z`. Match the style of `changelog/v1.1.2.mdx` (which is the current latest — read it first for tone and structure). - -### 3a. Gather commits per scope - -```bash -git log --pretty=format:'%h %s' "$LAST_TAG..origin/main" -- apps/desktop apps/ade-cli -git log --pretty=format:'%h %s' "$LAST_TAG..origin/main" -- apps/ios +```text +ios-v-build ``` -For each commit, you can pull the body when you need more than the subject: +Find the latest one: ```bash -git show --no-patch --pretty=format:'%B' +IOS_LAST_TAG=$(git tag --list 'ios-v*-build*' --sort=-creatordate | head -n 1) ``` -### 3b. Write `changelog/v.mdx` - -Required frontmatter (match existing files): +If no iOS shipped tag exists, do not guess in cron mode. Ask once to bootstrap +from the latest known TestFlight build and create the first tag at the current +release commit after the next successful upload. -```mdx ---- -title: "v" -description: "Release notes for ADE v" ---- -``` +iOS release is needed if any changed file since `IOS_LAST_TAG` matches: -Body structure — **two top-level sections, exactly these headings when both are in scope**: +- `apps/ios/**` +- Swift/iOS-specific shared files +- iOS signing/export configuration -```mdx - - ---- +Do not use desktop tags to decide iOS scope once iOS shipped tags exist. -## Desktop +### Scope outcomes - +Print one concise decision: ---- +```text +Scope: desktop= ios= +Desktop since: +iOS since: +``` -## iOS +If both are `no`, write state `phase=done` and stop. - -``` +## Phase 2: Resolve Versions -If only one platform is in scope, include only that section (no placeholder "No changes" block for the other). +### Desktop -**Tone rules** (from `AGENTS.md` style preferences): -- Direct and operational, not marketing. -- Concrete and stateful: say what changed and why it matters. -- Sentence case for headings unless an existing UI pattern uses something else. -- Bold the headline of each bullet (e.g. `**Chat continuity.**`) — see `v1.1.2.mdx` for pattern. +If desktop is in scope: -### 3c. Register the new page in `docs.json` +1. Parse latest tag `vMAJOR.MINOR.PATCH`. +2. Increment only `PATCH`. +3. New tag is `vMAJOR.MINOR.PATCH+1`. +4. Verify the tag and GitHub Release do not already exist. -Open `docs.json`, find the `"Changelog"` group's `pages` array, and insert `"changelog/v"` at the **top** of the list (above the current latest). Do not touch any other `docs.json` entries. +Example: -### 3d. Satisfy `validate-docs` — the easy-to-miss trio +```text +v1.2.14 -> v1.2.15 +``` -> **Why this exists:** CI runs `node scripts/validate-docs.mjs`, and the release `verify` job will not run until `ci-pass` is green. The changelog MDX + `docs.json` entry is **not enough** — `validate-docs` also enforces the three things below. Skipping any one makes the whole release fail at `verify` with the build already tagged. (Learned the hard way on v1.2.7.) +### iOS -1. **Root `CHANGELOG.md`** (Keep a Changelog format). Insert a new section directly under `## [Unreleased]` and above the previous release: +If iOS is in scope: - ```md - ## [] - +1. Read the current marketing version from the Xcode project or latest ASC + pre-release version. Do not change it. +2. Ask ASC for the next build number: - ### Added - - ... - ### Changed - - ... - ### Removed - - ... - ### Fixed - - ... + ```bash + asc builds next-build-number --app 6762759870 --version "$MARKETING_VERSION" --platform IOS ``` - Then, at the bottom link-reference block, add a `[]:` line and repoint `[Unreleased]`: +3. Use that build number. Do not hand-increment from local files if ASC says a + different number is next. - ```md - [Unreleased]: https://github.com/arul28/ADE/compare/v...HEAD - []: https://github.com/arul28/ADE/compare/v...v - ``` +## Phase 3: Release Notes and Docs - The validator checks: the top release heading equals the latest git tag, the heading for `v` exists, and the `[]` link reference exists. Use only `### Added/Changed/Removed/Fixed` subsections. +Only create public docs/changelog entries when desktop is in scope. A mobile-only +TestFlight build does not need a public desktop changelog unless the user asks. -2. **`changelog/index.mdx`** — update the "Latest release" `` so its `href` is `/changelog/v` **and** the copy mentions `v`. The validator checks both. +For desktop releases, update all release-doc surfaces: -3. **Brand assets referenced by `docs.json` must be committed.** `docs.json` points `logo`/`favicon` at files like `/logo/ade-wordmark.png` and `/favicon.png`. The repo **gitignores `*.png`**, so a plain `git add -A` silently skips them and the validator reports `missing target /...png`. Any such asset must be force-added in Phase 4 (`git add -f `). +- `changelog/v.mdx` +- `docs.json` +- `changelog/index.mdx` +- root `CHANGELOG.md` -### 3e. Self-check — run the validator locally before committing +Then run: ```bash -ls changelog/v.mdx -grep -n "changelog/v" docs.json -node scripts/validate-docs.mjs # MUST print "Documentation validation passed" -mint broken-links # optional but cheap; should be clean +node scripts/validate-docs.mjs ``` -Do not proceed to Phase 4 until `scripts/validate-docs.mjs` passes locally. It catches every gotcha above before CI does. - ---- - -## Phase 4 — Commit and push changelog to main - -### Respect the "never edit main directly" rule - -The user's standing guidance is to land changes through a lane/worktree, not by pushing directly to `main`. For the release changelog: - -1. **Preferred path — PR merge:** - - From the current ADE worktree branch, commit the changelog + `docs.json` change: - ```bash - # Stage the changelog page + docs.json + the Phase 3d trio, and FORCE-ADD - # any gitignored brand assets docs.json references (*.png is gitignored). - git add changelog/v.mdx docs.json CHANGELOG.md changelog/index.mdx - git add -f favicon.png logo/*.png 2>/dev/null || true - git commit -m "release: changelog for v" - git push -u origin HEAD - gh pr create --fill --title "release: changelog for v" \ - --body "Changelog for v. Tag will be cut after this lands on main." - ``` - - Then hand off to `/ship` to drive the PR to merge, OR merge it yourself with `gh pr merge --admin --squash` if the user has already said "merge it". - - Wait for `origin/main` to contain the new commit, **then wait for `ci-pass` to be green on it** (CI gate below) before Phase 5. Admin-merging does **not** wait for CI — and the release `verify` job rejects any tag whose `ci-pass` is not `success`. - -2. **Admin-bypass path (only if user explicitly says "push directly"):** - - `git push origin HEAD:main` with admin bypass. Note in the summary that the ruleset was bypassed. +Commit and land the docs/release metadata on `main` before building artifacts. +The desktop release tag must point at the final `main` commit that includes the +changelog. -Do not force-push. Do not `--no-verify`. If the push is rejected, investigate (rebase onto latest `origin/main`) — do not bypass checks. +## Phase 4: Desktop Local Build and Publish -After the commit is on `origin/main`, re-fetch and record the SHA you will tag: +Preferred local command: ```bash -git fetch origin main -RELEASE_SHA=$(git rev-parse origin/main) +npm --prefix apps/desktop run release:mac:local -- v ``` -### CI gate — `ci-pass` MUST be green on `RELEASE_SHA` before you tag +This script: -The release `verify` job calls `repos//commits//check-runs` and fails if there is no `ci-pass` check run, or if it is not `completed/success`. Push-to-`main` CI takes a few minutes; admin-merge does not wait for it. Poll until terminal **before** Phase 5: +- reads `.env.local` and installed keychain identities +- can use the installed Developer ID Application identity +- sets package versions temporarily from `ADE_RELEASE_TAG` +- builds/signs/notarizes mac artifacts +- restores `apps/desktop/package.json` and `apps/ade-cli/package.json` + +If local x64 inputs are needed, pass them explicitly: ```bash -# wait until ci-pass is completed on RELEASE_SHA, then assert success -for i in $(seq 1 40); do - row=$(gh api "repos/$GH_REPO/commits/$RELEASE_SHA/check-runs" \ - --jq '[.check_runs[]|select(.name=="ci-pass")]|sort_by(.completed_at//"")|last//empty | "\(.status)\t\(.conclusion//"")"') - status=$(printf '%s' "$row" | cut -f1); concl=$(printf '%s' "$row" | cut -f2) - echo "ci-pass: $status/$concl" - [ "$status" = "completed" ] && break - sleep 30 -done -[ "$concl" = "success" ] || { echo "ci-pass is $concl — fix CI before tagging"; } +npm --prefix apps/desktop run release:mac:local -- v \ + --x64-app=/path/to/ADE.app \ + --x64-zip=/path/to/ADE-x64.zip ``` -If `ci-pass` is red, **do not tag.** Fix the failing check on a new commit, land it on `main`, re-fetch `RELEASE_SHA`, and re-run this gate. (For docs-only releases the usual culprit is `validate-docs` — see Phase 3d.) +If the local script cannot produce x64, do not publish until one of these is +true: ---- +- the local x64 build succeeds under Rosetta +- a remote x64 artifact is available and verified +- the user explicitly approves an arm64-only desktop release -## Phase 5 — Tag and trigger the release workflow +### Desktop asset verification -1. Create the tag on the exact release SHA (only after the CI gate above is green): +Before uploading or publishing, verify: - ```bash - git tag -a "v" "$RELEASE_SHA" -m "v" - git push origin "v" - ``` +```bash +npm --prefix apps/desktop run validate:mac:artifacts +``` - **Recovery — re-pointing a prematurely-created tag.** If `verify` already failed because the tag landed on a red-CI commit, fix CI, land it, wait for green `ci-pass` on the new SHA, then move the tag to it. Force-updating the tag is acceptable here **only because no release was published** from the bad tag (the draft is created by `publish-release`, which never ran): +Also verify updater references: - ```bash - git fetch origin main --tags - git tag -f "v" "$NEW_RELEASE_SHA" - git push origin "v" --force # re-fires release.yml on the tag-update push - ``` +```bash +grep -oE 'ADE-[^ ]+\.(zip|dmg)' apps/desktop/release/latest-mac.yml | sort -u +``` -2. `.github/workflows/release.yml` triggers on `push` of `v*` tags and calls `release-core.yml`. Confirm the workflow registered: +Every referenced ZIP/DMG must exist in the upload set. - ```bash - sleep 10 - gh run list --workflow release.yml --limit 1 - ``` +### GitHub Release publication - If no run appears within ~60s, fall back to a manual dispatch: +1. Create/update a draft release for `v`. +2. Upload desktop assets. +3. Upload runtime/installer assets when they are available and part of the + release contract. +4. Verify expected assets and `latest-mac.yml`. +5. Flip public/latest: ```bash - gh workflow run release.yml \ - -f tag_name="v" \ - -f target_sha="$RELEASE_SHA" + gh release edit "v" --draft=false --latest ``` -3. Once the draft release appears (the workflow creates it), make sure the release body links to the Mintlify changelog page: +6. Verify: ```bash - gh release view "v" --json body,isDraft,url,assets - gh release edit "v" --notes "$(cat < - - Full changelog: https://www.ade-app.dev/docs/changelog/v - - - EOF - )" + gh api repos/arul28/ADE/releases/latest --jq '{tag_name,draft,prerelease,html_url,asset_count:(.assets|length)}' ``` - Leave `isDraft=true`. Do not publish. - - Expect the draft to carry the macOS **per-arch** desktop assets plus the - standalone runtime assets once `publish-release` runs: - - `ADE--arm64.dmg`, `ADE--arm64.zip`, `ADE--x64.dmg`, `ADE--x64.zip`, `latest-mac.yml` - - `install.sh` - - `SHA256SUMS` - - `ade-darwin-arm64`, `ade-darwin-arm64.native.tar.gz` - - `ade-darwin-x64`, `ade-darwin-x64.native.tar.gz` - - `ade-linux-arm64`, `ade-linux-arm64.native.tar.gz` - - `ade-linux-x64`, `ade-linux-x64.native.tar.gz` - ---- - -## Phase 6 — Poll the release workflow - -Release runs can take 20–40 minutes. Wait between polls instead of holding the turn open. - -After kicking off the workflow, schedule a wake-up for +5 minutes and **exit the current turn**: - -``` -ScheduleWakeup({ - delaySeconds: 300, - reason: "release v workflow running; poll in 5m", - prompt: "/release $ARGUMENTS" -}) -``` - -On each re-invocation, read a small state file at `.ade/release/v.json` (create it on first run) so you know what phase to resume in: - -```json -{ - "version": "v1.1.3", - "releaseSha": "", - "scope": { "desktop": true, "ios": true }, - "workflowRunId": 1234567, - "status": "running | release-done | ios-running | done | blocked", - "iosBuildNumber": null -} -``` - -Per iteration: - -```bash -gh run view "$RUN_ID" --json status,conclusion,url,jobs -``` - -- `status=queued|in_progress` → schedule another `+300s` wake, exit. -- `status=completed conclusion=success` → set `status=release-done`, move to Phase 7 (or Phase 8 if iOS is out of scope). -- `status=completed conclusion=failure|cancelled|timed_out` → stop, dump the failing job logs: - ```bash - gh run view "$RUN_ID" --log-failed | head -400 - ``` - Surface to the user and set `status=blocked`. Do not re-tag automatically. - -Do not loop in-turn. One poll per wake-up. +## Phase 5: iOS TestFlight Build and Distribution ---- - -## Phase 7 — iOS build via `asc` - -Skip entirely if `scope.ios=false`. - -### 7a. Ask the user: build number + target group(s) - -Always pause for these two inputs (even if build number came in via `$ARGUMENTS`, confirm the group choice). Ask together so the user answers once: - -> 1. **Build number.** The last one uploaded for `` was ``. New build will be ``. Override if you want a different number. -> 2. **Target TestFlight group(s).** The workspace has the groups below. Which should receive this build? (comma-separated names or IDs; default = `Internal Testers` if only you will be testing.) - -Before asking, enumerate the groups and their tester counts so the user can pick knowingly: - -```bash -# List all groups (note isInternal) -asc testflight groups list --app "$APP_ID" - -# For each group ID, count actual testers -for gid in ; do - count=$(asc testflight groups links view --group-id "$gid" --type betaTesters \ - | jq -r '.meta.paging.total') - echo "$gid testers=$count" -done -``` - -**Rules of thumb:** -- **Internal** groups (`isInternalGroup=true`): builds appear for testers as soon as the build is `VALID` and added to the group. No beta app review needed. Use this for dev-only testing. -- **External** groups (`isInternalGroup=false` or `None`): need beta app review (usually auto-approved for subsequent builds of the same marketing version). Use for wider-audience betas. -- **A group with zero testers is invisible.** If you add a build only to an empty group, nobody sees it and no emails go out. Verify tester counts before choosing. - -Validate build number: strictly greater than the last recorded for that marketing version — confirm with `asc builds next-build-number --app "$APP_ID" --version "$MARKETING_VERSION" --platform IOS`. - -### 7b. Pre-flight +Do this only if iOS scope is `yes`. -`AGENTS.md` and the `asc-*` skills are the source of truth. Re-read before every release; the gotchas below are stable but the skill contents may change: - -- `asc-xcode-build` -- `asc-testflight-orchestration` -- `asc-release-flow` -- `asc-signing-setup` -- `asc-submission-health` - -Quick sanity: +Preflight: ```bash asc doctor +asc testflight groups list --app 6762759870 --paginate ``` -Fail fast if keychain auth is broken. - -### 7c. iOS signing gotchas (mirrored from AGENTS.md — keep in sync) - -- Project uses **automatic** signing (`CODE_SIGN_STYLE = Automatic`, `DEVELOPMENT_TEAM = VQ372F39G6`). `apps/ios/ExportOptions.plist` ships with `signingStyle = manual` + named profiles for CI determinism. Local ad-hoc exports need `signingStyle = automatic` instead (drop the per-bundle profile map). `apps/ios/ExportOptions.auto.plist` is the ready-to-use auto-signing variant. -- **Three embedded bundles must all be provisioned for a manual-signing export**: the app (`com.ade.ios`), widgets (`com.ade.ios.widgets`), and the **App Clip** (`com.ade.ios.Clip`, added PR #706). `ExportOptions.plist` maps all three to named App Store profiles; the clip's — **`ADE App Clip App Store`** — is already minted in ASC. If a manual export fails signing the clip, the profile is missing/expired: `asc profiles create --name "ADE App Clip App Store" --profile-type IOS_APP_STORE --bundle 97ZL5TPJB8 --certificate `. Using `ExportOptions.auto.plist` avoids the issue entirely (Xcode provisions the clip via `-allowProvisioningUpdates`). -- `asc signing fetch` only downloads provisioning profiles and the `.cer` — it does **not** include the private key. Don't expect it to make local signing work on its own. -- Local exports need the ASC API key passed to `xcodebuild`. In addition to `-allowProvisioningUpdates`: - ``` - -authenticationKeyPath ~/.apple/asc/keys/AuthKey_WRRA7YU7RA.p8 \ - -authenticationKeyID WRRA7YU7RA \ - -authenticationKeyIssuerID 4d523a6c-e68c-49b2-8560-34e59786d8e3 - ``` - Pull current values from `~/.asc/config.json`; do not hard-code. -- Override the build number at archive time via `--archive-xcodebuild-flag "CURRENT_PROJECT_VERSION="` so you do not need to commit a `pbxproj` bump just to ship a build. +Normal build rule: -### 7d. `asc publish testflight` requires `--group` — don't use it for the build/upload step +- Use current `MARKETING_VERSION`. +- Use ASC next build number. +- Include app, widgets, and App Clip. +- Do not omit App Clip unless the user explicitly accepts an emergency build. -In the current `asc`, `asc publish testflight` **requires `--group`** and will just print help (exit 0, nothing uploaded) without it. Its one-shot local-build form also races encryption (`--wait` returns at `processingState=VALID` while `usesNonExemptEncryption` is still unanswered). So **do not** use `asc publish testflight` to build+upload. Use the explicit sequence in 7e instead. (Both gotchas hit on v1.2.7.) - -### 7e. Safe sequenced flow (archive → export → upload → wait → encryption → distribute) - -Each step is its own command so you can see exactly where it fails. Run the heavy steps with `run_in_background` and poll the log. +Recommended explicit sequence: ```bash -APP_ID=6762759870 -BUILD_NUMBER= -MARKETING_VERSION= # keep the SAME version, bump only the build -OUT=/tmp/ade-ios-build${BUILD_NUMBER}; mkdir -p "$OUT" -# ASC_KEY_PATH/ID/ISSUER from ~/.asc/config.json + `asc doctor` +OUT=.ade/tmp/ios-testflight-$MARKETING_VERSION-build$BUILD_NUMBER +mkdir -p "$OUT" + +ASC_KEY_PATH=$(jq -r '.profiles.ade.keyPath // .keyPath // .privateKeyPath // .private_key_path // empty' ~/.asc/config.json) +ASC_KEY_ID=$(jq -r '.profiles.ade.keyId // .keyId // .key_id // empty' ~/.asc/config.json) +ASC_ISSUER_ID=$(jq -r '.profiles.ade.issuerId // .profiles.ade.issuer_id // .issuerId // .issuer_id // empty' ~/.asc/config.json) -# 1) Archive (automatic signing needs the ASC API key for -allowProvisioningUpdates) asc xcode archive \ --project apps/ios/ADE.xcodeproj --scheme ADE \ + --configuration Release --clean \ --archive-path "$OUT/ADE.xcarchive" --overwrite --output json \ + --xcodebuild-flag=-destination --xcodebuild-flag=generic/platform=iOS \ --xcodebuild-flag=-allowProvisioningUpdates \ --xcodebuild-flag=-authenticationKeyPath --xcodebuild-flag="$ASC_KEY_PATH" \ --xcodebuild-flag=-authenticationKeyID --xcodebuild-flag="$ASC_KEY_ID" \ @@ -482,176 +341,108 @@ asc xcode archive \ --xcodebuild-flag=CURRENT_PROJECT_VERSION=$BUILD_NUMBER \ --xcodebuild-flag=MARKETING_VERSION=$MARKETING_VERSION -# 2) Export the IPA (auto-signing variant) asc xcode export \ --archive-path "$OUT/ADE.xcarchive" \ --export-options apps/ios/ExportOptions.auto.plist \ - --ipa-path "$OUT/ADE.ipa" --output json - -# 3) Upload — NOTE: `asc builds upload` has NO --timeout (only --wait / --poll-interval). -# Passing --timeout makes it print help and upload nothing. -asc builds upload --app "$APP_ID" --ipa "$OUT/ADE.ipa" - -# 4) Wait for VALID (this is where --timeout lives) -asc builds wait --app "$APP_ID" --build-number "$BUILD_NUMBER" --version "$MARKETING_VERSION" \ - --platform IOS --timeout 40m + --ipa-path "$OUT/ADE.ipa" --overwrite --output json \ + --xcodebuild-flag=-allowProvisioningUpdates \ + --xcodebuild-flag=-authenticationKeyPath --xcodebuild-flag="$ASC_KEY_PATH" \ + --xcodebuild-flag=-authenticationKeyID --xcodebuild-flag="$ASC_KEY_ID" \ + --xcodebuild-flag=-authenticationKeyIssuerID --xcodebuild-flag="$ASC_ISSUER_ID" -# 5) Resolve the build ID, then answer encryption -BUILD_ID=$(asc builds list --app "$APP_ID" --limit 8 \ - | jq -r --arg v "$BUILD_NUMBER" '.data[]|select(.attributes.version==$v)|.id' | head -n1) -asc builds update --build-id "$BUILD_ID" --uses-non-exempt-encryption=false +asc builds upload --app 6762759870 --ipa "$OUT/ADE.ipa" -# 6) Distribute. INTERNAL groups auto-receive every processed build — do NOT add-groups them -# (it errors "Cannot add internal group to a build"). add-groups is for EXTERNAL groups only: -for gid in "${EXTERNAL_GROUP_IDS[@]}"; do - asc builds add-groups --build-id "$BUILD_ID" --group "$gid" --submit --confirm -done -# If you have a mixed list, pass --skip-internal so internal IDs are ignored. +asc builds wait \ + --app 6762759870 \ + --build-number "$BUILD_NUMBER" \ + --version "$MARKETING_VERSION" \ + --platform IOS \ + --timeout 40m ``` -For **internal-only** releases (the common case — "ship to internal testers") steps 1–5 are the whole job: once the build is `VALID` with encryption answered, it is already live for every internal group. There is no add-groups step. - -If a heavy step (archive/upload) errors, the IPA from a successful export is reusable — re-run only from the failing step. Update `.ade/release/v.json` with `status=ios-running` while waiting. - -### 7f. Post-upload verification (always run this) +If automatic export fails due signing, use the repo's signing gotchas in +`AGENTS.md` and the `asc-*` skills. Fix signing/profiles; do not silently remove +targets. -Do not declare iOS done based on `BETA_APPROVED` alone. Verify the build is in a **non-empty** group: +After processing: ```bash -asc builds info --build-id "$BUILD_ID" # processingState=VALID, usesNonExemptEncryption=false -asc builds build-beta-detail view --build-id "$BUILD_ID" # externalBuildState=BETA_APPROVED, internalBuildState=READY_FOR_BETA_TESTING -for gid in "${GROUP_IDS[@]}"; do - count=$(asc testflight groups links view --group-id "$gid" --type betaTesters | jq -r '.meta.paging.total') - members=$(asc testflight groups links view --group-id "$gid" --type builds | jq -r '.data[].id' | grep -Fx "$BUILD_ID" || true) - echo "group=$gid testers=$count build_present=$([ -n "$members" ] && echo yes || echo no)" -done -``` - -All three must be true for a given group: -- `testers > 0` (otherwise no humans see the build) -- build appears in the group's builds list — **automatic for internal groups** the moment the build is `VALID` + encryption answered; only present for external groups after `add-groups` -- internal → `internalBuildState` is `READY_FOR_BETA_TESTING` or `IN_BETA_TESTING`; external → `externalBuildState` is `BETA_APPROVED` - -If any fails, fix it explicitly and re-verify. Do not trust `autoNotifyEnabled=true` alone — it only controls push notifications, not distribution. - -**External (public-link) builds need Beta App Review.** A freshly uploaded external build sits at `externalBuildState=WAITING_FOR_BETA_REVIEW` until Apple approves it — and the public TestFlight link keeps serving the **last approved** build until then. Review is per *marketing version*, not per build: the first external build of a new version is reviewed (hours–a day); later builds of the **same** version generally auto-clear. Internal distribution never needs review. - ---- +BUILD_ID=$(asc builds list --app 6762759870 --version "$MARKETING_VERSION" --platform IOS --limit 10 \ + | jq -r --arg b "$BUILD_NUMBER" '.data[]|select(.attributes.version==$b)|.id' | head -n1) -## Phase 8 — Summary +asc builds update --build-id "$BUILD_ID" --uses-non-exempt-encryption=false +``` -Before printing the summary, verify the draft release carries every expected asset. Do not flip the draft and do not report `done` if anything is missing — surface the gap. +Attach all non-empty beta groups: ```bash -gh release view "v" --json assets --jq '.assets[].name' | sort +asc testflight groups list --app 6762759870 --paginate +asc builds add-groups --build-id "$BUILD_ID" --group "" --submit --confirm ``` -The mac build runs a **per-arch matrix** (arm64 + x64), and the runtime build -publishes darwin/linux standalone brain artifacts, so the expected set is -**15 assets**: -- `ADE--arm64.dmg` -- `ADE--arm64.zip` -- `ADE--x64.dmg` -- `ADE--x64.zip` -- `latest-mac.yml` -- `install.sh` -- `SHA256SUMS` -- `ade-darwin-arm64` -- `ade-darwin-arm64.native.tar.gz` -- `ade-darwin-x64` -- `ade-darwin-x64.native.tar.gz` -- `ade-linux-arm64` -- `ade-linux-arm64.native.tar.gz` -- `ade-linux-x64` -- `ade-linux-x64.native.tar.gz` - -> This skill previously expected a single `-universal.*` set plus a `.blockmap`. -> That changed when the build moved to the parallel-arch matrix (v1.2.5). There -> are **no** separate `.blockmap` assets in this layout — do not flag their absence. - -electron-updater consumes `latest-mac.yml` → the per-arch `.zip`s (macOS updates -install from the zip, not the DMG). The real check is that `latest-mac.yml` -references only assets that are actually present — otherwise auto-update breaks: +Use `--submit --confirm` for external groups. Internal groups may also be added +explicitly if they do not automatically receive the build. + +Verify every group: ```bash -gh release download "v" --pattern latest-mac.yml --dir /tmp --clobber -assets=$(gh release view "v" --json assets --jq '.assets[].name') -grep -oE 'ADE-[^ ]+\.(zip|dmg)' /tmp/latest-mac.yml | sort -u | while read f; do - echo "$assets" | grep -qx "$f" && echo " ok $f" || echo " MISSING referenced asset: $f" +asc builds info --build-id "$BUILD_ID" +asc builds build-beta-detail view --build-id "$BUILD_ID" +for gid in ; do + asc testflight groups links view --group-id "$gid" --type betaTesters + asc testflight groups links view --group-id "$gid" --type builds done ``` -If a referenced file is missing, or the 15-asset set is incomplete → the build -or upload broke; re-inspect the `build-mac-release` matrix jobs and the runtime -artifact download/publish step. +Success requires: -Then print a single final block and stop: +- build `processingState=VALID` +- `usesNonExemptEncryption=false` +- `internalBuildState` is `READY_FOR_BETA_TESTING` or `IN_BETA_TESTING` +- external groups are `IN_BETA_TESTING` or otherwise clearly submitted/approved +- each target group has at least one tester +- each target group contains the new build -``` -Release v — summary - -- Changelog: https://www.ade-app.dev/docs/changelog/v -- Draft release: (still draft — flip manually) -- Desktop assets: mac= -- Workflow run: (conclusion: success) -- iOS TestFlight build : -- Beta group: - -Next steps: -1. Review the draft release, then `gh release edit v --draft=false` to publish. -2. Publishing automatically bumps the Homebrew tap (arul28/homebrew-ade) via the - `update-brew-tap.yml` workflow — verify with - `gh run list --workflow update-brew-tap.yml --limit 1` after publishing. - Manual fallback if that run fails: `scripts/update-brew-tap.sh v`. -``` - -If any phase ended in `blocked`, the summary says `BLOCKED` at the top with the failing phase and the command to resume. - ---- +After successful distribution, tag the shipped iOS build: -## State file schema +```bash +git tag -a "ios-v${MARKETING_VERSION}-build${BUILD_NUMBER}" "$RELEASE_SHA" \ + -m "iOS ${MARKETING_VERSION} build ${BUILD_NUMBER}" +git push origin "ios-v${MARKETING_VERSION}-build${BUILD_NUMBER}" +``` -`.ade/release/v.json` — created in Phase 5, read/written on every wake-up. +## Phase 6: Recovery Rules -```json -{ - "version": "v1.1.3", - "lastTag": "v1.1.2", - "releaseSha": "", - "scope": { "desktop": true, "ios": true }, - "workflowRunId": 1234567, - "workflowStatus": "queued | in_progress | success | failure | cancelled", - "iosBuildNumber": 42, - "iosBuildId": "", - "iosStatus": "pending | uploading | processing | valid | distributed | failed", - "phase": "5 | 6 | 7 | 8", - "status": "running | done | blocked", - "notes": [] -} -``` +Desktop: -On wake-up: -1. Read the state file. If `status=done` or `status=blocked`, print the summary and exit. -2. Otherwise resume at `phase`. +- If notarization stalls, do not restart everything. Preserve artifacts and + retry only notarization/publish when possible. +- If GitHub upload is interrupted, inspect existing release assets first, then + upload missing assets only. +- If `latest-mac.yml` references a missing asset, keep the release draft/private + until fixed. ---- +iOS: -## Things this command will NOT do +- If archive succeeds but upload fails, reuse the IPA. +- If upload succeeds but processing waits, use `asc builds wait`. +- If distribution fails, reuse the same `BUILD_ID`; do not upload another build + unless the binary itself is wrong. +- If App Clip signing fails, fix Developer Portal/App Store Connect capability + and profiles. Do not repeat the v1.1.10 build 16 emergency omission unless + explicitly directed. -- Publish the GitHub draft release (human must flip `--draft=false`). -- Force-push to `main` or any tag. -- Bypass CI, pre-commit hooks, or rulesets without an explicit user ask. -- Edit existing changelog files (only creates the new `vX.Y.Z.mdx`). -- Guess the version number or iOS build number — always ask. -- Re-release an already-tagged version. If `vX.Y.Z` exists, stop and surface. +## Final Report ---- +Report: -## References +- desktop scope decision and tag +- GitHub Release URL and asset count +- whether `latest-mac.yml` references only present assets +- iOS marketing/build number +- TestFlight build ID +- group membership verification +- any skipped surface and why +- whether the repo is clean -- `AGENTS.md` — release + `asc` guidance (canonical). -- `docs/playbooks/ship-lane.md` — how to drive the changelog PR to merge in Phase 4. -- `.github/workflows/release.yml`, `release-core.yml`, `prepare-release.yml` — desktop release pipeline. -- `changelog/v1.1.2.mdx` — template to match for tone, structure, and section shape. -- `docs.json` — Mintlify page registration (insert new entry at top of the `Changelog` group). -- `asc-*` skills — iOS build/publish specifics. +Keep the report short, but include exact version/build numbers.