diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 4873c2f..e5faaa2 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -25,8 +25,8 @@ ['released-113', 'recovery-113', 'via113-110', 'via113-111', 'via113-112'] for stamps in ['normal', 'stamps']] MATRIX115 = ['World upgrade matrix', 'Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(6)] -CORE117 = ['Build, test, and audit', 'Analyze Java', 'Validation'] -MATRIX117 = ['Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(12)] + ['Native117 ' + x for x in ['servers','default','config','skin','issues','names','rejected','multiplayer','visibility','respawn','control']] +CORE117 = ['Build, test, and audit', 'Analyze Java', 'Validation', 'Validate release workflow'] +MATRIX117 = [] MATRIX116 = ['World upgrade matrix', 'Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(12)] MATRIX = [origin + ' / ' + stamps for origin in ['original-110', 'released-110', 'original-111', 'recovery-110'] @@ -56,7 +56,7 @@ def select(self, version, repository=REPOSITORY, ref='refs/heads/master-1.12'): values = dict(line.split('=', 1) for line in output.read_text().splitlines()) if output.exists() else {} return result.returncode, values - def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=None): + def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=None, local_receipt=None): suffix, branch, forge = { '1.10.2': ('110021', '1.10.2', '12.18.3.2511'), '1.11.2': ('111021', '1.11.2-dev', '13.20.1.2588'), @@ -67,6 +67,9 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.16.5': ('116051', 'master-1.16.5', '36.2.42'), '1.17.1': ('117011', 'master-1.17.1', '37.1.1')}[minecraft] version = '2.4.2.' + suffix + files={name:'C'*64 for name in ['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS']} + receipt={'schema':1,'source':SHA,'version':version,'files':files,'evidence_sha256':'F'*64,'qualified_locally':True,'manual_accepted':True} + if local_receipt is not None:receipt=local_receipt values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', java_version='16' if minecraft=='1.17.1' else '8', java_toolchain_version='16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', mod_artifact='LootableBodies', mod_group='zone.moddev.mc.lootablebodies', mod_license='LGPL-2.1-only', @@ -100,8 +103,9 @@ def existing(args, **options): with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, { 'RELEASE_VERSION': version, 'GITHUB_REPOSITORY': REPOSITORY, 'MINECRAFT_VERSION': minecraft, 'SOURCE_BRANCH': branch, - 'GITHUB_OUTPUT': str(Path(directory) / 'output')}), \ - patch('pathlib.Path.read_text', return_value='\n'.join(k+'='+v for k,v in values.items())), \ + 'GITHUB_OUTPUT': str(Path(directory) / 'output'), + 'LOCAL_QUALIFICATION':json.dumps(receipt) if receipt else ''}), \ + patch('pathlib.Path.read_text', side_effect=['\n'.join(k+'='+v for k,v in values.items()),json.dumps({'files':files})]), \ patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) @@ -231,12 +235,25 @@ def test_117_routes_to_the_accepted_target(self): code,values=self.select('2.4.8.117011') self.assertEqual(0,code);self.assertEqual('master-1.17.1',values['source_branch']) - def test_117_requires_all_native_scenarios_and_twelve_historical_shards(self): + def test_117_requires_build_security_and_archive_checks_without_hosted_games(self): self.validate('1.17.1') for missing in CORE117+MATRIX117: with self.subTest(missing=missing),self.assertRaises(SystemExit): self.validate('1.17.1',names=[x for x in CORE117+MATRIX117 if x!=missing]) + def test_117_requires_a_complete_hash_bound_local_qualification_receipt(self): + self.validate('1.17.1') + files={name:'C'*64 for name in ['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS']} + good={'schema':1,'source':SHA,'version':'2.4.2.117011','files':files,'evidence_sha256':'F'*64,'qualified_locally':True,'manual_accepted':True} + for mutation in [{'source':'b'*40},{'version':'2.4.8.117011'},{'files':{}},{'evidence_sha256':''},{'evidence_sha256':'0'*64},{'qualified_locally':False},{'manual_accepted':False},{'schema':True}]: + with self.subTest(mutation=mutation),self.assertRaises(SystemExit): + self.validate('1.17.1',local_receipt=dict(good,**mutation)) + with self.assertRaises(SystemExit):self.validate('1.17.1',local_receipt={}) + + def test_local_receipt_is_passed_to_the_actual_preflight_step(self): + step=WORKFLOW.read_text().split('- name: Validate identity, exact commit and successful checks',1)[1].split(' run: |',1)[0] + self.assertIn('LOCAL_QUALIFICATION: ${{ inputs.local_qualification }}',step) + def test_117_requires_an_exact_successful_merged_native_run(self): for mutation in [{'head_sha':'b'*40},{'head_branch':'master-1.16.5'},{'event':'pull_request'},{'path':'.github/workflows/build.yml'},{'conclusion':'failure'}]: with self.subTest(mutation=mutation),self.assertRaises(SystemExit): diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 746e8de..a87ebd1 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -19,6 +19,11 @@ on: required: true default: false type: boolean + local_qualification: + description: Hash-bound local qualification receipt, required for the 1.17.1 release. + required: false + default: '' + type: string permissions: actions: read @@ -75,6 +80,7 @@ jobs: SOURCE_BRANCH: ${{ steps.target.outputs.source_branch }} GH_TOKEN: ${{ github.token }} RELEASE_VERSION: ${{ inputs.release_version }} + LOCAL_QUALIFICATION: ${{ inputs.local_qualification }} run: | set -euo pipefail python3 - <<'PY' @@ -126,18 +132,28 @@ jobs: ['Historical world shard ' + str(i) for i in range(6 if minecraft == '1.15.2' else 12)]) qualification_run = '' if native17: - required = {'Prepare verified migration inputs', 'Build, test, and audit', - 'Analyze Java', 'Validation', 'Validate release workflow'} - required.update('Historical world shard ' + str(i) for i in range(12)) - required.update('Native117 ' + group for group in - ['servers', 'default', 'config', 'skin', 'issues', 'names', - 'rejected', 'multiplayer', 'visibility', 'respawn', 'control']) + required = {'Build, test, and audit', 'Analyze Java', 'Validation', 'Validate release workflow'} + # The authorised local controller rehashes actual gameplay proof + # before dispatch. Hosted CI verifies its exact source/artifact + # binding; it does not rerun games or claim a fresh gameplay pass. + try: + local=json.loads(os.environ.get('LOCAL_QUALIFICATION','')) + except (ValueError,TypeError): + raise SystemExit('A local qualification receipt is required') + keys={'schema','source','version','files','evidence_sha256','qualified_locally','manual_accepted'} + accepted=json.loads(Path('.github/release117.json').read_text())['files'] + if (not isinstance(local,dict) or set(local)!=keys or type(local['schema']) is not int or local['schema']!=1 + or local['source']!=sha or local['version']!=version or local['files']!=accepted + or local['qualified_locally'] is not True or local['manual_accepted'] is not True + or not isinstance(local['evidence_sha256'],str) or not re.fullmatch('[A-F0-9]{64}',local['evidence_sha256']) + or local['evidence_sha256']=='0'*64): + raise SystemExit('Local qualification is missing or belongs to another source/artifact bundle') runs = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/native117.yml/runs?event=push&branch=master-1.17.1&head_sha={sha}&per_page=100'], text=True))['workflow_runs'] exact = [r for r in runs if r['head_sha'] == sha and r['head_branch'] == 'master-1.17.1' and r['event'] == 'push' and r['path'] == '.github/workflows/native117.yml'] if not exact or max(exact, key=lambda r:r['id'])['conclusion'] != 'success': - raise SystemExit('No successful exact-merge native117 qualification') + raise SystemExit('No successful exact-merge native117 build and unit checks') qualification_run = str(max(exact, key=lambda r:r['id'])['id']) for name in required: matching = [check for check in checks if check['name'] == name]