Skip to content

Commit 63ffaa2

Browse files
committed
FEATURE-ci-v3-integration: bind PR artifact origin to API branch head
The real PR artifact and run APIs report the branch head while GITHUB_SHA identifies the test merge checkout. Supply the trusted event head explicitly to download origin checks, retaining exact selected merge source, archive, run, attempt, repository and digest validation. Reproduced the actual captured artifact failure twice before repair. Real ZIP replay now validates the branch-head origin and unchanged merge descriptor/archive; 15 release contracts and 10 workflow contracts pass, including negative source/origin/run/attempt/digest controls. Actionlint and Python compilation pass. Native/fuzz/docs/public443 evidence is reused unchanged; fresh actual CI remains root-owned.
1 parent 08c5b7b commit 63ffaa2

7 files changed

Lines changed: 78 additions & 6 deletions

File tree

‎.github/workflows/v3-release-acceptance.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ name: v3 selected archive acceptance
2626
permissions:
2727
contents: read
2828
actions: read
29+
env:
30+
# Artifact/run APIs identify the PR branch head; checkout tests its merge SHA.
31+
WORKFLOW_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
2932
jobs:
3033
prepare:
3134
runs-on: ubuntu-24.04

‎docs/v3-ci.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ a download action is insufficient. Artifact creation must belong to the current
9393
run attempt; rerun all jobs to produce fresh release proof rather than reusing
9494
artifacts from successful jobs in an earlier attempt.
9595

96+
On PR runs, artifact/run API metadata identifies the PR branch head, while
97+
`GITHUB_SHA` and the selected checkout identify the test merge commit. Downloads
98+
bind the API head to the trusted event's PR head. Selected archive preparation
99+
and platform proof still require the exact selected checkout commit, source
100+
snapshot and archive bytes; these two source identities are not interchangeable.
101+
96102
The final publisher alone receives `contents: write`. It checks successful
97103
mandatory dependencies, same-run artifacts, manifest and archive hashes and the
98104
current tag target before `gh release create --verify-tag`. It runs only inline

‎scripts/lib/release_ci.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def validate_request(root, commit, version):
8585

8686
def verify_download(archive, metadata, run, expected, expected_digest, destination):
8787
require(str(metadata['id']) == expected['artifact_id'] and metadata['expired'] is False, 'wrong/expired artifact ID')
88-
require(str(metadata['workflow_run']['id']) == expected['run_id'] and metadata['workflow_run']['head_sha'] == expected['source_commit'], 'artifact from another workflow source/run')
88+
require(str(metadata['workflow_run']['id']) == expected['run_id'] and metadata['workflow_run']['head_sha'] == expected['workflow_head'], 'artifact from another workflow source/run')
8989
require(str(run['id']) == expected['run_id'] and str(run['run_attempt']) == expected['run_attempt']
90-
and run['repository']['full_name'] == expected['repository'] and run['head_sha'] == expected['source_commit'], 'wrong repository/source/run attempt')
90+
and run['repository']['full_name'] == expected['repository'] and run['head_sha'] == expected['workflow_head'], 'wrong repository/source/run attempt')
9191
require(datetime.fromisoformat(metadata['created_at']) >= datetime.fromisoformat(run['run_started_at']), 'artifact belongs to an earlier run attempt')
9292
from release_artifact import sha256
9393
digest = 'sha256:' + sha256(archive)

‎scripts/run-v3-release-ci.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def download(args):
129129
import subprocess
130130
from release_ci import verify_download
131131
expected=dict(repository=os.environ['GITHUB_REPOSITORY'],run_id=os.environ['GITHUB_RUN_ID'],
132-
run_attempt=os.environ['GITHUB_RUN_ATTEMPT'],source_commit=os.environ['GITHUB_SHA'],artifact_id=args.artifact_id)
132+
run_attempt=os.environ['GITHUB_RUN_ATTEMPT'],workflow_head=os.environ['WORKFLOW_HEAD_SHA'],artifact_id=args.artifact_id)
133133
require(args.artifact_id.isdigit(), 'invalid artifact ID')
134134
endpoint='repos/'+expected['repository']+'/actions/artifacts/'+args.artifact_id
135135
metadata=json.loads(subprocess.check_output(['gh','api',endpoint],text=True))

‎scripts/test-v3-ci-workflows.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ def workflow(name):
1717

