From ea2832c7a70e48d5d6bb1429139cb6f0f3f9a77e Mon Sep 17 00:00:00 2001 From: {xi-zhao} Date: Sun, 15 Nov 2026 14:37:25 +0800 Subject: [PATCH] test(ci): guard workflow contexts and numerical failure propagation --- .github/workflows/ci.yml | 11 +++++++++ tests/workflow-gates.test.mjs | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/workflow-gates.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bc3b816..093d83da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,17 @@ jobs: - name: Checkout uses: actions/checkout@v7 + - name: Check workflow definitions + shell: bash + run: | + actionlint_dir="$RUNNER_TEMP/actionlint" + mkdir -p "$actionlint_dir" + curl --fail --location --retry 3 --output "$actionlint_dir/actionlint.tar.gz" \ + https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz + printf '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 %s\n' "$actionlint_dir/actionlint.tar.gz" | sha256sum --check + tar -xzf "$actionlint_dir/actionlint.tar.gz" -C "$actionlint_dir" actionlint + "$actionlint_dir/actionlint" -color -shellcheck= -pyflakes= + - name: Setup Node.js uses: actions/setup-node@v7 with: diff --git a/tests/workflow-gates.test.mjs b/tests/workflow-gates.test.mjs new file mode 100644 index 00000000..666b7e5c --- /dev/null +++ b/tests/workflow-gates.test.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { parse } from "yaml"; + +for (const name of ["ppvm", "rcmps", "tnqs"]) { + test(`${name} numerical CI preserves computation status through log capture`, { skip: process.platform === "win32" }, async t => { + const workflow = parse(await readFile(new URL(`../.github/workflows/${name}-sdk.yml`, import.meta.url), "utf8")); + const job = workflow.jobs["native-sdk"]; + const step = job.steps.find(item => item.run?.includes("numerical.log")); + assert.ok(step, "The numerical validation step must remain covered"); + for (const config of [job, step]) { + assert.ok(config["continue-on-error"] === undefined || config["continue-on-error"] === false, + "Numerical validation must fail the job on error"); + } + const shell = step.shell ?? job.defaults?.run?.shell ?? workflow.defaults?.run?.shell; + assert.ok(shell === undefined || shell === "bash", `Unsupported shell in regression: ${shell}`); + // Match GitHub's runner templates: an implicit bash shell omits pipefail. + const args = shell === "bash" ? ["--noprofile", "--norc", "-eo", "pipefail"] : ["-e"]; + for (const status of [0, 23]) { + await t.test(`computation exits ${status}`, async t => { + const directory = await mkdtemp(path.join(tmpdir(), "oq-workflow-gate-")); + t.after(() => rm(directory, { recursive: true, force: true })); + await mkdir(path.join(directory, "bin")); + await writeFile(path.join(directory, "bin/sandbox-exec"), + `#!/bin/bash\nprintf 'numerical fixture exit ${status}\\n' >&2\nexit ${status}\n`, { mode: 0o700 }); + const script = path.join(directory, "run.sh"); + await writeFile(script, step.run); + const result = spawnSync("bash", [...args, script], { encoding: "utf8", timeout: 5000, + env: { ...process.env, PATH: `${path.join(directory, "bin")}${path.delimiter}${process.env.PATH}`, + OPENQUANTUM_PYTHON_ENV_ROOT: path.join(directory, "python"), + OPENQUANTUM_SDK_CHECK_OUTPUT: path.join(directory, "evidence") } }); + assert.ifError(result.error); + assert.equal(result.status, status, `Log capture changed the computation exit status: ${result.stderr}`); + assert.match(await readFile(path.join(directory, "evidence/numerical.log"), "utf8"), + new RegExp(`numerical fixture exit ${status}`)); + }); + } + }); +}