first cycle lab release
This commit is contained in:
Executable
+38
@@ -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
|
||||
@@ -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();}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;}});
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+5
@@ -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" "$@"
|
||||
Executable
+34
@@ -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"
|
||||
@@ -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"
|
||||
Executable
+12
@@ -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" "$@"
|
||||
Executable
+19
@@ -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
|
||||
@@ -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"
|
||||
Executable
+75
@@ -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'
|
||||
Executable
+84
@@ -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'
|
||||
Executable
+130
@@ -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"
|
||||
Executable
+122
@@ -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"
|
||||
Executable
+46
@@ -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"
|
||||
Reference in New Issue
Block a user