first cycle lab release
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse, stringify } from 'smol-toml';
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||
import { build } from '../../platform/build.mjs';
|
||||
|
||||
const repository = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const sha = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const git = (root, args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
||||
|
||||
export function fixture(t) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpack-test-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
for (const file of ['pack', 'server', 'platform', 'scripts', '.gitignore', 'package.json', 'package-lock.json', 'toolchain.lock.env']) {
|
||||
fs.cpSync(path.join(repository, file), path.join(root, file), { recursive: true, filter: file => path.basename(file) !== '.env' && path.basename(file) !== 'compose.override.yaml' });
|
||||
}
|
||||
fs.mkdirSync(path.join(root, '.tools/packwiz'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'dist'));
|
||||
fs.writeFileSync(path.join(root, 'dist/.gitkeep'), '');
|
||||
fs.copyFileSync(path.join(repository, '.tools/docker-compose'), path.join(root, '.tools/docker-compose'));
|
||||
const mock = path.join(root, '.tools/packwiz/packwiz');
|
||||
fs.writeFileSync(mock, `#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { parse } from ${JSON.stringify(import.meta.resolve('smol-toml'))};
|
||||
const state = JSON.parse(fs.readFileSync(${JSON.stringify(path.join(root, '.tools/state.json'))}, 'utf8'));
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('refresh')) {
|
||||
if (state.stale) fs.appendFileSync('index.toml', '\\n# mock refresh changed index\\n');
|
||||
process.exit(0);
|
||||
}
|
||||
if (state.exportFailure) { console.error('Simulated export failure'); process.exit(7); }
|
||||
const pack = parse(fs.readFileSync('pack.toml', 'utf8'));
|
||||
const index = parse(fs.readFileSync(pack.index.file, 'utf8'));
|
||||
const archive = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpack-mock-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(archive, 'overrides'));
|
||||
const metadata = [];
|
||||
for (const entry of index.files) {
|
||||
if (entry.metafile) {
|
||||
const mod = parse(fs.readFileSync(entry.file, 'utf8'));
|
||||
metadata.push({path:path.posix.join(path.posix.dirname(entry.file),mod.filename),hashes:{sha512:mod.download.hash,sha1:'1'.repeat(40)},env:{client:mod.side==='server'?'unsupported':'required',server:mod.side==='client'?'unsupported':'required'},downloads:[mod.download.url]});
|
||||
} else {
|
||||
const destination = path.join(archive, 'overrides', entry.file);
|
||||
fs.mkdirSync(path.dirname(destination), {recursive:true});
|
||||
fs.copyFileSync(entry.file, destination);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(archive,'modrinth.index.json'),JSON.stringify({formatVersion:1,game:'minecraft',name:pack.name,versionId:pack.version,dependencies:pack.versions,files:metadata}));
|
||||
execFileSync('bsdtar',['--format','zip','-cf',args[args.indexOf('--output')+1],'-C',archive,'modrinth.index.json','overrides']);
|
||||
} finally { fs.rmSync(archive,{recursive:true,force:true}); }
|
||||
`);
|
||||
fs.chmodSync(mock, 0o755);
|
||||
fs.writeFileSync(path.join(root, '.tools/state.json'), '{}');
|
||||
let lock = fs.readFileSync(path.join(root, 'toolchain.lock.env'), 'utf8');
|
||||
lock = lock.replace(/^PACKWIZ_BINARY_SHA256=.*$/m, `PACKWIZ_BINARY_SHA256=${sha(mock)}`);
|
||||
fs.writeFileSync(path.join(root, 'toolchain.lock.env'), lock);
|
||||
git(root, ['init', '-q']);
|
||||
commit(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
export function commit(root) {
|
||||
git(root, ['add', '-A']);
|
||||
git(root, ['-c', 'user.name=Test', '-c', 'user.email=test@localhost', 'commit', '--allow-empty', '-qm', 'Fixture']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fixture, seal } from './runtime-fixture.mjs';
|
||||
import { runtime } from '../../platform/runtime.mjs';
|
||||
import { deployOnHost, deployableRelease, releaseName, sha256 } from '../../platform/deployment.mjs';
|
||||
|
||||
export function setup(t){
|
||||
const f=fixture(t);
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(f.release,'manifest.json')));
|
||||
manifest.git={dirty:false,commit:'1'.repeat(40)};manifest.managed_files={server:[]};
|
||||
fs.writeFileSync(path.join(f.release,'manifest.json'),JSON.stringify(manifest));seal(f.release);
|
||||
// Health's actual waiting/state machine is tested separately. This adapter
|
||||
// substitutes readiness, while retaining real binding/lock/Compose checks.
|
||||
f.execute=options=>{
|
||||
const args=options.args[0]==='health'?['ps']:options.args.filter((item,index,array)=>item!=='--wait'&&item!=='--wait-timeout'&&array[index-1]!=='--wait-timeout');
|
||||
const result=runtime({...options,args});
|
||||
if(result===0&&args[0]==='up'){
|
||||
const config=path.join(options.env.MC_RELEASE_DIR,'server/config');
|
||||
for(const file of fs.readdirSync(config)){
|
||||
fs.mkdirSync(path.join(options.env.MC_ROOT,'runtime/config'),{recursive:true});fs.copyFileSync(path.join(config,file),path.join(options.env.MC_ROOT,'runtime/config',file));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
f.deploy=(release=f.release,receive)=>{
|
||||
const verified=deployableRelease(release);
|
||||
return deployOnHost({repository:f.root,root:f.host,targetName:releaseName(verified.manifest),targetIdentity:verified.identity,targetChecksums:sha256(path.join(release,'SHA256SUMS')),releaseBytes:1024,timeout:30,env:f.env,execute:f.execute,receive:receive??(stage=>fs.cpSync(release,stage,{recursive:true}))});
|
||||
};
|
||||
f.next=(version='2.0.0',edit=()=>{})=>{
|
||||
const release=path.join(f.root,'release-'+version);fs.cpSync(f.release,release,{recursive:true});
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(release,'manifest.json')));manifest.pack.version=version;
|
||||
fs.writeFileSync(path.join(release,'manifest.json'),JSON.stringify(manifest));edit(release);seal(release);return release;
|
||||
};
|
||||
f.state=()=>JSON.parse(fs.readFileSync(path.join(f.host,'deployment-state.json')));
|
||||
f.events=()=>JSON.parse(fs.readFileSync(path.join(f.root,'.tools/events.json')));
|
||||
return f;
|
||||
}
|
||||
export function addConfig(release,value='original'){
|
||||
fs.writeFileSync(path.join(release,'server/config/marker.txt'),value);
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(release,'manifest.json')));
|
||||
manifest.release_files.push({path:'server/config/marker.txt'});
|
||||
manifest.managed_files.server.push({path:'config/marker.txt',owner:'server',policy:'replace',hashes:{sha256:sha256(path.join(release,'server/config/marker.txt'))}});
|
||||
fs.writeFileSync(path.join(release,'manifest.json'),JSON.stringify(manifest));seal(release);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { runtime } from '../../platform/runtime.mjs';
|
||||
|
||||
const repository = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const sha = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
|
||||
export function seal(release) {
|
||||
const manifestFile = path.join(release, 'manifest.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestFile));
|
||||
for (const entry of manifest.release_files) entry.sha256 = sha(path.join(release, entry.path));
|
||||
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
|
||||
const files = [...manifest.release_files.map(entry => entry.path), 'manifest.json'].sort();
|
||||
fs.writeFileSync(path.join(release, 'SHA256SUMS'), files.map(file => `${sha(path.join(release, file))} ${file}\n`).join(''));
|
||||
}
|
||||
|
||||
export function fixture(t) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpack-runtime-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(root, 'platform'));
|
||||
fs.copyFileSync(path.join(repository, 'platform/compatibility.json'), path.join(root, 'platform/compatibility.json'));
|
||||
fs.writeFileSync(path.join(root, 'package.json'), '{"type":"module"}');
|
||||
fs.mkdirSync(path.join(root, '.tools'));
|
||||
// Build a minimal sealed release so tests work in a clean clone without dist.
|
||||
const release = path.join(root, 'release');
|
||||
fs.mkdirSync(path.join(release, 'server/config'), { recursive: true });
|
||||
fs.writeFileSync(path.join(release, 'fixture.mrpack'), 'runtime fixture payload');
|
||||
fs.writeFileSync(path.join(release, 'server/compose.yaml'), 'services: {}\n');
|
||||
fs.copyFileSync(path.join(repository, 'server/.env.example'), path.join(release, 'server/.env.example'));
|
||||
const executable = path.join(root, '.tools/docker-compose');
|
||||
fs.writeFileSync(executable, `#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const args=process.argv.slice(2),env=process.env;
|
||||
const m=JSON.parse(fs.readFileSync(path.join(env.MC_RELEASE_DIR,'manifest.json')));
|
||||
const stateFile=${JSON.stringify(path.join(root, '.tools/docker-state.json'))};
|
||||
const state=fs.existsSync(stateFile)?JSON.parse(fs.readFileSync(stateFile)):{present:false,running:false};
|
||||
if(args.includes('config')){
|
||||
const wrong=fs.existsSync(${JSON.stringify(path.join(root, '.tools/wrong-contract'))});
|
||||
console.log(JSON.stringify({services:{minecraft:{image:m.server.image,environment:{TYPE:'MODRINTH',ONLINE_MODE:wrong?'true':'false',ENFORCE_SECURE_PROFILE:'false',MODRINTH_MODPACK:'/modpacks/'+m.artifact},volumes:[{type:'bind',source:path.join(env.MC_ROOT,'runtime'),target:'/data'},{type:'bind',source:env.MC_RELEASE_DIR,target:'/modpacks',read_only:true},{type:'bind',source:path.join(env.MC_RELEASE_DIR,'server/config'),target:'/config',read_only:true}]}}}));
|
||||
}else if(args.includes('ps')){
|
||||
console.log(JSON.stringify(state.present||fs.existsSync(${JSON.stringify(path.join(root, '.tools/existing-project'))})?[{ID:'a'.repeat(64)}]:[]));
|
||||
}else{
|
||||
fs.writeFileSync(${JSON.stringify(path.join(root, '.tools/invocation.json'))},JSON.stringify({args,uid:env.MC_UID,gid:env.MC_GID,bind:env.MC_BIND_ADDRESS,root:env.MC_ROOT,release:env.MC_RELEASE_DIR}));
|
||||
const command=args[6];
|
||||
const eventsFile=${JSON.stringify(path.join(root, '.tools/events.json'))};
|
||||
const events=fs.existsSync(eventsFile)?JSON.parse(fs.readFileSync(eventsFile)):[];
|
||||
events.push({command,locked:fs.existsSync(path.join(env.MC_ROOT,'.mcpack-runtime.lock'))});
|
||||
fs.writeFileSync(eventsFile,JSON.stringify(events));
|
||||
if(command==='up'&&fs.existsSync(${JSON.stringify(path.join(root, '.tools/fail-start'))}))process.exit(19);
|
||||
if(command==='stop'&&fs.existsSync(${JSON.stringify(path.join(root, '.tools/fail-stop'))}))process.exit(18);
|
||||
if(command==='up'||command==='start')Object.assign(state,{present:true,running:true});
|
||||
if(command==='stop')state.running=false;
|
||||
if(command==='down')Object.assign(state,{present:false,running:false});
|
||||
fs.writeFileSync(stateFile,JSON.stringify(state));
|
||||
}
|
||||
`);
|
||||
fs.chmodSync(executable, 0o755);
|
||||
const docker = path.join(root, '.tools/docker');
|
||||
fs.writeFileSync(docker, `#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const stateFile=${JSON.stringify(path.join(root, '.tools/docker-state.json'))};
|
||||
const state=fs.existsSync(stateFile)?JSON.parse(fs.readFileSync(stateFile)):{present:false,running:false};
|
||||
const other=fs.existsSync(${JSON.stringify(path.join(root, '.tools/existing-project'))});
|
||||
if(process.argv[2]==='exec'){console.log('Minecraft status fixture');process.exit(0);}
|
||||
console.log(JSON.stringify([{Id:'a'.repeat(64),RestartCount:0,Mounts:[{Type:'bind',Destination:'/data',Source:other?'/another/runtime':path.join(process.env.MC_ROOT,'runtime')}],State:{Status:state.running?'running':'exited',Running:state.running,Restarting:false,Paused:false,Dead:false,StartedAt:'fixture-start',ExitCode:0,Health:{Status:'healthy'}}}]));
|
||||
`);
|
||||
fs.chmodSync(docker, 0o755);
|
||||
const verified = JSON.parse(fs.readFileSync(path.join(root, 'platform/compatibility.json'))).verified[0];
|
||||
fs.writeFileSync(path.join(release, 'manifest.json'), JSON.stringify({ schema_version: 1, artifact: 'fixture.mrpack', pack: { name: 'fixture', version: '1.0.0', minecraft: verified.minecraft, loader: { id: verified.loader, version: verified.loader_version } }, server: { image: verified.image, java_major: verified.java_major, compose_project: 'fixture-server' }, tools: { compose: { sha256: sha(executable) } }, release_files: ['fixture.mrpack', 'server/compose.yaml', 'server/.env.example'].map(file => ({ path: file })) }));
|
||||
seal(release);
|
||||
const host = path.join(root, 'host');
|
||||
fs.mkdirSync(host);
|
||||
fs.copyFileSync(path.join(repository, 'server/.env.example'), path.join(host, '.env'));
|
||||
fs.writeFileSync(path.join(host, '.env'), fs.readFileSync(path.join(host, '.env'), 'utf8').replace('MC_EULA=FALSE', 'MC_EULA=TRUE'));
|
||||
const env = { ...process.env, PATH: `${path.join(root, '.tools')}${path.delimiter}${process.env.PATH}`, MC_ROOT: host, MC_RELEASE_DIR: release, MC_PROJECT_NAME: 'fixture-server' };
|
||||
return { root, host, release, env, run: args => runtime({ repository: root, args, env }) };
|
||||
}
|
||||
Reference in New Issue
Block a user