first cycle lab release

This commit is contained in:
2026-10-04 00:30:09 +07:00
parent 85f6897365
commit d134daafd8
92 changed files with 5623 additions and 9 deletions
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Snapshot current source into an isolated clean Git checkout; never commit root.
set -euo pipefail
repository=$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repository"
check=$(mktemp -d "$repository/.tools/clean-release.XXXXXX")
printf '%s\n' "$check" > .tools/latest-clean-release-path
mkdir "$check/source"
git ls-files --cached --others --exclude-standard -z > "$check/files.list"
tar --null -T "$check/files.list" -cf "$check/source.tar"
tar --no-same-owner -xf "$check/source.tar" -C "$check/source"
git -C "$check/source" init -q
git -C "$check/source" add -A
git -C "$check/source" -c user.name='Local release snapshot' -c user.email='release@localhost' commit -qm 'Local source snapshot for clean release'
git clone -q "$check/source" "$check/clone"
mkdir -p "$check/clone/node_modules" "$check/clone/.tools/packwiz"
cp -a "$repository/node_modules/yaml" "$repository/node_modules/smol-toml" "$check/clone/node_modules/"
cp "$repository/.tools/packwiz/packwiz" "$check/clone/.tools/packwiz/packwiz"
cp "$repository/.tools/docker-compose" "$check/clone/.tools/docker-compose"
cd "$check/clone"
./mcpack build --release
node --input-type=module - "$repository" <<'JS'
import fs from 'node:fs';
import path from 'node:path';
import {parse} from 'smol-toml';
import {verifyRelease} from './platform/runtime.mjs';
const repository=process.argv[2],pack=parse(fs.readFileSync('pack/pack.toml','utf8'));
const name=pack.name+'-'+pack.version,source=path.resolve('dist',name);
const release=verifyRelease(source),destination=path.join(repository,'dist',name);
if(fs.existsSync(destination))throw new Error('Immutable release already exists: '+destination);
const stage=fs.mkdtempSync(path.join(repository,'dist','.clean-release-'));
try{
fs.cpSync(source,stage,{recursive:true});
if(verifyRelease(stage).identity!==release.identity)throw new Error('Copied release identity differs');
fs.renameSync(stage,destination);
}catch(error){fs.rmSync(stage,{recursive:true,force:true});throw error;}
console.log('Published clean release: '+destination);
JS
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env node
import { createInterface } from 'node:readline';
import { fileURLToPath } from 'node:url';
import { deployOnHost } from '../platform/deployment.mjs';
import { rollbackOnHost } from '../platform/rollback.mjs';
const input=createInterface({input:process.stdin,terminal:false});
const messages=input[Symbol.asyncIterator]();
const send=value=>process.stdout.write('MCPACK:'+JSON.stringify(value)+'\n');
try{
const first=await messages.next();
if(first.done)throw new Error('Missing deploy request');
const request=JSON.parse(first.value);
const repository=fileURLToPath(new URL('../',import.meta.url));
const state=request.operation==='rollback' ? rollbackOnHost({...request,repository}) : await deployOnHost({...request,repository,receive:async stage=>{
send({type:'ready',stage});
const next=await messages.next();
if(next.done)throw new Error('SSH upload/session interrupted');
const message=JSON.parse(next.value);
if(message.type!=='uploaded')throw new Error('Release transfer failed or aborted');
}});
send({type:'complete',state});
}catch(error){send({type:'error',message:error.message});process.exitCode=1;}
finally{input.close();}
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env node
import { build } from '../platform/build.mjs';
import { release } from '../platform/release.mjs';
import { fileURLToPath } from 'node:url';
import { parseArgs, manageMod, usage } from '../platform/commands.mjs';
import { deploy, rollback } from '../platform/ssh-deploy.mjs';
import { backup, restoreSnapshot } from '../platform/backups.mjs';
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(usage);
process.exit(args.length === 0 ? 1 : 0);
}
try {
const options = parseArgs(args);
const root = fileURLToPath(new URL('../', import.meta.url));
if (options.command === 'build') console.log(`Release: ${build({ root, configFile: options.configFile, releaseOnly: options.releaseOnly })}`);
else if(options.command==='release')console.log(JSON.stringify(release({root,version:options.version,configFile:options.configFile}),null,2));
else if (options.command === 'deploy') await deploy({ root, configFile: options.configFile, artifact: options.artifact, sshConfig: options.sshConfig, remoteNode: options.remoteNode });
else if (options.command === 'rollback' || options.command === 'rollback-pack') await rollback({ root, ...options });
else if (options.command === 'backup') console.log(`Backup: ${backup({ repository: root, targetRelease: options.targetRelease, waitTimeout: options.waitTimeout })}`);
else if (options.command === 'restore') console.log(`Restored: ${restoreSnapshot({ backupDirectory: options.backupDirectory, destination: options.destination })}`);
else process.exitCode = manageMod({ root, ...options });
} catch (error) {
console.error(`mcpack: ${error.message}`);
process.exitCode = 1;
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
// Small Minecraft status-protocol query, used to verify real network access.
import net from 'node:net';
const host=process.argv[2],port=Number(process.argv[3]??25565);
if(!host||!Number.isInteger(port)||port<1||port>65535){console.error('Usage: node scripts/query-minecraft.mjs HOST [PORT]');process.exit(1);}
const encode=value=>{const bytes=[];let number=value>>>0;do{let byte=number&127;number>>>=7;if(number)byte|=128;bytes.push(byte);}while(number);return Buffer.from(bytes);};
const read=(buffer,offset=0)=>{let value=0,shift=0;for(let i=offset;i<buffer.length&&i<offset+5;i++){const byte=buffer[i];value|=(byte&127)<<shift;if(!(byte&128))return{value:value>>>0,end:i+1};shift+=7;}return null;};
const address=Buffer.from(host),portBytes=Buffer.alloc(2);portBytes.writeUInt16BE(port);
const handshake=Buffer.concat([encode(0),encode(-1),encode(address.length),address,portBytes,encode(1)]);
const socket=net.createConnection({host,port});let data=Buffer.alloc(0),complete=false;
socket.setTimeout(10000,()=>socket.destroy(new Error('Minecraft status timeout')));
socket.on('connect',()=>socket.write(Buffer.concat([encode(handshake.length),handshake,Buffer.from([1,0])])));
socket.on('data',chunk=>{
data=Buffer.concat([data,chunk]);
if(data.length>1024*1024){socket.destroy(new Error('Oversized status packet'));return;}
const length=read(data);if(!length||data.length<length.end+length.value)return;
try{
const packet=read(data,length.end);if(!packet||packet.value!==0)throw new Error('Unexpected status packet');
const text=read(data,packet.end);if(!text||text.end+text.value!==length.end+length.value)throw new Error('Invalid status JSON length');
const status=JSON.parse(data.subarray(text.end,text.end+text.value).toString('utf8'));
if(!status.version?.name||!status.players)throw new Error('Invalid Minecraft status response');
complete=true;console.log(JSON.stringify({host,port,version:status.version,players:status.players,description:status.description},null,2));socket.end();
}catch(error){socket.destroy(error);}
});
socket.on('error',error=>{console.error(error.message);process.exitCode=1;});
socket.on('close',()=>{if(!complete&&!process.exitCode){console.error('Connection closed before Minecraft status');process.exitCode=1;}});
+9
View File
@@ -0,0 +1,9 @@
import { runtime } from '../platform/runtime.mjs';
import { fileURLToPath } from 'node:url';
try {
process.exitCode = runtime({ repository: fileURLToPath(new URL('../', import.meta.url)), args: process.argv.slice(2) });
} catch (error) {
console.error(`server: ${error.message}`);
process.exitCode = 1;
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Local lifecycle wrapper for a complete immutable release.
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
exec node "$task_root/scripts/server.mjs" "$@"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Integration spike only: no release CLI or deployment state machine.
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
task_source=${1:-"$task_root/pack"}
task_source=$(cd -- "$task_source" && pwd)
source "$task_root/toolchain.lock.env"
printf '%s %s\n' "$PACKWIZ_BINARY_SHA256" "$task_root/.tools/packwiz/packwiz" | sha256sum -c -
task_name=$(sed -n 's/^name = "\([a-zA-Z0-9._-]*\)"$/\1/p' "$task_source/pack.toml")
task_version=$(sed -n 's/^version = "\([a-zA-Z0-9._-]*\)"$/\1/p' "$task_source/pack.toml")
[[ -n "$task_name" && -n "$task_version" ]] || { echo 'Invalid pack name/version.' >&2; exit 1; }
task_release="$task_root/dist/$task_name-$task_version"
[[ ! -e "$task_release" ]] || { echo "Release already exists: $task_release; use a new pack version." >&2; exit 1; }
mkdir -p "$task_root/dist"
task_tmp=$(mktemp -d "$task_root/.tools/spike-build.XXXXXX")
trap 'rm -rf -- "$task_tmp"' EXIT
cp -a "$task_source" "$task_tmp/source"
mkdir "$task_tmp/release"
cd "$task_tmp/source"
task_packwiz=("$task_root/.tools/packwiz/packwiz" --cache "$task_root/.tools/packwiz/cache" --config "$task_root/.tools/packwiz/config.toml")
"${task_packwiz[@]}" refresh
for task_file in index.toml pack.toml; do
task_original=$(sha256sum "$task_source/$task_file")
task_refreshed=$(sha256sum "$task_file")
[[ ${task_original%% *} == "${task_refreshed%% *}" ]] || {
echo "Stale source $task_file: run packwiz refresh explicitly before building." >&2
exit 1
}
done
"${task_packwiz[@]}" modrinth export --output "$task_tmp/release/$task_name-$task_version.mrpack"
cd "$task_tmp/release"
sha256sum "$task_name-$task_version.mrpack" > SHA256SUMS
mv -- "$task_tmp/release" "$task_release"
printf 'Artifact: %s/%s-%s.mrpack\n' "$task_release" "$task_name" "$task_version"
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Disposable integration fixtures; no runtime changes or deployment.
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
task_dir="$task_root/.tools/repro-neoforge-variants"
mkdir "$task_dir"
task_packwiz=("$task_root/.tools/packwiz/packwiz" --cache "$task_root/.tools/packwiz/cache" --config "$task_root/.tools/packwiz/config.toml")
cp -a "$task_root/pack" "$task_dir/update-pack"
cd "$task_dir/update-pack"
sed -i 's/^version = ".*"$/version = "0.0.2"/' pack.toml
printf 'neoforge-pack-v2\n' > config/spike-marker.txt
mkdir -p defaultconfigs world/datapacks
cp -a "$task_root/testdata/spike-datapack" world/datapacks/phase0
printf 'removeErroringEntities = true\n' > defaultconfigs/neoforge-server.toml
"${task_packwiz[@]}" modrinth add --project-id TyCTlI4b --version-id pJxNzk4X -y
"${task_packwiz[@]}" refresh
bash "$task_root/scripts/spike-build.sh" "$PWD"
cp -a "$task_root/pack" "$task_dir/loader-only-pack"
cd "$task_dir/loader-only-pack"
sed -i 's/^version = ".*"$/version = "0.0.3"/' pack.toml
printf 'neoforge-loader-only-v3\n' > config/spike-marker.txt
"${task_packwiz[@]}" remove sophisticated-backpacks -y
"${task_packwiz[@]}" remove sophisticated-core -y
"${task_packwiz[@]}" remove mouse-tweaks -y
"${task_packwiz[@]}" refresh
bash "$task_root/scripts/spike-build.sh" "$PWD"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Thin local Compose wrapper for Phase 0. No automatic backup/deploy/rollback.
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
set -a
source "$task_root/toolchain.lock.env"
set +a
export MC_ROOT="$task_root"
export MC_PACK=${MC_PACK:-minecraft-lab-neoforge-spike-0.0.1/minecraft-lab-neoforge-spike-0.0.1.mrpack}
task_env="$task_root/server/.env"
[[ -f "$task_env" ]] || task_env="$task_root/server/.env.example"
exec "$task_root/.tools/docker-compose" --project-name minecraft-lab-spike --env-file "$task_env" --file "$task_root/server/compose.yaml" "$@"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
source "$task_root/toolchain.lock.env"
[[ $(uname -sm) == 'Linux x86_64' ]] || { echo 'Phase 0 tools require Linux x86_64.' >&2; exit 1; }
for task_tool in curl bsdtar sha256sum; do command -v "$task_tool" >/dev/null; done
mkdir -p "$task_root/.tools/packwiz"
if [[ ! -f "$task_root/.tools/packwiz/packwiz" ]]; then
curl -fsSL --retry 2 "$PACKWIZ_URL" -o "$task_root/.tools/packwiz/packwiz.zip"
printf '%s %s\n' "$PACKWIZ_ARCHIVE_SHA256" "$task_root/.tools/packwiz/packwiz.zip" | sha256sum -c -
bsdtar -xf "$task_root/.tools/packwiz/packwiz.zip" -C "$task_root/.tools/packwiz"
fi
if [[ ! -f "$task_root/.tools/docker-compose" ]]; then
curl -fsSL --retry 2 "$COMPOSE_URL" -o "$task_root/.tools/docker-compose"
fi
printf '%s %s\n' "$PACKWIZ_BINARY_SHA256" "$task_root/.tools/packwiz/packwiz" | sha256sum -c -
printf '%s %s\n' "$COMPOSE_SHA256" "$task_root/.tools/docker-compose" | sha256sum -c -
chmod +x "$task_root/.tools/packwiz/packwiz" "$task_root/.tools/docker-compose"
"$task_root/.tools/docker-compose" version
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Real Modrinth integration in a disposable Git clone. Never changes runtime.
set -euo pipefail
task_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$task_root"
task_check=$(mktemp -d "$task_root/.tools/phase3-check.XXXXXX")
printf '%s\n' "$task_check" > .tools/phase3-check-path
mkdir "$task_check/source"
git ls-files --cached --others --exclude-standard -z > "$task_check/files.list"
tar --null -T "$task_check/files.list" -cf "$task_check/source.tar"
tar --no-same-owner -xf "$task_check/source.tar" -C "$task_check/source"
git -C "$task_check/source" init -q
git -C "$task_check/source" add -A
git -C "$task_check/source" -c user.name='Phase 3 verification' -c user.email='phase3@localhost' commit -qm 'Temporary integration snapshot'
git clone -q "$task_check/source" "$task_check/clone"
cd "$task_check/clone"
ln -s "$task_root/node_modules" node_modules
mkdir -p .tools/packwiz
cp "$task_root/.tools/packwiz/packwiz" .tools/packwiz/packwiz
cp "$task_root/.tools/docker-compose" .tools/docker-compose
task_version() {
sed -i "s/^version = \".*\"$/version = \"$1\"/" pack/pack.toml
(cd pack && ../.tools/packwiz/packwiz --cache ../.tools/packwiz/cache --config ../.tools/packwiz/config.toml refresh)
}
./mcpack remove sophisticated-backpacks --yes
test ! -e pack/mods/sophisticated-backpacks.pw.toml
task_version phase3-remove
./mcpack build
./mcpack add https://modrinth.com/mod/sophisticated-backpacks/version/pJxNzk4X --yes
rg -q 'version = "pJxNzk4X"' pack/mods/sophisticated-backpacks.pw.toml
task_version phase3-add
./mcpack build
./mcpack update sophisticated-backpacks --yes
if rg -q 'version = "pJxNzk4X"' pack/mods/sophisticated-backpacks.pw.toml; then
echo 'Expected update away from older Backpacks version' >&2
exit 1
fi
task_version phase3-update
./mcpack build
# Seed the older version again so --all proves an actual update, not just no-op.
./mcpack add https://modrinth.com/mod/sophisticated-backpacks/version/pJxNzk4X --yes
rg -q 'version = "pJxNzk4X"' pack/mods/sophisticated-backpacks.pw.toml
./mcpack update --all --yes
if rg -q 'version = "pJxNzk4X"' pack/mods/sophisticated-backpacks.pw.toml; then
echo 'Expected update --all to replace older Backpacks version' >&2
exit 1
fi
task_version phase3-all
./mcpack build
git diff -- pack > "$task_check/source-changes.diff"
printf 'PASS: remove, add pinned version, update one, update all; four full builds.\nClone: %s\n' "$task_check/clone"
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Dedicated local integration fixture; never uses the primary Phase 0 runtime.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
export MC_ROOT="$PWD/.tools/phase4-server"
export MC_RELEASE_DIR="$PWD/dist/minecraft-lab-neoforge-spike-0.0.4"
evidence="$PWD/.tools/phase5-evidence"
restored="$PWD/.tools/phase5-restored-server"
mkdir -p "$evidence"
if [[ -e "$restored" ]]; then
echo "Restore fixture already exists; choose a new destination before repeating." >&2
exit 1
fi
scripts/server.sh up -d --wait --wait-timeout 120
mkdir -p "$MC_ROOT/runtime/mod-state"
printf '{"phase":5,"progress":42}\n' > "$MC_ROOT/runtime/mod-state/phase5.json"
printf 'preserve-unmanaged-phase5\n' > "$MC_ROOT/runtime/config/phase5-unmanaged.txt"
./mcpack backup > "$evidence/backup.log" 2>&1
node --input-type=module <<'JS' > "$evidence/snapshot-path.txt"
import fs from 'node:fs';
const operation=JSON.parse(fs.readFileSync(process.env.MC_ROOT+'/.mcpack-backup-operation.json'));
if(operation.phase!=='complete'||!operation.snapshot)throw new Error('Backup did not complete');
console.log(operation.snapshot);
JS
snapshot=$(cat "$evidence/snapshot-path.txt")
scripts/server.sh exec -T minecraft mc-health > "$evidence/resumed-health.txt"
scripts/server.sh logs --tail 150 > "$evidence/resumed-start.log"
mkdir -p "$evidence/failure-bin"
cat > "$evidence/failure-bin/tar" <<'SH'
#!/bin/sh
echo 'intentional Phase 5 archive failure' >&2
exit 42
SH
chmod +x "$evidence/failure-bin/tar"
if PATH="$evidence/failure-bin:$PATH" ./mcpack backup > "$evidence/failed-backup.log" 2>&1; then
echo 'Invalid archive unexpectedly succeeded' >&2
exit 1
fi
node --input-type=module <<'JS' > "$evidence/failure-verification.txt"
import fs from 'node:fs';
import {execFileSync} from 'node:child_process';
const root=process.env.MC_ROOT;
const operation=JSON.parse(fs.readFileSync(root+'/.mcpack-backup-operation.json'));
if(operation.phase!=='failed'||!operation.stopped_confirmed||operation.snapshot!==null)throw new Error('Incorrect backup failure state');
const state=JSON.parse(execFileSync('docker',['inspect','minecraft-lab-phase4-minecraft-1']))[0].State;
if(state.Running)throw new Error('Failed backup unexpectedly restarted server');
const snapshots=fs.readdirSync(root+'/backups');
if(snapshots.length!==1||snapshots.some(name=>name.startsWith('.')))throw new Error('Incomplete snapshot published or left behind');
console.log('Failed backup: non-zero, server stopped, no additional snapshot, no staging directories.');
JS
scripts/server.sh down
./mcpack restore --backup "$snapshot" --destination "$restored" > "$evidence/restore.log" 2>&1
SNAPSHOT="$snapshot" RESTORED="$restored" node --input-type=module <<'JS' > "$evidence/restore-verification.txt"
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {verifySnapshot,runtimeInventory} from './platform/backups.mjs';
import {verifyRelease} from './platform/runtime.mjs';
const metadata=verifySnapshot(process.env.SNAPSHOT);
assert.deepEqual(runtimeInventory(process.env.RESTORED+'/runtime'),metadata.runtime_inventory);
assert.equal(verifyRelease(process.env.RESTORED+'/release').identity,metadata.source_release.manifest_sha256);
assert.equal(fs.readFileSync(process.env.RESTORED+'/runtime/mod-state/phase5.json','utf8'),'{"phase":5,"progress":42}\n');
assert.equal(fs.readFileSync(process.env.RESTORED+'/runtime/config/phase5-unmanaged.txt','utf8'),'preserve-unmanaged-phase5\n');
assert(!fs.existsSync(process.env.RESTORED+'/.env'));
console.log(`Verified ${metadata.runtime_inventory.length} runtime entries including bytes/modes/UID/GID; retained release ${metadata.source_release.name}.`);
console.log('Mod data outside world, unmanaged config, server.properties, operators and whitelist preserved; host settings not copied.');
JS
export MC_ENV_FILE="$MC_ROOT/.env"
export MC_ROOT="$restored"
export MC_RELEASE_DIR="$restored/release"
scripts/server.sh up -d --wait --wait-timeout 120
scripts/server.sh exec -T minecraft mc-health > "$evidence/restored-health.txt"
scripts/server.sh logs --tail 150 > "$evidence/restored-start.log"
scripts/server.sh down
scripts/spike-server.sh ps > "$evidence/primary-server-status.txt"
printf 'Phase 5 backup, failure and restored server health checks passed.\n'
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Disposable runtime restored from Phase 5; primary server is never selected.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
evidence="$PWD/.tools/phase6-evidence"
root="$PWD/.tools/phase6-server"
loop_root="$PWD/.tools/phase6-loop-server"
mkdir -p "$evidence"
[[ ! -e "$root" && ! -e "$loop_root" ]] || { echo 'Phase 6 fixtures already exist; use new roots to repeat.' >&2; exit 1; }
snapshot=$(cat .tools/phase5-evidence/snapshot-path.txt)
./mcpack restore --backup "$snapshot" --destination "$root"
export MC_ROOT="$root" MC_RELEASE_DIR="$root/release"
cp .tools/phase4-server/.env "$root/.env"
chmod 600 "$root/.env"
# A real cold JVM start cannot finish within two seconds.
if scripts/server.sh up --wait --wait-timeout 2 --stable-seconds 1 > "$evidence/timeout.txt" 2>&1; then
echo 'Short startup timeout unexpectedly passed' >&2; exit 1
fi
scripts/server.sh health --wait-timeout 120 --stable-seconds 3 > "$evidence/healthy.txt" 2>&1
scripts/server.sh logs --tail 100 > "$evidence/healthy-start.log"
# Abrupt termination only affects this disposable copied world.
docker kill --signal KILL minecraft-lab-phase4-minecraft-1 > "$evidence/kill.txt"
if scripts/server.sh health --wait-timeout 30 --stable-seconds 2 > "$evidence/crash.txt" 2>&1; then
echo 'Crashed container unexpectedly healthy' >&2; exit 1
fi
scripts/server.sh up --wait --wait-timeout 120 --stable-seconds 3 > "$evidence/recovered.txt" 2>&1
scripts/server.sh down
# Deliberately broken Compose fixture, separate from immutable public release.
mkdir -p "$loop_root/release"
cp -a "$MC_RELEASE_DIR/." "$loop_root/release/"
cp "$root/.env" "$loop_root/.env"
LOOP_ROOT="$loop_root" node --input-type=module <<'JS'
import fs from 'node:fs';
import crypto from 'node:crypto';
import {parse,stringify} from 'yaml';
const release=process.env.LOOP_ROOT+'/release';
const composePath=release+'/server/compose.yaml';
const compose=parse(fs.readFileSync(composePath,'utf8'));
compose.services.minecraft.restart='on-failure:5';
compose.services.minecraft.entrypoint=['/bin/sh','-c','echo phase6-intentional-restart-loop; sleep 1; exit 42'];
fs.writeFileSync(composePath,stringify(compose));
const hash=file=>crypto.createHash('sha256').update(fs.readFileSync(release+'/'+file)).digest('hex');
const manifest=JSON.parse(fs.readFileSync(release+'/manifest.json'));
manifest.pack.version='phase6-failure-fixture';
for(const file of manifest.release_files)file.sha256=hash(file.path);
fs.writeFileSync(release+'/manifest.json',JSON.stringify(manifest,null,2)+'\n');
const files=[...manifest.release_files.map(file=>file.path),'manifest.json'].sort();
fs.writeFileSync(release+'/SHA256SUMS',files.map(file=>`${hash(file)} ${file}\n`).join(''));
JS
export MC_ROOT="$loop_root" MC_RELEASE_DIR="$loop_root/release"
scripts/server.sh up -d
# Wait until the real Docker container has restarted at least once.
for attempt in {1..20}; do
count=$(docker inspect minecraft-lab-phase4-minecraft-1 --format '{{.RestartCount}}')
if [[ "$count" -ge 1 ]]; then break; fi
sleep 0.2
done
docker inspect minecraft-lab-phase4-minecraft-1 --format '{{json .State}} {{.RestartCount}}' > "$evidence/loop-docker-state.txt"
if scripts/server.sh health --wait-timeout 15 --stable-seconds 2 > "$evidence/restart-loop.txt" 2>&1; then
echo 'Restart loop unexpectedly healthy' >&2; exit 1
fi
scripts/server.sh down
ROOT="$root" LOOP_ROOT="$loop_root" node --input-type=module <<'JS' > "$evidence/assertions.txt"
import fs from 'node:fs';
import assert from 'node:assert/strict';
const reports=root=>fs.readdirSync(root+'/diagnostics').filter(name=>name.endsWith('.json')).map(name=>JSON.parse(fs.readFileSync(root+'/diagnostics/'+name)));
const normal=reports(process.env.ROOT),loop=reports(process.env.LOOP_ROOT);
assert(normal.some(report=>report.reason==='timeout'));
assert(normal.some(report=>report.reason==='crash'&&report.state.exit_code===137));
assert(loop.some(report=>report.reason==='restart-loop'));
for(const root of [process.env.ROOT,process.env.LOOP_ROOT]){
assert(!fs.existsSync(root+'/.mcpack-runtime.lock'));
assert(fs.existsSync(root+'/.mcpack-runtime.json'));
for(const report of reports(root)){
assert(fs.existsSync(report.logs));
assert.equal(report.recovery.automatic_rollback,false);
assert.equal(report.recovery.runtime_preserved,true);
}
}
assert(fs.existsSync(process.env.ROOT+'/runtime/mod-state/phase5.json'));
console.log('Real success, timeout, crash (exit 137), restart-loop, diagnostics and runtime/binding preservation passed.');
JS
scripts/spike-server.sh ps > "$evidence/primary-server-status.txt"
printf 'Phase 6 integration passed.\n'
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# Real SSH/rsync + Docker on an isolated loopback endpoint and a disposable root.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
repository="$PWD"
node_binary=$(command -v node)
check=$(mktemp -d "$PWD/.tools/phase7-check.XXXXXX")
printf '%s\n' "$check" > .tools/phase7-check-path
mkdir -p "$check/evidence" "$check/ssh" "$check/bin" "$check/host" "$check/source"
git ls-files --cached --others --exclude-standard -z > "$check/files.list"
tar --null -T "$check/files.list" -cf "$check/source.tar"
tar --no-same-owner -xf "$check/source.tar" -C "$check/source"
git -C "$check/source" init -q
git -C "$check/source" add -A
git -C "$check/source" -c user.name='Phase 7 verification' -c user.email='phase7@localhost' commit -qm 'Temporary integration snapshot'
git clone -q "$check/source" "$check/clone"
clone="$check/clone"
mkdir "$clone/node_modules"
cp -a "$repository/node_modules/yaml" "$repository/node_modules/smol-toml" "$clone/node_modules/"
mkdir -p "$clone/.tools/packwiz"
cp "$repository/.tools/packwiz/packwiz" "$clone/.tools/packwiz/packwiz"
cp "$repository/.tools/docker-compose" "$clone/.tools/docker-compose"
(cd "$clone" && ./mcpack build --release) > "$check/evidence/build-first.log" 2>&1
sed -i 's/^version = ".*"$/version = "0.0.5"/' "$clone/pack/pack.toml"
printf 'server-override-phase7-v2\n' > "$clone/server/config/spike-marker.txt"
(cd "$clone/pack" && ../.tools/packwiz/packwiz refresh)
git -C "$clone" add pack server/config
git -C "$clone" -c user.name='Phase 7 verification' -c user.email='phase7@localhost' commit -qm 'Second test release'
(cd "$clone" && ./mcpack build --release) > "$check/evidence/build-second.log" 2>&1
ssh-keygen -q -t ed25519 -N '' -f "$check/ssh/host-key"
ssh-keygen -q -t ed25519 -N '' -f "$check/ssh/client-key"
cp "$check/ssh/client-key.pub" "$check/ssh/authorized_keys"
cat > "$check/ssh/sshd_config" <<CONFIG
Port 22227
ListenAddress 127.0.0.1
HostKey $check/ssh/host-key
PidFile $check/ssh/sshd.pid
AuthorizedKeysFile $check/ssh/authorized_keys
StrictModes no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers root
CONFIG
# Pin the generated host key directly; no trust-on-first-use.
printf '[127.0.0.1]:22227 ' > "$check/ssh/known_hosts"
cut -d ' ' -f 1-2 "$check/ssh/host-key.pub" >> "$check/ssh/known_hosts"
cat > "$check/ssh/client_config" <<CONFIG
Host phase7-fixture
HostName 127.0.0.1
Port 22227
User root
IdentityFile $check/ssh/client-key
IdentitiesOnly yes
UserKnownHostsFile $check/ssh/known_hosts
CONFIG
cat > "$check/bin/ssh" <<SCRIPT
#!/bin/sh
exec /usr/bin/ssh -F '$check/ssh/client_config' "\$@"
SCRIPT
chmod +x "$check/bin/ssh"
export PATH="$check/bin:$PATH"
/usr/sbin/sshd -D -e -f "$check/ssh/sshd_config" > "$check/evidence/sshd.log" 2>&1 &
sshd_pid=$!
cleanup(){
kill "$sshd_pid" 2>/dev/null || true
}
trap cleanup EXIT
sleep 0.3
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes phase7-fixture "'$node_binary' --version; docker info --format "{{.ServerVersion}}"" > "$check/evidence/preflight.txt"
cat > "$check/host/.env" <<CONFIG
MC_EULA=TRUE
MC_MEMORY=2G
MC_PORT=25566
MC_UID=1000
MC_GID=1000
MC_PROJECT_NAME=minecraft-lab-phase7
CONFIG
chmod 600 "$check/host/.env"
REPOSITORY="$clone" TARGET_ROOT="$check/host" node --input-type=module <<'JS'
import fs from 'node:fs';
import {parse,stringify} from 'yaml';
const config=parse(fs.readFileSync(process.env.REPOSITORY+'/platform/config.yaml','utf8'));
config.server.host='phase7-fixture';config.server.directory=process.env.TARGET_ROOT;
fs.writeFileSync(process.env.REPOSITORY+'/deploy-fixture.yaml',stringify(config));
JS
first="$clone/dist/minecraft-lab-neoforge-spike-0.0.4"
second="$clone/dist/minecraft-lab-neoforge-spike-0.0.5"
deploy(){ (cd "$clone" && ./mcpack deploy --config deploy-fixture.yaml --artifact "$1" --remote-node "$node_binary"); }
deploy "$first" > "$check/evidence/deploy-first.log" 2>&1
before=$(docker inspect minecraft-lab-phase7-minecraft-1 --format '{{.Id}} {{.State.StartedAt}}')
deploy "$first" > "$check/evidence/deploy-noop.log" 2>&1
after=$(docker inspect minecraft-lab-phase7-minecraft-1 --format '{{.Id}} {{.State.StartedAt}}')
[[ "$before" == "$after" ]] || { echo 'No-op restarted server' >&2; exit 1; }
printf '%s\n' "$before" > "$check/evidence/noop-container.txt"
mkdir -p "$check/host/runtime/mod-state"
printf 'phase7-persistent-progress\n' > "$check/host/runtime/mod-state/phase7.txt"
# Test lock before updating. A rejected parallel operation must not overwrite state.
mkdir "$check/host/.mcpack-runtime.lock"
if deploy "$second" > "$check/evidence/locked.log" 2>&1; then echo 'Parallel deploy unexpectedly passed' >&2; exit 1; fi
rmdir "$check/host/.mcpack-runtime.lock"
deploy "$second" > "$check/evidence/deploy-second.log" 2>&1
HOST_ROOT="$check/host" node --input-type=module <<'JS' > "$check/evidence/update-assertions.txt"
import fs from 'node:fs';import assert from 'node:assert/strict';
const root=process.env.HOST_ROOT,state=JSON.parse(fs.readFileSync(root+'/deployment-state.json'));
assert.equal(state.phase,'complete');assert(state.backup_snapshot);
assert.equal(fs.readFileSync(root+'/active-release','utf8').trim(),'minecraft-lab-neoforge-spike-0.0.5');
assert.equal(fs.readFileSync(root+'/previous-release','utf8').trim(),'minecraft-lab-neoforge-spike-0.0.4');
assert.equal(fs.readFileSync(root+'/runtime/mod-state/phase7.txt','utf8'),'phase7-persistent-progress\n');
assert.equal(fs.readFileSync(root+'/runtime/config/spike-marker.txt','utf8'),'server-override-phase7-v2\n');
assert(fs.existsSync(root+'/runtime/world/level.dat'));
assert(fs.existsSync(state.backup_snapshot+'/runtime.tar.zst'));
console.log('First deploy, no-op, lock, backup-gated update, pointers, world and mod-state preserved.');
JS
# A manual config edit must be detected before stop/replacement, even for same release.
printf 'manual-phase7-edit\n' > "$check/host/runtime/config/spike-marker.txt"
if deploy "$second" > "$check/evidence/manual-conflict.log" 2>&1; then echo 'Manual conflict unexpectedly passed' >&2; exit 1; fi
HOST_ROOT="$check/host" node --input-type=module <<'JS' > "$check/evidence/conflict-assertions.txt"
import fs from 'node:fs';import assert from 'node:assert/strict';
const root=process.env.HOST_ROOT,state=JSON.parse(fs.readFileSync(root+'/deployment-state.json'));
assert.equal(state.phase,'failed');assert.equal(state.applied,false);
assert.equal(state.conflicts[0].path,'config/spike-marker.txt');
assert.equal(fs.readFileSync(root+'/runtime/config/spike-marker.txt','utf8'),'manual-phase7-edit\n');
console.log('Manual config conflict retained, rejected before applying/stopping.');
JS
MC_ROOT="$check/host" MC_RELEASE_DIR="$check/host/releases/minecraft-lab-neoforge-spike-0.0.5" scripts/server.sh down
scripts/spike-server.sh ps > "$check/evidence/primary-server-status.txt"
printf 'Phase 7 real SSH integration passed. Evidence: %s\n' "$check/evidence"
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Rollback an isolated copy over real SSH. Never selects the user's live root.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
repository="$PWD"
node_binary=$(command -v node)
check=$(mktemp -d "$PWD/.tools/phase8-check.XXXXXX")
printf '%s\n' "$check" > .tools/phase8-check-path
mkdir -p "$check/evidence" "$check/ssh" "$check/bin"
snapshot="$repository/.tools/phase7-check.6dbBak/host/backups/20261003T153844610Z-732a1d62-9bee-4a9c-a095-68bde6c8406f"
./mcpack restore --backup "$snapshot" --destination "$check/host" > "$check/evidence/initial-restore.log"
HOST_ROOT="$check/host" node --input-type=module <<'JS'
import fs from 'node:fs';import {releaseName,deployableRelease} from './platform/deployment.mjs';
const root=process.env.HOST_ROOT,release=deployableRelease(root+'/release'),name=releaseName(release.manifest);
fs.mkdirSync(root+'/releases');fs.renameSync(root+'/release',root+'/releases/'+name);
fs.writeFileSync(root+'/active-release',name+'\n');
fs.writeFileSync(root+'/deployment-state.json',JSON.stringify({schema_version:1,id:'restored-phase8-fixture',phase:'complete',target_release:name}));
fs.writeFileSync(root+'/.env','MC_EULA=TRUE\nMC_MEMORY=2G\nMC_PORT=25566\nMC_BIND_ADDRESS=0.0.0.0\nMC_UID=1000\nMC_GID=1000\nMC_PROJECT_NAME=minecraft-lab-phase7\n',{mode:0o600});
fs.writeFileSync(root+'/runtime/world/phase8-progress.txt','snapshot-progress\n');
fs.mkdirSync(root+'/runtime/mod-state',{recursive:true});fs.writeFileSync(root+'/runtime/mod-state/phase8-progress.txt','snapshot-progress\n');
JS
ssh-keygen -q -t ed25519 -N '' -f "$check/ssh/host-key"
ssh-keygen -q -t ed25519 -N '' -f "$check/ssh/client-key"
cp "$check/ssh/client-key.pub" "$check/ssh/authorized_keys"
cat > "$check/ssh/sshd_config" <<CONFIG
Port 22228
ListenAddress 127.0.0.1
HostKey $check/ssh/host-key
PidFile $check/ssh/sshd.pid
AuthorizedKeysFile $check/ssh/authorized_keys
StrictModes no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers root
CONFIG
printf '[127.0.0.1]:22228 ' > "$check/ssh/known_hosts"
cut -d ' ' -f 1-2 "$check/ssh/host-key.pub" >> "$check/ssh/known_hosts"
cat > "$check/ssh/client_config" <<CONFIG
Host phase8-fixture
HostName 127.0.0.1
Port 22228
User root
IdentityFile $check/ssh/client-key
IdentitiesOnly yes
UserKnownHostsFile $check/ssh/known_hosts
CONFIG
cat > "$check/bin/ssh" <<SCRIPT
#!/bin/sh
exec /usr/bin/ssh -F '$check/ssh/client_config' "\$@"
SCRIPT
chmod +x "$check/bin/ssh"
export PATH="$check/bin:$PATH"
/usr/sbin/sshd -D -e -f "$check/ssh/sshd_config" > "$check/evidence/sshd.log" 2>&1 &
sshd_pid=$!
cleanup(){
if [[ -f "$check/host/.mcpack-runtime.json" ]]; then
for release in "$check/host/releases/"*; do
if MC_ROOT="$check/host" MC_RELEASE_DIR="$release" scripts/server.sh down > "$check/evidence/cleanup.log" 2>&1; then break; fi
done
fi
kill "$sshd_pid" 2>/dev/null || true
}
trap cleanup EXIT
sleep 0.3
FIXTURE_ROOT="$check/host" FIXTURE_CONFIG="$check/config.yaml" node --input-type=module <<'JS'
import fs from 'node:fs';import {parse,stringify} from 'yaml';
const config=parse(fs.readFileSync('platform/config.yaml','utf8'));config.server.host='phase8-fixture';config.server.directory=process.env.FIXTURE_ROOT;
fs.writeFileSync(process.env.FIXTURE_CONFIG,stringify(config));
JS
./mcpack deploy --config "$check/config.yaml" --artifact dist/minecraft-lab-neoforge-spike-0.0.5 --remote-node "$node_binary" > "$check/evidence/update-to-five.log" 2>&1
rollback_snapshot=$(HOST_ROOT="$check/host" node --input-type=module <<'JS'
import fs from 'node:fs';import path from 'node:path';
console.log(path.basename(JSON.parse(fs.readFileSync(process.env.HOST_ROOT+'/deployment-state.json')).backup_snapshot));
JS
)
printf '%s\n' "$rollback_snapshot" > "$check/evidence/requested-snapshot-id.txt"
printf 'current-progress\n' > "$check/host/runtime/world/phase8-progress.txt"
printf 'current-progress\n' > "$check/host/runtime/mod-state/phase8-progress.txt"
printf 'only-after-snapshot\n' > "$check/host/runtime/phase8-after-only.txt"
ls -A "$check/host" > "$check/evidence/root-before-plan.txt"
before=$(docker inspect minecraft-lab-phase7-minecraft-1 --format '{{.Id}} {{.State.StartedAt}}')
./mcpack rollback --config "$check/config.yaml" --remote-node "$node_binary" > "$check/evidence/plan.txt"
./mcpack rollback-pack 0.0.4 --config "$check/config.yaml" --remote-node "$node_binary" > "$check/evidence/pack-plan.txt"
ls -A "$check/host" > "$check/evidence/root-after-plan.txt"
[[ "$before" == "$(docker inspect minecraft-lab-phase7-minecraft-1 --format '{{.Id}} {{.State.StartedAt}}')" ]]
node -e 'const fs=require("fs"),assert=require("assert/strict");assert.equal(fs.readFileSync(process.argv[1],"utf8"),fs.readFileSync(process.argv[2],"utf8"))' "$check/evidence/root-before-plan.txt" "$check/evidence/root-after-plan.txt"
./mcpack rollback-pack 0.0.4 --confirm-compatible --config "$check/config.yaml" --remote-node "$node_binary" > "$check/evidence/pack-rollback.log" 2>&1
HOST_ROOT="$check/host" node --input-type=module <<'JS' > "$check/evidence/pack-assertions.txt"
import fs from 'node:fs';import assert from 'node:assert/strict';import {execFileSync} from 'node:child_process';
const root=process.env.HOST_ROOT,state=JSON.parse(fs.readFileSync(root+'/rollback-state.json'));
assert.equal(state.phase,'complete');assert.equal(state.mode,'pack');assert(state.backup_snapshot);
assert.equal(fs.readFileSync(root+'/runtime/world/phase8-progress.txt','utf8'),'current-progress\n');
assert.equal(fs.readFileSync(root+'/runtime/mod-state/phase8-progress.txt','utf8'),'current-progress\n');
assert(fs.existsSync(root+'/runtime/phase8-after-only.txt'));
const container=JSON.parse(execFileSync('docker',['inspect','minecraft-lab-phase7-minecraft-1']))[0];
assert.equal(container.State.Health.Status,'healthy');assert.equal(container.NetworkSettings.Ports['25565/tcp'][0].HostIp,'0.0.0.0');
console.log('Pack-only rollback: retained 0.0.4, current progress preserved, backup verified, LAN bind and health retained.');
JS
./mcpack rollback --backup "$rollback_snapshot" --accept-progress-loss --config "$check/config.yaml" --remote-node "$node_binary" > "$check/evidence/snapshot-rollback.log" 2>&1
HOST_ROOT="$check/host" node --input-type=module <<'JS' > "$check/evidence/snapshot-assertions.txt"
import fs from 'node:fs';import assert from 'node:assert/strict';import {execFileSync} from 'node:child_process';import {verifySnapshot} from './platform/backups.mjs';
const root=process.env.HOST_ROOT,state=JSON.parse(fs.readFileSync(root+'/rollback-state.json'));
assert.equal(state.phase,'complete');assert.equal(state.mode,'snapshot');assert.equal(state.runtime_replaced,true);
assert.equal(fs.readFileSync(root+'/runtime/world/phase8-progress.txt','utf8'),'snapshot-progress\n');
assert.equal(fs.readFileSync(root+'/runtime/mod-state/phase8-progress.txt','utf8'),'snapshot-progress\n');
assert(!fs.existsSync(root+'/runtime/phase8-after-only.txt'));
assert.equal(fs.readFileSync(state.retired_runtime+'/world/phase8-progress.txt','utf8'),'current-progress\n');
assert(fs.existsSync(state.retired_runtime+'/phase8-after-only.txt'));
assert.equal(verifySnapshot(state.backup_snapshot).source_release.name,'minecraft-lab-neoforge-spike-0.0.4');
assert.equal(fs.readFileSync(root+'/.env','utf8').includes('MC_BIND_ADDRESS=0.0.0.0'),true);
const container=JSON.parse(execFileSync('docker',['inspect','minecraft-lab-phase7-minecraft-1']))[0];
assert.equal(container.State.Health.Status,'healthy');assert.equal(container.NetworkSettings.Ports['25565/tcp'][0].HostIp,'0.0.0.0');
assert(!fs.existsSync(root+'/.mcpack-runtime.lock'));
console.log('Snapshot rollback: all runtime restored, subsequent progress removed only from active runtime, retired progress and current backup retained; source release, host settings, LAN bind and health verified.');
JS
node scripts/query-minecraft.mjs 127.0.0.1 25566 > "$check/evidence/restored-status.json"
MC_ROOT="$check/host" MC_RELEASE_DIR="$check/host/releases/minecraft-lab-neoforge-spike-0.0.4" scripts/server.sh down
scripts/spike-server.sh ps > "$check/evidence/primary-server-status.txt"
printf 'Phase 8 real SSH/Docker rollback passed. Evidence: %s\n' "$check/evidence"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Validate the real release workflow without committing or tagging the workspace.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
repository="$PWD"
check=$(mktemp -d "$repository/.tools/phase9-check.XXXXXX")
printf '%s\n' "$check" > .tools/phase9-check-path
mkdir -p "$check/source" "$check/evidence"
git rev-parse HEAD > "$check/evidence/workspace-head-before.txt"
git status --porcelain --untracked-files=all > "$check/evidence/workspace-status-before.txt"
git ls-files --cached --others --exclude-standard -z > "$check/files.list"
tar --null -T "$check/files.list" -cf "$check/source.tar"
tar --no-same-owner -xf "$check/source.tar" -C "$check/source"
git -C "$check/source" init -q
git -C "$check/source" add -A
git -C "$check/source" -c user.name='Phase 9 fixture' -c user.email='phase9@localhost' commit -qm 'Isolated source snapshot'
git clone -q "$check/source" "$check/clone"
git -C "$check/clone" config user.name 'Phase 9 fixture'
git -C "$check/clone" config user.email phase9@localhost
mkdir -p "$check/clone/node_modules" "$check/clone/.tools/packwiz"
cp -a node_modules/yaml node_modules/smol-toml "$check/clone/node_modules/"
cp .tools/packwiz/packwiz "$check/clone/.tools/packwiz/packwiz"
cp .tools/docker-compose "$check/clone/.tools/docker-compose"
cd "$check/clone"
./mcpack release 0.0.6 > "$check/evidence/release.log" 2>&1
node --input-type=module <<'JS' > "$check/evidence/assertions.txt"
import fs from 'node:fs';import path from 'node:path';import assert from 'node:assert/strict';import {execFileSync} from 'node:child_process';
import {parse} from 'smol-toml';import {verifyRelease} from './platform/runtime.mjs';
const git=args=>execFileSync('git',args,{encoding:'utf8'}).trim(),pack=parse(fs.readFileSync('pack/pack.toml','utf8'));
const release=verifyRelease(path.resolve('dist/'+pack.name+'-0.0.6'));
assert.equal(pack.version,'0.0.6');assert.equal(release.manifest.git.dirty,false);
assert.equal(release.manifest.git.commit,git(['rev-parse','HEAD']));
assert.equal(git(['rev-parse','v0.0.6^{}']),release.manifest.git.commit);
assert.equal(git(['cat-file','-t','v0.0.6']),'tag');
assert.equal(git(['diff-tree','--no-commit-id','--name-only','-r','HEAD']),'pack/pack.toml');
assert.equal(git(['status','--porcelain','--untracked-files=all']),'');
assert(!fs.existsSync('.tools/release.lock'));
assert(fs.readdirSync('dist').every(name=>!name.startsWith('.release-check-')));
console.log('Real pinned packwiz and Compose: candidate build, version commit, clean final build, complete checksums and annotated tag passed.');
JS
if ./mcpack release 0.0.6 > "$check/evidence/duplicate-refusal.log" 2>&1; then exit 1; fi
cd "$repository"
git rev-parse HEAD > "$check/evidence/workspace-head-after.txt"
git status --porcelain --untracked-files=all > "$check/evidence/workspace-status-after.txt"
node -e 'const fs=require("fs"),assert=require("assert/strict"),root=process.argv[1];for(const field of ["head","status"])assert.equal(fs.readFileSync(root+"/workspace-"+field+"-before.txt","utf8"),fs.readFileSync(root+"/workspace-"+field+"-after.txt","utf8"))' "$check/evidence"
printf 'Phase 9 real release workflow passed. Evidence: %s\n' "$check/evidence"