# Cherrypick To Release

> Resolves a commit and cherry-picks it into one or more client release branches. By default, cherry-picks into BOTH preview and stable. Use when the user asks to cherrypick, backport, or apply a hotfix to a release branch. Optionally waits for a public commit to sync into an internal repo before creating release PRs assigned to the current on-call.

- Skill: `warpdotdev/cherrypick-to-release` (Agent Skill)
- Install (CLI): `npx skillmds@latest add warpdotdev/cherrypick-to-release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/warpdotdev/cherrypick-to-release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: warpdotdev (https://skillmd.com/u/warpdotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/warpdotdev/cherrypick-to-release

---


# cherrypick-to-release

Cherry-pick a commit into release branches and open PRs assigned to the current on-call.

**By default, cherry-pick into every channel in `$RELEASE_CHANNELS` (e.g. `preview` and `stable`).** Only cherry-pick into a single channel if the user explicitly names one in their prompt.

## Configuration

This skill reads environment-specific values from environment variables (see the repo `README.md`):

- `INTERNAL_REPO` — `owner/repo` that holds your release branches (release cherry-picks always land here).
- `PUBLIC_REPO` — optional `owner/repo` of a public repo that syncs into `INTERNAL_REPO`. Only relevant if your workflow maps public commits to internal ones.
- `REPO_DIR` — absolute path to your local checkout of `INTERNAL_REPO`.
- `GITHUB_ORG` — GitHub org used to resolve the on-call's GitHub username.
- `SLACK_BOT_TOKEN` — Slack bot token (used to look up the on-call).
- `ONCALL_SLACK_GROUP` — Slack usergroup handle for the on-call (e.g. `oncall-primary`).
- `RELEASE_CHANNELS` — space-separated, ordered release channels (default `preview stable`). Cherry-picks run in this order.
- `RELEASE_BRANCH_PREFIX` — per-channel branch path prefix; `{channel}` is replaced with the channel name (default `{channel}_release/`).
- `DEFAULT_BRANCH` — internal default branch searched for the synced commit (default `main`).
- `CHERRYPICK_BRANCH_PREFIX` — prefix for created cherry-pick branches (default `cherrypick/`).
- `SYNC_TRAILER_KEY` — commit trailer key identifying a synced commit's public origin (default `Repo-Sync-Origin`).

**All release-branch git operations run inside `INTERNAL_REPO`.** Run `cd "$REPO_DIR"` before fetching internal history or creating cherry-picks. If `REPO_DIR` is not set, ask the user for the local path to their release repo.

## Step 1: Clarify inputs and identify the source repo

Before proceeding, ensure you have:
- **Channels**: Default to **all channels in `$RELEASE_CHANNELS`**. Only use a single channel if the user explicitly requested it.
- **Source repo**: Determine whether the commit comes from `INTERNAL_REPO` or (if configured) `PUBLIC_REPO`.
  - Set `SOURCE_REPO` to the identified `owner/repo` value for the commands below.
  - An `INTERNAL_REPO` commit is already the synced commit the user wants to cherry-pick. Use it directly.
  - A `PUBLIC_REPO` commit or PR must be mapped to its synced `INTERNAL_REPO` commit in Step 2.
  - For a bare SHA, check whether it exists in `INTERNAL_REPO` first, then `PUBLIC_REPO`. If neither repo contains it, ask the user for the source repo or URL.
  - For a bare PR number, ask which repo it belongs to.
- **Source commit hash**: Resolve the full SHA in the source repo. If the user gave a PR URL, get its merge commit SHA:
  ```bash
  gh pr view <PR_NUMBER> --repo "$SOURCE_REPO" --json mergeCommit --jq '.mergeCommit.oid'
  ```
  If the PR is not merged, stop and tell the user.
- **Original PR title**: If the user provided a PR URL or number, fetch the title for use in the cherry-pick PR title:
  ```bash
  gh pr view <PR_NUMBER> --repo "$SOURCE_REPO" --json title --jq '.title'
  ```
  If the user only provided a commit hash, use the first line of the source commit message:
  ```bash
  gh api "repos/$SOURCE_REPO/commits/<SOURCE_COMMIT_HASH>" --jq '.commit.message | split("\n")[0]'
  ```
- **Release branch(es)**: Check whether the user specified a branch name or version string (e.g. `stable_release/v0.YYYY.MM.DD.HH.MM.stable_NN` or just `v0.YYYY.MM.DD`). If so, use it directly and **skip Step 3**.

## Step 2: Resolve the internal cherry-pick commit

The final `COMMIT_HASH` must identify a commit in `INTERNAL_REPO`. Never cherry-pick a public `PUBLIC_REPO` SHA directly into an internal release branch.

### If the source is INTERNAL_REPO

Set `COMMIT_HASH` to the full `INTERNAL_REPO` source commit SHA and continue to Step 3. Do not search for a corresponding public commit.

### If the source is PUBLIC_REPO (optional public↔internal sync)

If your workflow syncs public commits into `INTERNAL_REPO`, the sync typically rewrites commit SHAs and adds a trailer keyed by `$SYNC_TRAILER_KEY` (default `Repo-Sync-Origin`) to the synced internal commit identifying its origin, for example:

```text
Repo-Sync-Origin: <PUBLIC_REPO>@<PUBLIC_SHA>
```

From `"$REPO_DIR"`, fetch and search the internal default branch history for the synced commit. If it is not present yet, poll once per minute for up to one hour:

```bash
PUBLIC_SHA=<full-public-source-commit-sha>
COMMIT_HASH=""

