Skip to content

Commit c6e8526

Browse files
authored
Merge pull request #66 from TrentBrown/tb-portreeve-pr-publication-02-wiring
Publish release metadata through audited pull requests
2 parents 95a2216 + 4ae17fb commit c6e8526

20 files changed

Lines changed: 991 additions & 149 deletions

.agents/skills/release-portreeve/SKILL.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ Gatekeeper, or native evidence must remain a failure.
8282
## Preserve the publication boundary
8383

8484
Do not set `publish=true`, call `release:publish`, create or move a tag, create a GitHub
85-
Release, push the Homebrew tap, update public Desktop metadata, or publish npm unless
86-
the user explicitly requests those public mutations after the exact plan is available.
85+
Release, create or merge a Homebrew/desktop-metadata PR, or publish npm unless the user
86+
explicitly requests those public mutations after the exact plan is available.
8787

8888
When publication is explicitly requested:
8989

@@ -94,7 +94,13 @@ When publication is explicitly requested:
9494
4. Use the normal `release-publication` environment or the documented direct command.
9595
5. Never bypass `--confirm`, remote preflight, environment review, or immutable-version
9696
refusal.
97-
6. Keep npm deferred; npm Trusted Publishing is independent of this pipeline.
97+
6. Expect GitHub Release publication first, followed by deterministic merge-commit PRs
98+
for the tap and Desktop update metadata. Never write directly to destination `main`.
99+
7. If checks or independent review block a generated PR, report its exact URL, leave it
100+
unchanged, and retry the same approved record after normal repository policy is met.
101+
8. On retry, verify and reuse exact open or merged PR state; never force-push, retarget,
102+
add unrelated files, bypass protection, or invent PR provenance for legacy evidence.
103+
9. Keep npm deferred; npm Trusted Publishing is independent of this pipeline.
98104

99105
## Report the outcome
100106

@@ -106,6 +112,7 @@ Separate:
106112
- generated artifact names and digests;
107113
- preview/stable, maturity, and Desktop trust;
108114
- public mutations performed, or an explicit statement that none occurred;
115+
- generated Homebrew and Desktop PR URLs plus verified merge commits when published;
109116
- exact recovery or next command.
110117

111118
Never describe a cross-compiled file as natively verified, an unsigned preview as

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ jobs:
218218
environment: release-publication
219219
permissions:
220220
contents: write
221+
pull-requests: write
221222
steps:
222223
- uses: actions/checkout@v7
223224
- uses: actions/setup-node@v7

docs/issues/tb-portreeve-pr-publication/issues.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Operational task breakdown derived from the plan.
99

1010
## I-1 - Build the deterministic repository PR adapter
1111

12-
- **Status:** in-review
12+
- **Status:** closed
1313
- **Estimate:** 1d
1414
- **Plan steps:** P1
1515
- **Rubric criteria:** R2, R3
@@ -21,48 +21,48 @@ no real public mutation.
2121

2222
## I-2 - Route publication through exact PRs
2323

