Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/release118.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.4.9.118021",
"minecraft": "1.18.2",
"forge": "40.3.12",
"local_qualification_complete": true,
"files": {
"CHANGELOG.txt": "272160F79F5B4B58206FB258CF6C33C062E950DE8784E29B59A1B10DD2B8F319",
"LootableBodies-2.4.9.118021-javadoc.jar": "3E66A160D1E5E02F040107A53C8772B36AEE2A67D9F2F6CCAB0092B7A9E8D695",
"LootableBodies-2.4.9.118021-sources.jar": "23CAC620238DBF0C64525769631CF582BD2BD07F923944FCE128FEB2FFC63466",
"LootableBodies-2.4.9.118021.jar": "34EF41EFCE4666A684EA202DAF8712162A1C403A90120BA11EF0260533FB690F",
"LootableBodies-2.4.9.118021.pom": "051FD00F47E53B7F7B6B30CC907B1AA662864D8911FDB0B0C8326987E8F88988",
"SHA256SUMS": "B09D84DDE620B3D10984788331298F443001EAAF4DBC0B4444D36805331685E4"
},
"qualification_integrity_status": "verified"
}
116 changes: 109 additions & 7 deletions .github/scripts/test_release_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
from pathlib import Path
import shutil
import shlex
import sys
import subprocess
import tempfile
import textwrap
Expand All @@ -26,6 +28,7 @@
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', 'Validate release workflow']
CORE118 = ['Check hosted work policy', 'Build and audit locally qualified archives', '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
Expand Down Expand Up @@ -65,13 +68,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w
'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'),
'1.17.1': ('117011', 'master-1.17.1', '37.1.1')}[minecraft]
'1.17.1': ('117011', 'master-1.17.1', '37.1.1'),
'1.18.2': ('118021', 'master-1.18.2', '40.3.12')}[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',
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',
mod_artifact='LootableBodies', mod_group='zone.moddev.mc.lootablebodies', mod_license='LGPL-2.1-only',
curseforge_project_id='240968', mod_version=version)
if mutation:
Expand All @@ -81,12 +85,14 @@ 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

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 any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118']):
code='118' if minecraft=='1.18.2' else '117'
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]})
if '/actions/workflows/' in args[-1]:
Expand All @@ -105,12 +111,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})]), \
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('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing):
exec(compile(code, 'release-preflight', 'exec'), {})

def test_future_118_candidate_cannot_dispatch_publication(self):
code, values = self.select('2.4.9.118021')
def test_future_119_candidate_cannot_dispatch_publication(self):
code, values = self.select('2.4.10.119041')
self.assertNotEqual(0, code)
self.assertEqual({}, values)

Expand Down Expand Up @@ -282,6 +288,35 @@ def test_117_promotes_exact_six_files_and_rejects_changed_or_extra_bytes(self):
with self.assertRaises(SystemExit):exec(compile(code,'extra-native117-promotion','exec'),{})
finally:os.chdir(previous)

def test_118_routes_to_the_accepted_target_and_requires_only_artifact_checks(self):
code,values=self.select('2.4.9.118021')
self.assertEqual(0,code);self.assertEqual('master-1.18.2',values['source_branch'])
self.validate('1.18.2')
for missing in CORE118:
with self.subTest(missing=missing),self.assertRaises(SystemExit):
self.validate('1.18.2',names=[x for x in CORE118 if x!=missing])

def test_118_rejects_other_run_inputs_missing_receipt_audit_or_java(self):
for mutation in [{'head_sha':'b'*40},{'head_branch':'master-1.17.1'},{'event':'pull_request'},{'path':'.github/workflows/native117.yml'},{'conclusion':'failure'}]:
with self.subTest(mutation=mutation),self.assertRaises(SystemExit):self.validate('1.18.2',qualification=mutation)
for kwargs in [{'audit_success':False},{'local_receipt':{}},{'mutation':{'java_version':'8'}},{'mutation':{'java_toolchain_version':'17.0.1+12'}}]:
with self.subTest(kwargs=kwargs),self.assertRaises(SystemExit):self.validate('1.18.2',**kwargs)

def test_118_staging_downloads_exact_inputs_without_build_tests_or_sdk(self):
workflow=WORKFLOW.read_text()
step=workflow.split('- name: Verify all six native118 bytes before publication',1)[1].split(' - name:',1)[0]
self.assertIn('python3 -B tools/native118_build.py --bundle build/release/bundle',step)
for forbidden in ['gradlew','native_runtime','unittest','prepare_native','xvfb']:self.assertNotIn(forbidden,step)
legacy=workflow.split('- name: Build, test and audit the release',1)[1].split(' - name:',1)[0]
self.assertIn("minecraft_version != '1.18.2'",legacy)

def test_118_setup_java_notation_keeps_the_qualified_runtime(self):
text=WORKFLOW.read_text()
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'))

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):
Expand All @@ -291,6 +326,73 @@ def test_existing_release_is_not_uploaded_again(self):
with self.assertRaises(SystemExit):
self.validate('1.10.2', release_exists=True)



def run_staging_fixture(self,root,version,mutation=None):
bundle=root/'build/release/bundle';bundle.mkdir(parents=True)
prefix='LootableBodies-'+version
names=[prefix+s for s in ['.jar','-sources.jar','-javadoc.jar','.pom']]+['CHANGELOG.txt']
for name in names:(bundle/name).write_text('qualified '+name)
(bundle/'SHA256SUMS').write_bytes(''.join(hashlib.sha256((bundle/name).read_bytes()).hexdigest()+' '+name+'\n' for name in names if name.endswith('.jar')).encode())
if mutation=='missing':(bundle/(prefix+'.pom')).unlink()
if mutation=='extra':(bundle/'unapproved.txt').write_text('extra')
runner=root/'runner';runner.mkdir()
env=dict(os.environ,RUNNER_TEMP=runner.as_posix(),RELEASE_VERSION=version,RELEASE_SHA=SHA,
GITHUB_RUN_ID='1',GITHUB_RUN_ATTEMPT='1',GITHUB_OUTPUT=(root/'output.txt').as_posix(),
GITHUB_STEP_SUMMARY=(root/'summary.txt').as_posix(),CHERE_INVOKING='1')
shell=shutil.which('bash')
if os.name=='nt':
# Use the installed Git Bash, not the Windows WSL launcher.
shell=next((str(parent/'bin/bash.exe') for parent in Path(shutil.which('git')).parents
if (parent/'bin/bash.exe').is_file()),None)
if not shell:raise RuntimeError('Bash is required to test actual release staging')
code=script('Stage the exact audited bundle')
# Only select this test interpreter; execute the actual shell staging,
# checksum checks and summary without a legacy parent checksum file.
code=code.replace("python3 - <<'PY'",shlex.quote(Path(sys.executable).as_posix())+" - <<'PY'")
return subprocess.run([shell,'-lc',code],cwd=root,env=env,capture_output=True,text=True)


def test_staging_uses_only_the_promoted_six_file_bundle(self):
for version in ['2.4.7.116051','2.4.8.117011','2.4.9.118021']:
with self.subTest(version=version),tempfile.TemporaryDirectory() as d:
root=Path(d);result=self.run_staging_fixture(root,version)
self.assertEqual(0,result.returncode,result.stdout+'\n'+result.stderr)
self.assertFalse((root/'build/release/SHA256SUMS').exists())
self.assertIn('LootableBodies-'+version+'.jar',(root/'summary.txt').read_text())


def test_staging_missing_or_extra_file_fails_before_upload(self):
for mutation in ['missing','extra']:
with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as d:
root=Path(d);result=self.run_staging_fixture(root,'2.4.8.117011',mutation)
self.assertNotEqual(0,result.returncode)
self.assertIn('missing or unexpected files',result.stdout+result.stderr)


def test_112_artifact_audit_skips_only_explicitly_scoped_out_builds(self):
text=WORKFLOW.with_name('artifact-audit-1.12.yml').read_text()
code=text.split('def artifact_required(jobs):',1)[1].split('\n # Fetch',1)[0]
code='def artifact_required(jobs):'+code
import textwrap
code=code.replace('\n ','\n')
namespace={};exec(compile(code,'artifact-scope','exec'),namespace)
check=namespace['artifact_required']
def job(name,conclusion,status='completed'):return dict(name=name,conclusion=conclusion,status=status)
build=job('Build, test, and audit','success')
self.assertTrue(check([build]))
skipped=[job('Check runtime inputs','success'),job('Build, test, and audit','skipped'),job('Cold Forge bootstrap','skipped')]
self.assertFalse(check(skipped))
for invalid in [[],[build,build],skipped[1:],skipped[:2],
[dict(build,status='in_progress')],
[dict(build,conclusion='failure')],
[dict(skipped[0],conclusion='failure'),*skipped[1:]],
[*skipped[:2],dict(skipped[2],conclusion='failure')]]:
with self.subTest(invalid=invalid),self.assertRaises(ValueError):check(invalid)
self.assertIn("needs: scope",text)
self.assertIn("if: needs.scope.outputs.required == 'true'",text)


