diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index ad9fc4f..794f71b 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -69,13 +69,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.15.2': ('115021', 'master-1.15.2', '31.2.57'), '1.16.5': ('116051', 'master-1.16.5', '36.2.42'), '1.17.1': ('117011', 'master-1.17.1', '37.1.1'), - '1.18.2': ('118021', 'master-1.18.2', '40.3.12')}[minecraft] + '1.18.2': ('118021', 'master-1.18.2', '40.3.12'), + '1.19.4': ('119041', 'master-1.19.4', '45.4.5')}[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='17' if minecraft=='1.18.2' else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='17.0.20.1+1' if minecraft=='1.18.2' else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', + java_version='17' if minecraft in ('1.18.2','1.19.4') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='17.0.20.1+1' if minecraft in ('1.18.2','1.19.4') else '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', curseforge_project_id='240968', mod_version=version) if mutation: @@ -85,13 +86,13 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.14.4': MATRIX114 + ['Validate release workflow'], '1.15.2': MATRIX115, '1.16.5': MATRIX116}.get(minecraft, []) if names is None else names if minecraft=='1.17.1' and names is None:checks=CORE117+MATRIX117 - if minecraft=='1.18.2' and names is None:checks=CORE118 + if minecraft in ('1.18.2','1.19.4') and names is None:checks=CORE118 def command(args, **options): if args[0] == 'git': return SHA + '\n' - if any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118']): - code='118' if minecraft=='1.18.2' else '117' + if any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118','119']): + code={'1.17.1':'117','1.18.2':'118','1.19.4':'119'}[minecraft] row=dict(id=int(code),head_sha=SHA,head_branch=branch,event='push',path='.github/workflows/native'+code+'.yml',conclusion='success') if qualification:row.update(qualification) return json.dumps({'workflow_runs':[row]}) @@ -111,12 +112,12 @@ def existing(args, **options): 'MINECRAFT_VERSION': minecraft, 'SOURCE_BRANCH': branch, '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,'local_qualification_complete':True,'qualification_integrity_status':'verified'})]), \ + patch('pathlib.Path.read_text', side_effect=['\n'.join(k+'='+v for k,v in values.items()),json.dumps({'files':files,'local_qualification_complete':True,'qualification_integrity_status':'verified','manual_accepted':True})]), \ patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) - def test_future_119_candidate_cannot_dispatch_publication(self): - code, values = self.select('2.4.10.119041') + def test_future_120_candidate_cannot_dispatch_publication(self): + code, values = self.select('2.4.11.120011') self.assertNotEqual(0, code) self.assertEqual({}, values) @@ -315,7 +316,25 @@ def test_118_setup_java_notation_keeps_the_qualified_runtime(self): pins=[line for line in text.splitlines() if 'java-version:' in line and '17.0.20' in line] self.assertEqual(4,len(pins)) self.assertTrue(all('17.0.20+101' in line and '17.0.20.1+1' not in line for line in pins)) - self.assertIn("'17.0.20.1+1' if native18",script('Validate identity, exact commit and successful checks')) + self.assertIn("'17.0.20.1+1' if archive_only",script('Validate identity, exact commit and successful checks')) + + def test_119_requires_manual_acceptance_and_exact_artifacts(self): + code, values = self.select('2.4.10.119041') + self.assertEqual(0, code) + self.assertEqual('master-1.19.4', values['source_branch']) + self.validate('1.19.4') + for missing in CORE118: + with self.subTest(missing=missing), self.assertRaises(SystemExit): + self.validate('1.19.4', names=[name for name in CORE118 if name != missing]) + for kwargs in [{'audit_success':False}, {'local_receipt':{}}, + {'qualification':{'path':'.github/workflows/native118.yml'}}, + {'mutation':{'java_version':'8'}}]: + with self.subTest(kwargs=kwargs), self.assertRaises(SystemExit): + self.validate('1.19.4', **kwargs) + step = WORKFLOW.read_text().split('- name: Verify all six native119 bytes before publication', 1)[1].split(' - name:', 1)[0] + self.assertIn('python3 -B tools/native119_build.py --bundle build/release/bundle', step) + self.assertNotIn('gradlew', step) + self.assertIn("minecraft_version == '1.19.4' && 'Java 17'", WORKFLOW.read_text()) def test_identity_changes_are_rejected(self): for mutation in [{'mod_id': 'other'}, {'minecraft_version': '1.10.2'}, {'mod_license': 'CC0-1.0'}]: diff --git a/.github/workflows/artifact-audit-1.19.yml b/.github/workflows/artifact-audit-1.19.yml new file mode 100644 index 0000000..81be46b --- /dev/null +++ b/.github/workflows/artifact-audit-1.19.yml @@ -0,0 +1,48 @@ +name: Audit LootableBodies 1.19 artifacts +run-name: Audit 1.19.4 artifacts from ${{ github.event.workflow_run.head_sha }} +on: + workflow_run: + workflows: [LootableBodies 1.19 CI] + types: [completed] +permissions: + contents: read + actions: read +jobs: + audit: + if: >- + github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && + github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'master-1.19.4' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.19.4-${{ github.event.workflow_run.head_sha }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: audited + - name: Independently verify every locally qualified byte + env: + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + python3 -B tools/native119_build.py --bundle audited + python3 - <<'PY' + import json,os + from pathlib import Path + accepted=json.loads(Path('.github/release119.json').read_text()) + Path('independent-audit.json').write_text(json.dumps({'source':os.environ['SOURCE_SHA'],'files':accepted['files'],'all_six_exact':True,'identical_to_locally_audited_bundle':True},indent=2)+'\n') + PY + # Exact hashes reuse the local class61/licence/identity/mapping audit. + # This independent receipt is a byte match, not a fresh gameplay run or + # another Gradle/Forge preparation. The build job audits new archives. + - name: Retain independent audit evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native119-independent-audit-${{ github.event.workflow_run.head_sha }} + if-no-files-found: error + path: independent-audit.json diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 7af606f..e8f824f 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: release_version: - description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011 or 118021. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021 or 119041. required: true type: string curseforge_release_level: @@ -20,7 +20,7 @@ on: default: false type: boolean local_qualification: - description: Hash-bound local qualification receipt, required for the 1.17.1 and 1.18.2 releases. + description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2 and 1.19.4 releases. required: false default: '' type: string @@ -54,7 +54,7 @@ jobs: set -euo pipefail [[ "$GITHUB_REPOSITORY" == 'MinecraftModDevelopmentMods/LootableBodies' ]] [[ "$GITHUB_REF" == 'refs/heads/master-1.12' ]] - [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021)$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021|119041)$ ]] case "$RELEASE_VERSION" in *.110021) minecraft_version=1.10.2; source_branch=1.10.2 ;; *.111021) minecraft_version=1.11.2; source_branch=1.11.2-dev ;; @@ -65,6 +65,7 @@ jobs: *.116051) minecraft_version=1.16.5; source_branch=master-1.16.5 ;; *.117011) minecraft_version=1.17.1; source_branch=master-1.17.1 ;; *.118021) minecraft_version=1.18.2; source_branch=master-1.18.2 ;; + *.119041) minecraft_version=1.19.4; source_branch=master-1.19.4 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -98,16 +99,17 @@ jobs: '1.15.2': ('master-1.15.2', '31.2.57', '115021'), '1.16.5': ('master-1.16.5', '36.2.42', '116051'), '1.17.1': ('master-1.17.1', '37.1.1', '117011'), - '1.18.2': ('master-1.18.2', '40.3.12', '118021')} + '1.18.2': ('master-1.18.2', '40.3.12', '118021'), + '1.19.4': ('master-1.19.4', '45.4.5', '119041')} branch, forge, suffix = targets[minecraft] if os.environ['SOURCE_BRANCH'] != branch: raise SystemExit('The selected branch does not match its Minecraft version') native17 = minecraft == '1.17.1' - native18 = minecraft == '1.18.2' - locally_qualified = native17 or native18 + archive_only = minecraft in ('1.18.2', '1.19.4') + locally_qualified = native17 or archive_only expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '17' if native18 else '16' if native17 else '8', - 'java_toolchain_version': '17.0.20.1+1' if native18 else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '17' if archive_only else '16' if native17 else '8', + 'java_toolchain_version': '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 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', 'curseforge_project_id': '240968'} @@ -137,7 +139,7 @@ jobs: qualification_run = '' if locally_qualified: required = {'Build, test, and audit', 'Analyze Java', 'Validation', 'Validate release workflow'} - if native18: + if archive_only: required = {'Check hosted work policy', 'Build and audit locally qualified archives', 'Analyze Java', 'Validation', 'Validate release workflow'} # The authorised local controller rehashes actual gameplay proof # before dispatch. Hosted CI verifies its exact source/artifact @@ -147,11 +149,13 @@ jobs: except (ValueError,TypeError): raise SystemExit('A local qualification receipt is required') keys={'schema','source','version','files','evidence_sha256','qualified_locally','manual_accepted'} - target_code='118' if native18 else '117' + target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119'}[minecraft] metadata=json.loads(Path(f'.github/release{target_code}.json').read_text()) accepted=metadata['files'] - if native18 and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): - raise SystemExit('The local 1.18.2 qualification declaration is incomplete') + if archive_only and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): + raise SystemExit(f'The local {minecraft} qualification declaration is incomplete') + if minecraft == '1.19.4' and metadata.get('manual_accepted') is not True: + raise SystemExit('The local 1.19.4 candidate has not been accepted') 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 @@ -169,7 +173,7 @@ jobs: matching = [check for check in checks if check['name'] == name] if not matching or max(matching, key=lambda c: c['id'])['conclusion'] != 'success': raise SystemExit(f'The selected commit has no successful {name} check') - if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19.4'): target = minecraft.rsplit('.', 1)[0] audits = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?event=workflow_run&per_page=100'], text=True))['workflow_runs'] @@ -203,27 +207,27 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - name: Install Mavenizer Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - name: Install production Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' run: | - if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.18.2' ]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" elif [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" @@ -232,7 +236,7 @@ jobs: fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -265,8 +269,19 @@ jobs: - name: Verify all six native118 bytes before publication if: needs.preflight.outputs.minecraft_version == '1.18.2' run: python3 -B tools/native118_build.py --bundle build/release/bundle + - name: Download the qualified native119 six-file bundle + if: needs.preflight.outputs.minecraft_version == '1.19.4' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.19.4-${{ needs.preflight.outputs.release_sha }} + run-id: ${{ needs.preflight.outputs.qualification_run }} + github-token: ${{ github.token }} + path: build/release/bundle + - name: Verify all six native119 bytes before publication + if: needs.preflight.outputs.minecraft_version == '1.19.4' + run: python3 -B tools/native119_build.py --bundle build/release/bundle - name: Build, test and audit the release - if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -371,15 +386,15 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain run: | - if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.18.2' ]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" elif [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" @@ -437,7 +452,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: ${{ needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} + java: ${{ needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} fail-mode: fail files: | ${{ runner.temp }}/release-input/LootableBodies-${{ needs.preflight.outputs.release_version }}.jar diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index a5e6f3f..2f3febd 100644 --- a/.github/workflows/release-workflow-checks.yml +++ b/.github/workflows/release-workflow-checks.yml @@ -24,7 +24,7 @@ jobs: - name: Check release workflow syntax run: >- "$RUNNER_TEMP/actionlint/actionlint" -shellcheck= -pyflakes= - .github/workflows/build.yml .github/workflows/migration-matrix.yml .github/workflows/deploy-release.yml .github/workflows/release-workflow-checks.yml .github/workflows/artifact-audit-1.11.yml .github/workflows/artifact-audit-1.12.yml .github/workflows/artifact-audit-1.13.yml .github/workflows/artifact-audit-1.14.yml .github/workflows/artifact-audit-1.15.yml .github/workflows/artifact-audit-1.16.yml .github/workflows/artifact-audit-1.17.yml .github/workflows/artifact-audit-1.18.yml + .github/workflows/build.yml .github/workflows/migration-matrix.yml .github/workflows/deploy-release.yml .github/workflows/release-workflow-checks.yml .github/workflows/artifact-audit-1.11.yml .github/workflows/artifact-audit-1.12.yml .github/workflows/artifact-audit-1.13.yml .github/workflows/artifact-audit-1.14.yml .github/workflows/artifact-audit-1.15.yml .github/workflows/artifact-audit-1.16.yml .github/workflows/artifact-audit-1.17.yml .github/workflows/artifact-audit-1.18.yml .github/workflows/artifact-audit-1.19.yml - name: Test version selection and publication checks run: python3 .github/scripts/test_release_workflow.py -v