24-
- **Status:** open
24+
- **Status:** in-review
2525
- **Estimate:** 1d
2626
- **Plan steps:** P2, P3
2727
- **Rubric criteria:** R1, R2, R3, R4
2828
- **Depends on:** I-1
29-
- **PR:** -
29+
- **PR:** [#66](https://github.com/TrentBrown/portreeve/pull/66)
3030

3131
Replace both direct repository mutation paths, preserve release-first ordering, and
3232
prove idempotent partial-publication recovery.
3333

3434
## I-3 - Persist complete and compatible publication evidence
3535

36-
- **Status:** open
36+
- **Status:** in-review
3737
- **Estimate:** 0.5d
3838
- **Plan steps:** P4
3939
- **Rubric criteria:** R5
4040
- **Depends on:** I-2
41-
- **PR:** -
41+
- **PR:** [#66](https://github.com/TrentBrown/portreeve/pull/66)
4242

4343
Record PR URLs and merge commits for new releases without fabricating them for the
4444
completed first preview.
4545

4646
## I-4 - Align hosted authority and operator surfaces
4747

48-
- **Status:** open
48+
- **Status:** in-review
4949
- **Estimate:** 0.5d
5050
- **Plan steps:** P5, P6
5151
- **Rubric criteria:** R6, R7
5252
- **Depends on:** I-2, I-3
53-
- **PR:** -
53+
- **PR:** [#66](https://github.com/TrentBrown/portreeve/pull/66)
5454

5555
Update workflow permissions, contracts, plan text, runbook, credential guidance, and
5656
the project-local release skill.
5757

5858
## I-5 - Verify and close the feature
5959

60-
- **Status:** open
60+
- **Status:** in-review
6161
- **Estimate:** 0.5d
6262
- **Plan steps:** P7
6363
- **Rubric criteria:** R1, R2, R3, R4, R5, R6, R7
6464
- **Depends on:** I-1, I-2, I-3, I-4
65-
- **PR:** -
65+
- **PR:** [#66](https://github.com/TrentBrown/portreeve/pull/66)
6666

6767
Run focused and broad verification plus every required PR-boundary and final feature
6868
gate.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"schemaVersion": 1,
3+
"scope": "feature-final",
4+
"featureId": "tb-portreeve-pr-publication",
5+
"repositoryAlias": "portreeve",
6+
"packetId": "pr-66",
7+
"pullRequest": {
8+
"repository": "TrentBrown/portreeve",
9+
"number": 66,
10+
"url": "https://github.com/TrentBrown/portreeve/pull/66",
11+
"state": "OPEN",
12+
"isDraft": true,
13+
"baseRefName": "main",
14+
"baseRefOid": "95a221670e19f859ae872aab3aa895341ce2e0d0",
15+
"headRefName": "tb-portreeve-pr-publication-02-wiring",
16+
"headRefOid": "6d304b9d9c5adeeef6927f2f5ac3919bccd3d404"
17+
},
18+
"mergeBaseSha": "95a221670e19f859ae872aab3aa895341ce2e0d0",
19+
"evaluatedSourceSha": "6d304b9d9c5adeeef6927f2f5ac3919bccd3d404",
20+
"featureBaseSha": "b13ccd5d8a86dcf36dfaf6986ab7214fcd74face",
21+
"applicability": {
22+
"specEvaluation": true,
23+
"judge": true,
24+
"patternReview": true
25+
},
26+
"gates": {
27+
"verification": { "disposition": "passed", "reason": null },
28+
"specEvaluation": { "disposition": "passed", "reason": null },
29+
"judge": { "disposition": "passed", "reason": null },
30+
"codeReview": { "disposition": "passed", "reason": null },
31+
"patternReview": { "disposition": "passed", "reason": null },
32+
"explainDiff": { "disposition": "passed", "reason": null }
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Code Review - PR #66
2+
3+
## Findings
4+
5+
No remaining findings.
6+
7+
The review found one sequencing hardening opportunity: adapter result identity was
8+
initially validated only after all three publication surfaces had run. Commit
9+
`6d304b9` now validates the GitHub Release URL immediately, then validates Homebrew's PR
10+
URL and merge commit before Desktop publication may begin, and finally validates
11+
Desktop evidence before the terminal transition. A regression assertion proves that a
12+
malformed Homebrew result cannot invoke Desktop publication.
13+
14+
## Review coverage
15+
16+
- Pinned slice: `95a221670e19f859ae872aab3aa895341ce2e0d0..6d304b9d9c5adeeef6927f2f5ac3919bccd3d404`.
17+
- Candidate-byte reading and checksum binding.
18+
- GitHub API-only Homebrew and Desktop adapter configuration.
19+
- Approval persistence and GitHub Release-first ordering.
20+
- Result-validation order and partial-publication behavior.
21+
- Release-record discriminator, terminal evidence equality, and legacy honesty.
22+
- Completion-document version and required PR provenance.
23+
- Workflow secret/permission placement.
24+
- Operator runbook and project skill consistency.
25+
- Absence of ordinary Git credentials, direct-main fallback, force-push, policy bypass,
26+
npm publication, or signing scope.
27+
28+
## Residual risks and test gaps
29+
30+
- No live publication was attempted because it would create public state. Deterministic
31+
fake GitHub responses cover the complete state matrix; the next explicitly authorized
32+
release remains the correct live integration exercise.
33+
- GitHub REST API version `2022-11-28` is explicit, so incompatible future API behavior
34+
should fail visibly rather than silently weaken evidence.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>PR #66 — Audited release publication</title>
7+
<style>
8+
:root{--ink:#142b36;--muted:#587078;--paper:#f7f3e8;--card:#fffdf7;--line:#c9d3cf;--sea:#0f5f4a;--sky:#dbeaf0;--sand:#eee2c5;--warn:#fff0c7;--ok:#d8efe3;--bad:#f4d8d3}
9+
*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:17px/1.58 system-ui,-apple-system,sans-serif}
10+
header{background:linear-gradient(135deg,#082f49,#0f5f4a);color:white;padding:4rem max(6vw,1.5rem) 3rem}header p{max-width:70ch;color:#e1f2ee}
11+
main{max-width:1120px;margin:auto;padding:2rem max(3vw,1rem) 5rem}nav{background:var(--card);border:1px solid var(--line);padding:1rem 1.25rem;border-radius:14px;margin:-2.8rem 0 2rem;position:relative}nav a{color:var(--sea);margin-right:1rem}
12+
h2{margin-top:3.5rem;border-bottom:2px solid var(--line);padding-bottom:.35rem}h3{margin-top:2rem}
13+
.card,.callout,.quiz{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.25rem;margin:1rem 0}
14+
.callout{border-left:6px solid var(--sea)}.warn{background:var(--warn);border-left-color:#ad6b00}
15+
.flow{display:grid;grid-template-columns:repeat(7,minmax(110px,1fr));gap:.55rem;align-items:center;margin:1.5rem 0;overflow:auto;padding-bottom:.5rem}
16+
.node{min-width:110px;padding:.75rem;border-radius:12px;background:var(--sky);border:1px solid #9cb9c5;text-align:center;font-weight:700}.node.audit{background:var(--sand)}.node.final{background:var(--ok)}
17+
.arrow{text-align:center;color:var(--sea);font-size:1.5rem}
18+
.compare{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.before{border-top:5px solid #9b4b40}.after{border-top:5px solid var(--sea)}
19+
.state{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.8rem}.state div{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:1rem}
20+
code,pre{font-family:ui-monospace,SFMono-Regular,monospace}code{background:#e8ece8;padding:.1rem .3rem;border-radius:4px}pre{white-space:pre-wrap;background:#102a35;color:#e8f5f1;padding:1rem;border-radius:12px;overflow:auto}
21+
button{display:block;width:100%;text-align:left;margin:.5rem 0;padding:.75rem;border:1px solid var(--line);border-radius:9px;background:white;color:var(--ink);cursor:pointer}button:hover{border-color:var(--sea)}button.correct{background:var(--ok)}button.wrong{background:var(--bad)}
22+
.feedback{min-height:1.5rem;color:var(--muted);font-weight:600}.small{color:var(--muted);font-size:.92rem}
23+
@media(max-width:760px){.compare{grid-template-columns:1fr}.flow{grid-template-columns:1fr}.arrow{transform:rotate(90deg)}}
24+
</style>
25+
</head>
26+
<body>
27+
<header>
28+
<p class="small">PortReeve • Pull request #66 • pinned slice</p>
29+
<h1>Release metadata now travels through audited pull requests</h1>
30+
<p>This explanation covers exactly <code>95a221670e19f859ae872aab3aa895341ce2e0d0..6d304b9d9c5adeeef6927f2f5ac3919bccd3d404</code>. It explains the final slice that connects the already-reviewed GitHub PR state machine to real PortReeve publication.</p>
31+
</header>
32+
<main>
33+
<nav><strong>Contents:</strong> <a href="#background">Background</a><a href="#intuition">Intuition</a><a href="#code">Code</a><a href="#recovery">Recovery</a><a href="#quiz">Quiz</a></nav>
34+
35+
<section id="background">
36+
<h2>Background</h2>
37+
<h3>The release is more than a downloadable file</h3>
38+
<p>A PortReeve preview publishes immutable executables and disk images in a GitHub Release. It also updates two mutable discovery surfaces: the Homebrew tap tells <code>brew</code> which checksummed artifacts to install, while <code>distribution/desktop-update.json</code> tells the Desktop application which preview is available.</p>
39+
<div class="flow">
40+
<div class="node">Approved candidate</div><div class="arrow"></div>
41+
<div class="node">GitHub Release</div><div class="arrow"></div>
42+
<div class="node audit">Homebrew metadata</div><div class="arrow"></div>
43+
<div class="node audit">Desktop metadata</div>
44+
</div>
45+
<p>The old adapters updated those mutable repositories in two different ways. Homebrew cloned a repository and pushed Git directly. Desktop metadata called the GitHub contents API against <code>main</code>. Both could work, but neither left the same reviewable, policy-governed record as ordinary development.</p>
46+
<div class="compare">
47+
<div class="card before"><h3>Before</h3><ul><li>Homebrew: clone, commit, push</li><li>Desktop: contents API PUT to <code>main</code></li><li>Two credential and mutation models</li><li>Only commit SHAs in terminal evidence</li></ul></div>
48+
<div class="card after"><h3>After</h3><ul><li>Both: exact GitHub API branch + PR</li><li>Merge commit only when policy permits</li><li>One fine-grained token model</li><li>PR URLs and merge SHAs in evidence</li></ul></div>
49+
</div>
50+
<div class="callout"><strong>Important boundary:</strong> The release-environment approval still authorizes the exact publication plan. Generated PRs are audit and transport records; they do not silently weaken repository checks or invent a second PortReeve approval system.</div>
51+
</section>
52+
53+
<section id="intuition">
54+
<h2>Intuition</h2>
55+
<p>Think of the approved publication plan as a sealed shipping manifest. The GitHub Release unloads the immutable cargo first. Each mutable repository then receives its own tamper-evident delivery note: a deterministic branch and a PR whose hidden marker names the source commit, plan digest, destination, generated commit, base commit, file paths, and file checksums.</p>
56+
<pre>{
57+
"releaseVersion": "0.2.0-preview.1",
58+
"sourceCommit": "abc…",
59+
"planSha256": "def…",
60+
"files": [
61+
{"path": "Formula/portreeve.rb", "sha256": "123…"}
62+
]
63+
}</pre>
64+
<p>A retry does not ask “did something probably happen?” It asks “does the exact branch or PR identified by this sealed manifest exist, and do its ancestry and bytes still match?” That turns partial publication from an improvised repair into a deterministic state machine.</p>
65+
<div class="state">
66+
<div><strong>Absent</strong><br>Create exact branch, commit, and PR.</div>
67+
<div><strong>Open and clean</strong><br>Verify identity, then merge.</div>
68+
<div><strong>Open and blocked</strong><br>Leave intact; report the URL.</div>
69+
<div><strong>Already merged</strong><br>Verify destination and reuse.</div>
70+
<div><strong>Conflicting</strong><br>Fail closed; never force-push.</div>
71+
</div>
72+
</section>
73+
74+
<section id="code">
75+
<h2>Code walkthrough</h2>
76+
<h3>1. Repository adapters become configurations</h3>
77+
<p><code>scripts/publication-adapters.js</code> no longer contains a Homebrew clone/push or a Desktop contents-PUT path. It reads the finalized candidate bytes and supplies the shared PR publisher with an exact repository, <code>main</code> branch, title, source commit, plan digest, and allowlisted files.</p>
78+
<ul><li>Homebrew may change only <code>Formula/portreeve.rb</code> and <code>Casks/portreeve-app.rb</code>.</li><li>PortReeve may change only <code>distribution/desktop-update.json</code>.</li></ul>
79+
80+
<h3>2. Orchestration validates each result before continuing</h3>
81+
<div class="flow">
82+
<div class="node">Release URL</div><div class="arrow"></div>
83+
<div class="node">Tap PR + merge SHA</div><div class="arrow"></div>
84+
<div class="node">Desktop PR + merge SHA</div><div class="arrow"></div>
85+
<div class="node final">Published record</div>
86+
</div>
87+
<p><code>scripts/publish-release.js</code> passes the exact plan digest into both PRs. It validates each surface's returned evidence immediately, so malformed Homebrew provenance cannot allow Desktop publication to begin. The record remains <code>publication-approved</code> until every surface has succeeded.</p>
88+
89+
<h3>3. Terminal evidence distinguishes new transport from real history</h3>
90+
<p>New approvals carry <code>transport: github-pull-request-v1</code>. That discriminator makes PR URLs mandatory for new completion evidence while allowing the already-completed first preview to remain exactly as it happened.</p>
91+
<div class="callout warn"><strong>No invented history:</strong> a completed legacy record without PR URLs remains readable. A partially approved legacy record cannot silently resume through the new transport; the operator must prepare a new candidate.</div>
92+
<p>The new <code>publication-complete.json</code> uses schema version 2 and includes the release URL, tag, both PR URLs, both merge commits, plan digest, and publication time. Release-record validation also requires the summary publication object to be deeply equal to its terminal stage evidence.</p>
93+
94+
<h3>4. Hosted authority and operator guidance match</h3>
95+
<p>The workflow remains globally read-only. Only the <code>release-publication</code> job declares <code>contents: write</code> and <code>pull-requests: write</code>, and only that job receives the fine-grained token. The runbook and project skill now explain automatic merge, independent-review fallback, safe retry, branch cleanup, and the absence of ordinary Git credentials.</p>
96+
</section>
97+
98+
<section id="recovery">
99+
<h2>What recovery looks like</h2>
100+
<p>Suppose the GitHub Release and Homebrew PR merge successfully, but Desktop publication is interrupted. The local record remains approved rather than published. On retry:</p>
101+
<ol><li>The immutable GitHub Release is byte-verified and reused.</li><li>The deterministic Homebrew PR is found, its merge commit and destination bytes are verified, and no duplicate PR is created.</li><li>The Desktop PR is created or recovered and merged when policy permits.</li><li>Only then are complete terminal evidence and the completion document written.</li></ol>
102+
<p>If checks or an independent reviewer are required, the publisher returns an actionable error containing the exact PR URL. It does not self-approve, bypass protection, close the PR, or delete its branch.</p>
103+
</section>
104+
105+
<section id="quiz">
106+
<h2>Quiz</h2>
107+
<p>Choose one answer for each question.</p>
108+
<div class="quiz" data-answer="1"><strong>1. What is the normal human authorization for publication?</strong>
109+
<button>The generated Homebrew PR approval</button><button>The release-publication environment approval of the exact plan</button><button>The Desktop metadata PR merge</button><p class="feedback"></p></div>
110+
<div class="quiz" data-answer="2"><strong>2. When may the publisher merge a generated PR automatically?</strong>
111+
<button>Whenever its branch name starts with tb-</button><button>After the GitHub Release exists, regardless of checks</button><button>Only when GitHub reports the exact PR clean and mergeable</button><p class="feedback"></p></div>
112+
<div class="quiz" data-answer="0"><strong>3. Why does the approval record contain a transport discriminator?</strong>
113+
<button>To require PR provenance for new releases without fabricating it for legacy history</button><button>To select between GitHub and GitLab dynamically</button><button>To choose preview versus stable update channels</button><p class="feedback"></p></div>
114+
<div class="quiz" data-answer="1"><strong>4. What happens if Homebrew succeeds and Desktop publication fails?</strong>
115+
<button>The release is marked published with Desktop pending</button><button>The record stays publication-approved and the exact state is reused on retry</button><button>The Homebrew merge is reverted automatically</button><p class="feedback"></p></div>
116+
<div class="quiz" data-answer="2"><strong>5. Which write authority is available during hosted preparation?</strong>
117+
<button>Contents write only</button><button>Pull requests write only</button><button>Neither; preparation remains contents-read and receives no publication secret</button><p class="feedback"></p></div>
118+
</section>
119+
</main>
120+
<script>
121+
document.querySelectorAll('.quiz').forEach(q=>{q.querySelectorAll('button').forEach((b,i)=>b.addEventListener('click',()=>{const correct=Number(q.dataset.answer);q.querySelectorAll('button').forEach((x,j)=>x.className=j===correct?'correct':j===i?'wrong':'');q.querySelector('.feedback').textContent=i===correct?'Correct — that matches the exact-plan, policy-preserving model.':'Not quite. Review the approval, recovery, and authority sections above.'}))})
122+
</script>
123+
</body>
124+
</html>

0 commit comments

Comments
 (0)