--- name: codap-v3-build description: Use when preparing a CODAP v3 release, creating release notes, updating version files, creating release PRs, tagging releases, or deploying to staging/production. Invoke with phase name or version number to resume. --- # CODAP v3 Build & Release ## Overview Interactive workflow for CODAP v3 releases. Guides you through Jira setup, release notes generation, version updates, PR creation, tagging, and deployment. ## Quick Reference | Phase | Command | Description | |-------|---------|-------------| | 1 | `/codap-v3-build` | Prepare release (Jira version, tag stories) | | 2 | `/codap-v3-build notes` | Prepare release notes (interactive) | | 3 | `/codap-v3-build files` | Update version files | | 4 | `/codap-v3-build pr` | Create release PR | | 5 | `/codap-v3-build tag` | Tag the release (triggers S3 build) | | 6 | `/codap-v3-build deploy [version]` | Deploy to staging/production, publish GitHub release | | fix | `/codap-v3-build fix {old-version}` | Revise release after staging QA failure | ## Getting Started When invoked, introduce the skill: > This skill will walk you through the process of building a release of CODAP v3. The process has 6 phases: > > 1. **Prepare the Release** - Set up Jira version and gather context > 2. **Prepare Release Notes** - Interactive walkthrough to create CHANGELOG entry > 3. **Update Version Files** - Update package.json, versions.md, CHANGELOG.md > 4. **Create Release PR** - Build, capture asset sizes, create PR > 5. **Tag** - After PR merge, create git tag (triggers the S3 build) > 6. **Deploy** - Stage, QA, deploy to production, then publish the GitHub release > > Are you ready to proceed? Wait for user confirmation before starting Phase 1. ## Approval Gates Every action below changes shared state that other people see or depend on. For each one: 1. **Show** exactly what will happen: the full command, the Jira issues and fields, or the full message text. 2. **Wait** for the user's approval. 3. **Do it**, then **read the result back** (the run's outcome, the stored Jira value, the posted message) and report it. An approval covers only the actions it names. Approving the staging deploy does not approve the Slack post that follows it, and "go ahead with everything" early in the session does not lift later gates. | Action | Where | Read back with | |--------|-------|----------------| | POEditor push | Phase 3, step 2b | the script's output | | POEditor translation fixes | Phase 3, step 2d | the API response, then the re-pulled file | | `git push` of a release branch, `gh pr create` | Phase 4; Fix, Step 4 | `gh pr view --json url,labels` | | Tag push or deletion | Phase 5; Fix, Step 5 | `git ls-remote --tags origin '{version}^{}'` | | Workflow dispatch (staging, production, beta) | Phase 6; Fix, Step 6 | [Dispatch, watch, verify](#dispatch-watch-verify) | | `gh release create` | Phase 6 | `gh release view {version} --json url,isDraft,isPrerelease` | | Jira edits (Fix Versions, version rename) | Phase 2, step 9; Fix, Step 6 | re-fetch the changed fields | | Slack posts outside the developer's self-DM | Phase 6; Fix, Step 6 | the posted message's `ts` (Slack's message ID) | **Not gated:** reads, builds, local commits, and previews posted to the developer's own Slack self-DM ([Slack Posts](#slack-posts)). ## Phase 1: Prepare the Release **Goal:** Create Jira release version and gather context. ### Steps 1. **Check workspace status:** ```bash git status ``` - If there are modified/staged files, ask user to: **Stash** / **Commit** / **Discard** - Untracked files are okay to leave 2. **Ensure on main branch with latest:** ```bash git checkout main git pull ``` 3. **Get current build number:** ```bash cat v3/build_number.json ``` Needed for the version string only in the pre-release phase (see step 6), but always worth showing as context. 4. **Get previous release tag:** ```bash git tag --sort=-creatordate | head -5 ``` 5. **Show context to user:** - Display last 3 releases (version and date) - Show current build number from `build_number.json` 6. **Determine recommended version string:** CODAP v3 has **two versioning conventions**, one per development phase. Identify the current phase from the newest release tag, then apply that phase's rule. ```bash git tag --sort=-creatordate | head -3 ``` | Phase | You are here when… | Version format | Rule | |-------|--------------------|----------------|------| | **Production release** | The newest release tag is plain semver with **no** prerelease suffix (`3.0.4`, `3.1.2`) | `MAJOR.MINOR.PATCH` | Increment from the last released version: **patch** for bug fixes, **minor** for new features, **major** for breaking changes. Example: after `3.0.4` → `3.0.5`. | | **Pre-release development** | The newest release tag carries a prerelease suffix (`-beta`, `-rc`, `-pre`) | `{major}.{minor}.{patch}-{suffix}.{buildNumber}` | Use build number **+ 1** (it auto-increments when the release PR merges), carrying over the previous tag's version prefix and suffix. Example: newest tag `3.0.0-beta.2662` with build `2662` → `3.0.0-beta.2663`. | **The build number is part of the version string ONLY in the pre-release phase.** In the production phase the build number still exists and still increments, but it is independent of the version — do **not** derive the version from `build_number.json`. (At the 3.0.5 release the build number was 2956, heading to 2957 on merge, while the version was `3.0.5`. The two are unrelated and will never match again.) CODAP v3 entered the production phase at `3.0.0` on 2026-06-04. The pre-release rule is retained because the project may re-enter a pre-release phase for a future major version (e.g. a `4.0.0-beta.N` series), at which point it applies again. > **Exception — starting a new prerelease series.** Inferring the phase from the newest > tag is reliable *within* a phase but blind to the moment you deliberately switch. When > the next major begins its prerelease series, the newest tag is still production semver > (e.g. `3.1.4`), so the rule above would wrongly say "production, bump the patch" > instead of `4.0.0-beta.N`. A tag can't signal an intent to change phase. So before > applying the rule, **ask the user whether this release starts a new prerelease series > for a new major version.** If yes, switch to the pre-release convention and confirm > the intended prefix (`4.0.0`) and suffix (`-beta`) with them rather than inferring. **Which component to bump** is a judgment call about the release's contents, not a mechanical rule — propose one and confirm it with the user along with the rest of the Jira release details (step 8). Count only what users can see: work behind a feature flag, logging, and docs don't make a release minor, and by convention neither does a new translation or language. 7. **Get previous release date from Jira** (for start date default) 8. **Ask user for Jira release details:** | Field | Default | Options | |-------|---------|---------| | Version name | The phase-appropriate next version from step 6 | Production phase: patch / minor / major bump. Pre-release phase: previous suffix + new build number | | Start date | Previous release date | User can modify | | Release date | Today's date | Today / Tomorrow / Custom future date | | Description | `Version {version}` | User can modify | **Note:** The release date chosen here is used throughout the process: - CHANGELOG.md header date - versions.md entry date - Jira release date 9. **Ensure the Jira release version exists** (status: `Unreleased`). **The Atlassian MCP tools cannot create a version or edit its release date** — they expose no version-management tool. This step is the user's to perform, in the Jira UI (CODAP → Releases). Setting issue Fix Versions (Phase 2, step 9) works fine through MCP; it is only the version object itself that is out of reach. - **Check whether it already exists first** — Jira automation may have created the version (and a `Release {version}` tracking issue) ahead of time. **Ask the user to check Jira → CODAP → Releases**; that is the only reliable check. Querying the `fixVersions` field via JQL is a weak fallback: it can only surface versions already assigned to at least one issue, so a freshly created version with nothing assigned to it — the very case this step is looking for — will not appear. Absence from JQL is **not** evidence the version is missing. - If it exists, confirm its release date matches the date agreed in step 8; if it doesn't, ask the user to correct it. - If it doesn't exist, ask the user to create it with the agreed name, dates, and description, and to confirm once done. The date only has to be correct before the release is marked `Released` in Phase 6, so this need not block the rest of the workflow. 10. **Put the release tracking issue in the current sprint.** Jira automation creates a `Release {version}` issue (type Release) along with the version, and it lands in the backlog. Once the user confirms the version exists, have a subagent find it and the active sprint: ``` project = CODAP AND issuetype = Release AND fixVersion = "{version}" -- with customfield_10020 project = CODAP AND sprint in openSprints() -- customfield_10020, maxResults 50 ``` If the tracking issue's `customfield_10020` already holds the active sprint, report that and skip the edit (the user may have moved it already). Sprint IDs are not sequential, so read the active sprint's `id`, `name`, and `endDate` from `customfield_10020` rather than guessing; scan all results, since an issue can carry several sprints. If more than one sprint is active, or the active sprint ends before the release date, ask which sprint to use. Setting the sprint is a gated Jira edit: show the issue key, its summary, and the sprint name and ID, then set `customfield_10020` to the sprint's ID (a plain integer, not an object) with `editJiraIssue`, and re-fetch the field to confirm it. If no tracking issue exists, say so; don't create one. ## Phase 2: Prepare Release Notes **Goal:** Generate CHANGELOG entry with user-selected titles. ### Steps 1. **Get PRs since last release:** ```bash git log ..HEAD --oneline | grep -E '\(#[0-9]+\)|Merge pull request #[0-9]+' ``` **Note:** This finds both regular merge commits AND squash-merged PRs (which include `(#123)` in the commit message). Using `--merges` alone misses squash merges. 2. **Get PR details from GitHub:** ```bash gh pr view --json number,title,headRefName ``` 3. **Match PRs to Jira stories by CODAP-XXX ID:** - Check branch name first (most reliable, e.g., `CODAP-1027-inbounds-url-param`) - Then PR title (e.g., `CODAP-1027: Implement inbounds parameter`) - Use caution with PR descriptions - they may reference related stories (e.g., "Follow-up to CODAP-XXX") that aren't the primary story for this PR - Extract unique CODAP-XXX IDs 4. **For each matched item, fetch:** - Jira story details (summary, issue type, **current status**) - PR title from GitHub - Generate AI-suggested title (concise, user-focused) 5. **Interactive walkthrough for each item:** **IMPORTANT - NO SHORTCUTS:** - Do NOT ask user to approve the entire list at once - Do NOT batch items together (e.g., "approve these 3 items") - Do NOT skip showing title options - ALWAYS go through items ONE BY ONE, presenting all title options for each **IMPORTANT - PUT THE TABLE IN THE QUESTION:** Text written in the same turn as an `AskUserQuestion` call is not shown to the user, so a table output just before the question is invisible and the user has nothing to decide from. Put each item's table in the `preview` of **every** option of the Section question (previews render as monospace markdown beside the options), and put the candidate titles in the option `description`s of the Title question. The preview for each item: ``` Item 1/8: CODAP-1027 (Story) — Jira: Done | Source | Title | |---------------|-------------| | AI suggestion | {ai_title} | | Jira | {jira_summary} | | PR | {pr_title} | PR #NNNN. {one or two lines of context: what the user would see, whether it is flag-gated, anything that bears on the section choice} ``` **Note:** Strip Jira IDs from PR titles before presenting (e.g., "CODAP-138: Fix point color" → "Fix point color") **Jira status notice (if not Done):** append the status to the preview's first line with a warning indicator, e.g. `Item 1/8: CODAP-1027 (Story) — Jira: In Project Team Review ⚠️`. The user can choose to exclude the story via the Section question. Ask using AskUserQuestion (Section and Title are TWO SEPARATE CALLS so Title is skipped if Exclude): **Section question:** - Question: "Item N/M: CODAP-XXXX ({type}) — which section?" - Options: Features / Bug Fixes / Under the Hood / Exclude, with the recommended one first - Recommend from **what users will see**, not from the issue type alone. Read the PR body when the type and the change disagree: - Work behind a feature flag → **Exclude**. But check each PR: a story in a flag-gated epic can still ship an ungated, visible change (in 3.1.1, a Format palette redesign and a legend-behavior fix both came from the flag-gated point-shapes epic). - A story that fixes broken behavior → **Bug Fixes**. - A fix to something no released version has shipped → **Exclude**; users never saw the bug (e.g. corrections to a language first released in the same version). - Docs, plans, logging, and CI/deploy infrastructure → **Exclude**. - Otherwise: Features for Stories, Bug Fixes for Bugs. - If user types in "Other", interpret as an instruction (e.g., "go back to previous item") and handle accordingly **If Section is NOT Exclude - ask Title question:** - Question: "CODAP-XXXX — which title? (or type your own in 'Other')" - Options: AI suggestion / Jira / PR, each with its full title as the option `description` (no "Custom" - user types preferred title in built-in "Other") - If the chosen section changes the framing (e.g. a Story moved to Bug Fixes), reword the AI suggestion to match and say so in the question - If user types in "Other", use their text as the title - **Title option order must ALWAYS be:** AI suggestion, Jira, PR (both in the preview and in question options) - Stories included in release notes will have their Fix Version updated automatically (tracked for step 9) **If Section IS Exclude - ask Fix Version question:** - Question: "Should CODAP-XXXX's Fix Version be set to this release?" - Options: Yes / No, with the recommended one first - Recommend **Yes** when the story's work is complete in this release, even if it isn't user-facing (flag-gated work, docs, infrastructure) — it should still be tracked in Jira. - Recommend **No** when the story is still In Progress or is an epic with open stories: more work will follow, so it isn't "fixed" in this version. - If **Yes**: Add to Fix Version update list (step 9) even though excluded from release notes - If **No**: Do not update Fix Version (e.g., if the story was fixed in a prior release, or the PR isn't part of this release) After selection, confirm: > ✓ **CODAP-1027** → Features: "Selected title here" 6. **For PRs without Jira IDs:** - Show PR title only - Default recommendation: **Exclude (Recommended)** for docs, dependencies, maintenance - Option to include in Under the Hood if relevant - No Fix Version to update (no Jira story) 7. **Generate CHANGELOG markdown** after all items are processed: ```markdown ## Version {version} - Month Day, Year ### ✨ Features & Improvements: - **CODAP-XXX:** Title here - **CODAP-YYY:** Another title ### 🐞 Bug Fixes: - **CODAP-AAA:** Fix description - **CODAP-BBB:** Another fix ### 🛠️ Under the Hood: - **CODAP-ZZZ:** Internal improvement ``` **Rules:** - Order items by **numeric** Jira ID (223 before 1027) - Only include sections that have items - Use the release date from Phase 1 - Date format: `Month Day, Year` (e.g., `February 1, 2026`) 8. **Present generated markdown for approval:** Show the complete CHANGELOG entry as a markdown code block and **end the turn with a plain question** — do not use AskUserQuestion here. The entry is too long for an option preview, and text in the same turn as a question isn't shown, so the user would be asked to approve notes they can't see. Ask whether to approve, edit an item (section or title), or reorder. The user often reviews the whole entry for consistency at this point (e.g. capitalization, or similar items landing in different sections), so expect edits. In the same message, list the issues step 9 will set the Fix Version on, so a single reply can approve both the notes and that gated Jira edit. Note: Mention that Asset Sizes will be added in Phase 4 after the build. 9. **Update Jira Fix Versions** for all stories where user approved the update (during step 5). This is a gated action: list the story IDs and the version first, and wait for approval. **IMPORTANT - Context Management:** Jira MCP responses can be verbose and consume significant context. Delegate this bulk operation to a subagent: > Use the Task tool to update Fix Versions for all approved stories. Provide the subagent with: > - The list of CODAP-XXX story IDs to update > - The version string to set (e.g., `3.0.5`) > > The subagent should report back ONLY: > - Success/failure count (e.g., "Updated 8/10 stories successfully") > - IDs of any stories that failed (e.g., "Failed: CODAP-123, CODAP-456") 10. **Check the fix version from the Jira side.** Steps 1–9 match only from PR to story, so a story that carries the fix version but has no merged PR in the release range goes unnoticed. Have a subagent run this JQL and report key, summary, issue type, status, assignee, and Project Team Approver for each result: ``` project = CODAP AND fixVersion = "{version}" ORDER BY key ``` Compare the results with the stories matched in step 3 and show the user: - **Stories on the version with no merged PR in the range.** Ask whether each belongs in this release (e.g. a story with no code, or a PR merged before the previous tag), or whether its Fix Version should be removed. Removing it is a gated Jira edit. The `Release {version}` issue (type Release) that Jira automation creates with the version is expected here and needs no action. - **Stories that are not Done**, grouped by status. These are expected at this point (most stories sit in "In Project Team Review" until after the release), so this is informational. They are checked again before the version is marked released (Phase 6). - **Stories whose PR is merged but whose status is earlier than In Project Team Review** (e.g. still "Ready for Merge"). Point these out. Before moving one to In Project Team Review (a gated Jira edit), check that it has testing instructions the Project Team Approver can follow, and offer to draft them from the PR if not. This check and the read-back of step 9 are the same query, so one subagent can do both. JQL can query this reliably now that step 9 has assigned the version to issues; the caveat in Phase 1, step 9 applies only to a version with no issues yet. ## Phase 3: Update Version Files **Goal:** Sync translations, update all version-related files, and create release branch. **IMPORTANT — Working Directory Awareness:** - Scripts in `v3/scripts/` use `cd v3` internally, which changes the shell's working directory for subsequent commands in the same Bash call. - After running a v3 script, always verify your working directory with `pwd` before running git commands. - **All `git` commands must be run from the repository root** (`/path/to/codap`), not from `v3/`. - If a `git diff` or `git status` command produces **no output**, do NOT assume "no changes" — verify by checking the working directory and trying again with correct paths. Empty output from git commands that should show changes is a red flag that something is wrong. ### Steps **IMPORTANT — Branch policy:** Never commit directly to `main`. The release branch must be created before any commits (translations, version files, etc.). 1. **Create release branch:** ```bash git checkout -b release-{version} ``` **Branch naming rules:** - Pattern: `release-{version}` where `{version}` is from Phase 1 (e.g., `release-3.0.5`) - Do NOT use `/` in branch names - Do NOT invent your own pattern 2. **Sync translations with POEditor:** V3 owns all string pushes to POEditor — both `DG.*` and `V3.*` keys. All English strings live in a single file: `src/utilities/translation/lang/en-US.json5`. **API Token:** All scripts resolve the token in order: `-a` argument > `~/.porc` > `$POEDITOR_API_TOKEN` env var. Only ask the user for a token if none of these are configured. **2a. Preview English string changes before pushing:** Before pushing, pull the current English strings from POEditor and diff them against the local `en-US.json5` so the user can validate the changes. ```bash cd v3 # Pull current English strings from POEditor to a temp file ./scripts/strings-pull.sh -p 125447 -l en-US -o /tmp # Convert local JSON5 to JSON for comparison node -e " const fs = require('fs'); const JSON5 = require('json5'); const data = JSON5.parse(fs.readFileSync('src/utilities/translation/lang/en-US.json5', 'utf8')); fs.writeFileSync('/tmp/en-US-local.json', JSON.stringify(data, null, 4) + '\n'); " # Detailed diff showing new keys, changed values, and keys only in POEditor node -e " const poeditor = require('/tmp/en-US.json'); const local = require('/tmp/en-US-local.json'); const changed = [], newKeys = [], missingLocally = []; for (const k of Object.keys(local)) { if (!(k in poeditor)) newKeys.push(k); else if (poeditor[k] !== local[k]) changed.push({key: k, old: poeditor[k], new: local[k]}); } for (const k of Object.keys(poeditor)) { if (!(k in local)) missingLocally.push(k); } console.log('=== VALUE CHANGES (' + changed.length + ' keys) ==='); changed.forEach(c => { console.log(' ' + c.key); console.log(' POEditor: ' + JSON.stringify(c.old)); console.log(' Local: ' + JSON.stringify(c.new)); console.log(); }); console.log('=== NEW KEYS (' + newKeys.length + ' keys) ==='); newKeys.forEach(k => console.log(' ' + k + ': ' + JSON.stringify(local[k]))); console.log(); console.log('=== KEYS IN POEDITOR BUT NOT LOCAL (' + missingLocally.length + ' keys) ==='); missingLocally.forEach(k => console.log(' ' + k + ': ' + JSON.stringify(poeditor[k]))); " ``` Show the diff to the user. Common expected changes: - New keys added since the last release (lines only in local) - Updated string values **Red flags to call out:** - Keys present in POEditor but missing locally (would NOT be deleted since `sync_terms=0`, but worth noting) - Unexpected value changes to existing keys **Decide whether to push — based solely on whether English strings changed:** - **No value changes AND no new keys** (English strings unchanged): **skip the push entirely — do not ask.** There is nothing to upload, so the push (2b) would be a no-op. Note that English is unchanged and go straight to the mandatory pull (2c). - **There ARE English string changes** (new keys or changed values): show the diff and ask the user to approve the push before proceeding to 2b. **2b. Push English strings to POEditor** (only when 2a found English changes): ```bash ./scripts/strings-push-project.sh ``` This pushes all strings from `en-US.json5` (both DG and V3 keys) to POEditor. The push is additive (`sync_terms=0`) — it adds new terms and updates existing values but never deletes terms. Push first so that the subsequent pull includes any new keys added since the last release. **2c. Pull non-English translations:** ```bash ./scripts/strings-pull-project.sh ``` This pulls translated strings for all supported languages. Report results to the user (the streaming output may be collapsed in the UI). **Always run the pull — never skip it and never ask whether to pull.** Every build must pull from POEditor, because translators may have added or updated non-English strings since the last release even when the English strings are unchanged. (The push in 2b is conditional; this pull is not.) **2d. Verify and commit pulled translations:** Return to the repository root before running git commands: ```bash cd /path/to/codap # repository root, NOT v3/ git status -- v3/src/utilities/translation/lang/ ``` Report results to the user. Every language normally gains the new English keys from 2b (untranslated keys arrive with the English text). **Also review changes to existing translations** — values translators edited in POEditor since the last release. These go straight into the release, and nothing else checks them. List them, filtering out the new keys: ```bash git diff -U0 -- v3/src/utilities/translation/lang/ | grep -E '^(\+\+\+|[-+] )' \ | grep -vE '' # e.g. 'pointShape|section\.graph|...' from 2a's NEW KEYS ``` A changed line can also be just a trailing comma, where a new key was appended after what used to be the last entry; ignore those. Show the user each changed value, old → new, and flag anything that looks wrong: typos, broken placeholders (`%@`), lost punctuation. In the 3.1.1 release, two French typos arrived this way. **If a translation needs fixing,** fix it in POEditor, not in the local file (the next pull would overwrite a local fix), then re-run 2c. Fixing it is a gated action. Write the corrections to a JSON file and call the POEditor API; `~/.porc` defines `API_TOKEN`: ```bash # fixes.json: [{"term":"","context":"","translation":{"content":""}}] source ~/.porc curl -s -X POST https://api.poeditor.com/v2/translations/update \ -d api_token="$API_TOKEN" -d id=125447 -d language= --data-urlencode data@fixes.json # expect: "translations":{"parsed":N,"updated":N} ``` After re-running 2c, grep the language file to confirm the corrected values arrived. Then commit: ```bash git add v3/src/utilities/translation/lang/ git commit -m "Update translations from POEditor" ``` **Zero-width space handling:** POEditor treats truly empty strings as "untranslated," so the scripts convert between empty strings and zero-width spaces (`\u200b`) at the boundary: - **Push** (`strings-push.sh`): `""` → `"\u200b"` before uploading - **Pull** (`strings-pull.sh`): `"\u200b"` → `""` after downloading The source file (`en-US.json5`) and all runtime language files use `""` for intentionally blank strings — zero-width spaces should never appear in the repository. 3. **Update package.json version:** ```bash cd v3 npm version --no-git-tag-version {version} ``` **IMPORTANT:** Use the `npm version` command - do NOT manually edit package.json. The npm command updates both package.json AND package-lock.json. 4. **Update versions.md:** Add new row at top of versions table (using release date from Phase 1): ```markdown | [{version}](https://codap3.concord.org/version/{version}/) | Month Day, Year | ``` 5. **Update CHANGELOG.md:** - Insert content from Phase 2 at top (after `# Changelog` heading) - Asset Sizes section added in Phase 4 6. **Stage version files:** ```bash git add v3/package.json v3/package-lock.json v3/versions.md v3/CHANGELOG.md ``` ## Phase 4: Create Release PR **Goal:** Build, capture asset sizes, commit, and create PR. ### Steps 1. **Run build:** ```bash cd v3 && npm run build ``` 2. **Get asset sizes:** ```bash ls -la v3/dist/assets ``` - Find `main.*.css` file, get its size - Find all `index.*.js` files, use the **largest** one - Strip hashes for display: `index.f6eac39a783c91ae9ea5.js` → `index.js` 3. **Calculate % change:** - Read previous sizes from top entry in CHANGELOG.md - Calculate: `((new - old) / old) * 100` - Format: `X.XX%`, `<0.01%` for very small increases, negative for decreases (e.g., `-0.50%`) 4. **Add Asset Sizes to CHANGELOG:** ```markdown ### Asset Sizes | File | Size | % Change from Previous Release | |-----------|---------------|--------------------------------| | main.css | XXXXXX bytes | X.XX% | | index.js | XXXXXXX bytes | X.XX% | ``` 5. **Commit and push:** ```bash git add v3/CHANGELOG.md git commit -m "Release {version}" git push -u origin release-{version} ``` **Note:** Only commit the version files (package.json, package-lock.json, versions.md, CHANGELOG.md). Do not commit the `dist/` build output. The push and the PR creation (step 6) are gated. Show the branch, the PR title, and the full PR body, and get one approval covering both. 6. **Create PR with labels:** ```bash gh pr create \ --title "Release {version}" \ --body "{release_notes_from_phase_2}" \ --label "v3" \ --label "run regression" ``` 7. **Inform user:** > **PR created:** {url} > > CI is running. The `run regression` label triggers the full Cypress test suite. > > After CI passes and PR is reviewed/merged, run `/codap-v3-build tag` to continue. 8. **If CI fails or stalls, check for a GitHub outage before suspecting the release.** During the 3.1.1 release, a GitHub Actions incident made jobs wait for runners that never came: they ran **no steps** and were cancelled **exactly 15 minutes** after being queued, while jobs that did get runners passed slowly. That pattern means the infrastructure, not the code: ```bash gh run view --json jobs \ --jq '.jobs[] | "\(.name)\t\(.conclusion)\t\(.startedAt) -> \(.completedAt)\t\(.steps|length) steps"' curl -s https://www.githubstatus.com/api/v2/incidents/unresolved.json \ | python3 -c "import json,sys; [print(i['name'], i['status'], i['created_at']) for i in json.load(sys.stdin)['incidents']]" ``` If an Actions incident is open, poll the status API in the background (e.g. every 3 minutes) until it clears, then re-run the affected runs (`gh run rerun `, or `--failed` for only the cancelled jobs) and watch them with `gh run watch --exit-status`. ## Phase 5: Tag **Goal:** After PR merge, create the git tag that triggers the S3 build. > **Do NOT create the GitHub release here.** Publishing the GitHub release at tag > time confused external users: they saw a published release for a version that > was not yet available in production (staging QA can take 1+ days). The GitHub > release is created later, in Phase 6, only **after** the build is live in > production. The tag still must be pushed now, because the tag push is what > triggers the CI build that deploys to S3 (needed for staging). **Prerequisite:** Release PR must be merged, **and** the automatic "Increment the build number" commit that follows the merge must have landed on `main` (see step 2). ### Steps 1. **Checkout main and pull:** ```bash git checkout main git pull ``` 2. **Verify the build-number increment commit has landed — do NOT skip this:** Merging the release PR triggers an automation that pushes an "Increment the build number" commit to `main`. **That increment commit is the one to tag** — not the `Release {version}` merge commit. ```bash git log --oneline -2 ``` The output must show the increment commit sitting on top of the release merge: ``` f999f1445 Increment the build number <- tag THIS one (HEAD) 936451ef5 Release {version} (#NNNN) ``` If `HEAD` is still the `Release {version}` merge commit, the automation has not pushed yet. **Wait, re-run `git pull`, and check again** until the increment commit appears. > **Why this matters:** tagging immediately after the merge, before the increment > lands, points the tag at the release merge commit instead. This has happened > before. The resulting build carries the wrong build number, and undoing it means > deleting the tag and its S3 deploy. Every correct release tag (3.0.0 through > 3.0.3) points at an "Increment the build number" commit — use that as your check. 3. **Create the annotated tag, confirm its target, then push it.** Create it locally and check that it is on the increment commit: ```bash git tag -a {version} -m "Version {version}" git log -1 --format='%h %s' {version} # expected: Increment the build number ``` The push is gated. Show the tag, its commit, and the command, then: ```bash git push origin {version} git ls-remote --tags origin '{version}^{}' # the commit it points at; must match git rev-list -n1 {version} ``` The tag push triggers a CI build that deploys to S3. The GitHub release is **not** created until after the production deploy (Phase 6). 4. **Watch the tag's CI run until the S3 deploy finishes.** The tag push starts the "Continuous Integration (CODAP v3)" workflow (`v3.yml`), whose `S3 Deploy` job publishes `version/{version}/`. Find the run for the tag's commit; it can take a few seconds to appear, so retry if the list is empty: ```bash gh run list --workflow v3.yml --branch {version} \ --json databaseId,headSha,status,createdAt git rev-list -n1 {version} # the run's headSha must match this gh run watch --exit-status ``` Then confirm the version folder is served: ```bash curl -s -o /dev/null -w '%{http_code}\n' https://codap3.concord.org/version/{version}/ # expected: 200 ``` **Do not trigger the staging workflow until the run has succeeded and the folder returns 200.** The staging workflow copies `version/{version}/index-top.html`, so it fails if the S3 deploy hasn't finished. If the run fails, stop and show the user the failed job (`gh run view --log-failed`). 5. **Inform user:** > **Tag pushed and deployed to S3:** https://codap3.concord.org/version/{version}/ > (The GitHub release will be created later, after the production deploy, so external users don't see a release for a version that isn't live yet.) > > Ready to deploy to staging? (Or run `/codap-v3-build deploy {version}` later.) ## Phase 6: Deploy **Goal:** Stage, test, deploy to production and beta, publish the GitHub release, finalize Jira, and announce. ### Steps 1. **Deploy to staging** (gated) — [dispatch, watch, and verify](#dispatch-watch-verify) `release-v3-staging.yml`. Expect `version/{version}/` on both `/index-staging.html` and `/staging`. > **Staging deployed and verified.** Test at: https://codap3.concord.org/index-staging.html 2. **Post the release announcement to `#codap-v3`** (gated), following [Slack Posts](#slack-posts): preview in the self-DM, get approval, post, and record the message's `ts` for step 7. **Announcement format** (standard markdown, same items, titles, and order as CHANGELOG.md): ```markdown CODAP {version} is available for testing at https://codap3.concord.org/staging. ### ✨ Features & Improvements: - **[CODAP-XXX](https://concord-consortium.atlassian.net/browse/CODAP-XXX):** Feature title here - **[CODAP-YYY](https://concord-consortium.atlassian.net/browse/CODAP-YYY):** Another feature ### 🐞 Bug Fixes: - **[CODAP-AAA](https://concord-consortium.atlassian.net/browse/CODAP-AAA):** Bug fix title ### 🛠️ Under the Hood: - **[CODAP-ZZZ](https://concord-consortium.atlassian.net/browse/CODAP-ZZZ):** Internal change The [beta](https://codap3.concord.org/beta) and [production](https://codap3.concord.org/) URLs will be updated once the staging build passes QA. ``` **Rules:** - Include only the sections that have items. - **Every item starts with `- `**, even when a section has only one item. Slack collapses consecutive non-list lines into one paragraph. The self-DM preview shows whether this went wrong. - **Every Jira key is a link**, never a bare `CODAP-XXX`. A bare key makes the Jira bot post a preview card for each item in the channel. - Items without a Jira key keep their plain `**Title**` form. 3. **Wait for external QA** (may take 1+ days). > **Let me know when staging QA is complete** and we can proceed with the production deployment. If QA finds a show-stopper, switch to [Staging QA Failure](#staging-qa-failure--revised-release). 4. **Deploy to production** (gated) — dispatch, watch, and verify `release-v3-production.yml`. Expect `version/{version}/` on `/`. 5. **Deploy to beta** (gated) — dispatch, watch, and verify `release-v3-beta.yml`. Expect `version/{version}/` on `/beta`. 6. **Publish the GitHub release** (gated). Do this only after step 4 has verified that production serves `{version}`, so external users never see a published release for a version that isn't live yet (staging QA can take 1+ days). Write the Phase 2 release notes to a file in the scratchpad and pass it with `--notes-file`: ```bash gh release create {version} --title "Version {version}" --notes-file /notes.md gh release view {version} --json url,isDraft,isPrerelease gh release list --limit 1 # {version} should be marked Latest ``` 7. **Announce that production is live** (gated) as a reply in the announcement's thread (`thread_ts` = the `ts` recorded in step 2), following [Slack Posts](#slack-posts): ```markdown CODAP {version} is now live on [production](https://codap3.concord.org/) and [beta](https://codap3.concord.org/beta). [GitHub release notes]() ``` Say "GitHub release notes", not "Release notes": there is a separate, user-facing release notes document, and the two shouldn't be confused. If the session was resumed and the `ts` is no longer known, find the announcement with `mcp__slack__conversations_history` on `#codap-v3` (text starting `CODAP {version} is available for testing`) and confirm with the user that it's the right message. 8. **Check unresolved issues on the fix version.** Have a subagent run: ``` project = CODAP AND fixVersion = "{version}" AND statusCategory != Done ORDER BY key ``` and report key, summary, status, assignee, and Project Team Approver. Show the list grouped by status. Stories in "In Project Team Review" are normal at this point; anything earlier (In Progress, In Code Review, Ready for Merge) suggests the story isn't actually in the build. The `Release {version}` tracking issue appears here too, with "Automation for Jira" as its Project Team Approver; that is expected. Ask the user whether to nudge the owners (a Slack message to anyone else is gated), to move a story's Fix Version (a gated Jira edit), or to release as is. 9. **Mark the Jira version released.** The Atlassian MCP tools have no version-management tool, so the user does this in the Jira UI: CODAPv3 → Releases → `{version}` → **Release**, with the release date agreed in Phase 1. Wait for the user to confirm. 10. **Go through the [Done when](#done-when) checklist** before calling the release finished. ### Done when Check every item and report each one's status. The release is not finished until all of them are true: - [ ] `/` and `/beta` serve `version/{version}/` (re-run the curl checks) - [ ] The GitHub release `{version}` is published, not a draft or pre-release, and marked Latest - [ ] The Jira version `{version}` is marked Released - [ ] Unresolved issues on the version were reviewed with the user (step 8) - [ ] The staging announcement is in `#codap-v3` and the production-live reply is in its thread ### Manual Completion Instructions If you prefer to complete deployment outside of Claude Code, run these in order. The GitHub release must not be published until the production deploy has succeeded. ```bash gh workflow run release-v3-production.yml -f version={version} gh workflow run release-v3-beta.yml -f version={version} gh release create {version} --title "Version {version}" --notes "{release_notes_from_phase_2}" ``` The workflows can also be run from the GitHub UI: [production](https://github.com/concord-consortium/codap/actions/workflows/release-v3-production.yml), [beta](https://github.com/concord-consortium/codap/actions/workflows/release-v3-beta.yml). Then mark the Jira version released (CODAPv3 → Releases → `{version}` → Release). ### Resume Later To complete deployment in Claude Code after QA: ``` /codap-v3-build deploy {version} ``` On resume, establish where things stand before acting: which pages serve `{version}` (the curl checks), whether `gh release view {version}` finds a release, and whether the `#codap-v3` announcement exists. Then continue from the first step that isn't done. ## Staging QA Failure — Revised Release **Trigger:** A show-stopper bug is found during Phase 6 staging QA, and a fix has been merged to `main`. **Invocation:** `/codap-v3-build fix {old-version}` (e.g., `/codap-v3-build fix 3.0.5`) When invoked, introduce the situation: > A bug was found during staging QA for **{old-version}** and a fix has been merged. > This workflow will create a revised release. In the pre-release phase that means a new > version number; in the production phase the version stays the same and only the build > number and tag move (see Step 1.3). > > I'll walk you through: > 1. Determine the new version number and release date > 2. Decide whether release notes need updating > 3. Update version files > 4. Build and create a new release PR > 5. Clean up the old tag/release and create new ones > 6. Update Jira and re-deploy to staging ### Step 1: Gather Context 1. **Ensure on main with latest:** ```bash git checkout main git pull ``` 2. **Get current build number and verify the fix is present:** ```bash cat v3/build_number.json git log --oneline {old-version}..HEAD ``` Confirm with the user that the expected fix commit(s) appear in the log. 3. **Determine the new version number — this is phase-dependent** (see Phase 1, step 6, for how to identify the phase): | Phase | Revised release version | |-------|-------------------------| | **Production release** | **The version does not change.** `{old-version}` is reused as-is. Only the build number changes (it increments when the revised release PR merges), and the build number is not part of the version. The respin is reflected in the CHANGELOG, not the version string. | | **Pre-release development** | The version **does** change, because the build number is part of it. Current build number is N; the release PR increments it once more on merge → new version is **N + 1**, matching `{old-version}`'s prefix. Example: build `2804` → `3.0.0-beta.2805`. | > **The production-phase rule reshapes this whole workflow.** With the version > unchanged there is no "old vs new version" to reconcile: `versions.md` needs no > edit, the Jira release needs no rename, and `npm version` is a no-op. What still > must happen is re-tagging — delete the `{version}` tag and recreate it on the new > increment commit (Step 5) — plus any CHANGELOG corrections and a fresh staging > deploy. Read the steps below with that in mind and skip the version-migration > parts; they apply only in the pre-release phase. > > **The steps below are written in `{old-version}` → `{new-version}` terms, which > collapses in the production phase** — the two are the same string. Read every > `{new-version}` as `{version}`. > > **This creates a branch-name collision in the commands below.** They create and push > `release-{new-version}`, which in the production phase is `release-{version}` — the > branch the original release already used, still present locally and on the remote. > **Choose a distinct respin branch name (e.g. `release-{version}-fix`) and substitute > it for `release-{new-version}` everywhere it appears below.** This note calls that name > `{respin-branch}`; in the pre-release phase `{respin-branch}` is just > `release-{new-version}` (no collision, since the version is new). Do not reuse or > force-push the original release branch. > > This production-phase path has **not yet been exercised** as of 3.0.5. Confirm the > approach with the user before running it rather than assuming these notes are > complete. 4. **Confirm release date:** - The original release date (from Phase 1) may no longer be appropriate if QA and the fix took multiple days. - Show the original release date and today's date. - Ask the user to confirm or update the release date. - This date is used in CHANGELOG.md and the Jira release — plus `versions.md` in the pre-release phase, where the row's version string changes. In the production phase the `versions.md` row already carries the right version, so it needs an edit only if the date itself changed. 5. **Confirm with user** — use the wording for the current phase: **Production phase** (version unchanged — do not present this as a version change): > The fix is on main. The version stays **{version}**; the respin changes only the > build number ({old-build} → {new-build}) and moves the `{version}` tag to the new > increment commit. > Release date: **{release-date}** > > Does this look correct? **Pre-release phase** (version changes): > The fix is on main. New version will be **{new-version}** (old was {old-version}). > Release date: **{release-date}** > > Does this look correct? ### Step 2: Release Notes Decision Ask the user: > Do the release notes need to be updated? > > - **No changes needed** — The bug was introduced in this release cycle, so users never saw it > - **Add the fix** — The bug existed in a prior release and the fix should be documented **If no changes needed:** - The existing CHANGELOG content will be reused with only the version number and date updated in the header. **If release notes need updating:** - Walk through the new fix item(s) using the same interactive process as Phase 2, step 5 (present title options, ask for section and title). - Insert the new item(s) into the appropriate section(s) of the existing release notes, maintaining numeric Jira ID order. - Present the updated CHANGELOG entry for approval. - Update Jira Fix Versions for any newly added stories. ### Step 3: Create Release Branch and Update Files Follow the same working directory rules as Phase 3. 1. **Create release branch** (`{respin-branch}` — see the collision note in Step 1.3; in the pre-release phase this is `release-{new-version}`): ```bash git checkout -b {respin-branch} ``` 2. **Sync translations:** - **Always pull** translations from POEditor (Phase 3, step 2c) — every build must pull, since translators may have added or updated non-English strings even when the English strings are unchanged. Do not skip this. - **Only push** English strings (Phase 3, steps 2a–2b) if the bug fix introduced new or changed translatable strings. For most bug fixes there are none, so the push is skipped — but the pull still runs. 3. **Update package.json:** ```bash cd v3 npm version --no-git-tag-version {new-version} ``` 4. **Update versions.md:** - **Replace** the `{old-version}` row with the `{new-version}` row (using the confirmed release date) - Do NOT add a second row — this is a revision, not a separate release 5. **Update CHANGELOG.md:** - **Replace** the `## Version {old-version}` header with `## Version {new-version}`, using the confirmed release date - If release notes content changed (Step 2), update the content as well - The Asset Sizes section will be updated after the build (Step 4) 6. **Commit version file changes:** ```bash cd /path/to/codap git add v3/package.json v3/package-lock.json v3/versions.md v3/CHANGELOG.md git commit -m "Release {new-version}" ``` ### Step 4: Build, Asset Sizes, and Release PR Follow the same process as Phase 4: 1. **Build:** ```bash cd v3 && npm run build ``` 2. **Update asset sizes** in CHANGELOG.md (same process as Phase 4, steps 2–4). - Compare against the **previous release before {old-version}** for % change (since `{old-version}` is being replaced, not used as baseline). 3. **Commit, push, and create PR:** ```bash cd /path/to/codap git add v3/CHANGELOG.md git commit --amend --no-edit git push -u origin {respin-branch} gh pr create \ --title "Release {new-version}" \ --body "{release_notes}" \ --label "v3" \ --label "run regression" ``` (`{respin-branch}` is the distinct respin branch from Step 1.3; in the pre-release phase it is `release-{new-version}`. The PR **title** still uses `{new-version}`, which equals `{version}` in the production phase.) 4. **Inform user:** > **PR created:** {url} > > After CI passes and PR is merged, I'll clean up the old release and create the new one. ### Step 5: After PR Merge — Clean Up and Re-tag **Prerequisite:** Release PR must be merged, **and** the automatic "Increment the build number" commit that follows the merge must have landed on `main`. 1. **Checkout main, pull, and wait for the increment commit:** ```bash git checkout main git pull git log --oneline -2 ``` As in Phase 5, the new tag must point at the **"Increment the build number"** commit that the merge automation pushes after the release merge — not at the `Release {new-version}` merge commit itself. If `HEAD` is still the release merge, wait, `git pull` again, and re-check until the increment commit appears. 2. **Delete the old tag** (gated; get one approval covering this deletion and the push in step 3, and show both tags and the commit the new one will point at): ```bash git push origin --delete {old-version} git tag -d {old-version} ``` The tag is safe to delete because it points to a known-buggy build that was never deployed to production or beta and that no external consumer depends on. **No GitHub release to delete:** under the current flow the GitHub release is only created after a production deploy (Phase 6). Since `{old-version}` failed staging QA, it never reached production and never had a release published. (If one somehow exists, remove it with `gh release delete {old-version} --yes`.) 3. **Create the new tag:** ```bash git tag -a {new-version} -m "Version {new-version}" git log -1 --format='%h %s' {new-version} # expected: Increment the build number git push origin {new-version} git ls-remote --tags origin '{new-version}^{}' # must match git rev-list -n1 {new-version} ``` As in Phase 5, do **not** create the GitHub release here. The tag push triggers the S3 build; the GitHub release for `{new-version}` is published only after the revised build reaches production (Step 6 / the deploy phase). 4. **Delete the merged release branch(es)** (optional cleanup): **Pre-release phase** — the buggy release's branch: ```bash git push origin --delete release-{old-version} git branch -d release-{old-version} ``` **Production phase** — `release-{old-version}` is `release-{version}`, the *original* release's branch (already merged for the first attempt), and `{respin-branch}` is the branch this workflow just merged. Both are now merged and can be removed; the respin branch is the one that would otherwise dangle: ```bash git push origin --delete {respin-branch} && git branch -d {respin-branch} # optionally also remove the original release branch if it still exists: git push origin --delete release-{version} 2>/dev/null; git branch -d release-{version} 2>/dev/null || true ``` 5. **Watch the tag's CI run until the S3 deploy finishes**, exactly as in Phase 5, step 4, with `{new-version}`. Do not trigger the staging workflow until the run has succeeded and `https://codap3.concord.org/version/{new-version}/` returns 200. ### Step 6: Update Jira and Re-deploy 1. **Update Jira:** - **Pre-release phase only:** the Jira release must be renamed from `{old-version}` to `{new-version}`. The Atlassian MCP tools can't edit versions, so ask the user to do it in the Jira UI (CODAPv3 → Releases). - If the release date changed, ask the user to update it in the same place. - If new stories were added to release notes (Step 2), update their Fix Versions. This is a gated Jira edit; delegate it to a subagent (same pattern as Phase 2, step 9). 2. **Re-deploy to staging** (gated) — [dispatch, watch, and verify](#dispatch-watch-verify) `release-v3-staging.yml` with `{new-version}`. 3. **Post the updated announcement** (gated) as a new top-level message in `#codap-v3`, following [Slack Posts](#slack-posts). Use the same format and rules as Phase 6, step 2 (linked Jira keys, `- ` on every item), with a line noting the revised build. Record the new message's `ts`; the production-live reply (Phase 6, step 7) goes in *this* message's thread. ```markdown CODAP {new-version} is available for testing at https://codap3.concord.org/staging. (Revised build — replaces {old-version} which had a staging QA issue.) ### ✨ Features & Improvements: - **[CODAP-XXX](https://concord-consortium.atlassian.net/browse/CODAP-XXX):** Feature title here ### 🐞 Bug Fixes: - **[CODAP-AAA](https://concord-consortium.atlassian.net/browse/CODAP-AAA):** Bug fix title The [beta](https://codap3.concord.org/beta) and [production](https://codap3.concord.org/) URLs will be updated once the staging build passes QA. ``` In the production phase `{new-version}` and `{old-version}` are the same string, so say "revised build of {version}" instead. 4. **Inform user:** > **Revised release {new-version} deployed to staging.** > > Test at: https://codap3.concord.org/index-staging.html > > When staging QA passes, run `/codap-v3-build deploy {new-version}` to continue with production deployment. ## Dispatch, Watch, Verify Use this for every staging, production, and beta deploy. The dispatch itself is gated. `gh workflow run` returns before the new run exists, so `gh run list --limit 1` can return the *previous* run and report a false success. Pick the run created after the dispatch instead: ```bash date -u +%Y-%m-%dT%H:%M:%SZ # note this as gh workflow run .yml -f version={version} gh run list --workflow .yml --event workflow_dispatch \ --json databaseId,createdAt \ --jq '.[] | select(.createdAt >= "") | .databaseId' ``` If no id appears yet, re-run the `gh run list` with the same `` value. If more than one appears, stop and ask. Then: ```bash gh run watch --exit-status ``` A non-zero exit means the deploy failed: stop and show the user `gh run view --log-failed`. **Verify what is served.** A successful run is not proof the page changed. Check the page references the new version folder: ```bash for p in index-staging.html staging "" beta; do printf '%-20s ' "/$p" curl -s "https://codap3.concord.org/$p" | grep -oE 'version/[^/"]+/' | sort -u | tr '\n' ' ' echo done ``` Each workflow updates only its own pages (staging: `/index-staging.html` and `/staging`; production: `/`; beta: `/beta`), so only those are expected to change. The pages are served with `cache-control: no-cache`, so the new version shows as soon as the run finishes. ## Slack Posts Every message to `#codap-v3` (or anyone other than the developer) goes through these steps: 1. **Find the developer's self-DM.** Call `mcp__slack__channels_me` with `channel_types: "im"`; the self-DM is the row named after the developer's own handle ("DM with "). 2. **Post the exact message to the self-DM** with `mcp__slack__conversations_add_message` (`content_type: text/markdown`). This isn't gated. The preview renders exactly as the channel will, which shows collapsed bullets, broken links, or stray formatting before anyone else sees them. 3. **Ask the user to check the preview** and approve posting it to the channel. Claude can't edit or delete a message once it's posted, so any fix happens here. 4. **Post the same text to the channel** (`channel_id: #codap-v3`, plus `thread_ts` for a thread reply). The result names the channel's ID (`C…`) and the message's `ts`; record and report both. A release spans days and often sessions, so also save them where a later session will find them (e.g. Claude's memory for this project), for the production-live thread reply. If the Slack MCP server isn't available, show the user the draft and ask them to paste it into Slack themselves. ## File Locations | File | Purpose | |------|---------| | `v3/build_number.json` | Current build number. Part of the version string **only** in the pre-release phase (Phase 1, step 6). In the production phase it is independent of the version, but its auto-increment commit is always the tag target (Phase 5). | | `v3/package.json` | Version field | | `v3/versions.md` | Version history table | | `v3/CHANGELOG.md` | Release notes | | `v3/dist/assets/` | Built assets (after `npm run build`) | | `v3/src/utilities/translation/lang/en-US.json5` | All English strings (DG + V3, JSON5, source of truth) | ## Jira Integration Use these constants for all Atlassian MCP tool calls: | Constant | Value | |----------|-------| | `cloudId` | `concord-consortium.atlassian.net` | | `projectKey` | `CODAP` | > **Note:** The Atlassian MCP tools accept either a UUID cloud ID or a site URL for the `cloudId` parameter. The site URL format is used here for readability. - Use Atlassian MCP tools for all Jira operations - Stories tagged via `Fix versions` field - Release marked `Released` after production deploy