for attempt in {1..60}; do
  git fetch origin "$DEFAULT_BRANCH"
  COMMIT_HASH=$(git log FETCH_HEAD --format='%H' \
    --grep="${SYNC_TRAILER_KEY}: ${PUBLIC_REPO}@$PUBLIC_SHA" -1)
  [[ -n "$COMMIT_HASH" ]] && break
  sleep 60
done
```

If `COMMIT_HASH` is still empty after the final attempt, stop and tell the user that the commit has not synced to `INTERNAL_REPO`. Do not create the cherry-picks until the synced internal commit is available.

## Step 3: Find the latest release branches

**Skip this step if the user already provided a specific branch name or version.**

```bash
git fetch
# For each target channel, resolve its branch prefix from RELEASE_BRANCH_PREFIX
# (replacing {channel}) and pick the newest matching branch. Example for `stable`:
CHANNEL=stable
PREFIX=$(printf '%s' "$RELEASE_BRANCH_PREFIX" | sed "s|{channel}|$CHANNEL|")
git branch -r | grep -oE "${PREFIX}.*" | sort | tail -n 1
```

Run this for each target channel in `$RELEASE_CHANNELS`. If the user gave a version string but not a full branch name, use the grep output to find the matching branch. Show the results to the user and confirm before proceeding.

## Step 4: Run the cherry-picks

**Repeat the following for each target channel**, in the order listed in `$RELEASE_CHANNELS`.

```bash
BRANCH_NAME=<release-branch-from-step-3>
CP_BRANCH="${CHERRYPICK_BRANCH_PREFIX}${BRANCH_NAME}_${COMMIT_HASH:0:7}"

git checkout $BRANCH_NAME
git pull
git checkout -b $CP_BRANCH
git cherry-pick $COMMIT_HASH
```

**If there is a conflict**, stop and tell the user. They must resolve it manually, run `git cherry-pick --continue`, then push:
```bash
git push --set-upstream origin $CP_BRANCH --no-verify
```

If no conflict, push immediately:
```bash
git push --set-upstream origin $CP_BRANCH --no-verify
```

After completing the first channel, proceed to the second channel.

## Step 5: Find the current on-call

Look up the current on-call automatically using the Slack API. Requires `$SLACK_BOT_TOKEN` to be set in the environment. The token needs these OAuth scopes: `usergroups:read`, `users:read`, `users:read.email`.

```bash
# 1. Get the on-call usergroup ID
GROUP_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/usergroups.list" | \
  jq -r --arg handle "$ONCALL_SLACK_GROUP" '.usergroups[] | select(.handle==$handle) | .id')

# 2. Get the current member(s)
USER_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/usergroups.users.list?usergroup=$GROUP_ID" | \
  jq -r '.users[0]')

# 3. Resolve to name and email
curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/users.info?user=$USER_ID" | \
  jq '.user.profile | {name: .real_name, email: .email}'
```

If `$SLACK_BOT_TOKEN` is not set, ask the user: **"Who is the current on-call (`@$ONCALL_SLACK_GROUP` in Slack)?"**

Once you have their name, find their GitHub username with:
```bash
gh api "/orgs/$GITHUB_ORG/members" --paginate --jq '.[] | select(.login | ascii_downcase | contains("<firstname>")) | .login'
```
Confirm with the user if the match is ambiguous.

## Step 6: Open the PRs

**Create a PR for each channel's cherry-pick branch.** For each:

Determine the channel label from the channel name — capitalize it and append ` CP` (e.g. `preview` → `Preview CP`, `stable` → `Stable CP`):

```bash
CP_LABEL="$(printf '%s' "${CHANNEL:0:1}" | tr '[:lower:]' '[:upper:]')${CHANNEL:1} CP"
```

```bash
gh pr create \
  --repo "$INTERNAL_REPO" \
  --base $BRANCH_NAME \
  --head $CP_BRANCH \
  --title "[$CP_LABEL] <original PR title>" \
  --body-file .github/PULL_REQUEST_TEMPLATE/cherrypick.md \
  --reviewer <oncall-github-username>
```

For example, if the original PR title is "Fix crash on startup" and the channel is preview, the title should be: `[Preview CP] Fix crash on startup`.

Then share both PR URLs with the user and notify the on-call.

## Step 7: Update release status

After cherry-pick PRs have been created, invoke the **post-release-status** skill to post or update the release status message in Slack. Pass the release branch from Step 3 and the Slack channel (ask the user if not already known).

## Notes

- By default, cherry-pick into **every channel in `$RELEASE_CHANNELS`**. Only target a single channel if the user explicitly requests it.
- An `INTERNAL_REPO` commit is authoritative. Use it directly rather than looking for a public origin.
- If you use a public↔internal sync, public commits must finish syncing to `INTERNAL_REPO` before release cherry-picks begin.
- After the PRs merge, the on-call must start the release-candidate build on each release branch (see the **cut-new-release-candidate** skill).

