From 7ac4303d8f4029660d781cf950bc710d5f84d501 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 8 Aug 2026 13:49:22 +0200 Subject: [PATCH] docs(release): streamline release workflow Make the release document the single source of truth. ACP drift is explicit opt-in, release preparation uses one remote batch, and a heartbeat owns completion through published artifacts and store submissions. Preserve one changelog entry per beta and collapse the series on promotion. --- .agents/skills/release-beta/SKILL.md | 8 +- .agents/skills/release-stable/SKILL.md | 6 +- docs/release.md | 156 ++++++++++++++++++------- 3 files changed, 114 insertions(+), 56 deletions(-) diff --git a/.agents/skills/release-beta/SKILL.md b/.agents/skills/release-beta/SKILL.md index fa8b729280..ec8b7cd971 100644 --- a/.agents/skills/release-beta/SKILL.md +++ b/.agents/skills/release-beta/SKILL.md @@ -1,13 +1,9 @@ --- name: release-beta -description: Cut a beta release of Paseo. Use when the user says "release beta", "cut a beta", "ship a beta", "beta release", or "/release-beta". Betas are release candidates on the beta channel — they carry an in-place changelog entry, don't move the website download target, and publish npm only on the beta dist-tag. +description: Cut a beta release of Paseo. Use when the user says "release beta", "cut a beta", "ship a beta", "beta release", or "/release-beta". user-invocable: true --- # Release beta -Read `docs/release.md` in the Paseo repo and follow the **Beta flow** section end-to-end. Run the **Beta release** completion checklist at the bottom of that doc. - -During preparation, classify the previous-stable-to-`HEAD` diff as patch or minor and show the target version and rationale to the user. Agents never select a major version autonomously. - -Each beta updates an in-place `CHANGELOG.md` entry (`## X.Y.Z-beta.N`) that gets overwritten at promotion, and npm publishes only on the explicit `beta` dist-tag. +Follow the **Beta flow** and **Beta release** completion checklist in `docs/release.md` end-to-end. diff --git a/.agents/skills/release-stable/SKILL.md b/.agents/skills/release-stable/SKILL.md index 9755e33e3e..df33b07d0b 100644 --- a/.agents/skills/release-stable/SKILL.md +++ b/.agents/skills/release-stable/SKILL.md @@ -6,8 +6,4 @@ user-invocable: true # Release stable -Read `docs/release.md` in the Paseo repo and follow the **Standard release (stable)** flow if cutting fresh, or the **Beta flow** promotion step if promoting an existing beta. Run the **Stable release (or promotion)** completion checklist at the bottom of that doc. - -For a fresh release, classify the previous-stable-to-`HEAD` diff as patch or minor and show the target version and rationale to the user. Agents never select a major version autonomously. - -The doc covers the changelog, pre-release sanity check, and post-release babysit pattern. Don't skip steps. +Follow the applicable flow and the **Stable release (or promotion)** completion checklist in `docs/release.md` end-to-end. diff --git a/docs/release.md b/docs/release.md index ebf2579586..96ecd3d362 100644 --- a/docs/release.md +++ b/docs/release.md @@ -9,19 +9,17 @@ A release has exactly two steps. The agent does the first, the user authorizes t **Preparation** (local, reversible — agent does this): - format, lint, typecheck all green -- ACP provider catalog drift checked with `npm run acp:version-drift:check`; - if stale package-runner pins are intentional, say so explicitly, otherwise run - `npm run acp:version-drift:update` and commit the updated catalog -- classify the previous-stable-to-`HEAD` diff as patch or minor, then show the +- resolve the release source to one commit and confirm that commit's existing CI is green +- classify the diff from the previous stable to the release source as patch or minor, then show the target version and rationale to the user - draft the changelog, show it to the user, wait for review - run the pre-release sanity check, surface findings to the user -- confirm CI is green **Go-ahead** (user says "go ahead"): -- commit the approved changelog -- run the release +- commit the approved release inputs locally +- run the release, which publishes npm and pushes the prepared branch and tag +- create the release heartbeat immediately and babysit it to completion Rules that apply to both steps: @@ -31,12 +29,43 @@ Rules that apply to both steps: - Invoking a release skill is intent to start the flow, not blanket authorization to publish. - If the user asks for a release preview, show the prospective changelog/release contents and answer questions, but do not commit, tag, publish, or run release commands until they explicitly authorize the release. +## Release source and CI + +The default release source is `origin/main`. Fetch `origin`, then record the +resolved commit. The default release checkout is a clean local `main` whose +`HEAD` equals `origin/main`. + +An explicit user instruction can select another ref, such as a hotfix commit or +tag. Resolve that ref once and apply every source, diff, and CI check to that +commit instead of `origin/main`. + +Before making release-preparation commits, confirm the existing CI run for the +resolved commit is green. Pending CI is watched to completion. Release +preparation then stays local through the changelog, any explicitly requested ACP +catalog update, lockfile preparation, and the version commit. After approval, +commit the prepared inputs locally and run the release command. Its branch and +tag push is the one remote release batch and starts CI for the complete release +commit. + +## ACP catalog updates + +ACP catalog work enters a release through an explicit user request: + +- **Check ACP drift** — run `npm run acp:version-drift:check`. When drift exists, + run `npm run acp:version-drift:update`, verify the catalog, and include the + update in the local release-preparation commits. +- **Update ACP** — run `npm run acp:version-drift:update`, verify the catalog, and + include the update in the local release-preparation commits. + +The release authorization covers the requested ACP commit. It ships in the same +release push as the changelog and version commit. + ## Two paths -There are two supported ways to ship from `main`: +There are two supported release paths: 1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately. -2. **Beta flow**: release candidates on the `beta` channel. Betas carry an in-place changelog entry (beta users check it), publish npm only on the explicit `beta` dist-tag, and never become the website's default download — they sit behind the Stable/Beta switch on `/download`. +2. **Beta flow**: release candidates on the `beta` channel. Each beta carries its own changelog entry, publishes npm only on the explicit `beta` dist-tag, and stays behind the Stable/Beta switch on `/download`. Paseo has one linear release track even though npm dist-tags are independent pointers. The npm invariant is: @@ -48,8 +77,9 @@ pointers. The npm invariant is: ## Release version decision -Every fresh release starts by classifying the full previous-stable-to-`HEAD` -diff. The highest-impact change determines the version: +Every fresh release starts by classifying the full diff from the previous +stable to the resolved release source. The highest-impact change determines the +version: - **Minor** — a user would experience the release as a significant upgrade. This includes substantial new workflows, providers, forges, platforms, integrations, @@ -72,7 +102,7 @@ as described in **Fixing a failed release build**. Before running any stable release command: -- Make sure the intended release commit is already committed to `main` and the working tree is clean. +- Make sure the resolved release source passed CI, the approved release inputs are committed locally on the intended branch, and the working tree is clean. - **Run `npm run format`, `npm run lint`, and `npm run typecheck` and commit any resulting changes BEFORE you start any `release:*` command.** `release:check` runs `npm install --workspaces --include-workspace-root` as part of `release:prepare`, which can mutate `package-lock.json` (e.g. churning `"dev": true` markers on optional deps). The next step, `version:all:*`, runs `npm version` which aborts when the working tree is dirty. If this happens mid-flight you have to commit the lockfile churn before retrying — and the pre-commit format hook will reject a lockfile-only commit because oxfmt internally skips `package-lock.json` while lefthook's glob still matches it. Avoid the whole mess by running format/lint/typecheck first, then `release:prepare` once on its own to absorb any lockfile churn into a normal commit, then start the release. - Do not use a release command as a substitute for checking whether the current commit is actually ready. @@ -133,7 +163,7 @@ npm run release:promote # Promote X.Y.Z-beta.N to stable X.Y.Z - `release:promote` creates a fresh stable tag like `v0.1.41`; the final release never reuses the beta tag - Desktop assets now come from the Electron package at `packages/desktop` - Beta releases use Electron's `beta` update channel. Users on the stable channel only receive stable releases; users on the beta channel receive beta releases and the final stable release when it is published. -- **Betas carry a changelog entry.** Beta users read release notes, so each beta updates an in-place `CHANGELOG.md` entry (`## X.Y.Z-beta.N`) that `Release Notes Sync` mirrors into the prerelease body on the tag push. The entry is intermediary: promotion overwrites it in place with the final stable entry, so no `-beta.N` heading is ever left behind. See the Changelog policy section. +- **Each beta carries its own changelog entry.** `Release Notes Sync` mirrors the matching `## X.Y.Z-beta.N` entry into that prerelease body. Promotion collapses every beta entry for the version into one final stable entry. See the Changelog policy section. Use the beta path when you need to: @@ -291,11 +321,30 @@ Do not treat `build_ios: SUCCESS` or `submit_ios: SUCCESS` as a completed iOS re To confirm the submission landed, inspect the EAS workflow with `npx eas workflow:view --json`. App Store Connect (review state for the matching version/build) and the Play Console track are the final ground truth. -### Babysitting mobile after a release +## Release completion and heartbeat -The user rarely opens the Expo dashboard. A failed EAS build or submit/review job can sit silently until users complain about a stale version. After every stable release, set up a long-delay babysit that re-checks GitHub Actions, EAS builds, and the EAS `Release Mobile` workflow for the release tag. If any build is `ERRORED`/`CANCELED`, any workflow is `FAILURE`, or any required submit/review job fails, surface it immediately. If all builds are `FINISHED` and all required submit/review jobs are `SUCCESS`, confirm and stop. +A release is **in progress** after npm publication and tag push. Report it as +**shipped** only after every applicable build, publication, asset, manifest, and +store submission passes the completion checklist. -**Use `create_heartbeat`, never `create_schedule`, for release babysitting.** Babysitting fires back into the current conversation as a wake-up prompt. `create_schedule` starts a fresh agent the user has to find and read; `create_heartbeat` surfaces the build status inline in the conversation that owns the release, where it is impossible to miss. If you find yourself reaching for `create_schedule` for a release babysit, you are about to ship a status report into a void. +Immediately after every beta, stable, or promotion tag push, create a heartbeat +that resumes the release in the current conversation. Create it automatically +with `create_heartbeat`. The heartbeat owns the release until it either reaches +the completion checklist or finds a failure that needs new user authority. + +Each heartbeat checks the release tag commit, all GitHub Actions runs for the +release branch and tag, npm dist-tags, the GitHub Release body and assets, +desktop updater manifests, the published Docker image, and the applicable EAS +workflow. Inspect the GitHub Release itself and confirm that the macOS, Linux, +Windows, and Android APK assets are present along with the channel manifests +(`latest-mac.yml`, `latest-linux.yml`, and `latest.yml` for stable; +`beta-mac.yml`, `beta-linux.yml`, and `beta.yml` for beta). + +For stable releases, also confirm every required mobile build, upload, store +submission, and review-submission job for the release commit. For betas, confirm +the beta EAS workflow completed its TestFlight distribution and Beta App Review +path. Delete the heartbeat only after every applicable checklist item passes, +then report the release as shipped. Pattern: @@ -303,13 +352,16 @@ Pattern: // mcp__paseo__create_heartbeat arguments { "name": "vX.Y.Z release babysit heartbeat", - "cron": "*/15 * * * *", - "maxRuns": 8, // covers ~2h of build + store-submission window - "prompt": "Heartbeat: check vX.Y.Z release. Run gh run list, eas build:list, eas workflow:runs, and eas workflow:view for the matching Release Mobile run. Report concisely. The release is not done until desktop/APK workflows are green, EAS builds are FINISHED, Android submit_android is SUCCESS, and iOS submit_ios + submit_ios_for_review are SUCCESS. Flag any ERRORED/FAILED/CANCELED/FAILURE loudly.", + "cron": "*/10 * * * *", + "timezone": "UTC", + "maxRuns": 120, + "expiresIn": "24h", + "prompt": "Resume the vX.Y.Z release babysit for commit . Check npm tags; every GitHub Actions run for the release branch and tag; the published GitHub Release body, expected desktop/APK assets, and channel manifests; the Docker image; and the matching EAS workflow. Completion requires every applicable checklist item. For stable, require build_ios, submit_ios, submit_ios_for_review, build_android, and submit_android to succeed. For beta, require the beta TestFlight distribution and Beta App Review path. If work is pending, wait for the next heartbeat. If a failure can be retried safely for the same version, follow the failed-release procedure; otherwise report the blocker. When every applicable completion-checklist item passes, delete THIS heartbeat, report shipped, and stop.", } ``` -Tight cadence on purpose. The first run fires immediately, giving a near-real-time status check before the conversation closes. Subsequent runs at 15-minute intervals catch transitions quickly: a failed EAS build or failed App Store review submission at +20m should not wait until +50m to surface. Keep the prompt short — the heartbeat is a status probe, not a research task — and have it bail out as soon as every platform is actually on its store path so the remaining runs do not generate noise. +Run an immediate status check after creating the heartbeat. The heartbeat handles +later transitions and stops itself when the release is complete. ## Release notes on GitHub @@ -322,7 +374,7 @@ The GitHub Release body is populated automatically by the `Release Notes Sync` w - Homebrew, the Play Store, the App Store, and `app.paseo.sh` have no beta. The Beta view drops those rows, and the whole Web section, rather than showing an inert "stable only" placeholder. When a surface gains a beta path — say a public TestFlight link — add its row back in `packages/website/src/routes/download.tsx`. - The default download target only moves when you publish the final stable release tag like `v0.1.41`. - The public `/changelog` page renders `CHANGELOG.md` as-is, so the in-flight `-beta.N` entry shows there once it lands on `main` — that's intended, it's where beta users check what's coming. Only the **default download target** stays pinned to the latest stable; the download links read GitHub's releases API, not the changelog, so a `-beta.N` heading on top never affects them. -- The download page's "What's new" link deep-links the **minor group** anchor (`/changelog#release-0.3`), not the exact entry: promotion rewrites the `-beta.N` entry in place, so that anchor dies while the group survives. A version with no entry in the bundled changelog — a tag whose changelog commit hasn't redeployed the site yet — links the plain `/changelog` instead of a dead anchor. +- The download page's "What's new" link deep-links the **minor group** anchor (`/changelog#release-0.3`), not the exact entry: promotion collapses the beta entries into one stable entry, so the minor group remains the durable target. A version with no entry in the bundled changelog — a tag whose changelog commit hasn't redeployed the site yet — links the plain `/changelog` instead of a dead anchor. - The website itself is deployed by `Deploy Website` (Cloudflare Workers), which redeploys on `release: published` for non-prerelease releases and on pushes to `main` that touch `CHANGELOG.md` or `packages/website/**`. ## Fixing a failed release build @@ -403,17 +455,16 @@ No prefix (`v`), no extra text. `Release Notes Sync` matches the `## X.Y.Z` (or ## Changelog policy -- `CHANGELOG.md` includes stable releases and the current beta line. +- `CHANGELOG.md` includes stable releases and every entry in the current beta series. - The first beta of a version inserts a top entry like `## 0.1.60-beta.1 - YYYY-MM-DD`. -- Each subsequent beta updates that same top entry in place — bump the heading (`0.1.60-beta.1` → `0.1.60-beta.2`) and fold in whatever else landed. -- Stable promotion updates that same entry in place one last time: heading to `0.1.60`, date to the promotion day. -- One entry per version line. The `-beta.N` heading is intermediary — overwrite it, never append. Don't leave stale `-beta.N` entries behind and don't create a duplicate entry per beta. -- It always covers the full diff from the previous stable tag, regardless of how many betas were cut in between. +- Each subsequent beta inserts a new top entry with the next beta number. Its notes cover the changes since the previous beta tag. +- Stable promotion replaces every beta entry for that version with one `## 0.1.60 - YYYY-MM-DD` entry. +- The promoted stable entry covers the full diff from the previous stable tag and collapses internal iterations across the beta series. ## Changelog ownership -- **The agent running the release writes the changelog entry — beta or stable.** Do not hand the changelog to another model or agent. The release agent has the release context and owns the final wording. -- Draft the entry from the previous-stable-to-`HEAD` diff, review it against the changelog policy below, show it to the user, and wait for approval before committing it. Each beta refreshes the same entry; promotion refreshes it one last time from the full previous-stable-to-`HEAD` diff. +- **The agent running the release writes the changelog entry — beta or stable.** The release context and final wording stay with that agent. +- For the first beta or a direct stable release, draft from the previous stable tag to the release source. For later betas, draft from the previous beta tag to the release source. Promotion replaces the beta series with one entry drafted from the previous stable tag to the release source. Review the result against the changelog policy below, show it to the user, and wait for approval before committing it. ## Changelog voice @@ -434,10 +485,8 @@ The changelog is shown on the Paseo homepage. Write it for **end users**, not de Test: would a non-developer reader recognise what changed when using the app? If they'd need an engineer to translate ("what's a remount?"), the bullet is still implementation-facing — rewrite it as the symptom the user experiences. -- **Collapse internal iterations.** If a feature was added and then fixed within the same release, just list the feature as working. Users never saw the broken version. -- **Only list changes relative to the previous stable release.** The diff is `v(previous)..HEAD`. If something was introduced and fixed between those two tags, it never shipped — don't mention the fix. - - **Common trap:** when drafting from `git log`, every commit looks like a separate bullet — including the "fix X" commits that landed on top of a brand-new feature in the same release window. Before listing a Fixed entry, check whether the thing being fixed was itself added in this same release. If so, drop the fix and fold it into the feature bullet. - - **Example:** if the release adds an in-app browser and also contains a commit "fix: browser pane keyboard handling no longer steals shortcuts", do **not** list the keyboard fix under Fixed. The browser is shipping for the first time, so users will only ever see the working version. The Added entry covers it. +- **Use the entry's release scope.** Include changes within the matching range in **Changelog scope**. +- **Collapse internal iterations within that scope.** Present a feature added and fixed in one range as working. A later beta can describe a fix to behavior delivered in an earlier beta; promotion folds the complete beta series into the final stable behavior. - **Cut low-signal entries.** "Toolbar buttons have consistent sizing" is too granular. Combine small polish items or drop them. ## Changelog conciseness @@ -480,7 +529,7 @@ Rules: This returns every distinct GitHub login that authored or co-authored a commit in the PR. Use those logins for attribution. Fall back to `gh pr view N --json author` only if the commits command returns nothing (which should not happen for merged PRs). - When listing PR numbers, `git log --format='%H %s' v..HEAD | grep -E '\(#[0-9]+\)$'` pulls the PR number out of squash commit subjects. + When listing PR numbers, `git log --format='%H %s' v.. | grep -E '\(#[0-9]+\)$'` pulls the PR number out of squash commit subjects. ## Changelog ordering @@ -494,51 +543,68 @@ Entries within each section (Added, Improved, Fixed) are ordered by user impact: Before cutting a **stable** release, the release agent reviews the diff as a last line of defence against shipping bugs. Skip this for betas — the beta itself is the smoke test, and gating each beta on a code review defeats the point of using betas as fast release candidates. -Review the diff between the latest release tag and `HEAD`. Focus on: +Review the diff between the latest release tag and the resolved release source. Focus on: 1. **Breaking changes** — especially in the WebSocket protocol, agent lifecycle, and any server↔client contract. 2. **Backward compatibility** — the important direction is old app clients talking to newly updated daemons. Users update desktop and daemon first, then keep running the old app for a while. Flag anything that breaks old clients against new daemons or requires both sides to update in lockstep. 3. **Regressions** — anything that looks like it could break existing functionality. -Use `git diff ..HEAD` as the review input. This is a deep sanity check, not a full code review. If anything looks risky, investigate before proceeding and surface the finding to the user. +Use `git diff ..` as the review input. This is a deep sanity check, not a full code review. If anything looks risky, investigate before proceeding and surface the finding to the user. ## Changelog scope -The changelog always covers **previous-stable-to-`HEAD`**, beta and stable alike: +Changelog scope follows the release being described: -- **Beta release**: the entry covers `previous stable tag → HEAD`. Update the current in-place beta entry; don't start a fresh one per beta. -- **Stable promotion**: the same entry is promoted in place. It still captures the full delta from the previous stable release, not just what changed since the last beta. +- **First beta**: `previous stable tag → release source` +- **Later beta**: `previous beta tag → release source` +- **Direct stable release**: `previous stable tag → release source` +- **Stable promotion**: replace the full beta series with one entry covering `previous stable tag → release source` -Betas are checkpoints along the way; the entry is the single record for the jump from one stable version to the next, and beta users read it in the meantime. +Each beta entry records what its testers receive. Promotion produces the single stable record for the full jump from one stable version to the next. ## Completion checklist ### Beta release -- [ ] Working tree is clean and the intended commit is on `main` -- [ ] Update the in-place beta entry in `CHANGELOG.md` (heading `## X.Y.Z-beta.N - YYYY-MM-DD`), review it against the changelog policy, get approval, and commit it before cutting the release -- [ ] The previous-stable-to-`HEAD` diff is classified as patch or minor, with the target version and rationale approved +- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green +- [ ] Add a new `CHANGELOG.md` entry for this beta (heading `## X.Y.Z-beta.N - YYYY-MM-DD`), review it against the changelog policy, get approval, and commit it before cutting the release +- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved +- [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag - [ ] `npm run release:beta:patch`, `npm run release:beta:minor`, or `npm run release:beta:next` completes successfully +- [ ] Every GitHub Actions run for the complete release commit and tag is green - [ ] npm shows the version under the `beta` dist-tag, not `latest` +- [ ] The GitHub prerelease exists with the changelog body and every expected macOS, Linux, Windows, and Android APK asset - [ ] GitHub `Desktop Release` workflow for the `v*-beta.N` tag is green +- [ ] The GitHub prerelease contains `beta-mac.yml`, `beta-linux.yml`, and `beta.yml` - [ ] GitHub `Android APK Release` workflow for the same tag is green +- [ ] GitHub `Docker` workflow is green and the versioned beta image is published without moving `latest` - [ ] GitHub `Release Notes Sync` mirrored the beta entry into the prerelease body +- [ ] EAS `Release iOS Beta` completed its build, TestFlight distribution, external beta group, and Beta App Review path +- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed ### Stable release (or promotion) - [ ] Run the pre-release sanity check (see above) and address any findings -- [ ] The previous-stable-to-`HEAD` diff is classified as patch or minor, with the target version and rationale approved -- [ ] Ensure the intended release commit is already committed and the git worktree is clean before running any release command +- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved +- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green +- [ ] Ensure the approved release inputs are committed locally and the git worktree is clean before running any release command - [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command -- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). When promoting from beta, overwrite the existing `## X.Y.Z-beta.N` heading in place (heading → `X.Y.Z`, date → promotion day) — do not add a new entry on top of the beta one +- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). Promotion replaces every `## X.Y.Z-beta.N` entry in the series with one `## X.Y.Z - YYYY-MM-DD` entry covering the full release - [ ] Verify the changelog heading follows strict `## X.Y.Z - YYYY-MM-DD` format +- [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag - [ ] `npm run release:patch`, `npm run release:minor`, or `npm run release:promote` completes successfully +- [ ] Every GitHub Actions run for the complete release commit and tag is green - [ ] Move npm's `beta` dist-tag to the new stable version for every published package and verify both `latest` and `beta` resolve to it +- [ ] The published GitHub Release exists with the changelog body and every expected macOS, Linux, Windows, and Android APK asset - [ ] GitHub `Desktop Release` workflow for the `v*` tag is green +- [ ] The GitHub Release contains `latest-mac.yml`, `latest-linux.yml`, and `latest.yml` - [ ] GitHub `Android APK Release` workflow for the same tag is green +- [ ] GitHub `Docker` workflow is green and both the versioned and `latest` images are published +- [ ] GitHub `Release Notes Sync` is green and the release body matches the stable changelog entry - [ ] EAS `Release Mobile` workflow for the same tag is green - [ ] EAS iOS `build_ios` completes for the same tag - [ ] EAS iOS `submit_ios` succeeds, uploading the build to App Store Connect/TestFlight - [ ] EAS iOS `submit_ios_for_review` succeeds, putting the build into App Store review - [ ] EAS Android `build_android` completes for the same tag - [ ] EAS Android `submit_android` succeeds, putting the build on its Play Store track +- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed