mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
b64709e7b7
Continuous development must not change the source of an admitted release. Allow the workflow to run at the qualified preparation PR head after its normal merge, verifying exact source and workflow identity, canonical PR provenance, and ancestry in the governed release line. Later branch commits remain outside that release. Document the immutable-candidate contract and verify source workflow compatibility before qualification. Validation: snapshot identity and workflow contract tests passed, including wrong-head, wrong-base, fork, unmerged and unbound dispatch rejection. The existing release workflow promotion policy test also passed.
86 lines
4.1 KiB
Python
86 lines
4.1 KiB
Python
"""Release provenance stays bound to the reviewed commit as its train advances."""
|
|
from copy import deepcopy
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import release_snapshot as snapshot
|
|
|
|
|
|
class SnapshotIdentityTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.sha = 'a' * 40
|
|
self.merge = 'b' * 40
|
|
self.ref = 'refs/heads/release-candidate/packet-1'
|
|
self.pr = {
|
|
'state': 'closed', 'merged': True, 'merge_commit_sha': self.merge,
|
|
'head': {'sha': self.sha, 'ref': 'release-candidate/packet-1', 'repo': {'full_name': 'rcourtman/Pulse'}},
|
|
'base': {'ref': 'release/v6.4', 'repo': {'full_name': 'rcourtman/Pulse'}},
|
|
}
|
|
|
|
def verify(self, pr):
|
|
return snapshot.reviewed_merge(pr, repository='rcourtman/Pulse', ref=self.ref,
|
|
branch='release/v6.4', sha=self.sha)
|
|
|
|
def test_merged_snapshot_identity(self):
|
|
self.assertEqual(self.merge, self.verify(self.pr))
|
|
self.assertEqual('release/v6.4', snapshot.source_branch(self.ref, 'release/v6.4', '42'))
|
|
self.assertEqual('main', snapshot.source_branch('refs/heads/main', '', ''))
|
|
|
|
def test_unreviewed_or_retargeted_identity_is_rejected(self):
|
|
changes = [('merged', False), ('state', 'open'), ('merge_commit_sha', ''),
|
|
('head.sha', 'c' * 40), ('head.ref', 'release-candidate/other'),
|
|
('base.ref', 'main'), ('head.repo.full_name', 'someone/Pulse'),
|
|
('base.repo.full_name', 'someone/Pulse')]
|
|
for path, value in changes:
|
|
with self.subTest(path=path):
|
|
pr = deepcopy(self.pr)
|
|
target = pr
|
|
fields = path.split('.')
|
|
for field in fields[:-1]:
|
|
target = target[field]
|
|
target[fields[-1]] = value
|
|
with self.assertRaises(ValueError):
|
|
self.verify(pr)
|
|
|
|
def test_unbound_dispatch_inputs_are_rejected(self):
|
|
for args in [(self.ref, '', '42'), (self.ref, 'main', ''),
|
|
(self.ref, 'feature/other', '42'), (self.ref, 'main', '../42'),
|
|
('refs/heads/main', 'main', '42'), ('refs/tags/v6.4.4-beta.1', '', '')]:
|
|
with self.subTest(args=args), self.assertRaises(ValueError):
|
|
snapshot.source_branch(*args)
|
|
|
|
def test_source_workflow_supports_snapshot_inputs_before_qualification(self):
|
|
path = Path(__file__).resolve().parents[2] / '.github/workflows/create-release.yml'
|
|
snapshot.check_workflow(path)
|
|
with tempfile.TemporaryDirectory() as raw:
|
|
old = Path(raw) / 'workflow.yml'
|
|
old.write_text(path.read_text().replace('release_pull_request:', 'removed_input:', 1))
|
|
with self.assertRaises(ValueError):
|
|
snapshot.check_workflow(old)
|
|
|
|
def test_workflow_verifies_ancestry_without_requiring_current_tip(self):
|
|
with tempfile.TemporaryDirectory() as raw:
|
|
output = Path(raw) / 'output'
|
|
env = {'GITHUB_REF': self.ref, 'RELEASE_SOURCE_BRANCH': 'release/v6.4',
|
|
'RELEASE_PULL_REQUEST': '42', 'GITHUB_REPOSITORY': 'rcourtman/Pulse',
|
|
'GITHUB_SHA': self.sha, 'GITHUB_WORKFLOW_SHA': self.sha,
|
|
'GITHUB_OUTPUT': str(output)}
|
|
with patch.dict(os.environ, env), patch('sys.argv', ['release_snapshot.py']), \
|
|
patch.object(subprocess, 'check_output', return_value=json.dumps(self.pr)), \
|
|
patch.object(subprocess, 'run') as run:
|
|
snapshot.main()
|
|
self.assertEqual('source_branch=release/v6.4\n', output.read_text())
|
|
commands = [call.args[0] for call in run.call_args_list]
|
|
self.assertIn(['git', 'merge-base', '--is-ancestor', self.sha, self.merge], commands)
|
|
self.assertIn(['git', 'merge-base', '--is-ancestor', self.merge, 'FETCH_HEAD'], commands)
|
|
self.assertFalse(any('reset' in command or 'checkout' in command for command in commands))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|