CI CD Setup
rshankras/claude-code-apple-skills
Generate CI/CD configuration for automated builds, tests, and distribution of iOS/macOS apps.
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…
$ npx skills add arul28/ADE --skill release -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install arul28/ADE release --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/release .claude/skills/release && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .claude/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/arul28/ADE/tree/main/.agents/skills/releaseType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add arul28/ADE --skill release -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install arul28/ADE release --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/release .agents/skills/release && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .agents/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add arul28/ADE --skill release -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install arul28/ADE release --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/release .cursor/skills/release && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .cursor/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/arul28/ADE.git --path .agents/skills/release--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add arul28/ADE --skill release -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install arul28/ADE release --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/release .gemini/skills/release && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .gemini/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install arul28/ADE releaseInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add arul28/ADE --skill release -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/release .github/skills/release && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .github/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add arul28/ADE --skill release -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install arul28/ADE release --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/arul28/ADE.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/release .opencode/skills/release && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/arul28/ADE/tree/main/.agents/skills/release into .opencode/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
releaseADE 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…
Release is an agent skill from arul28/ADE. 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…
Its SKILL.md is about 17k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Mobile, covering App store release, Scheduled and recurring tasks and CI/CD. It works with Cloudflare, iOS, App Store Connect and GitHub Actions. The licence is AGPL-3.0.
8 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 7390d95. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
ghnpxnpmgitwranglercurljqxcodebuildnodexcrunFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
ade-account-directory-production.arulsharma1028.workers.devade-app.devade-tunnel-relay.arulsharma1028.workers.devade-github-webhook-relay.arulsharma1028.workers.devFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
DIRECTORY_AUTH_SECRETASC_KEY_IDHOMEBREW_TAP_DEPLOY_KEYCLOUDFLARE_API_TOKENFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Release loads about 17k tokens when it runs. Until then it costs about 137 tokens; SKILL.md has 6,950 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from arul28/ADE at commit 7390d95, republished under its AGPL-3.0 licence (© arul28). 6,950 words, ~17,105 tokens.
.claude/skills/release/SKILL.md (or your agent's skills folder).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:
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.
v1.2.14 -> v1.2.15.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.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.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.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.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:
ci-pass on that SHA → desktop v* tag. Tagging
before ci-pass fails verify.--draft=false. npm versions are immutable.altool --validate-app → upload → asc builds wait →
group attach.ci-pass wait (docs PR just landed)deploy-web.yml is already running for that SHA. Start
Phase 5.5 verification. Do not wait for a desktop tag.asc doctor and the App Clip preflight. Do not archive yet — a red
ci-pass would burn a build number.Start every in-scope leg:
release.yml until the draft exists. If iOS or Cloudflare
is also in scope, do not sit in gh run watch.RELEASE_SHA. TestFlight does not need the GitHub draft.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.
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.
phase=doneEvery 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.
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.ymland, when ADE_WINDOWS_PUBLIC_RELEASE_ENABLED is 1, additionally on Windows:
latest.ymlADE-<VERSION>-win-x64.exe installer referenced by that fileADE-<VERSION>-win-x64.exe.blockmapWindows 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.
Create a state file before mutating release state:
mkdir -p .ade/releaseUse a path like:
.ade/release/release-YYYYMMDD-HHMMSS.jsonTrack:
{
"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.
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.
Sync repository state:
git fetch origin --tags --prune
git status --short
git rev-parse --abbrev-ref HEADRelease from main. If not on main, switch only after confirming the
worktree is clean.
Do not proceed with uncommitted changes unless they are the release docs changes created by this skill.
Verify tools:
gh auth statusDo not block a desktop-only release on ASC auth. Run asc doctor after scope
detection if iOS is in scope.
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/ADEFor 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.latest-mac.yml..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.
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.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.
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.jsonConfirm 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.
Do this separately for desktop and iOS.
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/**apps/desktop/scripts/**.github/workflows/release*.yml, .github/workflows/update-brew-tap.yml, .github/workflows/publish-runtime-packages.ymlDo not count these as product changes by themselves:
changelog/**docs/**sdk/** (Mintlify SDK tab — handled under SDK / public docs scope)docs.jsonCHANGELOG.mdiOS 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/**Do not use desktop tags to decide iOS scope once iOS shipped tags exist.
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 / parentDeathWatchdogsdk/*.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.
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:
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.sdk-docs is also no, write state phase=done and stop.If desktop is in scope:
vMAJOR.MINOR.PATCH.PATCH.vMAJOR.MINOR.PATCH+1.Example:
v1.2.14 -> v1.2.15If iOS is in scope:
Read the current marketing version from the Xcode project or latest ASC pre-release version. Do not change it.
Ask ASC for the next build number:
asc builds next-build-number --app 6762759870 --version "$MARKETING_VERSION" --platform IOSUse that build number. Do not hand-increment from local files if ASC says a different number is next.
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>.mdxdocs.json (changelog page list)changelog/index.mdxCHANGELOG.mdAdditionally, when sdk-docs is yes (whether or not desktop is in
scope):
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.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.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.docs.json before validating.Then run:
node scripts/validate-docs.mjsCommit 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.
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.
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 --failedThe pushed tag triggers .github/workflows/release.yml, which calls
.github/workflows/release-core.yml and creates a draft GitHub Release.
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 20Choose 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 60If 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:
arm64 mac release and x64 mac release build/sign/notarize independentlybuild-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
draftupdate-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.
Do not start duplicate full release workflows.
If a job fails or is cancelled:
gh run rerun "$RUN_ID" --repo arul28/ADE --failedIf 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 --failedIf 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.
When the workflow succeeds, the release should still be draft/private. Verify the draft before publishing:
gh release view "v<VERSION>" --repo arul28/ADE --json tagName,isDraft,url,assets
rm -rf ".ade/tmp/release-v<VERSION>-verify"
mkdir -p ".ade/tmp/release-v<VERSION>-verify"
gh release download "v<VERSION>" --repo arul28/ADE \
--pattern latest-mac.yml \
--dir ".ade/tmp/release-v<VERSION>-verify" \
--clobber
cat ".ade/tmp/release-v<VERSION>-verify/latest-mac.yml"When platforms includes win, also pull the Windows updater feed:
gh release download "v<VERSION>" --repo arul28/ADE \
--pattern latest.yml \
--dir ".ade/tmp/release-v<VERSION>-verify" \
--clobber
cat ".ade/tmp/release-v<VERSION>-verify/latest.yml"Required assets, always:
ADE-<VERSION>-arm64.dmgADE-<VERSION>-arm64.zipADE-<VERSION>-x64.dmgADE-<VERSION>-x64.ziplatest-mac.ymlinstall.shSHA256SUMSade-darwin-arm64, ade-darwin-x64, ade-linux-arm64, ade-linux-x64, and
the matching .native.tar.gz for eachRequired additionally when platforms includes win:
ADE-<VERSION>-win-x64.exeADE-<VERSION>-win-x64.exe.blockmaplatest.ymlinstall.ps1ade-win32-x64.exeade-win32-x64.native.tar.gzGate/asset agreement is a hard check, in both directions:
WINDOWS_GATE="$(gh variable get ADE_WINDOWS_PUBLIC_RELEASE_ENABLED --repo arul28/ADE 2>/dev/null || echo unset)"
WINDOWS_ASSETS="$(gh release view "v<VERSION>" --repo arul28/ADE --json assets \
--jq '[.assets[].name | select(test("win-x64|win32-x64|^latest\\.yml$|^install\\.ps1$"))] | length')"
echo "gate=$WINDOWS_GATE windows_assets=$WINDOWS_ASSETS"gate=1 and windows_assets=0 means the Windows build silently did not
contribute. Stop; keep the release draft/private.gate not 1 and windows_assets greater than 0 means Windows assets
reached a release that was not supposed to carry them. Stop; keep the release
draft/private.Also verify:
latest-mac.yml references the uploaded arm64 and x64 ZIPs.latest-mac.yml does not reference universal.latest-mac.yml referenced ZIP exists in the release assets.latest.yml references the uploaded
ADE-<VERSION>-win-x64.exe, and that installer and its .blockmap both
exist in the release assets.SHA256SUMS lists every published standalone runtime asset, including the
ade-win32-x64 entries when Windows is in scope, and lists nothing that is
not published.Only after verification passes:
gh release edit "v<VERSION>" --repo arul28/ADE --draft=false --latest
gh api repos/arul28/ADE/releases/latest \
--jq '{tag_name,draft,prerelease,html_url,asset_count:(.assets|length)}'Undrafting publishes the GitHub release. That event starts
publish-runtime-packages.yml and update-brew-tap.yml. Poll both. npm
versions are immutable, which is why this waits for --draft=false instead of
the tag push. Desktop is not done until the npm job succeeds and
npm view @ade-dev/runtime version equals this tag.
Do not exclusive-watch either run if iOS processing or Cloudflare is still
in flight. Keep the run IDs and poll them between those legs. gh run watch
belongs only to a desktop-only release, or to the join after the other legs
have finished.
set -euo pipefail
# The release event can take a few seconds to enqueue the runs.
find_run() {
local workflow="$1"
gh run list --repo arul28/ADE --workflow "$workflow" --event release \
--json databaseId,headBranch,status,conclusion,url \
--jq "[.[] | select(.headBranch==\"v<VERSION>\")][0].databaseId"
}
run_id_ready() {
[ -n "${1:-}" ] && [ "$1" != "null" ]
}
BREW_EXPECTED=0
if [ "$(gh secret list --repo arul28/ADE --json name --jq 'any(.[]; .name == "HOMEBREW_TAP_DEPLOY_KEY")')" = "true" ]; then
BREW_EXPECTED=1
fi
for _ in 1 2 3 4 5 6; do
NPM_RUN_ID=$(find_run publish-runtime-packages.yml)
BREW_RUN_ID=$(find_run update-brew-tap.yml)
if run_id_ready "$NPM_RUN_ID"; then
if [ "$BREW_EXPECTED" != "1" ] || run_id_ready "$BREW_RUN_ID"; then
break
fi
fi
sleep 10
done
if ! run_id_ready "${NPM_RUN_ID:-}"; then
echo "Publish ADE runtime packages did not start for v<VERSION>"
exit 1
fi
if [ "$BREW_EXPECTED" = "1" ] && ! run_id_ready "${BREW_RUN_ID:-}"; then
echo "update-brew-tap did not start for v<VERSION>"
exit 1
fiWhile iOS or Cloudflare is still in flight, poll — do not wait yet:
gh run view "$NPM_RUN_ID" --repo arul28/ADE --json status,conclusion,url
if [ -n "${BREW_RUN_ID:-}" ] && [ "$BREW_RUN_ID" != "null" ]; then
gh run view "$BREW_RUN_ID" --repo arul28/ADE --json status,conclusion,url
fiDesktop-only, or joining after the other legs finished. Watch both in parallel so neither wait starts only after the other has already completed:
gh run watch "$NPM_RUN_ID" --repo arul28/ADE --interval 30 --exit-status &
NPM_WATCH=$!
if [ -n "${BREW_RUN_ID:-}" ] && [ "$BREW_RUN_ID" != "null" ]; then
gh run watch "$BREW_RUN_ID" --repo arul28/ADE --interval 30 --exit-status &
BREW_WATCH=$!
fi
wait "$NPM_WATCH"
[ -n "${BREW_WATCH:-}" ] && wait "$BREW_WATCH"
gh run view "$NPM_RUN_ID" --repo arul28/ADE --json conclusion,url --exit-status
if [ -n "${BREW_RUN_ID:-}" ] && [ "$BREW_RUN_ID" != "null" ]; then
gh run view "$BREW_RUN_ID" --repo arul28/ADE --json conclusion,url --exit-status
fi
RUNTIME_VERSION=$(npm view @ade-dev/runtime version)
DARWIN_VERSION=$(npm view @ade-dev/runtime-darwin-arm64 version)
if [ "$RUNTIME_VERSION" != "<VERSION>" ] || [ "$DARWIN_VERSION" != "<VERSION>" ]; then
echo "expected @ade-dev/runtime@<VERSION> and @ade-dev/runtime-darwin-arm64@<VERSION>, got $RUNTIME_VERSION / $DARWIN_VERSION"
exit 1
fiIf the npm run fails because Trusted Publisher / RUNTIME_TRUSTED_PUBLISHING is
not configured, that is a release blocker, not a skip. workflow_dispatch on
the same workflow (tag + confirm publish) is recovery only.
A missing brew run is not a skip when HOMEBREW_TAP_DEPLOY_KEY is configured;
treat a failed tap bump as a desktop-leg failure.
Do this only if iOS scope is yes. Start it after the shared ci-pass +
tag gate (or immediately on an iOS-only release). Do not wait for the
desktop draft, undraft, npm publish, or brew tap — see Parallel schedule.
Preflight:
asc doctor
asc testflight groups list --app 6762759870 --paginateNormal build rule:
MARKETING_VERSION.Recommended explicit sequence:
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)
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" \
--xcodebuild-flag=-authenticationKeyIssuerID --xcodebuild-flag="$ASC_ISSUER_ID" \
--xcodebuild-flag=CURRENT_PROJECT_VERSION=$BUILD_NUMBER \
--xcodebuild-flag=MARKETING_VERSION=$MARKETING_VERSION
asc xcode export \
--archive-path "$OUT/ADE.xcarchive" \
--export-options apps/ios/ExportOptions.auto.plist \
--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"Before upload, unpack and inspect the exported IPA. This is mandatory for App Clip releases:
TMP_IPA_CHECK="$OUT/ipa-check"
rm -rf "$TMP_IPA_CHECK"
mkdir -p "$TMP_IPA_CHECK"
ditto -x -k "$OUT/ADE.ipa" "$TMP_IPA_CHECK"
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$TMP_IPA_CHECK/Payload/ADE.app/Info.plist"
/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$TMP_IPA_CHECK/Payload/ADE.app/Info.plist"
/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$TMP_IPA_CHECK/Payload/ADE.app/Info.plist"
find "$TMP_IPA_CHECK/Payload/ADE.app" -maxdepth 3 -name 'ADEClip.app' -print
find "$TMP_IPA_CHECK/Payload/ADE.app" -maxdepth 3 -name 'ADEWidgets.appex' -printBefore continuing, inspect ADEClip.app/Info.plist too. Continue only if the
main app, widgets, and App Clip all use the intended marketing version/build
number and the App Clip bundle has the expected icon/deployment metadata.
Use Apple package validation before upload so metadata errors surface before the final upload step:
xcrun altool --validate-app --type ios --file "$OUT/ADE.ipa" \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"Upload only after validation passes:
asc builds upload --app 6762759870 --ipa "$OUT/ADE.ipa"
asc builds wait \
--app 6762759870 \
--build-number "$BUILD_NUMBER" \
--version "$MARKETING_VERSION" \
--platform IOS \
--timeout 40mWhen desktop is also in scope, do not park the whole conductor on this wait
if the IPA is already uploaded. Poll asc builds list until
processingState=VALID while undraft, npm, and brew proceed. asc builds wait
is the iOS-only happy path.
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.
After processing:
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)
asc builds update --build-id "$BUILD_ID" --uses-non-exempt-encryption=falseAttach all non-empty beta groups:
asc testflight groups list --app 6762759870 --paginate
asc builds add-groups --build-id "$BUILD_ID" --group "<group-id>" --submit --confirmUse --submit --confirm for external groups. Internal groups may also be added
explicitly if they do not automatically receive the build.
Verify every group:
asc builds info --build-id "$BUILD_ID"
asc builds build-beta-detail view --build-id "$BUILD_ID"
for gid in <all-group-ids>; do
asc testflight groups links view --group-id "$gid" --type betaTesters
asc testflight groups links view --group-id "$gid" --type builds
doneSuccess requires:
processingState=VALIDusesNonExemptEncryption=falseinternalBuildState is READY_FOR_BETA_TESTING or IN_BETA_TESTINGIN_BETA_TESTING or otherwise clearly submitted/approvedAfter successful distribution, tag the shipped iOS build:
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}"The GitHub desktop workflow and TestFlight do NOT touch the hosted web tier. Every release reconciles it, or production silently drifts (this bit v1.2.28 and v1.2.29: a rewritten web client and two changed Workers sat undeployed while the desktop shipped).
Start this phase as soon as the docs PR (or any in-scope main push) lands.
deploy-web.yml runs on that push. Do not wait for the desktop tag, the
draft, npm, or TestFlight — see Parallel schedule.
A Cloudflare surface has two halves, and only one of them is in git. The code
half is wrangler.jsonc plus src/. The account half — whether the R2 bucket
exists, whether it has a lifecycle rule, whether a D1 migration was actually
applied, whether a secret is bound — lives in the Cloudflare account and in no
repository file. No test in this repo can fail on it. Phase 5.5 exists to check
the account half.
npm run deploy /
npm run deploy:production for each app. They own the D1 migrations, the
binding/secret preflights, and the post-deploy auth smokes. npx wrangler deploy
is a bypass, not a workaround. If an entry point stops on a preflight, a missing
migration, or a missing smoke credential, repair that blocker and rerun the entry
point./health green is not verification. Know what each endpoint actually
proves. ade-account-directory (apps/account-directory/src/directory.ts:1047)
and ade-github-webhook-relay (apps/webhook-relay/src/relay.ts:2701) return a
bare {"ok":true} from a handler that runs before any binding, migration, or
secret is touched — those prove reachability and nothing else.
ade-tunnel-relay reports its protocol version and deployed Worker version tag;
ade-push-relay (apps/push-relay/src/relay.ts:1333) reports whether APNs and
each Clerk issuer are configured — substantive, but still blind to D1 migration
state. No /health in this repo checks migrations. Green /health with
500s on every authenticated route is a state this repo has actually shipped.status: "blocked" and a note. Never silently skip a surface
and report the release as complete.main, never from a lane..github/workflows/deploy-web.yml already deploys these surfaces on push to
main, path-filtered per surface. The normal case is therefore verification,
not deployment: find the Deploy Web Surfaces run for the release SHA and
confirm each in-scope job succeeded. Deploy by hand only when that run failed,
was skipped, or the release commit predates it.
The Cloudflare baseline is not the desktop tag. This tier ships on merge, on
its own cadence; a desktop tag says nothing about what is live on Cloudflare. If
a web change landed before the last desktop tag and its deploy-web.yml job
failed or never ran, a LAST_TAG..origin/main diff reports that surface
unchanged forever. Derive each surface's baseline from what actually deployed —
the newest run whose job for that surface concluded success. Job names are
webclient, account-directory, webhook-relay, tunnel-relay, push-relay;
a skipped job is not a success and does not appear in jobs at all.
runs="$(gh run list --repo arul28/ADE --workflow deploy-web.yml --branch main \
--json databaseId --limit 20 --jq '.[].databaseId')"
for surface in webclient account-directory webhook-relay tunnel-relay push-relay; do
base=""
for id in $runs; do
sha="$(gh run view "$id" --repo arul28/ADE --json headSha,jobs \
--jq "if any(.jobs[]; .name==\"$surface\" and .conclusion==\"success\") then .headSha else empty end")"
if [ -n "$sha" ]; then base="$sha"; break; fi
done
echo "$surface baseline=${base:-UNKNOWN}"
doneUNKNOWN means nothing in the last 20 runs proves that surface is current.
Treat it as changed and reconcile it fully; never treat an unknown baseline
as "unchanged". Run this in bash — zsh does not word-split $runs.
Then diff each surface against its own baseline ($base from above), using
deploy-web.yml's paths-filter verbatim:
git log <base>..origin/main --oneline -- apps/account-directory # account-directory
git log <base>..origin/main --oneline -- apps/webhook-relay # webhook-relay
git log <base>..origin/main --oneline -- apps/tunnel-relay # tunnel-relay
git log <base>..origin/main --oneline -- apps/push-relay # push-relay
git log <base>..origin/main --oneline -- \
apps/desktop/src/renderer apps/desktop/src/shared apps/desktop/vite.webclient.config.ts # webclientThe desktop-tag diff (git log $LAST_TAG..origin/main -- apps/...) is a
secondary signal only — useful for the release note, never for the deploy
decision.
Now confirm the run for the release commit itself. Resolve RUN_ID from the
release SHA and fail loudly when there is no match — no matching run means CI
never deployed this commit, which is a manual-deploy case, not a pass:
RELEASE_SHA="$(git rev-parse origin/main)"
RUN_ID="$(gh run list --repo arul28/ADE --workflow deploy-web.yml \
--json databaseId,headSha --limit 50 \
--jq "[.[] | select(.headSha == \"$RELEASE_SHA\")] | .[0].databaseId // empty")"
[ -n "$RUN_ID" ] || echo "no deploy-web.yml run for $RELEASE_SHA — deploy by hand"
[ -n "$RUN_ID" ] && gh run view "$RUN_ID" --repo arul28/ADE --json status,conclusion,jobsA workflow_dispatch run deploys every surface — use it as the manual full
reconcile when the automatic run is untrustworthy:
gh workflow run deploy-web.yml --repo arul28/ADEAny surface whose baseline came back UNKNOWN goes through that dispatch.
Record each surface's baseline SHA, changed, and action (skip / ci /
manual) in the state file before doing anything else.
This table is derived from the committed wrangler configs. Re-read them if a release changes one; do not trust this table over the file.
| Surface | Config | Bindings and account resources | Deploy entry point |
|---|---|---|---|
ade-web-client (Pages) | none in repo; built by apps/desktop/vite.webclient.config.ts | custom domain app.ade-app.dev, Pages URL ade-web-client.pages.dev | npx wrangler@4.105.0 pages deploy apps/desktop/dist/web-client --project-name ade-web-client |
ade-account-directory / ade-account-directory-production | apps/account-directory/wrangler.jsonc | D1 DB → ade-account-directory (215bebd4-6601-4705-ab6e-e6f2d1397156) / ade-account-directory-production (38ebe0bb-ac4d-4b39-b73e-bab2f0092971), migrations_dir: migrations (0001–0012); R2 DIAGNOSTICS → ade-diagnostics / ade-diagnostics-production; vars ONLINE_WINDOW_MS, WEB_CLIENT_ORIGIN, PUSH_RELAY_URL, DIAGNOSTICS_DAILY_GLOBAL_LIMIT, USAGE_RESEARCH_DAILY_GLOBAL_LIMIT, USAGE_RESEARCH_RETENTION_DAYS, USAGE_RESEARCH_STORAGE_CEILING_MB; secrets DIRECTORY_AUTH_SECRET, CLERK_JWKS_URL, CLERK_ISSUER, CLERK_OAUTH_CLIENT_ID; cron * * * * *; observability on | npm run deploy:production |
ade-github-webhook-relay | apps/webhook-relay/wrangler.jsonc | D1 DB → ade-github-relay (65e81b4d-2894-444f-9546-390815533b3b), migrations_dir: migrations (0001–0007); Durable Object REPO_EVENTS → RepoEventsDurableObject, migration tag v1 (new_sqlite_classes); no vars, no secrets in config | npm run deploy |
ade-tunnel-relay | apps/tunnel-relay/wrangler.jsonc | Durable Object TUNNEL → TunnelDurableObject, migration tag v1 (new_sqlite_classes); version_metadata binding CF_VERSION_METADATA; no D1, no R2, no vars, no secrets; observability on | npm run deploy |
ade-push-relay | apps/push-relay/wrangler.jsonc | D1 DB → ade-push-relay (1fab2e8a-b269-4618-9402-7a49f9651f26), migrations_dir: migrations (0001–0007) plus schema/attention_triggers.sql applied by d1:triggers:remote; vars DAILY_REQUEST_BUDGET, IP_RATE_LIMIT_PER_MIN, CLAIM_RATE_LIMIT_PER_MIN, WEB_CLIENT_ORIGIN; secrets DIRECTORY_AUTH_SECRET, CLERK_JWKS_URL, CLERK_ISSUER, CLERK_OAUTH_CLIENT_ID, CLERK_SECONDARY_JWKS_URL, CLERK_SECONDARY_ISSUER, CLERK_SECONDARY_OAUTH_CLIENT_ID (apps/push-relay/scripts/verify-deployment-auth.mjs); cron 17 * * * *; observability on | npm run deploy locally, npm run deploy:ci in CI (mints Clerk smoke tokens) |
Two environment facts that are easy to get wrong:
ade-account-directory has a second wrangler environment. The release
ships the production environment (--env production). ade-account-directory
without --env is the development Worker and is not a release surface.vars or secrets. Every var is
restated under env.production in apps/account-directory/wrangler.jsonc for
exactly this reason, and every secret must be put a second time with
--env production. An omission here does not error — it silently falls back to
the code default.Surfaces this repo does not use today. No wrangler config in apps/ declares
KV namespaces, Queues, service bindings, Hyperdrive, Vectorize, Analytics Engine,
routes, or custom_domain. Do not run checks for them and do not invent
resource names. If a wrangler config ever declares one, this checklist applies to
it unchanged: reconcile the declared name against the account listing before
deploying, and verify it post-deploy.
Run this before any deploy. Every command runs inside an app directory whose
node_modules is installed, because that is the only thing that pins the
version: bare npx wrangler from the repo root has no wrangler dependency to
resolve and silently fetches whatever is newest on npm. Install first, then keep
every invocation wrapped:
(cd apps/account-directory && npm ci)
(cd apps/account-directory && npx wrangler --version) # expect 4.105.0
(cd apps/account-directory && npx wrangler whoami) # CLOUDFLARE_ACCOUNT_ID → right accountPinned versions: 4.105.0 in apps/account-directory, 4.112.0 in
apps/tunnel-relay, ^4.53.0 in the other two. Account-level reads (R2, D1,
whoami) are account-scoped, not app-scoped — run them from
apps/account-directory so the exact pin is the one talking to the account.
Every subcommand below is verified against wrangler 4.105.0.
R2 — buckets must exist before the deploy that first binds them:
(cd apps/account-directory && npx wrangler r2 bucket list)
(cd apps/account-directory && npx wrangler r2 bucket info ade-diagnostics --json)
(cd apps/account-directory && npx wrangler r2 bucket info ade-diagnostics-production --json)D1 — databases must exist, and IDs must match the config:
(cd apps/account-directory && npx wrangler d1 list --json)
(cd apps/account-directory && npx wrangler d1 info ade-account-directory-production --json)
(cd apps/account-directory && npx wrangler d1 info ade-github-relay --json)
(cd apps/account-directory && npx wrangler d1 info ade-push-relay --json)Secrets — names only; wrangler never prints values, so this is safe to run and safe to paste:
(cd apps/account-directory && npx wrangler secret list --env production --format json)
(cd apps/push-relay && npx wrangler secret list --format json)Current deployed state, so you know what you are replacing and what to roll back to:
(cd apps/account-directory && npx wrangler deployments list --env production)
(cd apps/account-directory && npx wrangler versions list --env production)Not-used surfaces, only if a config ever declares one:
npx wrangler kv namespace list, npx wrangler queues list.
Any declared binding whose resource is missing from the account listing is a release blocker. Create the resource, or ship the Worker with the binding removed if the code degrades gracefully — but never deploy a config that binds a resource which does not exist.
This bit us on 2026-08-19.
apps/account-directorygained anr2_bucketsbinding for diagnostics report storage, but R2 was not enabled on the Cloudflare account, so creating either bucket failed withPlease enable R2 through the Cloudflare Dashboard [code: 10042]— an account-level product enablement, not a token scope; the same token listed Workers, read D1, and deployed Pages. A Worker bound to a bucket that cannot exist fails to start, so deploying the config as written would have taken down machine registration, pairing, and heartbeat for every user in order to deliver a route nobody could reach. The recovery was to ship the Worker withr2_bucketscommented out (#1126) — the code types the binding optional and answers503on/diagnostics/uploadwithout it — then enable the subscription, create both buckets, and revert (#1127). Every test in the repo passed the whole time. Only a bindings-vs-account diff catches this.
These settings exist only in the Cloudflare account. A freshly created resource does not have them, and nothing in the repo will tell you they are missing. Verify each one explicitly; do not assume.
R2 bucket lifecycle rules. Nothing in the Worker ever deletes a diagnostics
report (apps/account-directory/src/diagnostics.ts). The 30-day expiry is the
third term of the cost ceiling — 400 uploads/day × 512 KB × 30 days ≈ 6 GB,
inside R2's 10 GB free tier — and it is the one term the repository cannot
enforce in code.
(cd apps/account-directory && npx wrangler r2 bucket lifecycle list ade-diagnostics)
(cd apps/account-directory && npx wrangler r2 bucket lifecycle list ade-diagnostics-production)Both buckets must show a rule expiring the reports/ prefix after 30 days. The
rules in the account were created as expire-reports-30d (#1127); the README
worked example at apps/account-directory/README.md uses the name
expire-reports. Match on the effect — prefix reports/, 30-day expiry — not on
the name. If a rule is missing:
(cd apps/account-directory && npx wrangler r2 bucket lifecycle add ade-diagnostics \
expire-reports-30d reports/ --expire-days 30)
(cd apps/account-directory && npx wrangler r2 bucket lifecycle add ade-diagnostics-production \
expire-reports-30d reports/ --expire-days 30)Lengthening the window moves the ceiling with it — 90 days is roughly 18 GB and off the free tier. Changing it is a deliberate act with arithmetic attached.
This bit us on 2026-08-19. The diagnostics buckets were created without lifecycle rules, because bucket creation and lifecycle configuration are two separate account operations and only the first one is mentioned by the binding. With clients auto-sending reports on failure and nothing in the Worker deleting them, the bucket grows forever and every report a user ever sent stays readable indefinitely. The rules were added out of band and the restore landed as #1127.
R2 public access posture. The diagnostics buckets hold user-submitted
failure reports and must stay private — no r2.dev public dev URL, no public
custom domain. r2 bucket info does not report this; two separate commands
do, and both must answer negatively for both buckets:
(cd apps/account-directory && npx wrangler r2 bucket dev-url get ade-diagnostics)
(cd apps/account-directory && npx wrangler r2 bucket domain list ade-diagnostics)
(cd apps/account-directory && npx wrangler r2 bucket dev-url get ade-diagnostics-production)
(cd apps/account-directory && npx wrangler r2 bucket domain list ade-diagnostics-production)Both buckets, all four commands. The development bucket takes real reports from
anyone running a development build, so "it is only dev" is not a reason to skip
it. Expect Public access via the r2.dev URL is disabled. and
There are no custom domains connected to this bucket. Anything else means
user-submitted failure reports are world-readable, and that is a release
blocker, not a note.
Subscription enablement. R2 was the case that bit us, but the class is general: a product that is not enabled on the account makes every resource of that type uncreatable, and the error is an account error, not a permissions error. If a resource cannot be created and the token works for everything else, check product enablement in the dashboard before debugging the token.
Cron triggers. ade-account-directory declares * * * * * and
ade-push-relay declares 17 * * * *. These deploy with the Worker; confirm the
deploy output lists them, since a Worker whose scheduled handler silently stopped
running looks perfectly healthy on every request path.
Only when the CI run did not cover a surface. Install locked dependencies first;
run from the release commit on main.
# Hosted web client (Cloudflare Pages project ade-web-client)
# Pinned: the repo root has no wrangler dependency, so an unpinned npx here
# resolves to whatever npm publishes that day.
(cd apps/desktop && npm ci && npm run build:webclient)
npx wrangler@4.105.0 pages deploy apps/desktop/dist/web-client \
--project-name ade-web-client --branch main \
--commit-hash "$(git rev-parse origin/main)" --commit-dirty=false
# Workers
(cd apps/account-directory && npm ci && npm run deploy:production)
(cd apps/webhook-relay && npm ci && npm run deploy)
(cd apps/tunnel-relay && npm ci && npm run deploy -- --tag "$(git rev-parse origin/main)" --message "release $(git rev-parse --short origin/main)")
(cd apps/push-relay && npm ci && npm run deploy)deploy-web.yml still runs the Pages step as floating npx wrangler@4
(.github/workflows/deploy-web.yml:77), so CI and a hand deploy can be on
different 4.x minors. Known and accepted for Pages, which uploads static assets;
if a Pages deploy ever behaves differently by hand than in CI, check that first.
What each entry point actually guards, so you know what you lose by bypassing it:
apps/account-directory deploy:production = verify-deployment-config.mjs production
(asserts DIRECTORY_AUTH_SECRET is bound and PUSH_RELAY_URL is set for that
environment) → d1:migrate:production → wrangler deploy --env production.
Known coverage gap: that preflight checks one of the four secrets and one
of the four vars. CLERK_JWKS_URL, CLERK_ISSUER, CLERK_OAUTH_CLIENT_ID,
ONLINE_WINDOW_MS, WEB_CLIENT_ORIGIN, and
DIAGNOSTICS_DAILY_GLOBAL_LIMIT are unchecked by any script and unchecked by
/health, which answers {"ok":true} before touching config. Confirm them
by hand from Step 3's secret list and the deployed env.production vars,
and list anything you could not confirm in accountState.config[...].unverified
rather than reporting the surface verified.apps/push-relay deploy = validate:migrations → verify:auth-preflight
(all seven required secrets) → d1:migrate:remote (migrations and
attention_triggers.sql) → wrangler deploy → verify:auth-health →
verify:auth-account (a real authenticated snapshot fetch per Clerk issuer).apps/webhook-relay deploy = d1:migrate:remote → wrangler deploy.apps/tunnel-relay deploy is a bare wrangler deploy — it has no D1, no
secrets, and nothing to preflight. Still invoke it through npm run deploy so
the --tag version metadata that /health reports is carried through.The --tag on tunnel-relay is load-bearing: CF_VERSION_METADATA is what makes
/health able to prove which code is live rather than merely that something is.
Green /health is the floor, not the check. Verify per surface:
ade-web-client — the live bundle hash must equal the one you just built:
built="$(basename apps/desktop/dist/web-client/assets/index-*.js)"
live="$(curl -fsS https://app.ade-app.dev | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1)"
echo "built=$built live=$live"
npx wrangler pages deployment list --project-name ade-web-client --environment productionPages propagation is not instant; poll for a couple of minutes before calling it
a failure, the way deploy-web.yml does.
ade-account-directory-production — migrations, then the diagnostics route:
curl -fsS https://ade-account-directory-production.arulsharma1028.workers.dev/health
(cd apps/account-directory && npx wrangler d1 migrations list DB --env production --remote)migrations list prints unapplied migrations. Post-deploy it must report
none pending; 0009_diagnostics_upload_budget.sql in particular is what enforces
the fleet-wide daily upload ceiling, so a green Worker with 0009 unapplied is a
Worker whose cost ceiling does not exist.
Then confirm the R2 binding is actually live. An empty unauthenticated POST distinguishes the two states without sending or printing any credential:
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
https://ade-account-directory-production.arulsharma1028.workers.dev/diagnostics/upload400 (missing report) means the DIAGNOSTICS bucket is bound and reachable.
503 means it is not — the exact degradation #1126 shipped deliberately, and a
silent regression if you did not intend it. Also confirm
DIAGNOSTICS_DAILY_GLOBAL_LIMIT is present in the deployed env.production
vars; unset falls back to the code default rather than erroring, so its absence
is invisible at runtime.
ade-push-relay — the guarded deploy already ran verify:auth-health and
verify:auth-account, which fetch /health and assert
accountAuthConfigured / primaryAccountAuthConfigured /
secondaryAccountAuthConfigured are all true, then perform a real authenticated
GET /attention/account/snapshot?since=0 per Clerk issuer. If you deployed by
any other route, run them explicitly:
(cd apps/push-relay && npm run verify:auth-bindings && npm run verify:auth-health && npm run verify:auth-account)
(cd apps/push-relay && npx wrangler d1 migrations list ade-push-relay --remote)migrations list only reports unapplied files in migrations/. It is blind to
schema/attention_triggers.sql, which d1:migrate:remote applies as a sidecar
and which nothing else verifies. Ask the database directly — read-only, safe to
run any time:
(cd apps/push-relay && npx wrangler d1 execute ade-push-relay --remote --json \
--command "SELECT name FROM sqlite_master WHERE type='trigger' ORDER BY name")Both attention_device_ownership_reject_stale and
attention_devices_enforce_user_limit must come back. They are the per-user
device cap and the stale-ownership rejection; missing, the Worker looks healthy
and enforces neither. Put the returned names in the push-relay report line.
verify:auth-account needs the Clerk smoke credentials in the environment. If
they are absent, say so in the release report — an unverified authenticated path
is not a verified one.
ade-tunnel-relay — /health here is genuinely substantive; assert the
protocol version and that workerVersion.tag equals the release SHA:
curl -fsS https://ade-tunnel-relay.arulsharma1028.workers.dev/healthExpect ok: true, service: "ade-tunnel-relay", protocolVersion: 2, and
workerVersion.tag matching git rev-parse origin/main.
ade-github-webhook-relay — /health returns a bare {"ok":true} and
proves nothing beyond reachability. Verify the D1 and Durable Object halves
directly:
curl -fsS https://ade-github-webhook-relay.arulsharma1028.workers.dev/health
(cd apps/webhook-relay && npx wrangler d1 migrations list ade-github-relay --remote)Durable Objects, both DO Workers. REPO_EVENTS/RepoEventsDurableObject and
TUNNEL/TunnelDurableObject are each declared under migration tag v1 with
new_sqlite_classes. Wrangler applies DO migrations at deploy time — read the
deploy output for migration errors rather than assuming success. A renamed or
deleted DO class needs a new migration tag in the config; changing the class
name under the existing v1 tag is how you lose a Durable Object's stored state.
This bit us on 2026-08-06. A direct Wrangler deployment published new account-directory code while production D1 migrations
0004and0005were still pending andDIRECTORY_AUTH_SECRETwas not bound. Authenticated machine list/register requests returned HTTP 500 across every device while/healthstayed green. The recovery was to restore the shared secret, apply the pending migrations, and rerun the guarded production deploy. The same shape recurred on 2026-08-19 with the diagnostics work:0009andDIAGNOSTICS_DAILY_GLOBAL_LIMITare what enforce the fleet-wide spend ceiling, so code deployed ahead of either one is code whose cost ceiling silently does not exist.
A Worker deploy that verifies badly is rolled back to the previous version; it does not sit broken while you debug. Rollback is an attempt, not a guarantee — Cloudflare refuses it when the target version binds a resource that no longer exists, or when a Durable Object class lifecycle changed between the two versions. Try it, and if it is refused, fix forward.
(cd apps/account-directory && npx wrangler versions list --env production)
(cd apps/account-directory && npx wrangler rollback <version-id> --env production --message "release <VERSION> verification failed")wrangler rollback with no version-id targets the previous deployment. Two
things it does not do:
Fix forward when rollback is unavailable. If the previous version cannot
tolerate the new schema, if the rollback is refused, or if the DO class changed:
revert the offending commit on main, let deploy-web.yml redeploy, and verify
with Step 6 — same guarded entry points, same checks. Do not hand-patch
production with raw wrangler deploy to escape a failed rollback, and do not
hand-write a "down" migration for D1; migrations here are forward-only. Record
the fix-forward commit in cloudflare.rollbacks the same way you would a
version id.
Pages has no rollback subcommand — wrangler pages deployment is
list/create/tail/delete only (verified on 4.105.0). Listing tells you which
deployment to go back to; it does not restore it:
npx wrangler@4.105.0 pages deployment list --project-name ade-web-client --environment productionRecord the target deployment id, then restore it from the Cloudflare dashboard
(Pages → ade-web-client → Deployments → Rollback to this deployment), or
rebuild the previous good commit and pages deploy it. Either way the id goes
in the report — "rolled back Pages" without one is not a record of anything.
Record every rollback in the state file's cloudflare.rollbacks.
Blocker discipline. If a surface cannot be deployed or cannot be verified,
set status: "blocked" with a note naming the surface, the failing check, and
the recovery command. Do not publish a release report that omits it, and do not
downgrade a blocked surface to "skipped". A guarded-deploy failure is a release
blocker, never a reason to fall back to raw Wrangler.
Credentials: CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID from ADE secrets.
Never echo their values; wrangler whoami and wrangler secret list are the
safe ways to prove they are right.
Desktop:
gh run rerun --failed.publish-runtime-packages.yml with the same tag and confirm publish.
Skip-if-exists means packages that already landed are left alone. Do not
unpublish. Do not invent a new version.latest-mac.yml references a missing asset, keep the release draft/private
until fixed.latest-mac.yml references a universal ZIP, keep the release draft/private
and fix the GitHub workflow. Do not publish the release.latest.yml is missing, or references an installer that is not in the
release assets, keep the release draft/private. Windows in-app update reads
that file; a broken feed strands installed Windows users.ADE_WINDOWS_PUBLIC_RELEASE_ENABLED to force a macOS-only draft under a
version that was announced as carrying Windows.iOS:
asc builds wait.BUILD_ID; do not upload another build
unless the binary itself is wrong.Report:
mac or mac,win) and the
ADE_WINDOWS_PUBLIC_RELEASE_ENABLED value it came fromlatest-mac.yml references only present assetslatest.yml references only present assets
and whether the gate and the published Windows assets agreedade-web-client,
ade-account-directory-production, ade-github-webhook-relay,
ade-tunnel-relay, ade-push-relay), each stating:UNKNOWN if none was resolvable/health
was green: live bundle hash for Pages, d1 migrations list showing nothing
pending for each D1-backed Worker, both sqlite_master triggers present for
push-relay, /diagnostics/upload returning 400 for the account directory's
R2 binding, workerVersion.tag matching the release SHA for tunnel-relay,
and the auth smokes for push-relaycloudflare.accountState, per resource and
environment — not one aggregate sentence:reports/ lifecycle rule, r2.dev
dev URL disabled, zero custom domainsade-account-directory-production
(* * * * *) and ade-push-relay (17 * * * *)ade-github-webhook-relay and ade-tunnel-relayKeep the report short, but include exact version/build numbers.
© arul28, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .agents/skills/release of arul28/ADE.
Open the folder on GitHubat commit 7390d95
Release next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Release this skillarul28/ADE | 113 | — | ~17k | Automated safety check: Pass | AGPL-3.0 | |
| CI CD Setuprshankras/claude-code-apple-skills | 781 | — | ~1.5k | Automated safety check: Notes | MIT | |
| Releasevaayne/mori | 303 | — | ~1.2k | Automated safety check: Pass | MIT | |
| App Store Deploymakifbaysal/tasktrooper | 109 | — | ~1.6k | Automated safety check: Pass | Apache-2.0 | |
| Releasemovieclaw/MovieClaw | 132 | — | ~2.3k | Automated safety check: Pass | Custom licence | |
| Wjs Auditing Projectjianshuo/claude-skills | 130 | — | ~2.5k | Automated safety check: Pass | MIT |
rshankras/claude-code-apple-skills
Generate CI/CD configuration for automated builds, tests, and distribution of iOS/macOS apps.
vaayne/mori
Release workflow for Mori macOS workspace terminal and MoriRemote iOS app.
makifbaysal/tasktrooper
A skill your agent uses when a mobile app needs a release pipeline - fastlane GitHub Actions to TestFlight (iOS) and Play internal track (Android), with code signing and the release naming that maps…
movieclaw/MovieClaw
发布 movieclaw 新版本。当用户要求发版、发布新版本、打 tag、发布 NER 模型、发布 Docker 镜像,或打包上传 iOS App 到 TestFlight / App Store、补传发版附件(IPA、Mac 转码器、Mac 版 App)时使用。涵盖版本号三处同步、应用/模型/镜像/iOS 的完整流程、可选附件失败补救与检查清单。
jianshuo/claude-skills
A skill your agent uses when the user asks to audit what's wrong with a project, "make it right", "看看项目出了什么问题", "为什么用户的需求还没上线", "为什么没提交App Store", "为什么没新build", or wants a holistic…
FerroxLabs/wayland
Expert mobile continuous integration and delivery covering Fastlane automation, code signing management, TestFlight and Google Play deployment pipelines, App Center distribution, automated testing…
arul28/ADE
A skill your agent uses when you need to run or drive a local Electron/desktop app and capture what it does — launch it or attach to a running renderer, read its logs or answer its terminal prompts…
arul28/ADE
Iteratively optimize an ADE tab's CPU/memory/IPC/render performance.
arul28/ADE
A skill your agent uses for any browser behavior at all — opening a URL, checking a localhost page, clicking or filling a form, logging in, screenshotting, inspecting the DOM, or verifying a page…
arul28/ADE
A skill your agent uses when an agent needs to mint, share, or open ADE deeplinks (lane, work session, file, commit, artifact, branch, PR, Linear issue) so users — or the agent itself — can jump…
arul28/ADE
A skill your agent uses when you need to run a chat, a CLI session, or a subagent on a specific setup — any model you pay for inside any harness (e.g.
arul28/ADE
A skill your agent uses when creating, inspecting, syncing, committing, pushing, archiving, or rebasing ADE lanes and lane worktrees through ade lanes and ade git.
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…. Release is an agent skill from arul28/ADE.
Release fits situations like: tasks that involve App store release; tasks that involve Scheduled and recurring tasks; tasks that involve CI/CD.
Run `npx skills add arul28/ADE --skill release -a claude-code`. Or copy the skill folder (.agents/skills/release in arul28/ADE) into .claude/skills/release in your project. Claude Code loads it when a task matches its description.
Run `npx skills add arul28/ADE --skill release -a codex`. Or copy the skill folder (.agents/skills/release in arul28/ADE) into .agents/skills/release in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add arul28/ADE --skill release -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/release, .gemini/skills/release, .github/skills/release and .opencode/skills/release in your project.
Going by SKILL.md and its folder, Release needs the command-line tools its instructions call (gh, npx, npm, git, wrangler and curl) and credentials named DIRECTORY_AUTH_SECRET, ASC_KEY_ID, HOMEBREW_TAP_DEPLOY_KEY and CLOUDFLARE_API_TOKEN.
SKILL.md names 4 domains. In commands or code: ade-account-directory-production.arulsharma1028.workers.dev, ade-app.dev, ade-tunnel-relay.arulsharma1028.workers.dev and ade-github-webhook-relay.arulsharma1028.workers.dev; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Release is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 17k tokens (SKILL.md is roughly 68k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Release: CI CD Setup (rshankras/claude-code-apple-skills, 781 stars), Release (vaayne/mori, 303 stars), App Store Deploy (makifbaysal/tasktrooper, 109 stars) and Release (movieclaw/MovieClaw, 132 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
arul28 (a GitHub user) maintains it in arul28/ADE, which has 113 GitHub stars. The repository holds 30 skills in this directory. The repository was last updated on October 7, 2026.
Source: arul28/ADE on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.