name: release
description: >
Drives php bin/console monorepo:release end-to-end. Runs pre-flight checks, starts
the releaser, watches for confirm prompts, auto-presses Enter on prompts that are
safe (waitFor already verified readiness, or ceremonial), and surfaces judgement
calls to the operator via AskUserQuestion. Use when the user invokes /release.
user_invocable: true
version: 1.1.0
Release Skill
Orchestrates a single stage of the Shopsys release process. The releaser CLI keeps doing the work; this skill drives it and decides what to do at each confirm prompt.
Invocation
/release <version> --stage <stage> --initial-branch <branch> [--resume-step N] [--dry-run]
<version>— e.g.v19.1.0orv19.0.1-rc1.<stage>— exactly one ofrelease-candidate,release,after-release.<initial-branch>— e.g.19.0.--resume-step Nand--dry-runare passed through unchanged tomonorepo:release.
Reject any other stage with a clear message. The skill does only one stage per invocation.
Phase 1 — Pre-flight (fail fast, do not auto-fix)
Run all of these; abort with the specific fix instruction if any fail. Never start/stop containers, never modify Docker state.
git status --porcelainempty ANDgit rev-parse --abbrev-ref HEADequals<initial-branch>(release-candidate stage) or the rc branch (release / after-release stages — the branch name isrc-<webalized-version>; if unsure, surface it to the operator).- Container git status matches host's:
docker compose exec -T php-fpm git status --porcelainis empty too. Host and container can diverge when mutagen ignore patterns affect tracked files, or when files are globally-gitignored on the host but not in the repo. If the container reports changes the host doesn't, fix the divergence first — otherwise the releaser'sgit add .calls inside the container will commit unintended files. For per-clone fixes that don't touch tracked state, append paths to.git/info/exclude(mutagen syncs.git/so the container sees the same rules). gh auth tokenreturns a non-empty token (the value is passed to the releaser via the--github-tokenCLI option, see Phase 2). Ifgh auth tokenfails, surface and abort.docker compose ps --status running --format '{{.Service}}'containsphp-fpmandpostgres.- macOS only:
mutagen sync list 2>/dev/null | grep -q Watching. docker compose exec -T php-fpm test -f /home/www-data/.gitconfig.gh auth statussucceeds.
If anything fails, print the offending check and the exact command the operator should run, then stop.
Phase 2 — Run the releaser
See Releasing a new version of Shopsys Platform for the up-to-date manual release process this skill automates.
The releaser is interactive — it prints prompts of the shape <info>…</info> [<comment>Enter</comment>] and blocks on stdin. The skill drives it through a named pipe so it can both stream output and inject Enter.
PIPE="/tmp/release-stdin-$$"
LOG="/tmp/release-out-$$"
mkfifo "$PIPE"
# keep the pipe open for writing so the releaser doesn't see EOF
( while sleep 86400; do :; done ) > "$PIPE" &
KEEPALIVE=$!
docker compose exec -T php-fpm \
php bin/console monorepo:release <version> \
--stage <stage> --initial-branch <initial-branch> \
--github-token "$(gh auth token)" -v \
[--resume-step N] [--dry-run] \
< "$PIPE" > "$LOG" 2>&1
Important — do not pipe tail -f "$PIPE" into docker compose exec. On macOS, BSD tail block-buffers stdout when piped, so a single echo "" > "$PIPE" never flushes through to the container and the releaser appears hung at the first prompt. Redirecting docker exec's stdin directly from the FIFO (< "$PIPE") has no buffering layer in between.
Run that with run_in_background: true. Then loop:
- Prefer
Monitoron the bg bash to stream stdout line-by-line. Fallback:tail -c +$OFFSET "$LOG"between iterations, tracking byte offset. - Echo every new line to the operator as one-line status (helps them watch from the chat).
- When you see the prompt line, decide per the table in Phase 3. Press Enter by
echo "" > "$PIPE". The prompt line matches either of these (ANSI is stripped under-T, so both forms are possible):- ANSI-stripped:
^\s*(.+?)\s*\[Enter\]\s*:?\s*$ - With Symfony tags:
^\s*<info>(.+?)</info>\s*\[<comment>Enter</comment>\]\s*:?\s*$
- ANSI-stripped:
- Stop when the log contains
Stage "<stage>" for version "<version>" is now finished!, or when the bg process exits. - On exit, run a thorough cleanup. The bg bash exiting doesn't reap its descendants — orphaned
tail,sleep 86400, anddocker compose execchildren get reparented to PID 1 and keep holding the FIFO / docker socket:kill $KEEPALIVE 2>/dev/null kill $RELEASER_PID 2>/dev/null # Sweep any orphans the bg bash didn't reap ps -ef | grep -E 'monorepo:release|sleep 86400|tail.*release-stdin' | grep -v grep \ | awk '{print $2}' | xargs -I{} kill -9 {} 2>/dev/null # If the container's PHP releaser is still alive, kill it inside the container too docker compose exec -T php-fpm bash -c 'pgrep -f "monorepo:release" | xargs -r kill -TERM' 2>/dev/null rm -f "$PIPE" "$LOG"
If a prompt does not appear in the table, surface it to the operator. Never blindly press Enter on something you don't recognize.
Phase 3 — Prompt handler table
The releaser prints each step as <N>/<TOTAL>) <description> before the worker runs. Use that line + the prompt text together to pick a handler. Categories:
A. Auto-press Enter
Ceremonial or already-verified prompts. Press Enter without asking.
BeHappyReleaseWorker— after-release final step. Auto-press.CheckCopyrightYearReleaseWorker— if you've already verified the year (Phase 4 side-task). Otherwise surface.CheckLatestVersionOfReleaserReleaseWorker— comparesutils/releaser/on<initial-branch>against the latest supported release branch (e.g. when releasing14.5, diff against the most recent19.0-style branch, NOT against<initial-branch>). Auto-press only ifgit diff origin/<latest-branch> origin/<initial-branch> -- utils/releaser/is empty. Otherwise surface so the operator can backport the latest releaser changes first.ResolveDocsTodoReleaseWorker— ifgrep -rln '<!--- TODO' --include='*.md' .returns nothing. Otherwise surface.
Never auto-press the fallback confirm("Continue when \"…\" is satisfied") printed by waitFor() after MAX_WAIT_SECONDS — that one means polling gave up. Surface it.
B. Side-task then surface (skill does the safe automatable work; operator confirms)
Run the listed work, report results, ask the operator to confirm before Enter is sent. Never post, never commit, never push.
StopMergingReleaseWorker/EnableMergingReleaseWorker/PostInfoToSlackReleaseWorker— run the Slack message draft agent and print the result in chat. Operator copies and posts to#sho_group_sspmanually.EnsureReleaseHighlightsPostIsReleasedReleaseWorker—WebFetch https://blog.shopsys.comand look for the highlights post; report what you found.CheckReleaseBlogPostReleaseWorker— same probe. The blog post itself has to be drafted in the CMS by the marketing team, not by this skill — only report what's currently published and let the operator follow up.EnsureRoadmapIsUpdatedReleaseWorker— open the roadmap URL viaWebFetch, report current state; operator updates Jira manually.UpdateUpgradeReleaseWorkerpost-commit sanity check — after the worker commits, run the UPGRADE sanity-check agent. If clean, auto-press the threeconfirm()prompts in sequence. If issues are found, surface them.UpdateChangelogReleaseWorker— call/release-fetch-changelog <version> --target-branch <initial-branch>to fetch and insert the GitHub-generated notes intoCHANGELOG-<major>.<minor>.md. On success, report the inserted block and ask the operator to confirm before pressing Enter (the worker runsphing markdown-fixand commits after Enter). If the sub-skill fails, fall back to category C (surface only). If the operator wants to audit the auto-generated section/PR classification, batch proposed moves 4 perAskUserQuestioncall (the tool's max); "Other Changes" is the catch-all where misclassifications cluster, so start there.
C. Surface only (judgement call, no useful side-task)
Call AskUserQuestion with options Done — press Enter / Pause — exit and resume later. On Pause, kill the bg process cleanly and tell the operator to re-run with --resume-step <N>.
SendBranchForReviewAndTestsReleaseWorkerCheckShopsysInstallReleaseWorker(both stages)VerifyCliIsRunningReleaseWorkerVerifyMinorUpgradeReleaseWorkerMergeReleaseCandidateBranchReleaseWorkerCheckReleaseDraftAndReleaseItReleaseWorkerCreateAndCommitLockFilesReleaseWorker/RemoveLockFilesReleaseWorker(the manual push)CreateAndPushGitTagReleaseWorker/CreateAndPushGitTagsExceptProjectBaseReleaseWorkerCheckDocsReleaseWorkerForceYourBranchSplitReleaseWorker— pushes the local rc branch (rc-<webalized-version>) to origin, then dispatchesmonorepo-force-split-branch.yamlvia GH API withref+inputs.branch_name=$this->currentBranchName(the rc branch). The workflow checks outrefs/heads/<rc>from origin and runssplit-repositories.shagainst it, so the split repos receive the release-prep commits (CHANGELOG, UPGRADE notes, mutual deps, framework version). Theassert_split_branch_is_not_protectedguard on^[0-9]+\.[0-9]+$is fine —rc-19-0-0-style names don't trigger it. If the rc push or dispatch fails (auth, conflicts, GH 422), surface immediately with--resume-step <next>and pause options.TestYourBranchLocallyReleaseWorker— surface only. After any test failure, the worker re-promptsRun the checks again? [yes]:. Treat each rerun as a fresh round — surface, ask the operator, never auto-press. If CI is green on the same tree but local fails repeatedly, drift in the local env (container state, custom Postgres functions, stale per-packagevendor/, accumulated Elasticsearch state) is the likely cause; CI is the source of truth and trusting it (skip via--resume-step <next>) is a defensible call.- Anything not listed above — default to surface.
Polling status lines
waitFor() prints attempt N: <progressDescription>; sleeping Ms. Echo each to the operator as one short line; do not treat them as prompts.
Phase 4 — Side-task helpers
These are the safe checks the skill runs locally while the releaser is between prompts or during a long waitFor():
- Copyright year:
grep -rE "Copyright[^0-9]{0,20}[0-9]{4}" LICENSEand compare todate +%Y. (Note: the Shopsys License document carries aversion YYYY-MM-DDline, not a copyright year — there may be nothing to verify; that's fine, the worker just wants acknowledgement.) - Releaser parity:
git diff --stat origin/<latest-branch> origin/<initial-branch> -- utils/releaser/where<latest-branch>is the most recent supported release branch (e.g.19.0at the moment). The point is to bring the latest releaser changes onto older release branches, so always diff against the newest branch, not against<initial-branch>itself. - Docs TODOs:
grep -rln '<!--- TODO' --include='*.md' docs/. - Last tag date (for blog-draft input):
git log -1 --format=%cI $(git describe --tags --abbrev=0 origin/<initial-branch>). - Packages on Packagist missing CI workflows:
for p in packages/*/; do [ ! -f "$p/.github/workflows/run-checks-tests.yaml" ] && echo "no CI: $(basename $p)"; done. Any such package that's on Packagist appears inGithubActionsStatusReporteras "still pending" forever — the GH-Actions polling check at step 5 will stall until the 2h timeout. Verify each one is listed inGithubActionsStatusReporter::IGNORED_PACKAGES; if not, add it before launching. - Per-package vendor staleness: each
packages/*/may carry its owncomposer.lock+vendor/. The framework's test bootstrap typically prefers per-package vendor over root vendor. If a package'scomposer.jsonconstraint was bumped but its lock predates the bump,phing testsexercises the older vendor and tests can fail in ways CI never sees. If a specific package's functional/unit tests fail locally while CI is green on the same tree, suspect a stalepackages/<pkg>/vendor/and move it aside (mv packages/<pkg>/vendor /tmp/<pkg>-vendor-bak) so the bootstrap falls back to root. - Personal/tool artifacts: untracked files visible to the container's
git status(e.g..claude/settings.json,.playwright-mcp/) will be picked up by any workergit add .and committed. Add to.git/info/exclude(per-clone, syncs to container via mutagen):printf '\n.playwright-mcp/\n/.claude/settings.json\n' >> .git/info/exclude
Mutagen sync reload (macOS only; when you've edited mutagen.yml/mutagen.yml.dist and need ignore patterns to re-evaluate):
mutagen project terminate && mutagen project start
This reloads config without touching containers. Do NOT run the project's mutagen-up.sh — it also performs docker compose up --force-recreate, which kills any paused bg releaser.
Report results inline so the operator can decide.
Constraints
- Never start, stop, or
docker compose up/downcontainers (perAGENTS.md). - Never commit, amend, push, or create tags. The releaser does its own commits; pushes are always operator-driven.
- Never post to Slack, Jira, GitHub, or any external system on the user's behalf. Draft → chat → operator copies.
- Never auto-press Enter on a prompt that is not explicitly in category A.
- Never skip pre-flight failures.
UPGRADE sanity-check agent
Invoked from category B when UpdateUpgradeReleaseWorker finishes its commit and the releaser prints its three confirm() prompts. Goal: decide if it's safe to auto-press Enter on all three, or if the operator needs to fix something first.
Spawn a single Agent (subagent_type general-purpose) with this prompt, then act on its JSON output.
You're checking the staged changes to
UPGRADE-<initial-branch>.mdfor guideline violations before the release script proceeds. Important:UpdateUpgradeReleaseWorkerrunsphing upgrade-merge+ version replacement +git add ., but commits AFTER all threeconfirm()prompts. At the moment this agent runs, the changes are staged, not yet committed — inspect viagit diff --cached -- UPGRADE-<initial-branch>.md(NOTgit show HEAD). Re-readdocs/contributing/guidelines-for-writing-upgrade.mdfirst — it's the source of truth. Then examine the staged diff and check:
- PR-link integrity — every
#<number>link togithub.com/shopsys/shopsys/pull/<n>points to a real merged PR. Usegh pr view <n> --json state,mergedAtper link. Closed-without-merge or non-existent → error.- Section ordering — sections in the new block follow the canonical order from the guidelines. Misordered → error.
- Duplicate docker instructions — flag any two subsections with near-identical "modify your
docker-compose.yml" blocks.#project-base-diffplaceholders —grep -n '#project-base-diff' UPGRADE-<initial-branch>.md; remaining occurrences at the time of the first confirm are expected (the worker replaces them between confirms 1 and 2) — treat as a warning so the operator knows what's still pending, not an error. If they're still present at the time of confirm 3, then escalate to error.- Branch correctness in links — every URL to
github.com/shopsys/project-base/...references<initial-branch>(or the right tag/commit), notmaster. Wrong-branch → warning.Return a single JSON object:
{"clean": true|false, "issues": [{"severity": "error"|"warning", "section": "...", "line": N, "message": "..."}], "summary": "..."}. Nothing else.
After the agent returns:
clean: true→ press Enter on each of the three confirms as they appear.clean: false→ useAskUserQuestionwith options:- "Pause — let me fix" (kill the bg process, tell the operator to fix and resume via
--resume-step <N>). - "Acknowledge and proceed" (operator owns the risk; press Enter on the three confirms).
- "Show details" (print the
issueslist and re-ask).
- "Pause — let me fix" (kill the bg process, tell the operator to fix and resume via
Slack message draft agent
Invoked from category B for the three Slack workers. Produces a single message body that the operator pastes into Slack. The skill never posts to Slack on the operator's behalf.
Spawn a single Agent (subagent_type general-purpose) with this prompt, substituting <variant> for the worker name:
Draft a single Slack message for
#sho_group_sspabout the Shopsys release<version>on initial branch<initial-branch>, release date<YYYY-MM-DD>. Variant:<variant>.
StopMergingReleaseWorker— announce that PRs targeting<initial-branch>are temporarily frozen for the release cut, and the freeze will be lifted once the release ships.EnableMergingReleaseWorker— announce that the freeze on<initial-branch>is lifted and merging is allowed again.PostInfoToSlackReleaseWorker— announce the release: include the version, a link tohttps://github.com/shopsys/shopsys/releases/tag/<version>, a one-line summary of the headline change (usegh pr list --base <initial-branch> --state merged --search 'merged:>=<last-tag-date>'to find it), and a link toUPGRADE-<initial-branch>.md.Keep it short — two to four lines. Plain text only, no code fences, no preamble, no trailing notes. Output the message body and nothing else.
Print the agent's output verbatim to chat, then use the standard category-B confirm — operator pastes the message to Slack and presses "Done — press Enter".