ADE Release Skill

SkillCloud & infra

ADE release conductor: detect whether desktop, iOS, and/or the Cloudflare web tier actually changed, bump desktop patch versions, keep iOS marketing versions fixed while bumping build numbers, ship desktop through the GitHub Actions release workflow, distribute TestFlight builds to all beta users, and reconcile every Cloudflare surface (Pages web client, the four Workers, D1 migrations, R2 buckets and lifecycle rules, Durable Object migration tags, cron triggers, vars and secrets) against what actually exists in the Cloudflare account.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the ADE Release Skill skill

What this skill tells your AI

The instructions your AI receives, as published by arul28/ade in .agents/skills/release/SKILL.md and read by ahel’s review.

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, ship a TestFlight build, or reconcile the hosted Cloudflare surfaces.

ADE ships three independent tiers, and a release conductor owns all three:

  • desktop — GitHub Actions release workflow, macOS + Windows assets
  • iOS — local ASC archive/export/upload to TestFlight
  • Cloudflare web tier — the ade-web-client Pages project and four Workers, plus the account-side state they depend on (D1 databases and their applied migrations, R2 buckets and their lifecycle rules, Durable Object migration tags, cron triggers, vars, secrets). Phase 5.5 owns this. Neither the desktop workflow nor TestFlight touches it, and most of its failure modes are invisible to every test in the repository because they live in account state, not code.

This is a GitHub desktop + local ASC iOS release flow. Desktop releases must use the repository GitHub Actions release workflow for both platforms: macOS updater assets are produced reproducibly as per-arch ZIP/DMG artifacts on a macOS runner, and the signed Windows installer is produced on a Windows runner. Neither is built from the release host, whatever that host is — macOS and Windows are peers here, not a primary and a follow-up.

The release host runs checks, creates release docs/tags, and monitors and recovers the workflow. The only host-dependent phase is iOS: building and uploading TestFlight releases through ASC requires a macOS host. On a Windows host, run the desktop release normally and stop before the mobile phase, stating that iOS needs a macOS host — do not report a desktop-only release as complete when iOS was also in scope.

A preflight is a cheap check that runs before expensive build/upload work. Use preflights to catch release blockers while fixes can still be committed without burning a notarization, TestFlight upload, or build number.

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.
  • Desktop release uses GitHub Actions only. Do not build, sign, notarize, or upload desktop release assets from the release host unless the user explicitly asks for a one-off manual recovery.
  • No universal updater ZIPs. latest-mac.yml must reference per-arch arm64 and x64 ZIPs. Never publish a latest-mac.yml that points to ADE-*-universal.zip; v1.2.16 proved that giant universal updater ZIPs can crash Squirrel.Mac during in-app update.
  • Do not publish broken updater metadata. Before making a desktop release public/latest, verify latest-mac.yml references assets that exist and that the expected arm64/x64 DMGs and ZIPs are present. When Windows is enabled, apply the same rule to latest.yml and the Windows installer.
  • Do not publish a half-platform release. The Windows gate and the Windows assets must agree. If ADE_WINDOWS_PUBLIC_RELEASE_ENABLED is 1 the draft must carry Windows assets; if it is not 1 the draft must carry none. Either mismatch means the workflow did not do what you think it did, so keep the release draft/private and investigate before publishing.
  • Do not discover obvious release blockers after upload. Preflight iOS App Clip packaging metadata before starting the expensive mobile phase.
  • Do not wait forever. If GitHub notarization or TestFlight processing exceeds its normal window by a lot, preserve state, retry only the failed phase when possible, or stop with a clear recovery command.
  • Do not serialize independent tiers. Desktop GitHub Actions, iOS TestFlight, and Cloudflare are independent once the shared docs/ci-pass gate is behind you. Numbered phases are a catalog, not a queue. Follow Parallel schedule below whenever more than one is in scope.

Parallel schedule

This overrides the Phase 4 → 5 → 5.5 order when more than one tier is in scope. Exclusive gh run watch is forbidden while another leg can make progress — poll the run and keep working.

Stay serial only for:

  1. Docs merge → green ci-pass on that SHA → desktop v* tag. Tagging before ci-pass fails verify.
  2. Draft asset verification → --draft=false. npm versions are immutable.
  3. iOS IPA inspect / altool --validate-app → upload → asc builds wait → group attach.
  4. A Windows release host cannot archive iOS. Keep desktop and Cloudflare in flight and say iOS needs a macOS host. Do not call the release done.