1818

1919
class CIWorkflows(unittest.TestCase):
20+
def test_artifact_api_head_is_bound_to_trusted_event_head(self):
21+
lane = workflow('v3-release-acceptance.yml')
22+
self.assertEqual(lane.get('env', {}).get('WORKFLOW_HEAD_SHA'),
23+
'${{ github.event.pull_request.head.sha || github.sha }}')
24+
2025
def test_freebsd_stages_selected_archive_for_vm_transfer(self):
2126
steps = workflow('v3-release-acceptance.yml')['jobs']['freebsd']['steps']
2227
run = next(step['run'] for step in steps if 'ci-selected-input' in step.get('run', ''))

‎specs/tasks/FEATURE-ci-v3-integration.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ protocol behavior, default-branch setting, tag or release changes.
2020
and recheck the existing tag before the release side effect.
2121
- [x] Exercise publication refusal and success through the actual validator,
2222
with only the final publication side effect mocked.
23-
- [ ] Root: run the actual GitHub PR pipeline; verify manual dispatch availability.
23+
- [x] Root: verify manual native campaign dispatch invocation (accepted at
24+
08c5b7b5; campaign execution outcomes remain pending).
25+
- [ ] Root: complete actual GitHub PR pipeline verification. First selected
26+
archive attempt failed at API PR-head versus merge-SHA origin binding;
27+
causal repair and negative controls passed locally; fresh CI is pending.
2428
- [ ] CI / v3 PR: obtain fresh full local gate and FreeBSD/Darwin/Windows receipts
2529
for the selected commit/archive. Missing evidence is not a release pass.
2630

@@ -29,7 +33,8 @@ are unchanged and cannot replace fresh evidence.
2933

3034
Local verification: native TLS-off/on suites (239/270 tests, two documented
3135
nonlocal backend skips in each), all eleven sanitizer fuzz targets, genuine
32-
Doxygen/Graphviz, GCC QUIC replay and 100 focused CI/release/fuzz contracts passed.
36+
Doxygen/Graphviz, GCC QUIC replay and 100 initial CI/release/fuzz contracts passed.
37+
The subsequent artifact-origin repair adds three focused contracts (103 total).
3338
The fresh Linux installed attempt passed TLS-off but failed TLS-on public TLS at
3439
unprivileged TCP443 binding. Its unsuccessful receipt remains unchanged. A fresh
3540
focused public TLS run using the existing isolated network path passed all four

