From def83dcb32fe912cb9176c146d939c6790bdafb4 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Wed, 7 Oct 2026 22:15:19 -0400 Subject: [PATCH] fix(ci): package locally verified source without remote tests --- .gitea/workflows/ci.yml | 5 +- .gitea/workflows/release.yml | 12 ++-- README.md | 15 +++- package.json | 1 + scripts/packaging-workflow.test.ts | 110 +++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 scripts/packaging-workflow.test.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 57f3078..64b74f3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -43,8 +43,9 @@ jobs: run: | npx -y -p @commitlint/cli -p @commitlint/config-conventional \ commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --extends @commitlint/config-conventional - - name: Build and test - run: ./gradlew clean check jar + # Full clean checks are required locally for this exact candidate before integration. + - name: Compile and package + run: ./gradlew clean jar - name: Upload development artifact uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3 with: diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index a92f903..a12c5bf 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -39,8 +39,9 @@ jobs: with: node-version: 20 cache: npm - - name: Build and test before release - run: ./gradlew clean check jar + # Full clean checks are required locally for this exact candidate before integration. + - name: Compile and package before release + run: ./gradlew clean jar - name: Capture previous tag id: previous run: | @@ -86,11 +87,14 @@ jobs: else printf 'created=false\n' >> "$GITHUB_OUTPUT" fi - - name: Verify versioned plugin and release tooling + - name: Package versioned plugin and compile release tooling if: steps.release.outputs.created == 'true' env: VERSION: ${{ steps.release.outputs.version }} - run: ./gradlew clean check jar -PreleaseVersion="$VERSION" + run: | + ./gradlew clean jar -PreleaseVersion="$VERSION" + npm ci --ignore-scripts --no-audit --no-fund + npm run build - name: Upload release workflow artifact if: steps.release.outputs.created == 'true' uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3 diff --git a/README.md b/README.md index 675e327..cbe75a6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,19 @@ Fantasy Stick is the proposed transferable-character RPG plugin for Social Minec ./gradlew check jar -PreleaseVersion=0.1.0 ``` +The full local suite remains mandatory before integrating a candidate. Retain passing reports against its exact Git tree and unchanged verification inputs; changes require renewed verification. This requirement is not satisfied by a packaging-only CI pass. + +For packaging without running tests: + +```sh +./gradlew clean jar +./gradlew clean jar -PreleaseVersion=0.1.0 +npm ci --ignore-scripts --no-audit --no-fund +npm run build +``` + +`npm run build` compiles the TypeScript publication tools without executing tests; `npm test` still runs the complete tooling suite. + `check` runs Java packaging tests, TypeScript release-client/credential-helper tests, and a native compatibility test against the checksum-verified Purpur runtime. Native preparation downloads and patches the declared runtime and loads vanilla registries/components **without starting a listening server or creating a gameplay world**. The item tests decode signed written books with Minecraft's real codec, without a plugin instance or YAML lookup. A clean build repeats that preparation. Dependencies and runtime downloads require network access. Build output is `build/libs/purpur-fantasy-stick-.jar`. Server APIs, tests, credentials, and Node tooling are not bundled in the plugin. Runtime identity is `FantasyStick`, entrypoint `games.dmg.fantasystick.FantasyStickPlugin`, data folder `FantasyStick`, namespace `fantasystick`. Startup registers the manual crafting controller and automated-Crafter guard before the character-sheet recipe. Equipment sessions and permission-gated administration are registered; spell execution remains absent. @@ -213,7 +226,7 @@ Automated party tests exercise actual packaged listeners, native book custom-cli ## CI and publication -Gitea CI verifies pushes/PRs; main pushes run semantic-release and can publish immutable versioned JARs. Source checkout is anonymous from the public repository; external action revisions are pinned to full commit SHAs. CI and actual release publication must both be verified for the exact source commit before any deployment preparation. The operator has provisioned `RELEASE_TOKEN` and the bootstrap is pushed. Do not infer successful CI, publication or credential validity from the secret name alone; record actual exact-commit workflow and artifact results in the canonical wiki. +Gitea CI compiles/packages pushes and PRs without rerunning test suites; main pushes run semantic-release, package the exact version, compile publication tooling and can publish immutable versioned JARs. Complete local test proof for the exact candidate remains required before integration; verify that the fetched merge tree equals the locally tested tree. Remote success is packaging/publication evidence, not remote test evidence. This temporary separation does not delete tests, alter their limits, waive explicit CI-test criteria, or clear security findings. Source checkout is anonymous from the public repository; external action revisions are pinned to full commit SHAs. CI and actual release publication must both be verified for the exact source commit before any deployment preparation. The operator has provisioned `RELEASE_TOKEN` and the bootstrap is pushed. Do not infer successful CI, publication or credential validity from the secret name alone; record actual exact-commit workflow and artifact results in the canonical wiki. The release credential must be a repository-write credential authorized for `dmg/purpur-fantasy-stick`. Provision it through the approved protected-file/stdin workflow; never paste it into chat, commit it, or supply it as a command argument. diff --git a/package.json b/package.json index 39e824d..97e5c31 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "engines": { "node": ">=20" }, "scripts": { + "build": "tsc", "test": "rm -rf build/release-tools && tsc && node --test build/release-tools/*.test.js" }, "devDependencies": { diff --git a/scripts/packaging-workflow.test.ts b/scripts/packaging-workflow.test.ts new file mode 100644 index 0000000..ace1e8c --- /dev/null +++ b/scripts/packaging-workflow.test.ts @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +// Execute the actual checked-in run blocks against strict external build-tool ports. +// Real Gradle packaging is verified separately on the final candidate; these ports prove wiring. +function buildCommands(workflow: string): string[] { + const lines = readFileSync(`.gitea/workflows/${workflow}`, "utf8").split("\n"); + const commands: string[] = []; + for (let i = 0; i < lines.length; i++) { + const match = /^ {8}run: (.+)$/.exec(lines[i]!); + if (!match) continue; + let command = match[1]!; + if (command === "|") { + const body: string[] = []; + while (i + 1 < lines.length && /^ {10}/.test(lines[i + 1]!)) body.push(lines[++i]!.slice(10)); + command = body.join("\n"); + } + if (command.includes("./gradlew")) commands.push(command); + } + return commands; +} + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "fantasy-packaging-port-")); + mkdirSync(join(root, "bin")); + writeFileSync(join(root, "gradlew"), `#!/bin/sh +set -eu +version=0.1.0-SNAPSHOT +printf 'gradle %s\\n' "$*" >> "$CALLS" +if [ "$FAIL_PACKAGE" = true ]; then exit 87; fi +for arg do + case "$arg" in + clean|jar) ;; + -PreleaseVersion=*) version="\${arg#-PreleaseVersion=}" ;; + *) printf 'Unexpected task: %s\\n' "$arg" >&2; exit 91 ;; + esac +done +mkdir -p build/libs build/distributions +printf 'external packaging port\\n' > "build/libs/purpur-fantasy-stick-$version.jar" +printf 'external resource-pack port\\n' > "build/distributions/purpur-fantasy-stick-$version-resource-pack.zip" +`, { mode: 0o700 }); + writeFileSync(join(root, "bin/npm"), `#!/bin/sh +set -eu +printf 'npm %s\\n' "$*" >> "$CALLS" +case "$*" in + 'ci --ignore-scripts --no-audit --no-fund') ;; + 'run build') + [ "$BUILD_SCRIPT" = tsc ] || { printf 'Missing compile-only release-tool script\\n' >&2; exit 92; } + mkdir -p build/release-tools + printf 'external TypeScript compiler port\\n' > build/release-tools/publish-release.js + ;; + *) printf 'Unexpected npm command: %s\\n' "$*" >&2; exit 93 ;; +esac +`, { mode: 0o700 }); + const metadata = JSON.parse(readFileSync("package.json", "utf8")) as { scripts: Record }; + return { + root, + run(command: string, failPackage = false) { + return spawnSync("/bin/sh", ["-eu", "-c", command], { + cwd: root, encoding: "utf8", timeout: 10_000, + env: { PATH: `${join(root, "bin")}:/usr/bin:/bin`, CALLS: join(root, "calls"), VERSION: "3.2.1", + BUILD_SCRIPT: metadata.scripts.build ?? "", FAIL_PACKAGE: String(failPackage) }, + }); + }, + close() { rmSync(root, { recursive: true, force: true }); }, + }; +} + +test("CI packages its development artifact without dispatching verification tasks", () => { + const commands = buildCommands("ci.yml"); + assert.equal(commands.length, 1); + const port = fixture(); + try { + const result = port.run(commands[0]!); + assert.equal(result.status, 0, result.stderr); + assert.ok(existsSync(join(port.root, "build/libs/purpur-fantasy-stick-0.1.0-SNAPSHOT.jar"))); + assert.equal(readFileSync(join(port.root, "calls"), "utf8"), "gradle clean jar\n"); + } finally { port.close(); } +}); + +test("release packages its exact version and compiles publication tools without running suites", () => { + const commands = buildCommands("release.yml"); + assert.equal(commands.length, 2); + const port = fixture(); + try { + for (const command of commands) { + const result = port.run(command); + assert.equal(result.status, 0, result.stderr); + } + assert.ok(existsSync(join(port.root, "build/libs/purpur-fantasy-stick-3.2.1.jar"))); + assert.ok(existsSync(join(port.root, "build/distributions/purpur-fantasy-stick-3.2.1-resource-pack.zip"))); + assert.ok(existsSync(join(port.root, "build/release-tools/publish-release.js"))); + assert.equal(readFileSync(join(port.root, "calls"), "utf8"), + "gradle clean jar\ngradle clean jar -PreleaseVersion=3.2.1\nnpm ci --ignore-scripts --no-audit --no-fund\nnpm run build\n"); + } finally { port.close(); } +}); + +test("failed versioned packaging stops before preparing publication tools", () => { + const port = fixture(); + try { + const result = port.run(buildCommands("release.yml")[1]!, true); + assert.equal(result.status, 87); + assert.equal(readFileSync(join(port.root, "calls"), "utf8"), "gradle clean jar -PreleaseVersion=3.2.1\n"); + assert.ok(!existsSync(join(port.root, "build/release-tools/publish-release.js"))); + } finally { port.close(); } +}); -- 2.54.0