During the ci-pass wait (docs PR just landed)

  • Cloudflare: deploy-web.yml is already running for that SHA. Start Phase 5.5 verification. Do not wait for a desktop tag.
  • iOS: asc doctor and the App Clip preflight. Do not archive yet — a red ci-pass would burn a build number.

After the desktop tag (or immediately if desktop is out of scope)

Start every in-scope leg:

  • Desktop: poll release.yml until the draft exists. If iOS or Cloudflare is also in scope, do not sit in gh run watch.
  • iOS (macOS host): archive, export, inspect, validate, upload against RELEASE_SHA. TestFlight does not need the GitHub draft.
  • Cloudflare: finish any remaining reconcile or manual deploy.

Join before undraft

If desktop is in scope, the draft must be verified before it goes public. iOS may still be in asc builds wait. Cloudflare may still be verifying. That is expected — do not stall undraft on them.

After undraft

Poll publish-runtime-packages.yml and update-brew-tap.yml together and join both. Keep polling iOS processing and Cloudflare if those legs are not done. Desktop is not done until npm view @ade-dev/runtime version equals the tag.

Join before phase=done

Every in-scope leg is green. A green GitHub release with a missing npm package, a missing TestFlight build, or a drifted Worker is not a finished release.

Machine Notes

Detect the release host rather than assuming it (uname -s / process.platform) — this lane runs on Windows or on an Apple Silicon Mac. Either way, desktop release artifacts for both platforms are produced remotely by GitHub Actions; treat local desktop packaging scripts as diagnostic/recovery tools only. Host type affects exactly two things: shell syntax for the commands below, and whether the iOS/TestFlight phase can run at all (macOS only).

Desktop updater correctness requires, on macOS:

  • latest-mac.yml
  • one arm64 ZIP and one x64 ZIP referenced by that file
  • one arm64 DMG and one x64 DMG
  • no universal ZIP in the updater feed

and, when ADE_WINDOWS_PUBLIC_RELEASE_ENABLED is 1, additionally on Windows:

  • latest.yml
  • one ADE-<VERSION>-win-x64.exe installer referenced by that file
  • the matching ADE-<VERSION>-win-x64.exe.blockmap

Windows builds fresh on the tag alongside macOS. There is one repository variable, ADE_WINDOWS_PUBLIC_RELEASE_ENABLED; it decides whether the release carries Windows at all. Read it before verifying assets, because it determines which of the two asset matrices below is correct.

State and Locking

Create a state file before mutating release state:

mkdir -p .ade/release

Use a path like:

.ade/release/release-YYYYMMDD-HHMMSS.json

Track:

{
  "desktop": { "needed": false, "version": null, "tag": null, "lastTag": null, "platforms": null },
  "ios": { "needed": false, "marketingVersion": null, "buildNumber": null, "lastTag": null },
  "cloudflare": {
    "needed": false,
    "driftPreflight": "pending|pass|blocked",
    "surfaces": {
      "ade-web-client": { "changed": false, "action": "skip|ci|manual", "status": "pending|deployed|verified|blocked", "note": null },
      "ade-account-directory-production": { "changed": false, "action": "skip|ci|manual", "status": "pending|deployed|verified|blocked", "migrationsApplied": null, "note": null },
      "ade-github-webhook-relay": { "changed": false, "action": "skip|ci|manual", "status": "pending|deployed|verified|blocked", "migrationsApplied": null, "note": null },
      "ade-tunnel-relay": { "changed": false, "action": "skip|ci|manual", "status": "pending|deployed|verified|blocked", "note": null },
      "ade-push-relay": { "changed": false, "action": "skip|ci|manual", "status": "pending|deployed|verified|blocked", "migrationsApplied": null, "note": null }
    },
    "accountState": {
      "r2": {
        "ade-diagnostics": { "exists": null, "lifecycle30d": null, "devUrlDisabled": null, "customDomains": null },
        "ade-diagnostics-production": { "exists": null, "lifecycle30d": null, "devUrlDisabled": null, "customDomains": null }
      },
      "d1": {
        "ade-account-directory-production": { "exists": null, "idMatchesConfig": null, "migrationsPending": null },
        "ade-github-relay": { "exists": null, "idMatchesConfig": null, "migrationsPending": null },
        "ade-push-relay": { "exists": null, "idMatchesConfig": null, "migrationsPending": null, "triggersPresent": null }
      },
      "config": {
        "ade-account-directory-production": { "secretsBound": null, "varsSet": null, "unverified": [] },
        "ade-push-relay": { "secretsBound": null, "varsSet": null, "unverified": [] }
      },
      "cronTriggers": { "ade-account-directory-production": null, "ade-push-relay": null },
      "durableObjectMigrations": { "ade-github-webhook-relay": null, "ade-tunnel-relay": null },
      "productEnablement": { "r2": null }
    },
    "rollbacks": []
  },
  "phase": "detect|docs|desktop|ios|cloudflare|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: Preflight

Keep this phase read-only except for the .ade/release state/lock files. Do not edit release docs, bump versions, create tags, or upload artifacts until the relevant preflights pass.

  1. Sync repository state:

    git fetch origin --tags --prune
    git status --short
    git rev-parse --abbrev-ref HEAD
    
  2. Release from main. If not on main, switch only after confirming the worktree is clean.

  3. Do not proceed with uncommitted changes unless they are the release docs changes created by this skill.

  4. Verify tools:

    gh auth status
    

    Do not block a desktop-only release on ASC auth. Run asc doctor after scope detection if iOS is in scope.

  5. Verify the desktop GitHub release workflow exists:

    test -f .github/workflows/release.yml
    test -f .github/workflows/release-core.yml
    test -f .github/workflows/release-publish.yml
    gh workflow view release.yml --repo arul28/ADE
    
  6. For desktop releases, verify the workflow path is the intended one before tagging:

    • .github/workflows/release-core.yml builds dist:mac:arm64:signed.
    • .github/workflows/release-core.yml builds dist:mac:x64:signed.
    • .github/workflows/release-core.yml builds dist:win:signed in build-win-release.
    • The publish job merges per-arch manifests into one latest-mac.yml.
    • The publish job attaches the Windows installer, its .blockmap, and latest.yml when the Windows gate is on.

    If the workflow has been changed to publish universal updater ZIPs, stop and fix the workflow before releasing.

  7. For desktop releases, resolve the expected platform matrix before tagging. This decides what the draft must contain in Phase 4:

    gh variable get ADE_WINDOWS_PUBLIC_RELEASE_ENABLED --repo arul28/ADE 2>/dev/null || echo "unset"
    
    • 1 means the release must carry macOS and Windows assets. Record platforms=mac,win.
    • Anything else, including unset, means macOS only. Record platforms=mac.

    Windows signing is fail-closed: if the gate is 1 and the signing secrets are missing, the verify job stops the run in about a minute. Do not "fix" that by clearing the gate mid-release; fix the secrets or stop.

  8. For iOS releases, preflight App Clip packaging metadata before archiving:

    xcodebuild -showBuildSettings \
      -project apps/ios/ADE.xcodeproj \
      -scheme ADE \
      -configuration Release \
      -json > .ade/tmp/ios-release-build-settings.json
    

    Confirm from the JSON/build settings:

    • ADE, ADEWidgets, and ADEClip targets are present in the ADE scheme.
    • ADEClip Release IPHONEOS_DEPLOYMENT_TARGET matches the parent app baseline when Apple requires it. Current known-good value is 26.0.
    • ADEClip has ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon.
    • ADEClip/Info.plist includes valid App Clip store metadata, supported interface orientations, and device family values accepted by App Store validation.
    • apps/ios/ExportOptions.auto.plist exists; prefer it for local ASC-backed archive/export.

    If any of these fail, fix and commit before archiving. Do not upload an IPA produced from uncommitted project-signing or App Clip metadata changes.

Phase 1: Detect Release Scope

Do this separately for desktop and iOS.

Desktop scope

Find the latest public desktop release tag:

DESKTOP_LAST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
git diff --name-only "$DESKTOP_LAST_TAG..origin/main"

Desktop release is needed if any changed file matches:

  • apps/desktop/**
  • apps/ade-cli/**
  • desktop/runtime release scripts under apps/desktop/scripts/**
  • .github/workflows/release*.yml, .github/workflows/update-brew-tap.yml, .github/workflows/publish-runtime-packages.yml
  • shared package files that desktop imports
  • root package/build files that affect desktop packaging

Do not count these as product changes by themselves:

  • changelog/**
  • docs/**
  • sdk/** (Mintlify SDK tab — handled under SDK / public docs scope)
  • docs.json
  • CHANGELOG.md
  • pure website/docs assets

iOS scope

iOS needs its own shipped marker. Prefer tags of this shape:

ios-v<marketing-version>-build<build-number>

Find the latest one:

IOS_LAST_TAG=$(git tag --list 'ios-v*-build*' --sort=-creatordate | head -n 1)

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.

iOS release is needed if any changed file since IOS_LAST_TAG matches:

  • apps/ios/**
  • Swift/iOS-specific shared files
  • iOS signing/export configuration

Do not use desktop tags to decide iOS scope once iOS shipped tags exist.

SDK / public docs scope

This does not trigger a desktop GitHub release by itself. npm publish is publish-sdk-packages.yml on merge to main (OIDC), not this conductor.

SDK Mintlify pages (sdk/*.mdx) and the two npm READMEs need an update if since $DESKTOP_LAST_TAG (when desktop is in scope) — or, when checking an SDK-only window, since the last commit that already shipped those docs — any of:

  • packages/sdk/**
  • packages/chat-ui/**
  • apps/desktop/src/shared/callerMcpServers.ts (honesty table)
  • apps/ade-cli embedded profile / parentDeathWatchdog
  • sdk/*.mdx already in the diff (verify they still match the code)

Print SDK docs: <yes|no> with that decision. User-visible contract changes (install, threads, MCP residuals, chat-ui props, doctor() fields) are yes. Internal-only test or comment churn is no.

Scope outcomes

Print one concise decision:

Scope: desktop=<yes|no> ios=<yes|no> sdk-docs=<yes|no>
Desktop since: <DESKTOP_LAST_TAG>
iOS since: <IOS_LAST_TAG or bootstrap-needed>

If desktop and iOS are both no:

  • If sdk-docs is yes, do not tag a desktop release. Land or require the Mintlify/README updates on the SDK PR (or a docs-only follow-up). This conductor does not npm publish.
  • If sdk-docs is also no, write state phase=done and stop.

Phase 2: Resolve Versions

Desktop

If desktop is in scope:

  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.

Example:

v1.2.14 -> v1.2.15

iOS

If iOS is in scope:

  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:

    asc builds next-build-number --app 6762759870 --version "$MARKETING_VERSION" --platform IOS
    
  3. Use that build number. Do not hand-increment from local files if ASC says a different number is next.

Phase 3: Release Notes and Docs

Only create public desktop changelog entries when desktop is in scope. A mobile-only TestFlight build does not need a public desktop changelog unless the user asks.

For desktop releases, update all release-doc surfaces:

  • changelog/v<VERSION>.mdx
  • docs.json (changelog page list)
  • changelog/index.mdx
  • root CHANGELOG.md

Additionally, when sdk-docs is yes (whether or not desktop is in scope):

  • Update the Mintlify SDK tab: sdk/overview.mdx, sdk/install.mdx, sdk/quickstart.mdx, sdk/threads.mdx, sdk/mcp.mdx, sdk/chat-ui.mdx, sdk/runtime.mdx, sdk/reference.mdx, and the docs.json SDK tab / footer links if pages were added or renamed.
  • Keep the MCP honesty table aligned across sdk/mcp.mdx, packages/sdk/README.md, and docs/features/sdk/README.md. Strict MCP is enforced only on Claude; never market it as uniform. mcpCapability (strictRequested first, then level === "enforced") is the honesty mechanism.
  • Both npm READMEs (packages/sdk/README.md, packages/chat-ui/README.md) must link to https://www.ade-app.dev/docs/sdk/overview as the full docs. README edits publish on the next @ade-dev/sdk / @ade-dev/chat-ui version bump (publish-sdk-packages.yml). Do not npm publish from this skill.
  • Register new Mintlify pages in docs.json before validating.

Then run:

node scripts/validate-docs.mjs

Commit and land the docs/release metadata on main before tagging. The desktop release tag must point at the final main commit that includes the changelog. An SDK-docs-only commit does not get a v* tag.

Phase 4: Desktop GitHub Workflow Release

Do this only if desktop scope is yes. If iOS or Cloudflare is also in scope, start those legs as soon as the tag exists — see Parallel schedule. This phase is the desktop leg, not a barrier in front of the others.

The desktop happy path is GitHub Actions. Do not run local desktop release commands such as release:mac:local, dist:mac:universal:signed, dist:mac:perarch:signed, or manual gh release upload from the release host.

Create the release tag

The tag must point at a commit that already has a green ci-pass check run. release-core.yml's verify job is fail-closed on it: it looks up the ci-pass check run for the tagged SHA and exits 1 with No ci-pass check run was found for <sha>. Run CI before releasing. when the check is absent, still running, or red. Merging the release-docs PR and tagging immediately is exactly how you hit this — the squash-merge creates a brand-new commit on main whose CI has not started yet.

Wait for it before tagging:

RELEASE_SHA=$(git rev-parse origin/main)
gh api "repos/arul28/ADE/commits/$RELEASE_SHA/check-runs" \
  --jq '.check_runs[] | select(.name=="ci-pass") | {status,conclusion,html_url}'

Tag only when that prints completed / success. An empty result means CI has not reported on the commit yet.

After release docs are committed on main and ci-pass is green:

git fetch origin --tags --prune
git status --short
RELEASE_SHA=$(git rev-parse origin/main)
git rev-parse --verify "v<VERSION>" >/dev/null && {
  echo "Tag v<VERSION> already exists"
  exit 1
}
git tag -a "v<VERSION>" "$RELEASE_SHA" -m "ADE v<VERSION>"
git push origin "v<VERSION>"

If you tagged early and verify failed, the tag is still correct and nothing was published — do not delete or move it. Wait for ci-pass to go green on the same SHA, then rerun only the failed job:

gh run rerun "$RUN_ID" --repo arul28/ADE --failed

The pushed tag triggers .github/workflows/release.yml, which calls .github/workflows/release-core.yml and creates a draft GitHub Release.

Find and poll the workflow run

Find the run for the pushed tag/SHA:

gh run list --repo arul28/ADE --workflow release.yml --event push \
  --json databaseId,headBranch,headSha,status,conclusion,createdAt,url \
  --limit 20

Choose the run whose headBranch is v<VERSION> or whose headSha matches RELEASE_SHA.

If this is a desktop-only release, gh run watch is fine:

gh run view "$RUN_ID" --repo arul28/ADE --json status,conclusion,url,jobs
gh run watch "$RUN_ID" --repo arul28/ADE --interval 60

If iOS or Cloudflare is also in scope, poll instead and keep those legs moving. gh run watch blocks the conductor for the whole notarization window.

gh run view "$RUN_ID" --repo arul28/ADE --json status,conclusion,url,jobs
# Poll on a timer between iOS/Cloudflare steps; do not exclusive-watch.

Expected shape:

  • runtime/resource jobs run first
  • arm64 mac release and x64 mac release build/sign/notarize independently
  • build-win-release builds/signs/validates Windows independently, in parallel with the mac jobs, when platforms includes win. With the gate off it is skipped, and a skipped Windows job does not block the mac release.
  • publish-release (in release-publish.yml, called by release.yml after run-release succeeds) merges the per-arch updater manifests and creates the draft
  • update-brew-tap and Publish ADE runtime packages both run after the GitHub release is made public (release.published), not when the draft is created. After undraft, poll both runs together and join both. Do not finish the npm wait before starting the brew wait, or the reverse.

If platforms=mac,win and build-win-release did not run, stop. The gate and the run disagree, and publishing would ship a macOS-only release under a version that is supposed to carry Windows.

Retry policy

Do not start duplicate full release workflows.

If a job fails or is cancelled:

gh run rerun "$RUN_ID" --repo arul28/ADE --failed

If one mac notarization step sits far beyond recent normal history, treat it as stuck instead of waiting forever. Recent normal mac notarize/staple time has been about 6-8 minutes; use 12-15 minutes as the practical cutoff unless GitHub logs show useful progress. Cancel only the stuck run, then rerun failed jobs:

gh run cancel "$RUN_ID" --repo arul28/ADE
gh run rerun "$RUN_ID" --repo arul28/ADE --failed

If GitHub cannot recover after one narrow rerun, stop and report the failing job URL/log excerpt. Do not switch to local desktop publishing unless the user explicitly authorizes a manual recovery.

Draft release verification

When the workflow succeeds, the release should still be draft/private. Verify the draft before publishing:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
104
Forks
12
Last commit
Sep 2026
Hacker News mentions
20
Advanced
Catalog kind
skill
Gateway key
release-arul28
Source
github.com/arul28/ade