‎test/integ/release_ci_test.py‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def test_immutable_download_checks_source_run_attempt_digest_and_zip_members(sel
151151
ci=self.ci()
152152
archive=self.directory/'artifact.zip'
153153
with zipfile.ZipFile(archive,'w') as stream: stream.writestr('selected.json','selected bytes')
154-
expected=dict(repository='etr/libhttpserver',run_id='12',run_attempt='1',source_commit='d'*40,artifact_id='23')
154+
expected=dict(repository='etr/libhttpserver',run_id='12',run_attempt='1',workflow_head='d'*40,artifact_id='23')
155155
metadata=dict(id=23,expired=False,created_at='2026-10-15T03:01:00Z',workflow_run={'id':12,'head_sha':'d'*40},digest='sha256:'+hashlib.sha256(archive.read_bytes()).hexdigest())
156156
run=dict(id=12,run_attempt=1,run_started_at='2026-10-15T03:00:00Z',repository={'full_name':'etr/libhttpserver'},head_sha='d'*40)
157157
ci.verify_download(archive,metadata,run,expected,metadata['digest'],self.directory/'download')
@@ -168,6 +168,59 @@ def test_immutable_download_checks_source_run_attempt_digest_and_zip_members(sel
168168
with self.subTest(name=name),self.assertRaises(ValueError):
169169
ci.verify_download(bad,dict(metadata,digest=digest),run,expected,digest,self.directory/('badzip-%d'%index))
170170

171+
def test_pr_download_binds_api_branch_head_and_retains_merge_source_descriptor(self):
172+
import hashlib
173+
import zipfile
174+
ci=self.ci()
175+
head='08c5b7b5eb5e992f5a12d88136064010fc435f9d'
176+
merge='b5f8d51fe8b597fb3c720aa5459c04b9147c615b'
177+
archive=self.directory/'pr-artifact.zip'
178+
with zipfile.ZipFile(archive,'w') as stream:
179+
stream.writestr('selected.json',json.dumps({'commit':merge,'source':{'head':merge}}))
180+
digest='sha256:'+hashlib.sha256(archive.read_bytes()).hexdigest()
181+
expected=dict(repository='etr/libhttpserver',run_id='38848731938',run_attempt='1',workflow_head=head,artifact_id='12024142302')
182+
metadata=dict(id=12024142302,expired=False,created_at='2026-10-16T04:05:15Z',
183+
workflow_run=dict(id=38848731938,head_sha=head),digest=digest)
184+
run=dict(id=38848731938,run_attempt=1,head_sha=head,event='pull_request',
185+
repository={'full_name':'etr/libhttpserver'},run_started_at='2026-10-16T04:04:45Z')
186+
destination=self.directory/'pr-download'
187+
ci.verify_download(archive,metadata,run,expected,digest,destination)
188+
self.assertEqual(json.loads((destination/'selected.json').read_text())['commit'],merge)
189+
for index, altered in enumerate((dict(expected,workflow_head=merge),dict(expected,workflow_head='a'*40),
190+
dict(expected,run_id='38848731939'),dict(expected,run_attempt='2'),dict(expected,repository='other/repo'))):
191+
with self.subTest(expected=altered),self.assertRaises(ValueError):
192+
ci.verify_download(archive,metadata,run,altered,digest,self.directory/('wrong-pr-'+str(index)))
193+
for field,value in (('id',12024142303),('workflow_run',dict(id=38848731939,head_sha=head)),
194+
('workflow_run',dict(id=38848731938,head_sha=merge)),('digest','sha256:'+'a'*64)):
195+
with self.subTest(field=field,value=value),self.assertRaises(ValueError):
196+
ci.verify_download(archive,dict(metadata,**{field:value}),run,expected,digest,self.directory/'wrong-origin')
197+
198+
def test_download_entrypoint_uses_trusted_api_head_instead_of_pr_merge_sha(self):
199+
import argparse
200+
import hashlib
201+
import io
202+
import os
203+
import zipfile
204+
from unittest.mock import patch
205+
spec=importlib.util.spec_from_file_location('release_ci_entrypoint',ROOT/'scripts/run-v3-release-ci.py')
206+
entrypoint=importlib.util.module_from_spec(spec); spec.loader.exec_module(entrypoint)
207+
head='08c5b7b5eb5e992f5a12d88136064010fc435f9d'; merge='b5f8d51fe8b597fb3c720aa5459c04b9147c615b'
208+
stream=io.BytesIO()
209+
with zipfile.ZipFile(stream,'w') as archive: archive.writestr('selected.json',json.dumps({'commit':merge}))
210+
contents=stream.getvalue(); digest='sha256:'+hashlib.sha256(contents).hexdigest()
211+
metadata=dict(id=12024142302,expired=False,created_at='2026-10-16T04:05:15Z',
212+
workflow_run=dict(id=38848731938,head_sha=head),digest=digest)
213+
run=dict(id=38848731938,run_attempt=1,head_sha=head,repository={'full_name':'etr/libhttpserver'},run_started_at='2026-10-16T04:04:45Z')
214+
def transport(argv,stdout,check):
215+
self.assertEqual(argv,['gh','api','repos/etr/libhttpserver/actions/artifacts/12024142302/zip'])
216+
stdout.write(contents)
217+
env=dict(GITHUB_REPOSITORY='etr/libhttpserver',GITHUB_RUN_ID='38848731938',GITHUB_RUN_ATTEMPT='1',
218+
GITHUB_SHA=merge,WORKFLOW_HEAD_SHA=head)
219+
args=argparse.Namespace(artifact_id='12024142302',digest=digest,output=self.directory/'cli-download')
220+
with patch.dict(os.environ,env),patch('subprocess.check_output',side_effect=[json.dumps(metadata),json.dumps(run)]),patch('subprocess.run',side_effect=transport):
221+
entrypoint.download(args)
222+
self.assertEqual(json.loads((args.output/'selected.json').read_text())['commit'],merge)
223+
171224
def test_exact_commit_request_rejects_modified_and_untracked_source_inputs(self):
172225
import subprocess
173226
ci=self.ci(); root=self.directory/'checkout'; (root/'scripts').mkdir(parents=True)

0 commit comments

Comments
 (0)