def test_tag_at_another_commit_is_rejected(self):
with self.assertRaises(SystemExit):
self.validate('1.10.2', wrong_tag=True)
Expand Down
41 changes: 38 additions & 3 deletions .github/workflows/artifact-audit-1.12.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,48 @@ permissions:
actions: read

jobs:
audit:
# Pull-request merge trees are audited inside CI. Push artifacts use the
# workflow run's exact head SHA, which is also their declared artifact name.
scope:
name: Check artifact producer scope
if: >-
github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' &&
(github.event.workflow_run.head_branch == 'master-1.12.2' || github.event.workflow_run.head_branch == 'master-1.12')
runs-on: ubuntu-latest
outputs:
required: ${{ steps.scope.outputs.required }}
steps:
- id: scope
env:
GH_TOKEN: ${{ github.token }}
PRODUCER_URL: ${{ github.event.workflow_run.jobs_url }}
REPOSITORY: ${{ github.repository }}
run: |
python3 - <<'PY'
import json,os,urllib.request
def artifact_required(jobs):
def exactly(name):
found=[j for j in jobs if j['name']==name]
if len(found)!=1 or found[0]['status']!='completed':raise ValueError('Missing, duplicate or pending producer job: '+name)
return found[0]['conclusion']
build=exactly('Build, test, and audit')
if build=='success':return True
if build=='skipped' and exactly('Check runtime inputs')=='success' and exactly('Cold Forge bootstrap')=='skipped':return False
raise ValueError('Artifact producer did not succeed')
# Fetch the exact producer jobs; absent artifacts never imply a skip.
url=os.environ['PRODUCER_URL']+'?per_page=100'
if not url.startswith('https://api.github.com/repos/'+os.environ['REPOSITORY']+'/actions/runs/'):raise ValueError('Unexpected producer API URL')
request=urllib.request.Request(url,headers={'Authorization':'Bearer '+os.environ['GH_TOKEN'],'Accept':'application/vnd.github+json'})
with urllib.request.urlopen(request,timeout=30) as response:result=json.load(response)
if result['total_count']!=len(result['jobs']):raise ValueError('Incomplete producer job list')
required=artifact_required(result['jobs'])
with open(os.environ['GITHUB_OUTPUT'],'a') as output:output.write('required='+str(required).lower()+'\n')
print('Artifact audit required:',required)
PY
audit:
# Successful builds always require their exact artifact. A dispatcher-only
# change has an explicitly skipped producer and has no release bytes to audit.
needs: scope
if: needs.scope.outputs.required == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/artifact-audit-1.18.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Audit LootableBodies 1.18 artifacts
run-name: Audit 1.18.2 artifacts from ${{ github.event.workflow_run.head_sha }}
on:
workflow_run:
workflows: [LootableBodies 1.18 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.18.2'
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.18.2-${{ 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/native118_build.py --bundle audited
python3 - <<'PY'
import json,os
from pathlib import Path
accepted=json.loads(Path('.github/release118.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: native118-independent-audit-${{ github.event.workflow_run.head_sha }}
if-no-files-found: error
path: independent-audit.json
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- master-1.16.5
- 'feature/**'
pull_request:
branches:
- master-1.16.5
Expand Down
21 changes: 8 additions & 13 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name: CodeQL
on:
push:
branches:
- master-1.17.1
- master-1.18.2
- 'feature/**'
pull_request:
branches:
- master-1.17.1
- master-1.18.2
schedule:
- cron: '43 7 * * 4'

Expand All @@ -17,11 +17,12 @@ permissions:
security-events: write

concurrency:
group: lootablebodies-1.17-codeql-${{ github.ref }}
group: lootablebodies-1.18-codeql-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
if: github.repository == 'MinecraftModDevelopmentMods/LootableBodies'
name: Analyze Java
runs-on: ubuntu-latest
timeout-minutes: 45
Expand All @@ -36,21 +37,15 @@ jobs:
distribution: temurin
java-version: '25.0.3+9.0.LTS'

- name: Install pinned Java 16 compilation toolchain
- name: Install pinned Java 17 Gradle and compilation runtime
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: temurin
java-version: '16.0.2+7'

- name: Install pinned Java 17 Gradle runtime
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: temurin
java-version: '17.0.1+12'
java-version: '17.0.20+101'

- name: Pin production JDK
run: |
echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV"
echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV"

- name: Set up Gradle
Expand All @@ -68,7 +63,7 @@ jobs:
gradle_args=(
clean classes --no-build-cache --rerun-tasks
--no-daemon --stacktrace --max-workers=2
-Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_16_X64,$JAVA_HOME_25_X64"
-Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_25_X64"
-Dorg.gradle.java.installations.auto-detect=false
-Dorg.gradle.java.installations.auto-download=false
)
Expand Down
Loading
Loading