diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 524880c..4873c2f 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -1,5 +1,6 @@ """Test the dispatcher's actual selection and publication preflight scripts offline.""" import json +import hashlib import os from pathlib import Path import shutil @@ -24,6 +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']] 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'] @@ -53,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): + def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=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'), @@ -61,10 +64,11 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.13.2': ('113021', 'master-1.13.2', '25.0.223'), '1.14.4': ('114041', 'master-1.14.4', '28.2.26'), '1.15.2': ('115021', 'master-1.15.2', '31.2.57'), - '1.16.5': ('116051', 'master-1.16.5', '36.2.42')}[minecraft] + '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 values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', - java_version='8', java_toolchain_version='8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', + 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', curseforge_project_id='240968', mod_version=version) if mutation: @@ -73,9 +77,15 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.13.2': MATRIX113 + ['Validate release workflow'], '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 + def command(args, **options): if args[0] == 'git': return SHA + '\n' + if '/actions/workflows/native117.yml/' in args[-1]: + row=dict(id=117,head_sha=SHA,head_branch='master-1.17.1',event='push',path='.github/workflows/native117.yml',conclusion='success') + if qualification:row.update(qualification) + return json.dumps({'workflow_runs':[row]}) if '/actions/workflows/' in args[-1]: return json.dumps({'workflow_runs': [{'id': 1, 'display_title': 'Audit ' + minecraft + ' artifacts from ' + SHA, 'conclusion': 'success' if audit_success else 'failure'}]}) @@ -95,8 +105,8 @@ def existing(args, **options): patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) - def test_future_117_candidate_cannot_dispatch_publication(self): - code, values = self.select('2.4.8.117011') + def test_future_118_candidate_cannot_dispatch_publication(self): + code, values = self.select('2.4.9.118021') self.assertNotEqual(0, code) self.assertEqual({}, values) @@ -217,6 +227,44 @@ def test_116_audit_handles_both_registered_workflow_names(self): self.assertIn('LootableBodies 1.12 CI', [name.strip() for name in names]) self.assertIn('LootableBodies 1.16 CI', [name.strip() for name in names]) + 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): + 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_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): + self.validate('1.17.1',qualification=mutation) + + def test_117_requires_the_independent_audit_and_java16(self): + with self.assertRaises(SystemExit):self.validate('1.17.1',audit_success=False) + with self.assertRaises(SystemExit):self.validate('1.17.1',mutation={'java_version':'8'}) + + def test_117_promotes_exact_six_files_and_rejects_changed_or_extra_bytes(self): + code=script('Verify all six native117 bytes before publication').split("python3 - <<'PY'\n",1)[1].split('\nPY',1)[0] + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);bundle=root/'build/release/bundle';bundle.mkdir(parents=True);meta=root/'.github';meta.mkdir() + names=['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS'];expected={} + for name in names: + data=('approved '+name).encode();(bundle/name).write_bytes(data);expected[name]=hashlib.sha256(data).hexdigest().upper() + (meta/'release117.json').write_text(json.dumps({'files':expected})) + previous=Path.cwd() + try: + os.chdir(root);exec(compile(code,'immutable-native117-promotion','exec'),{}) + for name in names: + f=bundle/name;data=f.read_bytes();f.write_bytes(data+b'changed') + with self.assertRaises(SystemExit):exec(compile(code,'changed-native117-promotion','exec'),{}) + f.write_bytes(data) + (bundle/'extra.txt').write_text('unapproved') + with self.assertRaises(SystemExit):exec(compile(code,'extra-native117-promotion','exec'),{}) + finally:os.chdir(previous) + def test_identity_changes_are_rejected(self): for mutation in [{'mod_id': 'other'}, {'minecraft_version': '1.10.2'}, {'mod_license': 'CC0-1.0'}]: with self.subTest(mutation=mutation), self.assertRaises(SystemExit): diff --git a/.github/workflows/artifact-audit-1.17.yml b/.github/workflows/artifact-audit-1.17.yml new file mode 100644 index 0000000..debec8e --- /dev/null +++ b/.github/workflows/artifact-audit-1.17.yml @@ -0,0 +1,68 @@ +name: Audit LootableBodies 1.17 artifacts +run-name: Audit 1.17.1 artifacts from ${{ github.event.workflow_run.head_sha }} +on: + workflow_run: + workflows: [LootableBodies 1.17 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.17.1' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.17.1-${{ github.event.workflow_run.head_sha }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: audited + - name: Independently verify every accepted byte + env: + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + python3 - <<'PY' + import hashlib,json,os + from pathlib import Path + expected=json.loads(Path('.github/release117.json').read_text())['files'] + actual={p.name:hashlib.sha256(p.read_bytes()).hexdigest().upper() for p in Path('audited').iterdir() if p.is_file()} + if actual != expected:raise SystemExit('Native117 audited bytes differ from the accepted six files') + Path('independent-audit.json').write_text(json.dumps({'source':os.environ['SOURCE_SHA'],'files':actual,'all_six_exact':True},indent=2)+'\n') + PY + - name: Audit archive identity, class60, licences and mappings without rebuilding + env: + LOOTABLEBODIES_JAVA16_HOME: ${{ env.JAVA_HOME_16_X64 }} + LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} + run: | + chmod +x ./gradlew + ./gradlew verifyPreparedReleaseArtifacts -PpreparedReleaseDir=audited --no-daemon --console=plain --stacktrace | tee archive-audit.log + if grep -E '^> Task :(compileJava|jar|sourcesJar|javadocJar)( |$)' archive-audit.log; then exit 1; fi + - name: Retain independent audit evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-independent-audit-${{ github.event.workflow_run.head_sha }} + if-no-files-found: error + path: | + independent-audit.json + archive-audit.log diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 56db9a5..746e8de 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 or 116051. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051 or 117011. required: true type: string curseforge_release_level: @@ -37,6 +37,7 @@ jobs: outputs: release_sha: ${{ steps.validate.outputs.release_sha }} release_version: ${{ steps.validate.outputs.release_version }} + qualification_run: ${{ steps.validate.outputs.qualification_run }} minecraft_version: ${{ steps.target.outputs.minecraft_version }} source_branch: ${{ steps.target.outputs.source_branch }} steps: @@ -48,7 +49,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)$ ]] + [[ "$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)$ ]] 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 ;; @@ -57,6 +58,7 @@ jobs: *.114041) minecraft_version=1.14.4; source_branch=master-1.14.4 ;; *.115021) minecraft_version=1.15.2; source_branch=master-1.15.2 ;; *.116051) minecraft_version=1.16.5; source_branch=master-1.16.5 ;; + *.117011) minecraft_version=1.17.1; source_branch=master-1.17.1 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -87,13 +89,15 @@ jobs: '1.13.2': ('master-1.13.2', '25.0.223', '113021'), '1.14.4': ('master-1.14.4', '28.2.26', '114041'), '1.15.2': ('master-1.15.2', '31.2.57', '115021'), - '1.16.5': ('master-1.16.5', '36.2.42', '116051')} + '1.16.5': ('master-1.16.5', '36.2.42', '116051'), + '1.17.1': ('master-1.17.1', '37.1.1', '117011')} 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' expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '8', - 'java_toolchain_version': '8.0.502+7', 'gradle_java_version': '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '16' if native17 else '8', + 'java_toolchain_version': '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'} @@ -120,11 +124,26 @@ jobs: if minecraft in ('1.15.2', '1.16.5'): required.update(['Validate release workflow', 'Prepare verified migration inputs', 'World upgrade matrix'] + ['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']) + 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') + 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] 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'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1'): 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'] @@ -142,7 +161,7 @@ jobs: if tag.returncode != 0 and '404' not in tag.stderr: raise SystemExit('Could not verify the release tag') with open(os.environ['GITHUB_OUTPUT'], 'a') as out: - out.write(f'release_sha={sha}\nrelease_version={version}\n') + out.write(f'release_sha={sha}\nrelease_version={version}\nqualification_run={qualification_run}\n') PY build: @@ -166,7 +185,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: ${{ 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: @@ -174,16 +193,41 @@ jobs: java-version: '17.0.1+12' - name: Pin the production toolchain run: | - echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" + else + echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Download the qualified native117 six-file bundle + if: needs.preflight.outputs.minecraft_version == '1.17.1' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.17.1-${{ needs.preflight.outputs.release_sha }} + run-id: ${{ needs.preflight.outputs.qualification_run }} + github-token: ${{ github.token }} + path: build/release/bundle + - name: Verify all six native117 bytes before publication + if: needs.preflight.outputs.minecraft_version == '1.17.1' + run: | + python3 - <<'PY' + import hashlib,json + from pathlib import Path + expected=json.loads(Path('.github/release117.json').read_text())['files'] + actual={p.name:hashlib.sha256(p.read_bytes()).hexdigest().upper() for p in Path('build/release/bundle').iterdir() if p.is_file()} + if actual != expected:raise SystemExit('Qualified native117 bundle differs from the accepted six files') + PY + chmod +x ./gradlew + ./gradlew verifyPreparedReleaseArtifacts -PpreparedReleaseDir=build/release/bundle --no-daemon --stacktrace - name: Build, test and audit the release + if: needs.preflight.outputs.minecraft_version != '1.17.1' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ --no-daemon --stacktrace \ - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.paths="$JAVA_HOME,${LOOTABLEBODIES_JAVA16_HOME:-${LOOTABLEBODIES_JAVA8_HOME:-}},$JAVA_HOME_25_X64" \ -Dorg.gradle.java.installations.auto-detect=false \ -Dorg.gradle.java.installations.auto-download=false - name: Stage the exact audited bundle @@ -283,7 +327,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: ${{ 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: @@ -291,7 +335,11 @@ jobs: java-version: '17.0.1+12' - name: Pin the production toolchain run: | - echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" + else + echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -313,7 +361,7 @@ jobs: chmod +x ./gradlew ./gradlew publishMavenJavaPublicationToReleaseRepository \ -PpreparedReleaseDir="$RUNNER_TEMP/release-input" --no-daemon --stacktrace \ - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.paths="$JAVA_HOME,${LOOTABLEBODIES_JAVA16_HOME:-${LOOTABLEBODIES_JAVA8_HOME:-}},$JAVA_HOME_25_X64" \ -Dorg.gradle.java.installations.auto-detect=false \ -Dorg.gradle.java.installations.auto-download=false @@ -343,7 +391,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: Java 8 + java: ${{ 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 b598d57..9ac4472 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/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 - name: Test version selection and publication checks run: python3 .github/scripts/test_release_workflow.py -v