mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 17:28:23 +01:00
* ship finish: an owner-ruled note closes its finding, and every ruling is listed (GST-1537) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(windows): exclude ship-finish-ruled-notes from the Windows lanes like its ship-bin-fixture siblings (GST-1537) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: keep the pinned Step 9 brief unchanged; the PR body's Pre-Landing Review lists ruled notes (GST-1537) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Review-Finding: p1/specialist-batched#1 reader=state-machines Review-Finding: p1/red-team#2 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Review-Finding: p2/red-team#1 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: bind each ruled note to the stop head it was ruled at (GST-1537) Review-Finding: p2/red-team#1 reader=red-team Review-Finding: s11/outside-voice#1 reader=outside-voice Review-Finding: p1/specialist-batched#2 reader=testing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
51 lines
3.0 KiB
TypeScript
51 lines
3.0 KiB
TypeScript
/**
|
|
* Owner-ruled notes (GST-1537, docs/ship-policy.md#ruled-notes): the owner's ruling that a review finding ships as a
|
|
* disclosed note instead of a fix. A ruling is one approved gstack-approvals row (`gstack-approvals rule`), with scope
|
|
* `ruled_note <finding fingerprint> <head>` and ask `ruled_note <finding fingerprint> at <head>: <reason>`, where head is the
|
|
* full sha of the stop head the owner ruled at; the row's `by` is the ruler and its `recorded_at` the time. A review-log field
|
|
* never makes one, so the lane that logged a finding cannot close it.
|
|
*/
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { locate, readRecord, type ApprovalRow, type Env } from './approval-record';
|
|
|
|
export interface RuledNote { finding: string; head: string; by: string; reason: string; at: string; approval: string }
|
|
|
|
const SCOPE = /^ruled_note (\S+) ([0-9a-f]{40})$/;
|
|
const ASK = /^ruled_note (\S+) at ([0-9a-f]{40}): (\S(?:.*\S)?)$/;
|
|
/** A ruler gstack names for itself (the no-stop default's `gstack no-stop default (K1)`) is no owner. */
|
|
export const GSTACK_ACTOR = /^gstack\b/i;
|
|
export const RULED_NOTE_SCOPE = /^\s*ruled_note\b/;
|
|
|
|
export const ruledNoteRow = (finding: string, head: string, reason: string) => ({ scope: `ruled_note ${finding} ${head}`, ask: `ruled_note ${finding} at ${head}: ${reason}` });
|
|
|
|
/** The approved ruling rows, one per finding and head (the newest wins). */
|
|
export function ruledNotes(rows: readonly ApprovalRow[]): RuledNote[] {
|
|
const byFinding = new Map<string, RuledNote>();
|
|
for (const r of rows) {
|
|
const scope = SCOPE.exec(r.scope), ask = ASK.exec(r.ask);
|
|
if (r.outcome !== 'approved' || !scope || !ask || ask[1] !== scope[1] || ask[2] !== scope[2] || GSTACK_ACTOR.test(r.by)) continue;
|
|
const finding = scope[1]!, head = scope[2]!, key = `${finding} ${head}`;
|
|
byFinding.delete(key);
|
|
byFinding.set(key, { finding, head, by: r.by, reason: ask[3]!, at: r.recorded_at, approval: r.id });
|
|
}
|
|
return [...byFinding.values()];
|
|
}
|
|
|
|
/** The rulings in the checked-out branch's approval record (and its mirror). */
|
|
export function readRuledNotes(cwd: string, env: Env = process.env): RuledNote[] {
|
|
const loc = locate({ cwd, env });
|
|
return 'code' in loc ? [] : ruledNotes(readRecord(loc).rows);
|
|
}
|
|
|
|
/** The one-line form finish prints after `RULED_NOTE: ` and the PR body carries after `Ruled note: `. */
|
|
export const ruledNoteText = (n: RuledNote) => `${n.finding} at ${n.head.slice(0, 12)} ruled by ${n.by} at ${n.at}: ${n.reason} (approval ${n.approval})`;
|
|
|
|
/** The /ship run open on this checkout (its marker in the git dir, which finish removes), if any. */
|
|
export function openShipRun(cwd: string): string | undefined {
|
|
const p = spawnSync('git', ['rev-parse', '--git-path', 'gstack-ship-run'], { cwd, encoding: 'utf8', timeout: 10_000 });
|
|
if (p.status !== 0 || !p.stdout.trim()) return undefined;
|
|
try { return fs.readFileSync(path.resolve(cwd, p.stdout.trim()), 'utf8').trim() || undefined; } catch { return undefined; }
|
|
}
|