Add a release-manager agent that orchestrates the release skills (#1794)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b8535f33-08ac-44ef-84d1-aab96770ae3a
This commit is contained in:
parent
d3a6bc2535
commit
514cf68af1
4 changed files with 1586 additions and 46 deletions
166
.github/agents/release-manager/references/delegation.md
vendored
Normal file
166
.github/agents/release-manager/references/delegation.md
vendored
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# Delegation and Worktrees
|
||||
|
||||
The release-manager session is an **orchestrator**. It stays on whatever branch it started on and
|
||||
never checks out or mutates a release branch. Work that creates commits happens in a **child session
|
||||
on its own worktree**, based on the target release branch.
|
||||
|
||||
This mirrors how [`docs.yml`](../../../workflows/docs.yml) already works: the orchestration scripts run
|
||||
from a single fixed checkout, while each version's content is built from its own tag in a separate
|
||||
worktree.
|
||||
|
||||
## Why
|
||||
|
||||
- **Current orchestration.** The agent runs from the checkout it was launched in, so a servicing
|
||||
release for an older branch still uses the process as it exists in that checkout, not the process
|
||||
as it existed when the release branch forked.
|
||||
- **A clean working tree.** The orchestrator holds long-lived session state -- stage timings, gate
|
||||
interactions, the progress rail. Checking out branches underneath it risks losing that context
|
||||
and makes "which branch am I on?" a source of error at exactly the moment precision matters.
|
||||
- **Isolation of the risky part.** Only stage 1 writes to the repository. Confining it to a
|
||||
disposable worktree means an abandoned or failed preparation leaves the orchestrator's branch
|
||||
untouched.
|
||||
- **Concurrency.** A `2.0.0-preview.2` preparation and a `1.3.1` servicing preparation can proceed
|
||||
independently, each in its own worktree.
|
||||
|
||||
## What runs where
|
||||
|
||||
| Stage | Mutates the repo? | Runs where |
|
||||
|---|---|---|
|
||||
| 1. Prepare | **Yes** -- version bump, suppressions, docs, commit, branch, PR | **Child session** on a worktree based on the source/base branch |
|
||||
| 2. Review and merge | No -- reads CI and PR state | Orchestrator, in place |
|
||||
| 3. Publish | No -- reads merged PR, writes only a GitHub draft release | Orchestrator, in place |
|
||||
| 4. Release | No -- human action in the GitHub UI | Orchestrator, in place |
|
||||
| 5. Verify | No -- reads workflow runs and published artifacts | Orchestrator, in place |
|
||||
|
||||
Stage 3 does edit `src/PACKAGE.md` and `README.md` when the README checklist finds issues. **The
|
||||
release branch is already merged by this point, so those fixes cannot land on it.** They go to the
|
||||
base branch the release ships from — `main` or `release/{MAJOR}.x` — which is protected, so they
|
||||
need their own small PR, reviewed and merged like any other change.
|
||||
|
||||
Delegate that PR the same way as stage 1: a child session on a fresh worktree based on the base
|
||||
branch. Do not push directly to the base branch, and do not commit into the orchestrator's worktree.
|
||||
|
||||
A corrective commit merged at this point **is not in the draft release's tag**, because the draft is
|
||||
pinned to the merge commit the user approved. After the fix merges, re-target the draft to the new
|
||||
head and regenerate the notes per
|
||||
[publish-release Step 9](../../../skills/publish-release/SKILL.md). Skipping the re-target ships a
|
||||
tag that predates the fix while the notes describe the fixed state.
|
||||
|
||||
## Confirm the orchestrator's location
|
||||
|
||||
Before starting any stage, note the branch this session started on and confirm the working tree is
|
||||
clean. Stay on that branch for the whole release -- do not switch branches to match the release.
|
||||
|
||||
- **Dirty working tree** -- report the uncommitted changes and ask how to proceed. Do not stash,
|
||||
reset, or commit unrelated work.
|
||||
- **Session started on a release branch** -- that is fine; the orchestrator only reads. Still
|
||||
delegate stage 1 to a worktree rather than committing in place.
|
||||
|
||||
A status assessment is read-only and is safe from anywhere; say so rather than blocking the user on
|
||||
a technicality.
|
||||
|
||||
## Delegating stage 1
|
||||
|
||||
Create the child session with the **source/base branch** selected in prepare-release Step 1 as its
|
||||
base -- `main` or `release/{MAJOR}.x`. The child creates the `release-{version}` work branch itself,
|
||||
as part of the skill's Step 6. Do not create that branch yourself, and do not pass it as the base.
|
||||
|
||||
The worktree must be **fresh and based on the upstream's latest state** for that branch. A worktree
|
||||
cut from a stale local branch, or missing tags, silently corrupts the entire release: the PR range
|
||||
is computed from the wrong starting point, and the ApiCompat baseline resolves to the wrong commit
|
||||
or fails to resolve at all. Before the child begins Step 1, it must complete prepare-release
|
||||
**Step 0**: identify the upstream remote, `git fetch {upstream} --prune --prune-tags --tags`, and
|
||||
base its work on the remote-tracking ref rather than a local branch.
|
||||
|
||||
Reuse of an existing worktree is the common way this goes wrong. Prefer creating a new one per
|
||||
release. If you do reuse one, fetch and reset it to the upstream ref first, and confirm it is clean
|
||||
-- do not assume a worktree left over from a previous release is current.
|
||||
|
||||
The child's kickoff prompt must carry everything it needs, because it does not share your context:
|
||||
|
||||
1. The instruction to run the **prepare-release** skill, **starting at Step 0**.
|
||||
2. The source/base branch, already selected.
|
||||
3. The target commit or ref, if the user chose one.
|
||||
4. Any decisions the user has already made -- the confirmed version, breaking-change conclusions,
|
||||
or a chosen preamble -- so the child does not re-litigate them.
|
||||
5. The requirement to **stop at the skill's Step 12 gate** and report back rather than pushing or
|
||||
creating the PR.
|
||||
6. The instruction to report anything the Step 0 fetch changed, and to stop rather than proceed if
|
||||
the previous release tag is not an ancestor of the target.
|
||||
7. The requirement to **stop at the skill's Step 10b gate** and bring the categorization table and
|
||||
acknowledgements roster back to you, so the user reviews notes content before a PR exists.
|
||||
|
||||
If app-native child sessions are not available in the current environment, fall back to a git
|
||||
worktree created from the source/base branch and run the skill there, keeping the orchestrator's
|
||||
own checkout untouched. The invariant is the worktree, not the mechanism.
|
||||
|
||||
## Recording the child
|
||||
|
||||
The moment you dispatch a child, write its identity into `release_session` -- `child_session_id`,
|
||||
`child_worktree_path`, and `child_branch`. A release routinely outlives the session that started
|
||||
it, and a worktree with no recorded owner is very hard to tell apart from the dozens of unrelated
|
||||
worktrees a busy repository accumulates.
|
||||
|
||||
## Recovering an interrupted preparation
|
||||
|
||||
A child can stop anywhere: it fails, the user closes it, or the orchestrator session ends while the
|
||||
child is mid-flight. Recovery starts from what the worktree actually contains, never from the fact
|
||||
that it exists.
|
||||
|
||||
**Existence is not progress.** A `release-{version}` worktree proves only that a preparation was
|
||||
started. Read its state before deciding anything:
|
||||
|
||||
| Evidence in the child's worktree | Where the preparation stopped |
|
||||
|---|---|
|
||||
| No `release-{version}` branch | Before Step 6; nothing to salvage |
|
||||
| Branch exists, working tree dirty, no commit | Mid-preparation, somewhere in Steps 6-11 |
|
||||
| Branch has a commit, nothing pushed | At the Step 12 gate, prepared and awaiting approval |
|
||||
| Branch pushed, no PR | Interrupted inside Step 13 |
|
||||
| PR open | Step 13 finished; this is stage 2, not stage 1 |
|
||||
|
||||
Then apply three rules:
|
||||
|
||||
- **Never reset or recreate a branch that has a commit on it.** It may hold work the user already
|
||||
reviewed and corrected -- release-note categorization, acknowledgement edits, a chosen preamble --
|
||||
none of which is reproducible from the repository. Read the commit and the drafted notes and
|
||||
continue from there.
|
||||
- **Never inherit a validation result.** Build, pack, and ApiCompat outcomes leave no trace in git.
|
||||
A commit proves the files were written, not that anything passed. Re-run the checks rather than
|
||||
assuming the interrupted run got that far.
|
||||
- **Prefer resuming the recorded child over launching a replacement.** It still holds the context.
|
||||
If it is gone, dispatch a replacement pointed at the *existing* worktree and branch, and tell it
|
||||
to audit what is already there before continuing -- not to start over.
|
||||
|
||||
Report the stopping point and the evidence you read, and let the user confirm before continuing.
|
||||
|
||||
Decisions the user made at a gate are the hardest thing to recover, because session tracking does
|
||||
not survive the session. Their durable form is the artifact itself: the drafted release notes carry
|
||||
the categorization, and the acknowledgements roster carries the exclusions. On resume, re-derive the
|
||||
decisions by reading the drafted notes, and present them as *previously decided* for confirmation.
|
||||
Silently re-deriving them from scratch will quietly undo corrections the user already made once.
|
||||
|
||||
## Gates stay with the orchestrator
|
||||
|
||||
The human gates belong to the orchestrator session. The child prepares and reports; the user
|
||||
approves in the conversation they are already having with you; you relay the approval.
|
||||
|
||||
Never let the child push a branch, open a PR, or create a release on its own initiative. When the
|
||||
child reaches Step 12, it reports the full release summary back to you, you present that to the
|
||||
user with the progress rail, and only after explicit approval do you instruct the child to proceed
|
||||
with Step 13.
|
||||
|
||||
## Timing across sessions
|
||||
|
||||
Session tracking stays in the **orchestrator**. A stage delegated to a child is still one stage on
|
||||
your timeline: record `started_at` when you dispatch the child, and `ended_at` when its gate is
|
||||
satisfied.
|
||||
|
||||
Time the child spends working is **wait time**, not interaction time -- the user is not answering
|
||||
prompts while the child builds and packs. Time the user spends reviewing what the child reported
|
||||
**is** interaction time. See [session-tracking.md](session-tracking.md).
|
||||
|
||||
## Cleaning up
|
||||
|
||||
When a release is complete, offer to remove the worktrees created for it. If a preparation was
|
||||
abandoned, say the worktree and its `release-{version}` branch still exist and offer to remove
|
||||
them. Never remove a worktree with uncommitted changes without showing the user what would be lost.
|
||||
170
.github/agents/release-manager/references/monitoring.md
vendored
Normal file
170
.github/agents/release-manager/references/monitoring.md
vendored
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Monitoring
|
||||
|
||||
Two things in this process are easy to hand off passively and should not be: the release PR after
|
||||
it is opened, and the draft release after it is created. In both cases the agent has the context
|
||||
needed to interpret what happens next, and the user should not have to come back and report an
|
||||
outcome the agent could have observed.
|
||||
|
||||
Monitoring is **automatic and read-only**. It never merges, never pushes, and never publishes.
|
||||
Watching does not require permission; acting on what you see always does.
|
||||
|
||||
## Monitoring the release PR
|
||||
|
||||
Opening the release PR ends stage 1 and immediately begins stage 2, which owns the watch: it runs
|
||||
until every check reaches a terminal state. Reporting the PR URL and stopping leaves the user to
|
||||
discover failures themselves, which is exactly backwards.
|
||||
|
||||
Record the time accordingly. Stage 1 ends when the PR is created, and the CI watch that follows --
|
||||
including any red checks, corrective pushes, and re-runs -- belongs to stage 2. Attributing that
|
||||
time to stage 1 makes preparation look expensive and review look cheap, which is the opposite of
|
||||
what the summary should reveal.
|
||||
|
||||
### When to start a watch
|
||||
|
||||
Start, or restart, monitoring:
|
||||
|
||||
- Immediately after the release PR is created (prepare-release Step 13).
|
||||
- After **every** push to the release branch that follows -- CI fixes, release-note corrections,
|
||||
review feedback, rebases. Each push produces a new head SHA with its own set of runs.
|
||||
- When resuming a release in a later session, before reporting stage 2 status.
|
||||
|
||||
A restart is a fresh watch against the **new head SHA**. Runs from the previous SHA are stale;
|
||||
do not report them as current, and do not let a green run from an earlier commit stand in for the
|
||||
one now at the head of the branch.
|
||||
|
||||
### Running the watch
|
||||
|
||||
1. Resolve the current head SHA of the release branch.
|
||||
2. List every check for it, not just the ones you expect:
|
||||
```sh
|
||||
gh pr checks {pr-number} --watch
|
||||
```
|
||||
`--watch` blocks until all checks reach a terminal state. Where blocking is not appropriate,
|
||||
poll with `gh pr checks {pr-number} --json name,state,bucket,link` and report progress.
|
||||
3. Wait for **terminal** completion. A check that is queued, in progress, or pending is not a
|
||||
result. Do not summarize a partially-complete run as passing.
|
||||
4. Confirm the run set is complete. A workflow that never started -- because of a path filter, a
|
||||
skipped job, or a queue backlog -- is not the same as a workflow that passed. Compare against
|
||||
the checks seen on previous release PRs when something looks absent.
|
||||
|
||||
### Reporting
|
||||
|
||||
Report a compact per-check table plus a single overall verdict:
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Build / build (ubuntu-latest, net10.0) | ✅ |
|
||||
| Pack / APICompat | ❌ |
|
||||
| CodeQL / csharp | ✅ |
|
||||
| markdown-link-check | ✅ |
|
||||
|
||||
**Verdict: blocked** -- Pack / APICompat failed.
|
||||
|
||||
Use three states and name them explicitly: **green**, **running**, **blocked**. "Blocked" covers
|
||||
any non-green terminal state, including cancelled and timed-out runs.
|
||||
|
||||
### On failure
|
||||
|
||||
Diagnose before proposing anything. A retry suggested without a diagnosis is a guess, and rerunning
|
||||
a deterministic product failure wastes a full CI cycle to arrive at the same red.
|
||||
|
||||
1. **Retrieve the logs automatically.** Do not ask the user to paste them.
|
||||
```sh
|
||||
gh run view {run-id} --log-failed
|
||||
```
|
||||
2. **Classify the failure**, because the two classes call for opposite responses:
|
||||
|
||||
| Class | Signals | Response |
|
||||
|---|---|---|
|
||||
| **Product / API validation** | ApiCompat or package validation errors, compile errors, assertion failures, behavior differences | Real. Diagnose it. Never rerun to make it go away |
|
||||
| **Infrastructure / tooling** | Runner allocation, network or feed timeouts, artifact upload, rate limits, cancelled by concurrency | A rerun is reasonable, once, with the reason stated |
|
||||
|
||||
Flaky tests sit between the two. Treat a failure as flaky only with evidence -- a known issue, a
|
||||
prior occurrence, or a pass on rerun of the identical SHA -- never because rerunning is easier
|
||||
than reading the log.
|
||||
|
||||
3. **For ApiCompat and package validation failures specifically**, apply the interpretation rules in
|
||||
[apicompat-apidiff.md](../../../skills/prepare-release/references/apicompat-apidiff.md) before
|
||||
concluding the release is breaking. `Unnecessary suppressions found` and a stale baseline
|
||||
produce large, convincing, and entirely phantom break listings.
|
||||
|
||||
4. **Present the diagnosis with a proposed fix, and stop.** Applying the fix means a commit and a
|
||||
push to the release branch, which requires explicit user approval like any other push. Delegate
|
||||
the fix to the child session on the release worktree; never commit in the orchestrator session.
|
||||
|
||||
5. After an approved fix is pushed, **restart the watch** for the new SHA without being asked.
|
||||
|
||||
### Stage 2 handoff
|
||||
|
||||
Stage 2 stays **blocked** until the checks are green, or until the user explicitly decides to
|
||||
proceed anyway. Record that decision and who made it.
|
||||
|
||||
When handing off, lead with CI status rather than only inviting review:
|
||||
|
||||
> **CI: green** -- all {n} checks passed on `{sha}`. PR #{number} is ready for your review and merge.
|
||||
|
||||
or
|
||||
|
||||
> **CI: blocked** -- {check name} failed on `{sha}`. Diagnosis below. PR #{number} is not ready
|
||||
> to merge yet.
|
||||
|
||||
or
|
||||
|
||||
> **CI: running** -- {done} of {n} checks complete, none failed. I am still watching and will report when
|
||||
> they finish.
|
||||
|
||||
Never say only "the PR is up, please review and merge." Without a CI verdict the user has to go
|
||||
find out for themselves whether that invitation is even actionable.
|
||||
|
||||
## Monitoring the draft release
|
||||
|
||||
Creating the draft release ends stage 3. Stage 4 is a human action in the GitHub UI, and the
|
||||
temptation is to hand off and wait to be told it happened. Do not. Publishing is the moment the
|
||||
release becomes irreversible and the moment two workflows start, so it is the least useful point in
|
||||
the process to be uninformed about.
|
||||
|
||||
Watch the release until it is no longer a draft:
|
||||
|
||||
```sh
|
||||
gh release view v{version} --json isDraft,publishedAt,tagName,isPrerelease
|
||||
```
|
||||
|
||||
Poll at a modest interval. This gate is human-paced and may sit for hours or span a session, so
|
||||
prefer periodic checks over a tight loop, and say that you are watching rather than going silent.
|
||||
|
||||
**`isDraft: false` is the trigger.** The moment it flips:
|
||||
|
||||
1. Record the stage 4 end time from `publishedAt`, not from when you noticed. The user published
|
||||
when they published; polling latency is yours, not theirs, and it should not inflate the stage
|
||||
duration in the closing summary.
|
||||
2. Confirm the details that were the user's to choose and cannot be inferred: the tag actually
|
||||
created, and whether the release was marked as a prerelease. A stable release mistakenly left
|
||||
unflagged, or a prerelease flagged as stable, changes what consumers receive.
|
||||
3. **Begin stage 5 immediately** via the verify-release skill. Publishing starts the Release and
|
||||
Publish Docs workflows in parallel right away; waiting to be told to verify means arriving after
|
||||
the interesting part. Announce the transition rather than asking permission -- stage 5 is
|
||||
read-only, and the irreversible act has already occurred.
|
||||
|
||||
### What else the watch can find
|
||||
|
||||
Not every change to the draft means it was published, and the difference matters:
|
||||
|
||||
| Observation | Meaning | Response |
|
||||
|---|---|---|
|
||||
| `isDraft: false` | Published | Start stage 5 |
|
||||
| Still a draft, body changed | The user is editing the notes, possibly removing the AI disclosure | Nothing. Do not re-add anything they removed |
|
||||
| Draft no longer exists | Deleted, or published under a different tag | Check for a published release before assuming it was abandoned; ask |
|
||||
| Published with an unexpected tag | The tag differs from the prepared version | Stop and confirm before verifying. Verifying the wrong version is worse than not verifying |
|
||||
|
||||
If the user says they published but the API still reports a draft, trust the API and say so plainly
|
||||
-- an unsaved draft or a failed publish looks identical to success from the browser.
|
||||
|
||||
### Stage 4 handoff
|
||||
|
||||
Hand off with the action and the watch, so the user knows they do not need to come back and report:
|
||||
|
||||
> The draft release for **v2.1.0** is ready. Review the notes line by line, set the prerelease flag
|
||||
> if applicable, and click **Publish release**. Once you have signed off you may remove the AI
|
||||
> disclosure from the notes.
|
||||
>
|
||||
> I am watching for publication and will start verification automatically when it happens.
|
||||
224
.github/agents/release-manager/references/session-tracking.md
vendored
Normal file
224
.github/agents/release-manager/references/session-tracking.md
vendored
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# Session Tracking
|
||||
|
||||
Track release stage progress and timing so the closing summary is accurate. Use the **SQL tool** for
|
||||
storage. **Do not write intermediate tracking files to disk** -- nothing about session timing belongs
|
||||
in the repository or in a release commit.
|
||||
|
||||
## Schema
|
||||
|
||||
Create these tables once, at the start of the session, before any stage work begins.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS release_session (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
-- Expected keys: version, base_branch, release_branch, pr_number, draft_release_url,
|
||||
-- published_release_url, session_started_at,
|
||||
-- child_session_id, child_worktree_path, child_branch
|
||||
|
||||
CREATE TABLE IF NOT EXISTS release_stages (
|
||||
stage INTEGER NOT NULL, -- 1..5
|
||||
attempt INTEGER NOT NULL DEFAULT 1,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL, -- 'pending' | 'in_progress' | 'blocked' | 'done' | 'carried_over'
|
||||
started_at TEXT, -- ISO-8601 local time
|
||||
ended_at TEXT,
|
||||
notes TEXT,
|
||||
PRIMARY KEY (stage, attempt)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS release_interactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
stage INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL, -- 'gate' | 'question' | 'review' | 'decision'
|
||||
prompted_at TEXT NOT NULL, -- when you asked
|
||||
answered_at TEXT, -- when the user's answer arrived
|
||||
outcome TEXT, -- what they decided
|
||||
summary TEXT
|
||||
);
|
||||
|
||||
-- Unattended time: child-agent work, CI watches, workflow watches, index polling.
|
||||
-- Without this, wait time can only be inferred from stage wall-clock, which
|
||||
-- silently folds in discussion and rework.
|
||||
CREATE TABLE IF NOT EXISTS release_waits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
stage INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL, -- 'child_work' | 'ci' | 'workflow' | 'index' | 'other'
|
||||
reason TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL,
|
||||
ended_at TEXT,
|
||||
ref TEXT -- run id, PR number, package, etc.
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS release_workflow_runs (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
stage INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
head_sha TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
conclusion TEXT
|
||||
);
|
||||
```
|
||||
|
||||
Seed the five stages up front:
|
||||
|
||||
```sql
|
||||
INSERT OR IGNORE INTO release_stages (stage, attempt, name, status) VALUES
|
||||
(1, 1, 'Prepare', 'pending'),
|
||||
(2, 1, 'Review and merge', 'pending'),
|
||||
(3, 1, 'Publish', 'pending'),
|
||||
(4, 1, 'Release', 'pending'),
|
||||
(5, 1, 'Verify', 'pending');
|
||||
```
|
||||
|
||||
## Recording timestamps
|
||||
|
||||
Every timestamp comes from the current date/time available to you in the session. Use ISO-8601 local
|
||||
time, for example `2026-08-04T13:22:05-07:00`. Never estimate a timestamp you could have recorded.
|
||||
|
||||
**Prefer immutable external evidence over your own observation.** You notice things late; the event
|
||||
itself has a real timestamp. Query it and use it:
|
||||
|
||||
| Event | Authoritative source |
|
||||
|---|---|
|
||||
| PR merged | `gh pr view {n} --json mergedAt` |
|
||||
| Release published | `gh release view v{version} --json publishedAt` |
|
||||
| Workflow run start/end | `gh run view {id} --json startedAt,updatedAt,conclusion` |
|
||||
| Commit created | the commit's author date |
|
||||
|
||||
Recording stage 4's end from the moment you noticed publication, rather than from `publishedAt`,
|
||||
inflates that stage by your entire polling interval. The same applies to a merge you detect on a
|
||||
later poll, and to workflow runs you attach to after they started.
|
||||
|
||||
- **Session start** -- write `session_started_at` into `release_session` at initialization.
|
||||
- **Stage start** -- set `status = 'in_progress'` and `started_at` the moment you begin the stage's
|
||||
first substantive action (invoking the skill, or beginning a status assessment for a human-gate
|
||||
stage).
|
||||
- **Stage end** -- set `status = 'done'` and `ended_at` the moment the stage's gate is satisfied
|
||||
(PR opened, PR merged, draft created, release published, packages listed) -- **not** when the user
|
||||
next speaks.
|
||||
- **Blocked** -- set `status = 'blocked'` with a note when a stage cannot advance (red CI, an
|
||||
unresolved breaking-change decision, a failed release workflow). Leave `started_at` intact; the
|
||||
blocked span still counts toward that stage's wall-clock time.
|
||||
- **Carried over** -- when resuming a release and evidence shows a stage completed in an earlier
|
||||
session, record it as `status = 'carried_over'` with `started_at` and `ended_at` left NULL. Never
|
||||
invent durations for work you did not observe.
|
||||
- **Rework** -- when a stage that reached its gate has to be revisited (CI went red after the PR was
|
||||
opened, a corrective push, a re-run of a failed workflow), close the current attempt and insert a
|
||||
new row with `attempt + 1` rather than reopening the old one or stretching its `ended_at`. One
|
||||
aggregate row per stage hides the shape of the time: a stage 2 that reads as "2h 22m" tells you
|
||||
nothing about how much was CI, how much was review, and how much was remediation.
|
||||
|
||||
## Recording waits
|
||||
|
||||
Insert a `release_waits` row whenever you begin waiting on something that is not the user, and close
|
||||
it when the wait ends. Cover child-session work, CI watches, workflow watches, and index polling.
|
||||
|
||||
This is what makes the closing summary's split honest. Without it, wait time can only be inferred by
|
||||
subtracting interaction time from stage wall-clock, which quietly counts discussion, diagnosis, and
|
||||
rework as waiting.
|
||||
|
||||
**A wait records unattended time only, so it must not overlap an interaction.** These two clocks run
|
||||
in the same wall-clock window — a two-hour CI watch during which the user answers an eleven-minute
|
||||
gate is not two hours of waiting plus eleven minutes of interaction. Counting both in full makes the
|
||||
parts exceed the whole and drives the remainder negative:
|
||||
|
||||
```
|
||||
total 120m − active 11m − waiting 120m = −11m
|
||||
```
|
||||
|
||||
Keep them disjoint as you record, rather than reconciling later. When a wait is running and you turn
|
||||
to the user, **close the wait, handle the interaction, then open a new wait row** for the remainder.
|
||||
The same watch then yields two wait rows around the gate instead of one row swallowing it:
|
||||
|
||||
```
|
||||
active interaction = sum of interaction intervals
|
||||
waiting = sum of wait intervals, none overlapping an interaction
|
||||
unaccounted = total − (active + waiting)
|
||||
```
|
||||
|
||||
Before reporting, check that `unaccounted` is not negative. If it is, the rows overlap and the split
|
||||
is wrong — say so and report the measured totals plainly instead of publishing a negative remainder
|
||||
or clamping it to zero. A clamped number looks correct and hides the defect.
|
||||
|
||||
Report the unaccounted remainder rather than distributing it. A visible gap is information; a
|
||||
silently absorbed one is a wrong number.
|
||||
|
||||
Record every CI and release workflow run in `release_workflow_runs` as you watch it, using the run's
|
||||
own `startedAt` and `updatedAt`. This makes the longest-wait figure in the summary a lookup instead
|
||||
of a recollection. These rows are evidence about the run itself, so they are exempt from the
|
||||
non-overlap rule — never sum them into `waiting`.
|
||||
|
||||
## Recording interactions
|
||||
|
||||
Insert a `release_interactions` row every time you put a gate, question, or review in front of the
|
||||
user: write `prompted_at` when you ask, and fill `answered_at` from the timestamp of their reply.
|
||||
|
||||
**Close the open interaction before doing anything else with the user's reply.** At most one row per
|
||||
stage should have a NULL `answered_at` at any moment -- the question you are currently waiting on.
|
||||
When a reply arrives, your first action is to `UPDATE` that row with `answered_at` and `outcome`;
|
||||
only then act on what they said. Deferring the update is how rows end up permanently NULL, because
|
||||
by the time the work is done the arrival time is gone.
|
||||
|
||||
```sql
|
||||
UPDATE release_interactions
|
||||
SET answered_at = '{reply-timestamp}', outcome = '{what they decided}'
|
||||
WHERE id = (SELECT MAX(id) FROM release_interactions WHERE answered_at IS NULL);
|
||||
```
|
||||
|
||||
Two failure modes to avoid, both of which produce numbers that look fine and are wrong:
|
||||
|
||||
- **Never write `answered_at` equal to `prompted_at`.** A zero-duration interaction means the reply
|
||||
timestamp was unavailable, not that the user answered instantly. Leave it NULL and count the row
|
||||
as unmeasured.
|
||||
- **Record choice-style prompts too.** A gate answered by picking an option is still interaction; if
|
||||
the mechanism gives you no reply timestamp, log the row with NULL `answered_at` so it appears in
|
||||
the unmeasured count rather than vanishing.
|
||||
|
||||
At wrap-up, report how many interactions were measured and how many were not. That belongs in the
|
||||
narrative prose, not in the timing table: "~27m across six gates, four more unmeasured" is an honest
|
||||
floor, while the table's cells stay clean and carry only the `~` estimate marker.
|
||||
|
||||
**Always set `stage` on the interaction row.** The closing summary reports interaction time per stage
|
||||
alongside each stage's elapsed time, which is what makes the table actionable -- a 2h 22m stage
|
||||
costing ~11m of your attention reads very differently from one costing ~2h. That roll-up is only
|
||||
possible if every interaction is attributed when it is recorded:
|
||||
|
||||
```sql
|
||||
SELECT stage,
|
||||
SUM(strftime('%s', answered_at) - strftime('%s', prompted_at)) AS measured_seconds,
|
||||
SUM(answered_at IS NULL) AS unmeasured
|
||||
FROM release_interactions
|
||||
GROUP BY stage;
|
||||
```
|
||||
|
||||
A stage whose interactions all lack a usable reply timestamp reports `—`, not `~0m`. `~0m` means the
|
||||
stage had no interactions, or its measured intervals rounded to zero -- it asserts that the stage
|
||||
cost the user no material time, which is a claim you can only make from data you actually have.
|
||||
|
||||
The interval between `prompted_at` and `answered_at` is the user's **think-and-respond time**. Sum
|
||||
those intervals to estimate **active user-interaction time**. Do not treat the rest of the session as
|
||||
waiting by subtraction -- take waiting from `release_waits` and report the remainder as unaccounted.
|
||||
|
||||
Apply judgement when summing:
|
||||
|
||||
- Discard or cap any single interval that clearly represents the user stepping away rather than
|
||||
engaging -- an overnight gap between a gate and its answer is wait time, not interaction time.
|
||||
Note in the summary that such a gap was excluded.
|
||||
- Long stretches where the user reviews a diff, release notes, or a PR **are** interaction time even
|
||||
though you were idle.
|
||||
- Always label the result as an estimate, and state the measured/unmeasured split alongside it.
|
||||
|
||||
## Progress rail
|
||||
|
||||
Render the rail from the latest attempt of each stage in `release_stages` at every gating prompt:
|
||||
|
||||
```
|
||||
[✓] 1 Prepare → [●] 2 Review and merge → [ ] 3 Publish → [ ] 4 Release → [ ] 5 Verify
|
||||
```
|
||||
|
||||
Use `✓` for done, `●` for in progress, `⚠` for blocked, `↩` for carried over, and a blank for
|
||||
pending. Add the current sub-step after the rail when one is active, for example
|
||||
`current: waiting on CI (2 checks running)`.
|
||||
108
.github/agents/release-manager/references/summary-template.md
vendored
Normal file
108
.github/agents/release-manager/references/summary-template.md
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Release Wrap-Up Summary
|
||||
|
||||
Present this summary when the release is complete: the GitHub release is published, both the release
|
||||
and docs workflows have succeeded, the packages are listed on NuGet.org, and the docs site reflects
|
||||
the release.
|
||||
|
||||
The tone is short and celebratory. It is a chat message to the user -- **do not commit it, do not
|
||||
post it to GitHub, and do not write it to a file** unless the user explicitly asks.
|
||||
|
||||
Build the timing sections from the `release_stages`, `release_interactions`, `release_waits`, and
|
||||
`release_workflow_runs` tables described in [session-tracking.md](session-tracking.md).
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
🎉 **v{version} is released.**
|
||||
|
||||
{One or two sentences on the release theme, echoing the preamble that shipped in the release notes.}
|
||||
|
||||
**Shipped**
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Version | `v{version}` |
|
||||
| Base branch | `{base branch}` |
|
||||
| Release PR | #{pr} |
|
||||
| Release | {release URL} |
|
||||
| Release workflow | {run URL} — {conclusion} |
|
||||
| Docs workflow | {run URL} — {conclusion} |
|
||||
| NuGet | {listed package versions, or the package listing URL} |
|
||||
| Docs | https://csharp.sdk.modelcontextprotocol.io/{version-slug}/ — live |
|
||||
|
||||
**Packages**
|
||||
|
||||
* {package name} {version}
|
||||
* {package name} {version}
|
||||
|
||||
**Stage timing (this session)**
|
||||
|
||||
| Stage | Status | Elapsed | Interactions |
|
||||
|---|---|---|---|
|
||||
| 1. Prepare | ✓ | {h m} | ~{h m} |
|
||||
| 2. Review and merge | ✓ | {h m} | ~{h m} |
|
||||
| 2. Review and merge (attempt 2) | ✓ | {h m} | ~{h m} |
|
||||
| 3. Publish | ✓ | {h m} | ~{h m} |
|
||||
| 4. Release | ✓ | {h m} | ~{h m} |
|
||||
| 5. Verify | ✓ | {h m} | ~{h m} |
|
||||
| **Total session** | | **{h m}** | **~{h m}** |
|
||||
|
||||
{Include an attempt row only when a stage was reworked, and say what forced it -- "CI red, corrective
|
||||
push". Aggregating rework into one row hides where the time actually went.}
|
||||
|
||||
**Where the time went**
|
||||
|
||||
* Active interaction — ~{h m} across {n} measured gates{, plus {n} unmeasured}
|
||||
* Waiting on builds, CI, and the release and docs workflows — ~{h m}
|
||||
* Unaccounted — {h m}
|
||||
* Longest single wait — {h m} ({what you were waiting on})
|
||||
|
||||
{Optional: one line on anything notable — a blocked stage and how long it cost, an excluded
|
||||
step-away gap, or a stage that ran unusually long or short.}
|
||||
|
||||
**Follow-ups**
|
||||
|
||||
* {Anything deferred during the release, or "None."}
|
||||
* {Worktrees still on disk for this release, offered for cleanup, or omit this line.}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Only report what you observed.** Stages recorded as `carried_over` show `↩ carried over from a
|
||||
previous session` in the Status column and `—` for Elapsed. They are excluded from the total, and
|
||||
a footnote says the total covers this session only.
|
||||
2. **Total session** is wall-clock from `session_started_at` to now, not the sum of stage elapsed
|
||||
times -- gaps between stages belong to the session but to no stage.
|
||||
3. **The Interactions column is per-stage active user time.** Sum that stage's
|
||||
`release_interactions` prompt-to-answer intervals and prefix with `~`: `~5m`, `~1h 3m`. Show
|
||||
`~0m` when the stage had no interactions at all, or when its measured intervals round to zero.
|
||||
Show `—` when the stage *did* have interactions but none of them carry a usable reply timestamp --
|
||||
that is missing data, not zero time, and must never be rendered as `~0m` or fabricated.
|
||||
Carried-over stages show `—` in both time columns.
|
||||
4. **`~` is the only qualifier the table needs.** Never append "minimum", "at least", or a similar
|
||||
hedge to a cell -- the tilde already says the figure is estimated, and the table stays scannable.
|
||||
When stages show `—` or interactions went unmeasured, explain that in the narrative prose below
|
||||
rather than in the table.
|
||||
5. **Active interaction time is always an estimate.** Label it with `~` and say it is estimated from
|
||||
prompt-to-answer intervals. In the narrative, report it as a floor with the unmeasured count
|
||||
beside it -- interactions whose reply timestamp was unavailable are counted, not silently dropped.
|
||||
Name any interval you excluded as a step-away gap.
|
||||
6. **Reconcile the narrative with the table.** The `Active interaction` bullet must equal the table's
|
||||
total interaction cell. Where the two could differ -- excluded step-away gaps, `—` stages,
|
||||
zero-duration rows discarded as unmeasured -- name the discrepancy explicitly rather than letting
|
||||
the reader find it.
|
||||
7. **Waiting time is measured, not inferred.** Sum the `release_waits` intervals. Never derive it by
|
||||
subtracting interaction time from the session total; that counts diagnosis and rework as waiting.
|
||||
Show whatever the two do not account for as `Unaccounted` rather than folding it into either.
|
||||
**If `Unaccounted` computes negative, wait and interaction rows overlapped** — the split is
|
||||
unsound, so omit the `Unaccounted` line, report the two measured totals, and state plainly that
|
||||
they overlap. Never publish a negative figure and never clamp it to zero, which would present a
|
||||
broken split as a clean one.
|
||||
8. **Longest single wait comes from `release_waits` and `release_workflow_runs`**, not from the
|
||||
longest interaction. If waits were not recorded, say the data is unavailable instead of
|
||||
substituting the longest gate.
|
||||
9. **Round to readable units.** `2h 14m`, `47m`, `3m`. Never show seconds.
|
||||
10. **Omit rows and sections that do not apply.** No blocked stage means no note about one; no
|
||||
follow-ups means the section says `None.` rather than disappearing.
|
||||
11. **Never speculate about time.** If the session lacks the data for a section, say so plainly
|
||||
instead of estimating.
|
||||
Loading…
Reference in a new issue