Release Publish Trigger
Operationalises spec/project/release-skill-layer/<canonical_language>.md §"Skill B — Release publish trigger": validates every release-automation §Pre-publish verification gate locally, then dispatches release-publish.yml via gh workflow run. The workflow remains the audit-trail point for the Draft → Published transition; this skill is the local pre-flight plus dispatch.
Why this is a skill, not an agent
- Externally-visible mutation gates on user confirmation —
gh workflow run release-publish.yml triggers an external publish chain (release published → release-cd-refresh-master.yml → main fast-forward → packaging workflows); mid-flow operator gating is core to the contract and would be lost in an agent's fire-and-forget shape.
- Output flows back into the main conversation — the gate-by-gate validation report, the dispatch URL, and the post-dispatch status all surface in the conversation so the operator can decide.
- Orchestrator that routes to other skills on failure — red required checks route to
workflow-health triage; the skill-orchestrates pattern (per skill-vs-agent) defaults the orchestrator to skill form.
- Counter-dimension considered: a tool-restricted agent (read + a single
gh Bash) could perform the verification half cleanly, but the dispatch decision needs the operator in the loop and the skill stays in the conversation to surface the run URL and follow-up status — keeping the whole flow in one skill is simpler than a forced split.
User-language policy
Detect the operator's language and respond in it. All git, gh api, and gh workflow run invocations stay English so that release-publish.yml's job summary, release-drafter's draft body, and downstream automation stay consistent across the portfolio.
German trigger phrases
The frontmatter description keeps the trigger lexicon English-only per spec/claude/skill-management/ §Structure (plugin-distributed skills). Treat the following German paraphrases as equivalent and discoverable through this skill:
- "veröffentliche das Release"
- "stoße den Release-Publish an"
- "ship das Release"
Preconditions
Before doing anything:
- Confirm the working directory is a git repository (
git rev-parse --is-inside-work-tree) and the remote resolves to a GitHub repository.
- Confirm
gh is authenticated (gh auth status).
- Locate
spec/project/release-skill-layer/ and spec/project/release-automation/ — either in the target repo or via the nolte-shared plugin install path. Stop and ask if neither is reachable.
- Confirm the repo's default branch (
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) is the integration branch the spec applies to (typically develop).
Operations
Operation 1 resolves the open draft and Operation 2 detects the project type (the index into release-automation §Version-bearing files, shared with Skill A per the spec's §Skill split and shared shape MUST). Operations 3 to 5 then form a Plan-validate-execute cycle: Operation 3 walks every pre-publish gate, Operation 4 surfaces the validated state for explicit operator confirmation, and Operation 5 dispatches release-publish.yml. Operation 6 follows the dispatched run until it leaves the queue and reports from there; a landed dispatch isn't the skill's terminal state, because a queued run can still be superseded without publishing.
1. Resolve the open draft
- Run
gh release list --json isDraft,tagName,targetCommitish,createdAt,name.
- Filter to drafts whose
targetCommitish equals the default branch.
- Refuse and report when zero drafts match (operator should run
release-drafter.yml first) or when more than one matches without an explicit --tag argument from the operator (no "newest wins" heuristic, per release-automation §Operational contract).
Tooling (optional GitHub MCP) — go/no-go: GO (narrow). Prefer github:list_releases for the draft resolution here (Op 1) and github:get_release_by_tag for the post-publish verify (Op 6), falling back to the gh commands shown, per spec/claude/mcp-tool-preference/. The release-publish.yml dispatch, the required-check-runs gate, and all workflow-run status reads have no MCP tool and stay gh (OQ-D); the reachability / version-file / alignment gates stay local git. gh/git stays authoritative; output is identical.
2. Detect project type
Per spec/project/release-skill-layer/ §Skill split and shared shape (MUST), both release-layer skills follow the same six project-type detection signals used by github-issue-templates-apply and by release-notes-curate (Skill A). Walk these six signals in order and stop at the first match. Read the files via the standard read tools — never via filename heuristics alone:
- Claude Code plugin —
.claude-plugin/plugin.json exists; top-level skills/ and / or agents/ folder present.
- Python application —
pyproject.toml declares [project.scripts] (or equivalent application entry point), no library distribution metadata.
- Python library —
pyproject.toml declares a distributable package without an application entry point.
- Node / TypeScript library or app —
package.json exists; main / exports indicates library, bin / scripts.start indicates app.
- CLI tool — declared CLI entry point in
pyproject.toml ([project.scripts]), package.json (bin), or Cargo.toml ([[bin]]).
- Documentation-only repo —
mkdocs.yml, docusaurus.config.*, or similar exists with no application source.
When .github/release-skill-layer.yml declares an explicit project_type: value, use it instead of the autodetection (override path) — the same override that Skill A honours.
When no signal matches, stop and ask the operator to declare the project type manually. Never proceed with a generic fallback.
The detected type is the index into release-automation §Version-bearing files: the "Version-bearing files aligned" gate uses it to select the correct default version-bearing-file rows for this repo (Claude Code plugin, Python package, Node.js package, HACS integration, etc.) before reading and comparing each file.
3. Validate every pre-publish gate
Walk these gates in order. The skill MUST NOT proceed past a failed gate; surface the gate name, the failure detail, and the remediation path.
1. Draft tag reachable from develop
git fetch origin develop.
- Resolve the draft's
targetCommitish SHA.
- Run
git merge-base --is-ancestor <target-sha> origin/develop.
- Failure: the draft is stale relative to
develop. Remediation: re-run release-drafter.yml to refresh the draft target.
2. Version-bearing files aligned
- Read the version-bearing file list per
release-automation §Version-bearing files: select the default-table rows for the project type detected in step 2, or use the override at .github/release-automation.yml when the repo declares one.
- For every declared file, read the value at the
target-sha (git show <target-sha>:<path> plus the spec's selector) and compare against the target tag under the file's value transform (typically "strip leading v" if the existing convention omits it).
- Failure: any file whose value does not equal the target tag under transform. Remediation: open a
chore(release): <tag> PR (fallback path) or wait for the workflow-driven primary path to land its alignment commit.
3. Alignment commit present
- Identify the most recent commit on
develop that touched any version-bearing file: git log -1 --pretty=%s --follow -- <path>.
- Verify the subject prefix starts with
chore(release): <tag> (the prefix-match accepts the (#N) suffix GitHub appends on squash-merge, per release-automation §Pre-publish verification).
- Failure: no
chore(release): <tag> commit on the path. Remediation: same as the "Version-bearing files aligned" gate.
4. Required status checks SUCCESS where they were enforced
Evaluate this gate on the pull-request head the develop tip squash-merges, not on the tip.
- Read the required check list from
.github/settings.yml (branches[name=develop].protection.required_status_checks.contexts).
- Resolve the commit to judge:
gh api repos/<owner>/<repo>/commits/<tip-sha>/pulls --jq '.[0].head.sha'.
- A SHA comes back → that is the commit to query, and report it to the operator alongside the tip so the substitution is visible.
- Nothing comes back (a direct push, or a merge the API does not associate) → fall back to the tip SHA and say so in the report.
- Query
gh api repos/<owner>/<repo>/commits/<sha>/check-runs for the resolved SHA and confirm every required context appears with conclusion=success.
- Failure: any required check is
failure, cancelled, timed_out, still in_progress, or absent. Remediation:
- red checks → route to
workflow-health triage (classify as defect / flake / infra / stale pin / secret drift / other); never retry the dispatch blindly.
- pending checks → stop and ask the operator to wait, or opt in to wait mode (see "Wait mode" below).
- absent on a resolved pull-request head → a genuine gap: branch protection should have required it there. Report it as a protection-configuration problem, not as a missing run.
- absent on a fallback tip → the repository published from a commit on which its required contexts were never enforced. Refuse, and say that plainly rather than treating absence as green.
Why not the tip. Branch protection enforces its contexts on the pull-request head, which is where the always-run constructions that guarantee a report live. The tip is a different commit, produced by squash, and only receives whatever push-triggered workflows apply to it — so a required check whose push trigger carries a paths: filter is simply absent whenever the merge touched none of those paths. That is the normal case, not an edge one. Reading absence strictly blocks most releases; reading it leniently validates a subset while appearing to validate the set. Neither is a gate.
The tip's tree is still the tree those checks ran against: pull-request-workflow §Branch freshness requires required_status_checks.strict: true, so a pull request can only merge while up to date with develop. If a repository has turned strict off, say so in the report — the substitution's justification does not hold there.
5. Workflow file present
- Confirm
.github/workflows/release-publish.yml exists in the repo.
- Failure: the operator should adopt
release-automation first; this skill stops and reports.
4. Disclose the validated state and confirm
Before dispatch, surface to the operator as a single block:
- target tag and
target_commitish SHA;
- result of each gate (PASS / FAIL with detail);
- version-bearing-file diff summary (file, current value, target value);
- audience-coverage summary when the draft body carries the
release-skill-layer:project-context-start marker (Skill A has run); a non-blocking note offering to dispatch release-notes-curate first when the marker is absent;
- the exact
gh workflow run invocation that will be issued.
Block the dispatch until the operator confirms.
5. Dispatch
On confirmation:
- Run
gh workflow run release-publish.yml --ref develop -f tag=<tag>. The tag input is mandatory regardless of how many drafts are open (per release-automation §Operational contract — no "newest wins" heuristic in the workflow either).
- When the operator opts in to
--dry-run, dispatch with -f dry_run=true so the workflow validates without flipping draft: false.
- Never call
gh release edit --draft=false, gh api -X PATCH /repos/.../releases/<id> with draft=false, or any other body that flips the draft state from this skill. The workflow is the only path.
6. Follow the run out of the queue
Immediately after gh workflow run returns:
- Find the new run:
gh run list --workflow=release-publish.yml --limit 1 --json databaseId,status,conclusion,url,headSha. The match is the run whose headSha equals the draft's target SHA and whose status is queued or in_progress.
- A landed dispatch isn't the end of the job. While the run is
queued it can be superseded and cancelled without ever starting: GitHub drops a pending run as soon as a newer one queues into the same concurrency group. release-publish.yml shares the release-draft group with release-drafter.yml, which fires on every push to develop, so every merge opens the window. cancel-in-progress: false doesn't help here: it governs only whether an already running run is cancelled.
- Re-check
gh run view <id> --json status,conclusion until the run leaves the queue, under the same caps as wait mode (interval ≥60 s, default 90 s; wall-clock timeout ≤15 min, default 10 min; max 10 retries; a visible status line per round). Three outcomes:
status is in_progress or completed: the run started and can no longer be superseded. Report the run URL and the status. This is the terminal state of the single-shot default.
conclusion=cancelled without ever starting: the dispatch was superseded. Handle it per the terminal-conclusion branch below.
- still
queued when the caps run out: report an unresolved dispatch. Say that the run hasn't started, that it can still be superseded, and give the URL. Never report this as a success.
- The default stays single-shot: leaving the queue is the terminal state, not completion. The operator re-invokes (or opens the URL) once the run finishes.
- Wait mode is the explicit opt-in (via
--wait argument or unambiguous prompt phrasing like "warte bis der Publish durch ist"): keep polling past the queue until status=completed or the timeout, under the same caps. Bound caps mirror pull-request-merge's wait mode, and a failure short-circuits to workflow-health triage.
On a terminal conclusion (reached in wait mode, or on a later re-invocation):
- On
conclusion=success: confirm with the operator that the release is now published (gh release view <tag> --json isDraft returns {"isDraft": false}) and that release-cd-refresh-master.yml has started a downstream run (gh run list --workflow=release-cd-refresh-master.yml --limit 1); both checks are part of release-automation's acceptance criteria.
- On
conclusion=cancelled: the run was superseded, not broken. Report it as such, name the re-dispatch as the next action, and state plainly that the release was not published. Do not route to workflow-health: there's no red check to triage and it will find nothing. This is the one failure mode where a second dispatch from this skill is allowed. Re-run operation 3 before re-dispatching, because every gate is re-derived from live state and develop may have moved since the first attempt.
- On
conclusion=failure: do not retry. Route to workflow-health triage — classify per spec/project/workflow-health/. The most common cause is a merge_failed from pascalgn/automerge-action when release-publish.yml itself uses automerge-action; check the run logs for mergeResult: 'merge_failed' per pull-request-merge step 7b and route as a stale-pin incident if so.
Wait mode
Activated by --wait or unambiguous operator phrasing. Caps mirror pull-request-merge:
- Interval ≥ 60 s (default 90 s).
- Wall-clock timeout ≤ 15 min (default 10 min).
- Max retries ≤ 10.
- Visible status line per round.
- Failure short-circuits to
workflow-health.
The single-shot default exists because the prompt-cache TTL is 5 min; unbounded polling burns the cache. Caps balance: short waits stay cache-warm, long waits accept one cache miss but never balloon.
Single-shot by design (not resumable)
Every gate is re-derived from live GitHub state on each run, and the dispatch itself is idempotent to re-validate — a persisted checkpoint per spec/claude/resumable-work/ would only cache staleness. Re-invoking after an interruption simply re-walks the gates; this is the deliberate exception to the plugin's resumable convention, recorded here so reviewers don't flag the absent resumable flag as an omission.
Gotchas
- A queued run can vanish before it runs, and
cancel-in-progress: false doesn't protect it. release-publish.yml shares the release-draft lane with release-drafter.yml deliberately, and the next merge to develop supersedes a still-pending publish. Read references/supersession.md before acting on a cancelled run or proposing to split the lane.
pascalgn/automerge-action (used by some release-publish.yml implementations downstream) exits 0 even on mergeResult: 'merge_failed'. A green release-publish.yml run is not proof the publish happened. Always re-verify gh release view <tag> --json isDraft after a success conclusion.
- The
tag input is mandatory because release-automation forbids the workflow's "newest wins" heuristic. Even when only one draft is open, this skill passes -f tag=<tag> explicitly.
- A red required check on
develop's tip blocks publish but is not always recoverable by re-running. The triage path is workflow-health, not "retry until green."
release-cd-refresh-master.yml should fire automatically after release-publish.yml succeeds. When it doesn't fire (the known-platform-constraint case where release: published from GITHUB_TOKEN doesn't cascade to a fresh workflow run, per workflow-health §Known platform constraints), surface this and route to manual fast-forward of main per branching-model §Release flow.
- The skill never runs
gh release edit --draft=false even as a fallback — that flag is reserved for incident response and remains a manual operator action documented in release-automation §Non-Goals.
Examples
- Read
examples/01-clean-dispatch-all-gates-pass.md when all pre-publish gates pass and the skill dispatches the release workflow cleanly.
- Read
examples/02-version-bearing-files-misaligned.md when version-bearing files are out of sync and the gate blocks dispatch.
- Read
examples/03-required-checks-red-route-to-workflow-health.md when a required check on develop's tip is red and triage routes to workflow-health.
- Read
examples/04-dispatch-superseded-in-queue.md when a dispatched run is cancelled in the queue without publishing, or is still queued when the caps run out.
Hard rules
- Never dispatch when any pre-publish gate fails. Failures route to
workflow-health triage.
- Never call
gh release edit --draft=false, gh api -X PATCH .../releases/<id> with draft=false, or any other body that flips the draft state. The workflow is the only publish path.
- Never use a
--ref other than the default branch (typically develop).
- Never poll to completion outside an explicit operator opt-in to wait mode; the single-shot default ends when the run leaves the queue, not when the dispatch lands.
- Never report a landed dispatch, or a run still
queued, as a publish that happened. A dispatch that GitHub accepted can still be superseded and cancelled without publishing anything.
- Never route a
cancelled run to workflow-health. It's a superseded dispatch, not a workflow defect, and re-dispatching (after re-running the gates) is the remedy.
- Never retry a failed
release-publish.yml run blindly. Triage classifies the failure first.
- Never proceed without explicit operator confirmation of the disclosed validation state.
- When
spec/project/release-skill-layer/ or spec/project/release-automation/ disagrees with this skill's instructions, the spec wins. Propose updating this skill rather than silently diverging.
1---2name: release-publish-trigger3description: Validates every release-automation pre-publish gate locally, then dispatches release-publish.yml via `gh workflow run` for the open release-drafter draft on develop, per the canonical-language file under spec/project/release-skill-layer/ §"Skill B — Release publish trigger". Verifies that exactly one open draft exists, the draft tag is reachable from the develop tip, version-bearing files align under their declared transform, every required status check reports SUCCESS on the pull-request head the develop tip merges, and `.github/workflows/release-publish.yml` exists. Refuses to dispatch on any failed gate; routes red checks to workflow-health triage. Never calls `gh release edit --draft=false` directly. Invoke when the user asks to "publish the release", "trigger release publish", "ship the release", or equivalent German-language requests. Typically called by sprint-review's opt-in chain, not directly after sprint closure.4---56# Release Publish Trigger78Operationalises `spec/project/release-skill-layer/<canonical_language>.md` §"Skill B — Release publish trigger": validates every `release-automation` §Pre-publish verification gate locally, then dispatches `release-publish.yml` via `gh workflow run`. The workflow remains the audit-trail point for the Draft → Published transition; this skill is the local pre-flight plus dispatch.910## Why this is a skill, not an agent1112- **Externally-visible mutation gates on user confirmation** — `gh workflow run release-publish.yml` triggers an external publish chain (release published → `release-cd-refresh-master.yml` → `main` fast-forward → packaging workflows); mid-flow operator gating is core to the contract and would be lost in an agent's fire-and-forget shape.13- **Output flows back into the main conversation** — the gate-by-gate validation report, the dispatch URL, and the post-dispatch status all surface in the conversation so the operator can decide.14- **Orchestrator that routes to other skills on failure** — red required checks route to `workflow-health` triage; the skill-orchestrates pattern (per `skill-vs-agent`) defaults the orchestrator to skill form.15- Counter-dimension considered: a tool-restricted agent (read + a single `gh` Bash) could perform the verification half cleanly, but the dispatch decision needs the operator in the loop and the skill stays in the conversation to surface the run URL and follow-up status — keeping the whole flow in one skill is simpler than a forced split.1617## User-language policy1819Detect the operator's language and respond in it. All `git`, `gh api`, and `gh workflow run` invocations stay English so that `release-publish.yml`'s job summary, `release-drafter`'s draft body, and downstream automation stay consistent across the portfolio.2021## German trigger phrases2223The frontmatter `description` keeps the trigger lexicon English-only per `spec/claude/skill-management/` §Structure (plugin-distributed skills). Treat the following German paraphrases as equivalent and discoverable through this skill:2425- "veröffentliche das Release"26- "stoße den Release-Publish an"27- "ship das Release"2829## Preconditions3031Before doing anything:3233- Confirm the working directory is a git repository (`git rev-parse --is-inside-work-tree`) and the remote resolves to a GitHub repository.34- Confirm `gh` is authenticated (`gh auth status`).35- Locate `spec/project/release-skill-layer/` and `spec/project/release-automation/` — either in the target repo or via the `nolte-shared` plugin install path. Stop and ask if neither is reachable.36- Confirm the repo's default branch (`gh repo view --json defaultBranchRef --jq .defaultBranchRef.name`) is the integration branch the spec applies to (typically `develop`).3738## Operations3940Operation 1 resolves the open draft and Operation 2 detects the project type (the index into `release-automation` §Version-bearing files, shared with Skill A per the spec's §Skill split and shared shape MUST). Operations 3 to 5 then form a Plan-validate-execute cycle: Operation 3 walks every pre-publish gate, Operation 4 surfaces the validated state for explicit operator confirmation, and Operation 5 dispatches `release-publish.yml`. Operation 6 follows the dispatched run until it leaves the queue and reports from there; a landed dispatch isn't the skill's terminal state, because a queued run can still be superseded without publishing.4142### 1. Resolve the open draft4344- Run `gh release list --json isDraft,tagName,targetCommitish,createdAt,name`.45- Filter to drafts whose `targetCommitish` equals the default branch.46- **Refuse and report** when zero drafts match (operator should run `release-drafter.yml` first) or when more than one matches without an explicit `--tag` argument from the operator (no "newest wins" heuristic, per `release-automation` §Operational contract).4748**Tooling (optional GitHub MCP) — go/no-go: GO (narrow).** Prefer `github:list_releases` for the draft resolution here (Op 1) and `github:get_release_by_tag` for the post-publish verify (Op 6), falling back to the `gh` commands shown, per `spec/claude/mcp-tool-preference/`. The `release-publish.yml` dispatch, the required-check-runs gate, and all workflow-run status reads have no MCP tool and stay `gh` (OQ-D); the reachability / version-file / alignment gates stay local git. `gh`/git stays authoritative; output is identical.4950### 2. Detect project type5152Per `spec/project/release-skill-layer/` §Skill split and shared shape (MUST), both release-layer skills follow the **same** six project-type detection signals used by `github-issue-templates-apply` and by `release-notes-curate` (Skill A). Walk these six signals in order and stop at the first match. Read the files via the standard read tools — never via filename heuristics alone:53541. **Claude Code plugin** — `.claude-plugin/plugin.json` exists; top-level `skills/` and / or `agents/` folder present.552. **Python application** — `pyproject.toml` declares `[project.scripts]` (or equivalent application entry point), no library distribution metadata.563. **Python library** — `pyproject.toml` declares a distributable package without an application entry point.574. **Node / TypeScript library or app** — `package.json` exists; `main` / `exports` indicates library, `bin` / `scripts.start` indicates app.585. **CLI tool** — declared CLI entry point in `pyproject.toml` (`[project.scripts]`), `package.json` (`bin`), or `Cargo.toml` (`[[bin]]`).596. **Documentation-only repo** — `mkdocs.yml`, `docusaurus.config.*`, or similar exists with no application source.6061When `.github/release-skill-layer.yml` declares an explicit `project_type:` value, use it instead of the autodetection (override path) — the same override that Skill A honours.6263When no signal matches, stop and ask the operator to declare the project type manually. Never proceed with a generic fallback.6465The detected type is the index into `release-automation` §Version-bearing files: the "Version-bearing files aligned" gate uses it to select the correct default version-bearing-file rows for this repo (Claude Code plugin, Python package, Node.js package, HACS integration, etc.) before reading and comparing each file.6667### 3. Validate every pre-publish gate6869Walk these gates in order. **The skill MUST NOT proceed past a failed gate**; surface the gate name, the failure detail, and the remediation path.7071#### 1. Draft tag reachable from develop7273- `git fetch origin develop`.74- Resolve the draft's `targetCommitish` SHA.75- Run `git merge-base --is-ancestor <target-sha> origin/develop`.76- **Failure**: the draft is stale relative to `develop`. Remediation: re-run `release-drafter.yml` to refresh the draft target.7778#### 2. Version-bearing files aligned7980- Read the version-bearing file list per `release-automation` §Version-bearing files: select the default-table rows for the project type detected in step 2, or use the override at `.github/release-automation.yml` when the repo declares one.81- For every declared file, read the value at the `target-sha` (`git show <target-sha>:<path>` plus the spec's selector) and compare against the target tag under the file's value transform (typically "strip leading `v`" if the existing convention omits it).82- **Failure**: any file whose value does not equal the target tag under transform. Remediation: open a `chore(release): <tag>` PR (fallback path) or wait for the workflow-driven primary path to land its alignment commit.8384#### 3. Alignment commit present8586- Identify the most recent commit on `develop` that touched any version-bearing file: `git log -1 --pretty=%s --follow -- <path>`.87- Verify the subject prefix starts with `chore(release): <tag>` (the prefix-match accepts the `(#N)` suffix GitHub appends on squash-merge, per `release-automation` §Pre-publish verification).88- **Failure**: no `chore(release): <tag>` commit on the path. Remediation: same as the "Version-bearing files aligned" gate.8990#### 4. Required status checks SUCCESS where they were enforced9192Evaluate this gate on the **pull-request head** the develop tip squash-merges, not on the tip.9394- Read the required check list from `.github/settings.yml` (`branches[name=develop].protection.required_status_checks.contexts`).95- Resolve the commit to judge:96 - `gh api repos/<owner>/<repo>/commits/<tip-sha>/pulls --jq '.[0].head.sha'`.97 - A SHA comes back → that is the commit to query, and report it to the operator alongside the tip so the substitution is visible.98 - Nothing comes back (a direct push, or a merge the API does not associate) → fall back to the tip SHA and say so in the report.99- Query `gh api repos/<owner>/<repo>/commits/<sha>/check-runs` for the resolved SHA and confirm every required context appears with `conclusion=success`.100- **Failure**: any required check is `failure`, `cancelled`, `timed_out`, still `in_progress`, or **absent**. Remediation:101 - red checks → route to `workflow-health` triage (classify as `defect` / `flake` / `infra` / `stale pin` / `secret drift` / `other`); never retry the dispatch blindly.102 - pending checks → stop and ask the operator to wait, or opt in to wait mode (see "Wait mode" below).103 - absent on a resolved **pull-request head** → a genuine gap: branch protection should have required it there. Report it as a protection-configuration problem, not as a missing run.104 - absent on a **fallback tip** → the repository published from a commit on which its required contexts were never enforced. Refuse, and say that plainly rather than treating absence as green.105106**Why not the tip.** Branch protection enforces its contexts on the pull-request head, which is where the always-run constructions that guarantee a report live. The tip is a *different* commit, produced by squash, and only receives whatever `push`-triggered workflows apply to it — so a required check whose `push` trigger carries a `paths:` filter is simply absent whenever the merge touched none of those paths. That is the normal case, not an edge one. Reading absence strictly blocks most releases; reading it leniently validates a subset while appearing to validate the set. Neither is a gate.107108The tip's tree is still the tree those checks ran against: `pull-request-workflow` §Branch freshness requires `required_status_checks.strict: true`, so a pull request can only merge while up to date with `develop`. If a repository has turned `strict` off, say so in the report — the substitution's justification does not hold there.109110#### 5. Workflow file present111112- Confirm `.github/workflows/release-publish.yml` exists in the repo.113- **Failure**: the operator should adopt `release-automation` first; this skill stops and reports.114115### 4. Disclose the validated state and confirm116117Before dispatch, surface to the operator as a single block:118119- target tag and `target_commitish` SHA;120- result of each gate (PASS / FAIL with detail);121- version-bearing-file diff summary (file, current value, target value);122- audience-coverage summary when the draft body carries the `release-skill-layer:project-context-start` marker (Skill A has run); a non-blocking note offering to dispatch `release-notes-curate` first when the marker is absent;123- the exact `gh workflow run` invocation that will be issued.124125Block the dispatch until the operator confirms.126127### 5. Dispatch128129On confirmation:130131- Run `gh workflow run release-publish.yml --ref develop -f tag=<tag>`. The `tag` input is mandatory regardless of how many drafts are open (per `release-automation` §Operational contract — no "newest wins" heuristic in the workflow either).132- When the operator opts in to `--dry-run`, dispatch with `-f dry_run=true` so the workflow validates without flipping `draft: false`.133- **Never** call `gh release edit --draft=false`, `gh api -X PATCH /repos/.../releases/<id>` with `draft=false`, or any other body that flips the draft state from this skill. The workflow is the only path.134135### 6. Follow the run out of the queue136137Immediately after `gh workflow run` returns:138139- Find the new run: `gh run list --workflow=release-publish.yml --limit 1 --json databaseId,status,conclusion,url,headSha`. The match is the run whose `headSha` equals the draft's target SHA and whose `status` is `queued` or `in_progress`.140- **A landed dispatch isn't the end of the job.** While the run is `queued` it can be superseded and `cancelled` without ever starting: GitHub drops a *pending* run as soon as a newer one queues into the same concurrency group. `release-publish.yml` shares the `release-draft` group with `release-drafter.yml`, which fires on every push to `develop`, so every merge opens the window. `cancel-in-progress: false` doesn't help here: it governs only whether an already *running* run is cancelled.141- **Re-check `gh run view <id> --json status,conclusion` until the run leaves the queue**, under the same caps as wait mode (interval ≥60 s, default 90 s; wall-clock timeout ≤15 min, default 10 min; max 10 retries; a visible status line per round). Three outcomes:142 - `status` is `in_progress` or `completed`: the run started and can no longer be superseded. Report the run URL and the status. **This is the terminal state of the single-shot default.**143 - `conclusion=cancelled` without ever starting: the dispatch was **superseded**. Handle it per the terminal-conclusion branch below.144 - still `queued` when the caps run out: report an **unresolved dispatch**. Say that the run hasn't started, that it can still be superseded, and give the URL. **Never** report this as a success.145- **The default stays single-shot**: leaving the queue is the terminal state, not completion. The operator re-invokes (or opens the URL) once the run finishes.146- **Wait mode** is the explicit opt-in (via `--wait` argument or unambiguous prompt phrasing like "warte bis der Publish durch ist"): keep polling past the queue until `status=completed` or the timeout, under the same caps. Bound caps mirror `pull-request-merge`'s wait mode, and a failure short-circuits to `workflow-health` triage.147148On a terminal conclusion (reached in wait mode, or on a later re-invocation):149150- On `conclusion=success`: confirm with the operator that the release is now published (`gh release view <tag> --json isDraft` returns `{"isDraft": false}`) and that `release-cd-refresh-master.yml` has started a downstream run (`gh run list --workflow=release-cd-refresh-master.yml --limit 1`); both checks are part of `release-automation`'s acceptance criteria.151- On `conclusion=cancelled`: the run was **superseded, not broken**. Report it as such, name the re-dispatch as the next action, and state plainly that the release was **not** published. Do **not** route to `workflow-health`: there's no red check to triage and it will find nothing. This is the one failure mode where a second dispatch from this skill is allowed. Re-run operation 3 before re-dispatching, because every gate is re-derived from live state and `develop` may have moved since the first attempt.152- On `conclusion=failure`: do **not** retry. Route to `workflow-health` triage — classify per `spec/project/workflow-health/`. The most common cause is a `merge_failed` from `pascalgn/automerge-action` when `release-publish.yml` itself uses `automerge-action`; check the run logs for `mergeResult: 'merge_failed'` per `pull-request-merge` step 7b and route as a stale-pin incident if so.153154## Wait mode155156Activated by `--wait` or unambiguous operator phrasing. Caps mirror `pull-request-merge`:157158- Interval ≥ 60 s (default 90 s).159- Wall-clock timeout ≤ 15 min (default 10 min).160- Max retries ≤ 10.161- Visible status line per round.162- Failure short-circuits to `workflow-health`.163164The single-shot default exists because the prompt-cache TTL is 5 min; unbounded polling burns the cache. Caps balance: short waits stay cache-warm, long waits accept one cache miss but never balloon.165166## Single-shot by design (not resumable)167168Every gate is re-derived from live GitHub state on each run, and the dispatch itself is idempotent to re-validate — a persisted checkpoint per `spec/claude/resumable-work/` would only cache staleness. Re-invoking after an interruption simply re-walks the gates; this is the deliberate exception to the plugin's resumable convention, recorded here so reviewers don't flag the absent `resumable` flag as an omission.169170## Gotchas171172- **A queued run can vanish before it runs**, and `cancel-in-progress: false` doesn't protect it. `release-publish.yml` shares the `release-draft` lane with `release-drafter.yml` deliberately, and the next merge to `develop` supersedes a still-pending publish. Read `references/supersession.md` before acting on a `cancelled` run or proposing to split the lane.173- `pascalgn/automerge-action` (used by some `release-publish.yml` implementations downstream) exits 0 even on `mergeResult: 'merge_failed'`. A green `release-publish.yml` run is **not** proof the publish happened. Always re-verify `gh release view <tag> --json isDraft` after a `success` conclusion.174- The `tag` input is mandatory because `release-automation` forbids the workflow's "newest wins" heuristic. Even when only one draft is open, this skill passes `-f tag=<tag>` explicitly.175- A red required check on `develop`'s tip blocks publish but is not always recoverable by re-running. The triage path is `workflow-health`, not "retry until green."176- `release-cd-refresh-master.yml` should fire automatically after `release-publish.yml` succeeds. When it doesn't fire (the known-platform-constraint case where `release: published` from `GITHUB_TOKEN` doesn't cascade to a fresh workflow run, per `workflow-health` §Known platform constraints), surface this and route to manual fast-forward of `main` per `branching-model` §Release flow.177- The skill never runs `gh release edit --draft=false` even as a fallback — that flag is reserved for incident response and remains a manual operator action documented in `release-automation` §Non-Goals.178179## Examples180181- Read `examples/01-clean-dispatch-all-gates-pass.md` when all pre-publish gates pass and the skill dispatches the release workflow cleanly.182- Read `examples/02-version-bearing-files-misaligned.md` when version-bearing files are out of sync and the gate blocks dispatch.183- Read `examples/03-required-checks-red-route-to-workflow-health.md` when a required check on `develop`'s tip is red and triage routes to `workflow-health`.184- Read `examples/04-dispatch-superseded-in-queue.md` when a dispatched run is cancelled in the queue without publishing, or is still queued when the caps run out.185186## Hard rules187188- Never dispatch when any pre-publish gate fails. Failures route to `workflow-health` triage.189- Never call `gh release edit --draft=false`, `gh api -X PATCH .../releases/<id>` with `draft=false`, or any other body that flips the draft state. The workflow is the only publish path.190- Never use a `--ref` other than the default branch (typically `develop`).191- Never poll to completion outside an explicit operator opt-in to wait mode; the single-shot default ends when the run leaves the queue, not when the dispatch lands.192- Never report a landed dispatch, or a run still `queued`, as a publish that happened. A dispatch that GitHub accepted can still be superseded and cancelled without publishing anything.193- Never route a `cancelled` run to `workflow-health`. It's a superseded dispatch, not a workflow defect, and re-dispatching (after re-running the gates) is the remedy.194- Never retry a failed `release-publish.yml` run blindly. Triage classifies the failure first.195- Never proceed without explicit operator confirmation of the disclosed validation state.196- When `spec/project/release-skill-layer/` or `spec/project/release-automation/` disagrees with this skill's instructions, the spec wins. Propose updating this skill rather than silently diverging.