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/repothat holds your release branches (release cherry-picks always land here).PUBLIC_REPO— optionalowner/repoof a public repo that syncs intoINTERNAL_REPO. Only relevant if your workflow maps public commits to internal ones.REPO_DIR— absolute path to your local checkout ofINTERNAL_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 (defaultpreview 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 (defaultmain).CHERRYPICK_BRANCH_PREFIX— prefix for created cherry-pick branches (defaultcherrypick/).SYNC_TRAILER_KEY— commit trailer key identifying a synced commit's public origin (defaultRepo-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_REPOor (if configured)PUBLIC_REPO.- Set
SOURCE_REPOto the identifiedowner/repovalue for the commands below. - An
INTERNAL_REPOcommit is already the synced commit the user wants to cherry-pick. Use it directly. - A
PUBLIC_REPOcommit or PR must be mapped to its syncedINTERNAL_REPOcommit in Step 2. - For a bare SHA, check whether it exists in
INTERNAL_REPOfirst, thenPUBLIC_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.
- Set
- Source commit hash: Resolve the full SHA in the source repo. If the user gave a PR URL, get its merge commit SHA:
If the PR is not merged, stop and tell the user.gh pr view <PR_NUMBER> --repo "$SOURCE_REPO" --json mergeCommit --jq '.mergeCommit.oid' - Original PR title: If the user provided a PR URL or number, fetch the title for use in the cherry-pick PR title:
If the user only provided a commit hash, use the first line of the source commit message:gh pr view <PR_NUMBER> --repo "$SOURCE_REPO" --json title --jq '.title'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_NNor justv0.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:
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:
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.
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.
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:
git push --set-upstream origin $CP_BRANCH --no-verify
If no conflict, push immediately:
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.
# 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:
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):
CP_LABEL="$(printf '%s' "${CHANNEL:0:1}" | tr '[:lower:]' '[:upper:]')${CHANNEL:1} CP"
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_REPOcommit 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_REPObefore 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).