| name | ship-to-prod |
| description | Open a release PR into `main` in this repo. Handles the full flow — resolves the source branch from the user's prompt or the current checkout (never assumes `staging`), gathers commits, pulls Linear ticket references from commit messages and the PRs that merged in, drafts a title and body that match the repo's PR template, picks reviewers, and (after the user confirms) creates the PR with `gh pr create`. Use this whenever the user says anything along the lines of "open a release PR", "ship to prod", "cut a release", "PR into main", "promote this branch", "ship this branch", "release this", or any variant of opening/preparing a PR that targets `main`. Prefer this skill over running `gh pr create` directly — the manual flow misses Linear references, the version-bump check, and reviewer conventions. |
Release PR into main
Use this when the user wants to open a release PR into main in the connectedxm/admin-sdk repo (npm package @connectedxm/admin). The head branch is whatever the user names in their prompt; if they don't name one, fall back to the currently checked-out branch — never assume staging. (Production is main here — the backend repo uses prod, but this SDK ships from main.)
Pushing to main triggers publish.yml, which publishes two packages to npm: @connectedxm/admin (the source SDK) and the generated @connectedxm/admin-sdk (typescript-axios output of sdks/typescript/, regenerated from openapi.json). There is no manual gate — merging this PR auto-publishes both packages.
What you're producing
A draft PR (or, on confirmation, an actual PR via gh pr create) with:
- Base/head: base
main, head = the source branch resolved in step 1 — never invent a branch that the user didn't name and isn't the current checkout
- Title that reflects what's in the diff (see "Writing the title")
- Body matching
.github/pull_request_template.md, with Linear refs harvested from commits AND from any sub-PRs they reference, plus the Semantic Versioning section ticked
- Reviewers drawn from the repo's collaborators (excluding the PR author)
The user has asked to always confirm before creating the PR. Show the draft, wait for the go-ahead, then push (if needed) and call gh pr create.
Workflow
1. Resolve the branch and sync
Pick the head branch for the release PR:
- If the user named a branch in their prompt, use that.
- Otherwise, use the currently checked-out branch (
git rev-parse --abbrev-ref HEAD).
If the resolved branch is main, stop and ask the user which branch to ship — you can't PR main into main. Treat the resolved name as <source> everywhere below.
Then sync and check there's something to ship:
git fetch origin main <source>
git log --oneline origin/main..origin/<source>
If the log is empty, tell the user <source> has nothing ahead of main — there's no PR to open. Stop.
If <source> is behind origin/<source> locally, that's fine — the PR is opened against the remote refs, not your working copy. But mention it so the user knows.
If <source> exists only locally (not on origin), you'll need to push it before gh pr create will work. Note that for step 10; don't push yet.
If the branch already has an open PR against main, mention it (gh pr list --head <source> --base main --state open) and ask whether to add commits to that one or close it first. Don't open a duplicate.
2. Check the version bump (hard gate)
version-check.yml rejects any PR to main whose package.json version isn't strictly greater than main's, in plain x.y.z semver (no pre-release tags, no build metadata).
git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version"
git show origin/<source>:package.json | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version"
Compare the two. If <source>'s version is not strictly greater than main's, stop and tell the user — they need to land a version bump on <source> first (recent precedent: chore: bump version to X.Y.Z commits) before this release PR can pass CI. Don't open the PR; don't bump for them.
If the bump looks right, note which kind it is (major/minor/patch) — you'll tick the matching box in the body's Semantic Versioning section.
3. Gather the commits
git log origin/main..origin/<source> --pretty=format:'%H%x09%s%x09%b%x1e'
The %x1e (ASCII record separator) lets you split multi-line commit bodies cleanly. You want the subject AND the body — Linear references like closes ENG-1901 usually live in the body, not the subject.
Skip merge commits whose subject starts with Merge branch (e.g. Merge branch 'staging', Merge branch 'main') — they're noise from local merges, not a unit of work.
4. Resolve sub-PRs referenced in commit subjects
Squash-merged PRs leave a trail like feat(accounts): expose tier on account responses (#561). The (#NNNN) is a real PR with its own body that often holds the Linear reference. Pull each one:
gh api repos/connectedxm/admin-sdk/pulls/<NNNN> --jq '{title, body}'
Do this for every (#NNNN) you find in the commit subjects in step 3. These bodies feed into the Linear extraction below — and sometimes into the PR description summary, when the squashed commit message is terse.
5. Extract Linear ticket references
Linear refs in this repo look like [A-Z]{2,5}-\d+ — the dominant prefix is ENG, with CXM and others appearing occasionally. They appear as:
closes ENG-1234
ref CXM-1901
- bare
[ENG-1913] in PR titles, e.g. [ENG-1913] ci: gate auto-approve on AUTO_APPROVE_USERS allowlist
Important: don't confuse (#561) (a GitHub PR number) with ENG-1234 (a Linear ticket). Only the latter goes in the Linear Issues section.
linear-check.yml is a hard gate. It runs on every PR to main and requires at least one literal ENG-\d+ somewhere in the PR body. If you can only find CXM-… refs, the workflow will fail — flag this to the user before opening so they can decide whether to add an ENG-… ref or update the workflow.
Collect refs from:
- the body of every commit in step 3
- the title and body of every sub-PR resolved in step 4
Only include refs you actually saw in those sources for this PR. Don't carry refs over from prior release PRs, recent conversation context, or memory of past releases — even if a ticket "feels relevant," it doesn't go in the list unless it's literally present in the commits/PRs you just gathered. Re-listing a ticket that was already shipped in a previous release is a real failure mode and confuses Linear's auto-close on merge.
Dedupe. Preserve the verb the user wrote when possible — if a sub-PR said closes ENG-1901, your output should say closes ENG-1901, not ref. Only fall back to ref if the original said ref or no verb at all.
6. Pick reviewers
There's no CODEOWNERS file in this repo. Build the candidate list at runtime:
gh api repos/connectedxm/admin-sdk/collaborators \
--jq '[.[] | select(.permissions.push == true) | .login]'
gh api user --jq '.login'
Use the unfiltered collaborators endpoint — do not add ?affiliation=direct, because that excludes org admins (e.g., swiftoO is an admin and won't appear with the direct filter). The unfiltered endpoint plus a permissions.push == true jq filter gives you the right candidate set.
Take everyone with push permission, drop the author. Default to requesting all of them. If the user said something specific about reviewers in their prompt ("just Tyler", "skip Spencer"), honor that and don't ask.
If only one candidate remains, just use them — don't bother the user about it.
7. Write the title
Look at what's actually in the diff before reaching for a template. The right title depends on the shape of the changes.
One dominant change: copy the conventional-commit subject from the main commit/PR. Example: feat(accounts): expose tier and tags on account responses.
Several unrelated changes: use a short generic title like Release fixes or a noun phrase that captures the theme. Look at recent release PRs (gh pr list --base main --state merged --limit 10) to match the house style — they oscillate between conventional-commit and short noun phrases, both are fine.
Version-bump-only releases. When the only thing in the diff is chore: bump version to X.Y.Z, a short title like Release X.Y.Z is fine.
Keep it under ~70 characters.
8. Write the body
Use this exact template — it matches .github/pull_request_template.md:
### Description
<2-4 sentence summary of what's shipping. Group by theme if there are several unrelated changes — bullet list is fine when that's clearer than prose. Lead with SDK-visible changes (new hooks, new fields on response/params types, new error codes, OpenAPI schema changes), since this PR triggers an npm publish of both `@connectedxm/admin` and the generated `@connectedxm/admin-sdk`.>
### Linear Issues
- closes ENG-1234
- ref CXM-1901
<one line per ticket; use the verb the source used. Must include at least one ENG-#### or linear-check.yml will fail.>
### Testing
<Generated testing plan — see "Writing the testing plan" below. Do NOT use the template's default `I have manually tested the changes / New integration tests have been added` checkboxes; replace them with concrete steps grounded in the actual diff.>
### Semantic Versioning
Indicate the appropriate version bump for this PR:
- [ ] Breaking changes - Major version bump
- [ ] New features / improvements - Minor version bump
- [ ] Bug fixes - Patch version bump
Tick the Semantic Versioning box that matches the actual version delta you noted in step 2 (X.Y.0 → minor, X.Y.Z patch bump only → patch, major version bump → major). If multiple things shipped, tick the highest-impact box (one breaking change makes the whole release major).
Writing the testing plan. Replace the template's default Testing checkboxes with a per-PR plan derived from what actually changed. The reviewer should be able to read the plan and know exactly what to verify before merging triggers the npm publish. Aim for 3–7 concrete checkbox items.
Walk the file diff (git diff --name-only origin/main..origin/<source>) and turn it into specific verification steps. This is a TypeScript SDK consumed by other apps (notably admin-web), so most verification happens via type-check + a smoke pass from a consumer. Note: this repo also generates a second SDK in sdks/typescript/ from openapi.json via npm run generate / npm run generate:sdks — interface and param changes can affect what shows up there too.
- Query files (
src/queries/<resource>/use*.ts) → "Confirm useGet* returns the new field/shape; spot-check the query key and that 401/403/404/460/461 routes through onNotAuthorized / onNotFound / onModuleForbidden."
- Mutation files (
src/mutations/<resource>/use*.ts) → "Run the mutation from a consumer app and confirm the listed query keys invalidate / SET_*_QUERY_DATA updates as expected; on failure confirm onMutationError fires."
- Interface changes (
src/interfaces.ts) → "Run npm run lint and npx tsc (configured noEmit); npm pack the SDK and install into admin-web — confirm the new fields surface in IDE completions and tsc is clean on the consumer side. Run npm run generate and spot-check openapi.json if the change should appear in the generated SDK."
- Param/input shape changes (
src/params.ts) → "Confirm callers in admin-web still type-check against the new params; if a field became required, the consumer-side update needs to land before publish."
ConnectedXMProvider / context changes (src/ConnectedXMProvider.tsx, src/hooks/useConnectedXM.ts) → "Mount a consumer with the updated provider and confirm adminApiParams, organizationId, getToken, getExecuteAs, and the onNotAuthorized / onModuleForbidden / onNotFound / onMutationError callbacks still wire through."
AdminAPI / axios changes (src/AdminAPI.ts) → "Confirm GetAdminAPI(params) returns an axios instance with the expected baseURL, organization, authorization, api-key, and executeAs headers (when set)."
- OpenAPI generator changes (
scripts/generate.ts, scripts/generate-sdks.ts) → "Run npm run generate and inspect openapi.json for the expected diff; if the SDK output is in scope, run npm run generate:sdks and confirm sdks/typescript/ regenerates cleanly. Note: the PR triggers generate-openapi.yml, which re-runs the generator on the PR branch."
- Index/barrel changes (
src/index.ts, per-folder index.ts) → "If files were added/moved, confirm npm run exports was run (per .cursor/rules/index-exports.mdc — don't hand-edit). Then npm run build and inspect dist/index.d.ts to confirm the new symbols are exported."
- CI workflow changes (
.github/workflows/) → "Confirm tests.yml, linear-check.yml, version-check.yml, publish.yml, approve.yml, or generate-openapi.yml pass on this branch."
- Publish gate → "After merge, confirm
publish.yml runs cleanly on main and both @connectedxm/admin@X.Y.Z and the generated @connectedxm/admin-sdk@X.Y.Z are live on npm."
Group related items where it tightens the plan. If a single domain dominates the diff (e.g., the whole PR is account/auth polish), it's fine to have all 3–7 items in that domain. If you genuinely cannot derive a plan from the diff (rare — usually means something's wrong with how you read the commits), fall back to the original checkbox pair, but flag this to the user when presenting the draft.
Format as a markdown checklist:
### Testing
- [ ] Run `npm run lint` and `npm run build` and confirm both pass cleanly.
- [ ] `npm pack` the SDK and install into an `admin-web` checkout — confirm `tsc` passes on the consumer with the new types.
- [ ] On a staging consumer, exercise the affected mutation/query and confirm the new fields land in the cache.
- [ ] Run `npm run generate` and confirm `openapi.json` reflects the interface/param diff (CI's `generate-openapi.yml` will also run this on the PR branch).
- [ ] After merge, confirm `publish.yml` succeeds and both `@connectedxm/admin@X.Y.Z` and `@connectedxm/admin-sdk@X.Y.Z` appear on npm.
If there are zero Linear references, omit the bullet list and write - (none) under the Linear Issues heading rather than deleting the section — keeps the template recognizable. (But: linear-check.yml requires at least one ENG-#### in the body, so this case will fail CI. Flag it to the user before opening.)
9. Show the draft and wait
Print the title, body, reviewer list, the version delta from step 2, AND the push state from step 1 back to the user, clearly labelled. If a push is needed (<source> not on origin or local commits ahead of origin/<source>), state explicitly that creating the PR will push first. Ask whether to proceed, modify, or cancel. Don't run git push or gh pr create until they've said yes.
The reason for confirming: the title and body are interpretive — you're summarizing several commits' worth of work. The user knows things you don't (which change is the headline feature, which is incidental cleanup, whether the Semantic Versioning box is right, whether something is already covered by another ticket). Five seconds of their attention up front beats editing the PR after the fact — especially since merging this PR triggers an npm publish.
10. Push the branch (if needed) and create the PR
After confirmation:
git push -u origin <source>
git push
gh pr create \
--base main \
--head <source> \
--title "<title>" \
--reviewer "<comma,separated,logins>" \
--body "$(cat <<'EOF'
<body here>
EOF
)"
Return the PR URL from the command output so the user can click straight to it.
Things to watch out for
main, not prod. This repo's production branch is main (the backend repo uses prod). Don't paste backend instincts here — gh pr create --base prod will fail.
- Don't assume
staging. Most feature work now branches off main and PRs directly back into main. Resolve the head branch from the user's prompt or the current checkout — never default to staging.
- Merging this PR publishes two packages to npm.
publish.yml runs on push to main and publishes both @connectedxm/admin (the source SDK) and @connectedxm/admin-sdk (the typescript-axios output regenerated from openapi.json). There is no manual gate. Take the testing plan seriously.
- The version bump is a hard gate. If
<source>'s package.json version isn't strictly greater than main's in plain x.y.z semver, version-check.yml will fail. Don't open the PR until the bump is on the source branch.
linear-check.yml requires ENG-#### in the body. A CXM-####-only body will fail CI. If you only found CXM refs, surface that to the user before opening.
generate-openapi.yml runs on PRs to main. It regenerates openapi.json from the source. If the PR branch's openapi.json is stale relative to the source, expect this workflow to push a regen commit or fail. Don't be surprised by the extra activity on the PR.
- Don't paste
<!-- CURSOR_SUMMARY --> blocks from sub-PR bodies into the new PR body. Those are auto-generated by the Cursor Bugbot reviewer and re-including them looks weird.
- Don't include the HTML comment placeholders from the PR template (
<!-- A brief description -->) in your output — replace them with real content.
- Keep the Semantic Versioning section. Don't drop it from the body — the template explicitly includes it. Tick the box that matches the actual version bump.
closes vs ref: closes auto-closes the Linear ticket on merge to main. ref just links it. Preserve whichever the source commit/PR used. When in doubt, ref is the safer default — it doesn't change ticket state.
- Bot reviews are not human reviews. Don't infer reviewer preferences from
github-actions or cursor reviews on past PRs — those are automated. Look at human reviewers (authorAssociation: MEMBER) only.
- The author isn't a reviewer. GitHub will reject
--reviewer <self>. Always exclude gh api user --jq '.login' from the list.
- Don't push without confirmation. Pushing exposes work to the team and triggers CI. Bundle it with PR creation behind the user's go-ahead.
Why this exists
Opening these PRs by hand consistently misses Linear references buried in sub-PR bodies, drops the Semantic Versioning section, and either skips the version-bump check or has to back-patch it after CI fails. Because merging this PR auto-publishes two npm packages, the cost of a botched release is higher than for an app deploy — there's no easy "revert" once a version is live. Hitting the gh API once per sub-PR, comparing versions up front, and templating the body is mechanical work that's easy to get wrong when done manually but easy to get right in a script-shaped flow.