From 13a96ea2a6ef6e46ad4300da845417fd4be36810 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 14 Oct 2026 00:55:53 +0900 Subject: [PATCH 1/3] ci(naruon): add fixed isolated reusable CI suppliers --- .github/workflows/naruon-backend-ci.yml | 92 ++++ .github/workflows/naruon-bandit-ci.yml | 85 +++ .github/workflows/naruon-frontend-ci.yml | 167 ++++++ docs/doctoring/naruon-reusable-ci.md | 184 +++++++ tests/test_naruon_reusable_ci_contract.py | 606 ++++++++++++++++++++++ 5 files changed, 1134 insertions(+) create mode 100644 .github/workflows/naruon-backend-ci.yml create mode 100644 .github/workflows/naruon-bandit-ci.yml create mode 100644 .github/workflows/naruon-frontend-ci.yml create mode 100644 docs/doctoring/naruon-reusable-ci.md create mode 100644 tests/test_naruon_reusable_ci_contract.py diff --git a/.github/workflows/naruon-backend-ci.yml b/.github/workflows/naruon-backend-ci.yml new file mode 100644 index 0000000000..117ea4c939 --- /dev/null +++ b/.github/workflows/naruon-backend-ci.yml @@ -0,0 +1,92 @@ +name: Naruon backend CI + +# Fixed product supplier. Keep application-ci concurrency in the caller. +on: + workflow_call: + +permissions: + contents: read + +jobs: + backend: + name: backend (Python 3.14) + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] + env: + PYTHONWARNINGS: error + DISABLE_BACKGROUND_WORKERS: "1" + steps: + - name: Admit fixed caller before checkout + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + PR_BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + CALLER_REF: ${{ github.ref }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ "$CALLER_REPOSITORY" != "ContextualWisdomLab/naruon" ]; then + echo "Unsupported caller repository" >&2; exit 1 + fi + case "$CALLER_EVENT" in + pull_request) + if [ "$PR_HEAD_REPOSITORY" != "$CALLER_REPOSITORY" ] || [ "$PR_BASE_REPOSITORY" != "$CALLER_REPOSITORY" ]; then + echo "Unsupported pull request source" >&2; exit 1 + fi + case "$PR_BASE_REF" in + develop|master|release/?*) ;; + *) echo "Unsupported pull request base" >&2; exit 1 ;; + esac + ;; + push) + case "$CALLER_REF" in + refs/heads/develop|refs/heads/master) ;; + *) echo "Unsupported caller ref" >&2; exit 1 ;; + esac + ;; + *) echo "Unsupported caller event" >&2; exit 1 ;; + esac + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: backend/requirements-hashes.txt + - name: Verify backend in a job-private environment + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ ! -d "${RUNNER_TEMP:?}" ] || [ -L "$RUNNER_TEMP" ] || [ "$(cd "$RUNNER_TEMP" && pwd -P)" != "$RUNNER_TEMP" ]; then + echo "Refuse untrusted runner temporary root" >&2; exit 1 + fi + umask 077 + job_root="$(mktemp -d "${RUNNER_TEMP:?}/naruon-backend.XXXXXXXX")" + trap 'rm -rf -- "$job_root"' EXIT + mkdir -p "$job_root/home" "$job_root/tmp" + export HOME="$job_root/home" TMPDIR="$job_root/tmp" TMP="$job_root/tmp" TEMP="$job_root/tmp" + unset PYTHONHOME PYTHONPATH + export PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_CONFIG_FILE=/dev/null + python -m venv "$job_root/venv" + export PATH="$job_root/venv/bin:$PATH" + python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt + python -m pip install --disable-pip-version-check --require-hashes \ + -r backend/requirements-hashes.txt \ + -r backend/requirements-agent.txt + cd backend + python -m ruff check . + set -o pipefail + python -m pytest -q 2>&1 | tee pytest_output.log + if grep -qiE 'timeout|fatal|warn|denied' pytest_output.log; then + echo "::error::Tests produced Timeout, Fatal, Warn, or Denied outputs" + exit 1 + fi diff --git a/.github/workflows/naruon-bandit-ci.yml b/.github/workflows/naruon-bandit-ci.yml new file mode 100644 index 0000000000..0f2c5ad100 --- /dev/null +++ b/.github/workflows/naruon-bandit-ci.yml @@ -0,0 +1,85 @@ +name: Naruon Bandit Security Scan + +on: + workflow_call: + +permissions: + contents: read + +jobs: + security: + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] + permissions: + contents: read + security-events: write + steps: + - name: Admit fixed caller before checkout + id: admission + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + PR_BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + CALLER_REF: ${{ github.ref }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ "$CALLER_REPOSITORY" != "ContextualWisdomLab/naruon" ]; then + echo "Unsupported caller repository" >&2; exit 1 + fi + case "$CALLER_EVENT" in + pull_request) + if [ "$PR_HEAD_REPOSITORY" != "$CALLER_REPOSITORY" ] || [ "$PR_BASE_REPOSITORY" != "$CALLER_REPOSITORY" ]; then + echo "Unsupported pull request source" >&2; exit 1 + fi + case "$PR_BASE_REF" in + develop|master) ;; + *) echo "Unsupported pull request base" >&2; exit 1 ;; + esac + ;; + push|workflow_dispatch) + case "$CALLER_REF" in + refs/heads/develop|refs/heads/master) ;; + *) echo "Unsupported caller ref" >&2; exit 1 ;; + esac + ;; + *) echo "Unsupported caller event" >&2; exit 1 ;; + esac + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - name: Run Bandit in a job-private environment + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ ! -d "${RUNNER_TEMP:?}" ] || [ -L "$RUNNER_TEMP" ] || [ "$(cd "$RUNNER_TEMP" && pwd -P)" != "$RUNNER_TEMP" ]; then + echo "Refuse untrusted runner temporary root" >&2; exit 1 + fi + umask 077 + job_root="$(mktemp -d "${RUNNER_TEMP:?}/naruon-bandit.XXXXXXXX")" + trap 'rm -rf -- "$job_root"' EXIT + mkdir -p "$job_root/home" "$job_root/tmp" + export HOME="$job_root/home" TMPDIR="$job_root/tmp" TMP="$job_root/tmp" TEMP="$job_root/tmp" + unset PYTHONHOME PYTHONPATH + export PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_CONFIG_FILE=/dev/null + python -m venv "$job_root/venv" + export PATH="$job_root/venv/bin:$PATH" + python -m pip install --disable-pip-version-check --require-hashes -r requirements-bandit-ci-hashes.txt + bandit -r backend/ -x backend/tests/ -f sarif -o bandit-results.sarif + - name: Upload SARIF file + if: ${{ always() && steps.admission.outcome == 'success' }} + uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + with: + sarif_file: bandit-results.sarif + category: bandit diff --git a/.github/workflows/naruon-frontend-ci.yml b/.github/workflows/naruon-frontend-ci.yml new file mode 100644 index 0000000000..6fd3e614cb --- /dev/null +++ b/.github/workflows/naruon-frontend-ci.yml @@ -0,0 +1,167 @@ +name: Naruon frontend CI + +# Fixed product supplier. Keep application-ci concurrency in the caller. +on: + workflow_call: + +permissions: + contents: read + +jobs: + frontend: + name: frontend + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] + env: + NODE_ENV: test + steps: + - name: Admit fixed caller before checkout + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + PR_BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + CALLER_REF: ${{ github.ref }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ "$CALLER_REPOSITORY" != "ContextualWisdomLab/naruon" ]; then + echo "Unsupported caller repository" >&2; exit 1 + fi + case "$CALLER_EVENT" in + pull_request) + if [ "$PR_HEAD_REPOSITORY" != "$CALLER_REPOSITORY" ] || [ "$PR_BASE_REPOSITORY" != "$CALLER_REPOSITORY" ]; then + echo "Unsupported pull request source" >&2; exit 1 + fi + case "$PR_BASE_REF" in + develop|master|release/?*) ;; + *) echo "Unsupported pull request base" >&2; exit 1 ;; + esac + ;; + push) + case "$CALLER_REF" in + refs/heads/develop|refs/heads/master) ;; + *) echo "Unsupported caller ref" >&2; exit 1 ;; + esac + ;; + *) echo "Unsupported caller event" >&2; exit 1 ;; + esac + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Prepare job-private runtime + id: runtime + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ ! -d "${RUNNER_TEMP:?}" ] || [ -L "$RUNNER_TEMP" ] || [ "$(cd "$RUNNER_TEMP" && pwd -P)" != "$RUNNER_TEMP" ]; then + echo "Refuse untrusted runner temporary root" >&2; exit 1 + fi + umask 077 + job_root="$(mktemp -d "${RUNNER_TEMP:?}/naruon-frontend.XXXXXXXX")" + printf 'root=%s\n' "$job_root" >> "$GITHUB_OUTPUT" + mkdir -p "$job_root/home" "$job_root/tmp" "$job_root/bin" "$job_root/screenshots" + { + printf 'NARUON_JOB_ROOT=%s\n' "$job_root" + printf 'HOME=%s/home\n' "$job_root" + printf 'TMPDIR=%s/tmp\nTMP=%s/tmp\nTEMP=%s/tmp\n' "$job_root" "$job_root" "$job_root" + printf 'COREPACK_HOME=%s/corepack\n' "$job_root" + printf 'npm_config_cache=%s/npm-cache\n' "$job_root" + printf 'PLAYWRIGHT_BROWSERS_PATH=%s/playwright\n' "$job_root" + printf 'NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE=/tmp/naruon-full-product-smoke\n' + } >> "$GITHUB_ENV" + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + # setup-node's pnpm cache resolves pnpm during the action, not afterwards. + - name: Configure checkout-local pnpm store for setup-node post + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + printf 'PNPM_CONFIG_STORE_DIR=%s/.naruon-pnpm-store\n' "$GITHUB_WORKSPACE" >> "$GITHUB_ENV" + - name: Bootstrap Node.js without package-manager cache + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + package-manager-cache: false + - name: Enable Corepack pnpm + working-directory: frontend + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + node_path="$(command -v node)" + corepack_path="$(command -v corepack)" || { echo "Missing runner prerequisite: Corepack" >&2; exit 1; } + if [ ! -x "$node_path" ] || [ ! -x "$corepack_path" ]; then + echo "Missing executable Node/Corepack prerequisite" >&2; exit 1 + fi + case "$(node --version)" in + v24.*) ;; + *) echo "Node 24 prerequisite mismatch" >&2; exit 1 ;; + esac + corepack --version + corepack enable --install-directory "$NARUON_JOB_ROOT/bin" pnpm + corepack prepare pnpm@11.5.3 --activate + export PATH="$NARUON_JOB_ROOT/bin:$PATH" + printf '%s/bin\n' "$NARUON_JOB_ROOT" >> "$GITHUB_PATH" + if [ "$(pnpm --version)" != "11.5.3" ]; then + echo "pnpm package-manager pin mismatch" >&2; exit 1 + fi + store_path="$(cd "$GITHUB_WORKSPACE" && pnpm store path --silent)" + case "$store_path" in + */../*|*/./*|*/..|*/.) echo "Refuse pnpm store outside checkout-local cache root" >&2; exit 1 ;; + esac + case "$store_path" in + "$GITHUB_WORKSPACE/.naruon-pnpm-store"/?*) ;; + *) echo "Refuse pnpm store outside checkout-local cache root" >&2; exit 1 ;; + esac + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + - name: Install frontend dependencies + run: cd frontend && pnpm install --frozen-lockfile + - name: Run frontend tests + run: cd frontend && pnpm test + - name: Run frontend lint + run: cd frontend && pnpm run lint + - name: Cache Next.js build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: frontend/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('frontend/pnpm-lock.yaml') }}-${{ hashFiles('frontend/**/*.js', 'frontend/**/*.jsx', 'frontend/**/*.ts', 'frontend/**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('frontend/pnpm-lock.yaml') }}- + - name: Build frontend + env: + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: "1" + NODE_OPTIONS: "--max-old-space-size=4096" + NEXT_BUILD_CPUS: "2" + POSTCSS_WORKERS: "1" + DISABLE_POSTCSS_WORKERS: "true" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + rm -f -- "$HOME/package-lock.json" + cd frontend && pnpm run build + - name: Install Playwright Chromium + run: cd frontend && pnpm exec playwright install --with-deps chromium + - name: Run full product smoke + env: + NEXT_TELEMETRY_DISABLED: "1" + NARUON_FULL_PRODUCT_BASE_URL: "http://127.0.0.1:3001" + run: cd frontend && pnpm run full:smoke + - name: Clean job-private runtime + if: ${{ always() }} + env: + JOB_ROOT: ${{ steps.runtime.outputs.root }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if [ -n "$JOB_ROOT" ]; then + case "$JOB_ROOT" in + "${RUNNER_TEMP:?}"/naruon-frontend.????????) rm -rf -- "$JOB_ROOT" ;; + *) echo "Refuse nonowned runtime cleanup" >&2; exit 1 ;; + esac + fi diff --git a/docs/doctoring/naruon-reusable-ci.md b/docs/doctoring/naruon-reusable-ci.md new file mode 100644 index 0000000000..cb1445c61c --- /dev/null +++ b/docs/doctoring/naruon-reusable-ci.md @@ -0,0 +1,184 @@ +# Naruon central reusable CI suppliers — source candidate + +## Source and ownership + +This additive supplier lane starts at central commit +`58d12a9f1b46d611f75cfcf3e6c5a3fe19a55f96`. Its product workload reference is +`ContextualWisdomLab/naruon@042b0c70531b229af3acbd0421a2f23098d848b3`: +`.github/workflows/app-ci.yml` and `.github/workflows/bandit.yml`, read from +actual Git objects, not report placeholders. Parent composition owns publication, +consumer pinning, remote overlap checking, required-check reconciliation and merge. +Noema/OpenCode sources and their existing auto/LiteLLM owners are unchanged. + +The three new workflows accept only `workflow_call`: no command/runner inputs, +no declared secrets and no `secrets: inherit`. Their jobs use only `CWL CI isolated` +with `[self-hosted, linux, x64, cwlab-ci-isolated]`; no privileged control pool or +GitHub-hosted fallback is present. The first step follows +`pg-erd-cloud-quality.yml`'s inline pre-checkout fixed-caller pattern. It rejects +foreign repositories, forks, foreign PR bases and `pull_request_target` before +any action or checkout. Checkout remains full-SHA pinned, selects the caller's PR +head or event SHA and sets `persist-credentials: false`. All action pins retain +the consumer's exact revisions and trailing version comments. + +### Admission and concurrency + +| Supplier | PR base branches | Push branches | Manual dispatch | +|---|---|---|---| +| backend/frontend | develop, master, release/** | develop, master | rejected (application predecessor has none) | +| Bandit | develop, master | develop, master | develop/master only | + +Same-repository stacked PRs are admitted when their base matches the existing +branch contract. Bandit's formerly unfiltered manual dispatch is deliberately +narrowed to the maintained develop/master refs, matching the fixed-ref admission +boundary rather than permitting arbitrary branch code on self-hosted capacity. +Keep `application-ci-${{ github.event.pull_request.number || github.ref }}` and +`cancel-in-progress: true` in the application caller. Suppliers add no concurrency +namespace that could cancel their caller. Do not activate an unpublished SHA or +mutable-branch supplier ref. + +## Preserved workloads and thresholds + +- **Backend:** Python 3.14, existing pip cache/dependency path, core hash-locked + dependency install, second combined core+optional agent hash-locked install, + `python -m ruff check .`, and `python -m pytest -q` through `tee` with pipefail. + `PYTHONWARNINGS=error`, `DISABLE_BACKGROUND_WORKERS=1`, and the case-insensitive + `timeout|fatal|warn|denied` output rejection remain unchanged. No agent dependency + is omitted or replaced with a skip. +- **Frontend:** Node 24, pnpm 11.5.3 from the product packageManager contract, + frozen lock install, full tests, lint, exact Next.js cache key/restore key, + build, `playwright install --with-deps chromium`, and `full:smoke` against + `http://127.0.0.1:3001`. All original build resource flags remain. No typecheck, + test filter, substitute browser or reduced smoke is substituted. +- **Bandit:** Python 3.14, unchanged hash lock, exact + `bandit -r backend/ -x backend/tests/ -f sarif -o bandit-results.sarif`, unchanged + severity/default exit policy and SARIF category. Scanner findings remain + blocking. Upload still uses `always()` after successful admission, including + failed scans; a rejected caller cannot run the post-admission uploader. + +## Necessary self-hosted exceptions + +Exact workload preservation is not an arbitrary exact-step-count requirement. +The reference has backend 7, frontend 11 and Bandit 6 steps. Safety preparation +adds steps and groups Python commands inside one shell, preserving their order +and terminal failures rather than installing into shared Python. + +1. **Private Python:** backend and Bandit allocate unique mode-private RUNNER_TEMP + roots, venvs, HOME and TMP/TMPDIR/TEMP, unset inherited Python path overrides, + disable pip configuration/cache reuse for installs, and clean their root on + EXIT, including nonzero tool exits. The existing setup-python pip cache + declaration remains, but candidate dependency installs never use shared Python. +2. **Owned HOME and browser output:** frontend exports a unique HOME, temporary, + npm cache, Corepack prefix and Playwright browser directory. It selects + `NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE=/tmp/naruon-full-product-smoke`, a + supported **profile identifier**, not an output pathname. The exact consumer's + `createFullProductArtifactDirectory` uses `mkdtemp` beneath `os.tmpdir()`; + private TMPDIR places the actual uniquely named artifacts under the job root. + The legacy `NARUON_FULL_PRODUCT_SCREENSHOT_DIR` arbitrary-directory export is + removed. The predecessor's shared-home deletion `rm -f ~/package-lock.json` + is replaced by `rm -f -- "$HOME/package-lock.json"` inside that owned HOME. + No shared-home file is deleted or fixed `/tmp` artifact directory written. + Always-cleanup uses the exact runtime output plus the owned-root name pattern; + empty outputs are a no-op and foreign paths are refused. Cleanup preserves + unrelated runner files on success and failure. Abrupt runner loss still + requires disposable-runner reclamation; shell cleanup is not isolation proof. +3. **Prerequisite order:** Node 24 is installed with package-manager caching + disabled first. A real executable Corepack prerequisite is required next; + missing Corepack, Node mismatch, preparation failure or pnpm mismatch fails + explicitly. Corepack creates pnpm shims in the private prefix, activates + 11.5.3 and publishes that prefix through GITHUB_PATH. Only then does the + original setup-node pnpm-cache action execute. No unpinned npm-install + fallback or borrowed runner is supplied. +4. **Cache post lifecycle:** the pinned setup-node action discovers the pnpm + store by `pnpm store path --silent` and saves its stored paths in its post-job + action. `PNPM_CONFIG_STORE_DIR` selects `.naruon-pnpm-store` in the job's + checkout; pnpm 11.5.3 does **not** recognize `npm_config_store_dir` for this + setting. After version admission, before setup-node's cached action, the + workflow executes the real store-path command from the checkout root and + explicitly refuses an empty, foreign or traversal path outside that cache + root. Command failure remains terminal. The discovered versioned store + survives HOME cleanup through setup-node post, like preserved `.next/cache`; + the disposable runner must reclaim checkout bytes after all action posts. + Private HOME/browser/tmp files are removed at terminal cleanup. This is + cooperative pathname admission, not a defense against malicious same-UID + symlink replacement or a proof of a completed Actions cache post. +5. **Temporary-root admission:** nonsymlink, canonical RUNNER_TEMP is checked + explicitly before allocation. A rejected symbolic-link root reaches no + workload tool. This is a cooperative filesystem boundary, not protection + against same-UID malicious peers on a shared host. +6. **Persistent Node environment:** frontend defaults to `NODE_ENV=test` and + scopes `NODE_ENV=production` to build, so inherited production settings cannot + remove test development dependencies. There is no failure suppression. + +The predecessor safety probes reproduce missing venvs, shared-home deletion, +fixed screenshot path and the absence of a prior Node bootstrap. Those probes +were retained as additional predecessor characterization; the actual supplier +missing-file RED was observed before supplier implementation. Additional +supplier regressions observed RED before their fixes for SARIF admission and +symlink RUNNER_TEMP. The original cache-post probe fabricated a directory from +an unsupported environment key and did not establish pnpm behavior; it is +superseded, not counted as native evidence. Independent review identified two +frontend defects in that 78-case candidate. This successor observed the exact +consumer's approved-profile exception before fixing the export, the native +pinned pnpm HOME-store discovery before changing to `PNPM_CONFIG_STORE_DIR`, +and five false-success store-admission controls before adding the guard. +Additional approved/invalid profile and native pnpm-prefix controls are +first-GREEN characterization, not new product RED claims. + +## Local acceptance and unresolved execution gates + +Executed logs, JUnit, exact consumer snapshots, source hashes, full additive diff +and command receipts are under the implementation report packet supplied by the +parent. The permanent contracts execute the actual admission and workload shell +bytes with inert tool adapters, checking command order, real shell exit +propagation, strict output rejection, directory custody, missing prerequisites +and cleanup. The screenshot contracts execute byte-verified declarations from +`full-product-ui-smoke.mjs` with native Node filesystem APIs; they do not run +its server, browser or routes. The store contracts execute locally provisioned +pnpm 11.5.3 (retained bundle SHA-256 +`db24b8f44a6438e100eab2e690464b971d35ee3c9af4638dcab3af7ee90c8cdd`), +not a fabricated store path. Node24 and that pnpm executable must be on PATH; +missing prerequisites fail instead of downloading or skipping. This hash binds +the inspected retained distribution, not independent registry provenance. +Product dependencies/tests, Playwright installation, setup-node cache transport +and GitHub SARIF APIs remain unexecuted in this offline repair lane. + +Actionlint validates all three supplier files. The historical 78-case supplier +cohort passed despite the two defects. The successor preserves all 78 identities, +strengthens the old cache-store oracle and adds 15 cases (93 total), under +network-denied, minimal secret-free environment with warnings fatal. The repair +packet binds argv/environment, native exits, raw logs, JUnit and before/after +source hashes; final normal and `GITHUB_ACTIONS=true` results are recorded there. +The combined successor cohort has 197 cases: 195 pass and the same two inherited +failures remain, so it is **NONPASS**. The inherited 104-case cohort and exact-base +reproduction remain preserved in the predecessor packet: k-csap's existing list +runner selector is rejected by the all-runner oracle, and Noema's publication +permission test expects read rather than write. Neither owner is edited or waived. +No whole-repository suite, hosted execution or approval is claimed. + +An additional real local Corepack run installs/activates pnpm 11.5.3 in a private +prefix and exits 0. This is macOS prerequisite evidence, not Linux/x64 runner +parity or Actions execution. Publication must still establish: + +- Naruon repository and reusable-workflow access to the isolated group; +- an actual disposable Linux/x64 runner assignment and canonical temporary root; +- supported Actions runner/Node24 runtime and setup-python toolcache access; +- preprovisioned executable Corepack (Node 24 bootstrap alone is not proof); +- package/scanner/browser installation egress, Playwright Linux libraries and + the existing `--with-deps` privilege contract on isolated capacity; +- successful unchanged product workloads, cache posts, SARIF and full cleanup; +- independent exact-head review, formal approval and protected merge. + +No secrets were read, no remote was mutated and no commit/push/merge was +performed in this supplier lane. Source-contract GREEN cannot close these +operational gates. + +## Primary source references + +- `pg-erd-cloud-quality.yml` and `test_pg_erd_quality_workflow.py` at the exact + central base provide the fixed-caller/private-venv pattern. +- Naruon app/Bandit workflows and frontend/package.json at the exact consumer + SHA provide workload, action and package-manager requirements. +- `actions/setup-node@820762786026740c76f36085b0efc47a31fe5020`: + `src/cache-utils.ts`, `src/cache-save.ts` and `action.yml` provide the cache + discovery/post lifecycle and Node24 action-runtime contract. Exact copies are + retained in the implementation packet. diff --git a/tests/test_naruon_reusable_ci_contract.py b/tests/test_naruon_reusable_ci_contract.py new file mode 100644 index 0000000000..3b2333e15c --- /dev/null +++ b/tests/test_naruon_reusable_ci_contract.py @@ -0,0 +1,606 @@ +"""TDD contracts for the Naruon central reusable CI suppliers.""" + +from __future__ import annotations + +import hashlib +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOWS = ROOT / ".github" / "workflows" + + +EXPECTED = { + "backend": WORKFLOWS / "naruon-backend-ci.yml", + "frontend": WORKFLOWS / "naruon-frontend-ci.yml", + "bandit": WORKFLOWS / "naruon-bandit-ci.yml", +} +RUNNER = { + "group": "CWL CI isolated", + "labels": ["self-hosted", "linux", "x64", "cwlab-ci-isolated"], +} +CHECKOUT_RE = re.compile(r"^actions/checkout@[0-9a-f]{40}$") + + +def load(name: str) -> dict: + return yaml.load(EXPECTED[name].read_text(), Loader=yaml.BaseLoader) + + +def assert_sha_pins(workflow: dict) -> None: + for job in workflow["jobs"].values(): + for step in job["steps"]: + if "uses" in step: + assert re.search(r"@[0-9a-f]{40}(?:\s+#.*)?$", step["uses"]) + + +def test_suppliers_exist_and_are_workflow_call_only() -> None: + for path in EXPECTED.values(): + assert path.is_file() + for data in map(load, EXPECTED): + assert data["on"] == {"workflow_call": ""} + assert "inputs" not in data["on"]["workflow_call"] + assert "secrets" not in data["on"]["workflow_call"] + assert data["permissions"] == {"contents": "read"} + assert "concurrency" not in data + assert_sha_pins(data) + + +@pytest.mark.parametrize("name", EXPECTED) +def test_fixed_isolated_runner_and_no_executable_inputs(name: str) -> None: + data = load(name) + for job in data["jobs"].values(): + assert job["runs-on"] == RUNNER + assert "with" not in job + assert "secrets" not in job + assert "strategy" not in job + assert job["steps"][0]["name"] == "Admit fixed caller before checkout" + assert "uses" not in job["steps"][0] + assert job["steps"][0]["shell"] == "bash --noprofile --norc -e -o pipefail {0}" + checkout = next(step for step in job["steps"] if step.get("name") == "Checkout repository") + assert CHECKOUT_RE.fullmatch(checkout["uses"]) + assert checkout["with"]["persist-credentials"] == "false" + assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha || github.sha }}" + + +def _admission_script(name: str) -> str: + return load(name)["jobs"][next(iter(load(name)["jobs"]))]["steps"][0]["run"] + + +@pytest.mark.parametrize("name", EXPECTED) +@pytest.mark.parametrize( + "event,repository,head,base,base_ref,ref,expected", + [ + ("pull_request", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "develop", "refs/pull/1/merge", 0), + ("pull_request", "ContextualWisdomLab/naruon", "fork/naruon", "ContextualWisdomLab/naruon", "develop", "refs/pull/1/merge", 1), + ("pull_request", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "release/2026.10", "refs/pull/1/merge", "app-only"), + ("pull_request", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "feature", "refs/pull/1/merge", 1), + ("push", "ContextualWisdomLab/naruon", "", "", "", "refs/heads/develop", 0), + ("push", "ContextualWisdomLab/naruon", "", "", "", "refs/heads/master", 0), + ("push", "ContextualWisdomLab/naruon", "", "", "", "refs/heads/release/2026.10", 1), + ("workflow_dispatch", "ContextualWisdomLab/naruon", "", "", "", "refs/heads/develop", "bandit-only"), + ("workflow_dispatch", "ContextualWisdomLab/naruon", "", "", "", "refs/heads/feature", 1), + ("pull_request_target", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "ContextualWisdomLab/naruon", "develop", "refs/heads/develop", 1), + ("push", "ContextualWisdomLab/.github", "", "", "", "refs/heads/develop", 1), + ], +) +def test_actual_precheckout_admission(name, event, repository, head, base, base_ref, ref, expected): + step = load(name)["jobs"][next(iter(load(name)["jobs"]))]["steps"][0] + env = { + "PATH": "/usr/bin:/bin", + "CALLER_REPOSITORY": repository, + "CALLER_EVENT": event, + "PR_HEAD_REPOSITORY": head, + "PR_BASE_REPOSITORY": base, + "PR_BASE_REF": base_ref, + "CALLER_REF": ref, + } + result = subprocess.run( + ["/bin/bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", step["run"]], + env=env, + capture_output=True, + text=True, + timeout=5, + ) + if expected == "app-only": + expected = int(name == "bandit") + elif expected == "bandit-only": + expected = int(name != "bandit") + assert result.returncode == expected, result.stderr + assert "fork" not in result.stdout + result.stderr + + +def _all_run_text(name: str) -> str: + return "\n".join(step.get("run", "") for job in load(name)["jobs"].values() for step in job["steps"]) + + +def test_backend_preserves_workloads_and_private_venv() -> None: + text = _all_run_text("backend") + for command in ( + "python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt", + "-r backend/requirements-agent.txt", + "python -m ruff check .", + "python -m pytest -q 2>&1 | tee pytest_output.log", + "grep -qiE 'timeout|fatal|warn|denied' pytest_output.log", + ): + assert command in text + assert "python -m venv" in text + assert "PIP_NO_CACHE_DIR" in text + assert "HOME=" in text and "TMPDIR=" in text + assert "trap " in text and " EXIT" in text + + +def test_frontend_preserves_workloads_and_safe_node_order() -> None: + data = load("frontend") + steps = data["jobs"]["frontend"]["steps"] + names = [step.get("name") for step in steps] + assert names.index("Enable Corepack pnpm") < names.index("Set up Node.js") + setup = next(step for step in steps if step.get("name") == "Set up Node.js") + assert setup["with"]["node-version"] == "24" + assert setup["with"]["cache"] == "pnpm" + assert setup["with"]["cache-dependency-path"] == "frontend/pnpm-lock.yaml" + text = _all_run_text("frontend") + for command in ( + "pnpm install --frozen-lockfile", + "pnpm test", + "pnpm run lint", + "pnpm run build", + "pnpm exec playwright install --with-deps chromium", + "pnpm run full:smoke", + ): + assert command in text + assert 'rm -f ~/package-lock.json' not in text + assert 'rm -f -- "$HOME/package-lock.json"' in text + assert "NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE" in text + assert "NARUON_FULL_PRODUCT_SCREENSHOT_DIR" not in text + assert data["jobs"]["frontend"]["env"]["NODE_ENV"] == "test" + build = next(step for step in steps if step.get("name") == "Build frontend") + assert build["env"]["NODE_ENV"] == "production" + + +def test_bandit_preserves_sarif_gate_and_private_environment() -> None: + data = load("bandit") + job = data["jobs"]["security"] + assert job["permissions"] == {"contents": "read", "security-events": "write"} + text = _all_run_text("bandit") + assert "python -m pip install --disable-pip-version-check --require-hashes -r requirements-bandit-ci-hashes.txt" in text + assert "bandit -r backend/ -x backend/tests/ -f sarif -o bandit-results.sarif" in text + upload = next(step for step in job["steps"] if step.get("name") == "Upload SARIF file") + assert "always()" in upload["if"] + assert all(not step.get("continue-on-error") for step in job["steps"]) + assert "python -m venv" in text and "HOME=" in text and "TMPDIR=" in text + + +def test_bandit_upload_requires_successful_admission_but_survives_scan_failure(): + job = load("bandit")["jobs"]["security"] + assert job["steps"][0]["id"] == "admission" + upload = next(step for step in job["steps"] if step.get("name") == "Upload SARIF file") + assert upload["if"] == "${{ always() && steps.admission.outcome == 'success' }}" + + +def test_guard_precedes_all_checkouts_and_has_no_secrets() -> None: + for name, path in EXPECTED.items(): + data = load(name) + for job in data["jobs"].values(): + steps = job["steps"] + checkout_index = next(i for i, step in enumerate(steps) if "actions/checkout@" in step.get("uses", "")) + assert checkout_index > 0 + assert steps[0]["name"] == "Admit fixed caller before checkout" + assert all(not step.get("continue-on-error") for step in steps) + assert all("secrets." not in str(step) for step in steps) + assert "secrets." not in path.read_text() + + +def step_named(name, title): + return next(s for j in load(name)["jobs"].values() for s in j["steps"] if s.get("name") == title) + + +def shell(script, cwd, env): + return subprocess.run( + [shutil.which("bash") or "/bin/bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + cwd=cwd, env=env, capture_output=True, text=True, timeout=10, + ) + + +def executable(path, text): + path.write_text("#!/bin/bash\nset -eu\n" + text) + path.chmod(0o755) + + +@pytest.mark.parametrize("name,title", [ + ("backend", "Verify backend in a job-private environment"), + ("bandit", "Run Bandit in a job-private environment"), +]) +@pytest.mark.parametrize("failure,output,expected", [ + ("", "clean", 0), ("install", "clean", 17), + ("lint", "clean", 17), ("test", "clean", 17), + ("", "Warn", 1), ("", "Fatal", 1), ("", "Timeout", 1), ("", "Denied", 1), +]) +def test_actual_python_workload_order_failure_and_cleanup(tmp_path, name, title, failure, output, expected): + if name == "bandit": + expected = 17 if failure in ("install", "test") else 0 + checkout = tmp_path / "checkout"; checkout.mkdir() + (checkout / "backend").mkdir() + shared = tmp_path / "shared-home"; shared.mkdir() + sentinel = shared / "package-lock.json"; sentinel.write_text("keep") + temporary = tmp_path / "runner"; temporary.mkdir() + unrelated = temporary / "unrelated"; unrelated.write_text("keep") + tools = tmp_path / "tools"; tools.mkdir() + trace = tmp_path / "trace" + python = tools / "python" + executable(python, '''printf 'python %s HOME=%s TMPDIR=%s\\n' "$*" "$HOME" "$TMPDIR" >> "$TRACE" +if [ "$1 $2" = '-m venv' ]; then + mkdir -p "$3/bin" + ln -s "$PYTHON_STUB" "$3/bin/python" + ln -s "$BANDIT_STUB" "$3/bin/bandit" +elif [ "$1 $2" = '-m pip' ]; then + [ "$FAILURE" != install ] || exit 17 +elif [ "$1 $2" = '-m ruff' ]; then + [ "$FAILURE" != lint ] || exit 17 +elif [ "$1 $2" = '-m pytest' ]; then + printf '%s\\n' "$TEST_OUTPUT" + [ "$FAILURE" != test ] || exit 17 +fi +''') + bandit = tools / "bandit" + executable(bandit, '''printf 'bandit %s HOME=%s TMPDIR=%s\\n' "$*" "$HOME" "$TMPDIR" >> "$TRACE" +printf '{}\\n' > bandit-results.sarif +[ "$FAILURE" != test ] || exit 17 +''') + env = {"PATH": f"{tools}:/usr/bin:/bin", "HOME": str(shared), "TMPDIR": str(tmp_path), + "RUNNER_TEMP": str(temporary), "TRACE": str(trace), "PYTHON_STUB": str(python), + "BANDIT_STUB": str(bandit), "FAILURE": failure, "TEST_OUTPUT": output} + result = shell(step_named(name, title)["run"], checkout, env) + assert result.returncode == expected, result.stderr + assert sentinel.read_text() == "keep" and unrelated.read_text() == "keep" + assert list(temporary.iterdir()) == [unrelated] + rows = trace.read_text().splitlines() + assert rows[0].startswith("python -m venv ") + assert all(f"HOME={shared}" not in row and f"TMPDIR={tmp_path} " not in row for row in rows) + if failure == "install": + assert not any(row.startswith("python -m ruff ") or row.startswith("python -m pytest ") or row.startswith("bandit ") for row in rows) + if name == "backend" and failure == "lint": + assert not any(row.startswith("python -m pytest ") for row in rows) + if name == "bandit" and failure != "install": + assert (checkout / "bandit-results.sarif").is_file() + + +def frontend_fixture(tmp_path): + checkout = tmp_path / "checkout"; checkout.mkdir() + (checkout / "frontend").mkdir() + runner = tmp_path / "runner"; runner.mkdir() + (runner / "unrelated").write_text("keep") + shared = tmp_path / "shared"; shared.mkdir() + (shared / "package-lock.json").write_text("keep") + tools = tmp_path / "tools"; tools.mkdir() + env = {"PATH": f"{tools}:/usr/bin:/bin", "HOME": str(shared), "RUNNER_TEMP": str(runner), "GITHUB_WORKSPACE": str(checkout)} + for key in ("GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH"): + env[key] = str(tmp_path / key) + Path(env[key]).touch() + result = shell(step_named("frontend", "Prepare job-private runtime")["run"], checkout, env) + assert result.returncode == 0, result.stderr + result = shell(step_named("frontend", "Configure checkout-local pnpm store for setup-node post")["run"], checkout, env) + assert result.returncode == 0, result.stderr + env.update(dict(line.split("=", 1) for line in Path(env["GITHUB_ENV"]).read_text().splitlines())) + root = Path(env["NARUON_JOB_ROOT"]) + assert root.stat().st_mode & 0o777 == 0o700 + for child in ("home", "tmp", "bin", "screenshots"): + assert (root / child).stat().st_mode & 0o777 == 0o700 + env["JOB_ROOT"] = str(root) + return checkout, env, tools, root, shared, runner + + +# Exact declarations from naruon@042b0c70531b229af3acbd0421a2f23098d848b3, +# frontend/scripts/full-product-ui-smoke.mjs (blob 008d612678f9497b865a296a79038c3e875d864c). +# The outer evidence runner verifies these byte slices against the complete Git blob. +FULL_PRODUCT_ARTIFACT_SOURCE = '''const DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-smoke"; +const RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-responsive-qa"; + +export function resolveFullProductScreenshotProfile(environment = process.env) { + return ( + environment.NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE ?? + environment.NARUON_FULL_PRODUCT_SCREENSHOT_DIR + ); +} + +function resolveFullProductArtifactPrefix(rawProfile) { + switch (rawProfile) { + case undefined: + case DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE: + return "naruon-full-product-smoke-"; + case RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE: + return "naruon-full-product-responsive-qa-"; + default: + throw new Error( + "NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE (or legacy NARUON_FULL_PRODUCT_SCREENSHOT_DIR) must select an approved artifact profile", + ); + } +} + +export async function createFullProductArtifactDirectory(rawProfile) { + const prefix = resolveFullProductArtifactPrefix(rawProfile); + return mkdtemp(path.join(tmpdir(), prefix)); +} +''' + + +def artifact_directory(env, cwd): + node = shutil.which("node") + assert node, "Node is required for exact-consumer characterization" + script = ( + 'import { mkdtemp } from "node:fs/promises";\n' + 'import { tmpdir } from "node:os";\n' + 'import path from "node:path";\n' + + FULL_PRODUCT_ARTIFACT_SOURCE + + 'console.log(await createFullProductArtifactDirectory(resolveFullProductScreenshotProfile()));\n' + ) + return subprocess.run( + [node, "--input-type=module", "-e", script], cwd=cwd, env=env, + capture_output=True, text=True, timeout=10, + ) + + +def test_frontend_emitted_profile_reaches_exact_consumer_private_mkdtemp(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + result = artifact_directory(env, checkout) + assert result.returncode == 0, result.stderr + artifact = Path(result.stdout.strip()) + assert artifact.parent == root / "tmp" + assert artifact.name.startswith("naruon-full-product-smoke-") + assert artifact.stat().st_mode & 0o777 == 0o700 + sentinel = artifact / "home.png" + sentinel.write_bytes(b"owned-artifact-control") + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0, result.stderr + assert not artifact.exists() + assert (shared / "package-lock.json").read_text() == "keep" + assert (runner / "unrelated").read_text() == "keep" + + +@pytest.mark.parametrize("profile,prefix", [ + (None, "naruon-full-product-smoke-"), + ("/tmp/naruon-full-product-smoke", "naruon-full-product-smoke-"), + ("/tmp/naruon-full-product-responsive-qa", "naruon-full-product-responsive-qa-"), + ("arbitrary-owned-path", None), ("", None), +]) +def test_exact_consumer_artifact_profile_controls(tmp_path, profile, prefix): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE", None) + env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_DIR", None) + if profile is not None: + env["NARUON_FULL_PRODUCT_SCREENSHOT_DIR"] = ( + str(root / "screenshots") if profile == "arbitrary-owned-path" else profile + ) + result = artifact_directory(env, checkout) + if prefix is None: + assert result.returncode == 1 + assert "must select an approved artifact profile" in result.stderr + assert list((root / "tmp").iterdir()) == [] + else: + assert result.returncode == 0, result.stderr + artifact = Path(result.stdout.strip()) + assert artifact.parent == root / "tmp" + assert artifact.name.startswith(prefix) + + +def test_frontend_owned_home_build_screenshot_and_cleanup(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + executable(tools / "pnpm", 'printf "%s\\n" "$HOME $TMPDIR $NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE"\n') + (root / "home/package-lock.json").write_text("owned") + result = shell(step_named("frontend", "Build frontend")["run"], checkout, env) + assert result.returncode == 0, result.stderr + assert not (root / "home/package-lock.json").exists() + assert (shared / "package-lock.json").read_text() == "keep" + assert result.stdout.strip() == f"{root}/home {root}/tmp /tmp/naruon-full-product-smoke" + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0, result.stderr + assert not root.exists() + assert (runner / "unrelated").read_text() == "keep" + assert (shared / "package-lock.json").read_text() == "keep" + + +def corepack_adapter(tools): + executable(tools / "node", 'if [ "$MODE" = wrong-node ]; then echo v22.1.0; else echo v24.1.0; fi\n') + executable(tools / "corepack", '''printf 'corepack %s\\n' "$*" >> "$TRACE" +if [ "$1" = enable ]; then + printf '#!/bin/bash\\nset -eu\\nprintf "pnpm %%s\\\\n" "$*" >> "$TRACE"\\nif [ "$1" = --version ]; then\\n if [ "$MODE" = wrong-pnpm ]; then echo 10.0.0; else echo 11.5.3; fi\\nelif [ "$1 $2 $3" = "store path --silent" ]; then\\n [ "$MODE" != store-fails ] || exit 17\\n printf "%%s\\\\n" "$DISCOVERED_STORE"\\nfi\\n' > "$NARUON_JOB_ROOT/bin/pnpm" + chmod +x "$NARUON_JOB_ROOT/bin/pnpm" +fi +if [ "$1" = prepare ] && [ "$MODE" = prepare-fails ]; then exit 17; fi +if [ "$1" = --version ]; then echo 0.35.0; fi +''') + + +@pytest.mark.parametrize("mode,expected", [("ok", 0), ("missing", 1), ("prepare-fails", 17), ("wrong-node", 1), ("wrong-pnpm", 1)]) +def test_corepack_prerequisite_actual_order_and_failure(tmp_path, mode, expected): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env["TRACE"] = str(tmp_path / "trace") + env["MODE"] = mode + env["DISCOVERED_STORE"] = str(checkout / ".naruon-pnpm-store/v11") + corepack_adapter(tools) + if mode == "missing": + (tools / "corepack").unlink() + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == expected, result.stderr + if mode == "ok": + rows = Path(env["TRACE"]).read_text().splitlines() + assert rows == [ + "corepack --version", f"corepack enable --install-directory {root}/bin pnpm", + "corepack prepare pnpm@11.5.3 --activate", "pnpm --version", + "pnpm store path --silent", + ] + assert Path(env["GITHUB_PATH"]).read_text().strip() == f"{root}/bin" + assert (shared / "package-lock.json").read_text() == "keep" + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0 + + +@pytest.mark.parametrize("store", ["home", "sibling", "traversal", "empty", "command-fails"]) +def test_corepack_refuses_unowned_discovered_store_before_cache(tmp_path, store): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env["TRACE"] = str(tmp_path / "trace") + env["MODE"] = "store-fails" if store == "command-fails" else "ok" + env["DISCOVERED_STORE"] = { + "home": str(root / "home/.local/share/pnpm/store/v11"), + "sibling": str(checkout / ".naruon-pnpm-store-foreign/v11"), + "traversal": str(checkout / ".naruon-pnpm-store/../foreign/v11"), + "empty": "", "command-fails": "", + }[store] + corepack_adapter(tools) + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == (17 if store == "command-fails" else 1), result.stderr + assert "pnpm store path --silent" in Path(env["TRACE"]).read_text().splitlines() + if store != "command-fails": + assert "Refuse pnpm store outside checkout-local cache root" in result.stderr + cleanup = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert cleanup.returncode == 0 + assert (shared / "package-lock.json").read_text() == "keep" + assert (runner / "unrelated").read_text() == "keep" + + +@pytest.mark.parametrize("target", ["shared", "empty", "foreign"]) +def test_frontend_cleanup_refuses_nonowned_paths(tmp_path, target): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env["JOB_ROOT"] = {"shared": str(shared), "empty": "", "foreign": str(runner / "unrelated")}[target] + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == (0 if target == "empty" else 1) + assert (shared / "package-lock.json").read_text() == "keep" + assert (runner / "unrelated").read_text() == "keep" + assert root.exists() + + +def test_node_cache_prerequisites_are_causally_ordered(): + steps = load("frontend")["jobs"]["frontend"]["steps"] + bootstrap = next(i for i, s in enumerate(steps) if s.get("name") == "Bootstrap Node.js without package-manager cache") + corepack = next(i for i, s in enumerate(steps) if s.get("name") == "Enable Corepack pnpm") + cached = next(i for i, s in enumerate(steps) if s.get("name") == "Set up Node.js") + assert bootstrap < corepack < cached + assert steps[bootstrap]["with"] == {"node-version": "24", "package-manager-cache": "false"} + assert all(not s.get("continue-on-error") for s in steps) + + +def pinned_pnpm_store(env, cwd): + pnpm = shutil.which("pnpm") + node = shutil.which("node") + assert pnpm and node, "Node and locally provisioned pnpm 11.5.3 are required (no download/skip)" + package_root = Path(pnpm).resolve().parents[1] + assert json.loads((package_root / "package.json").read_text())["version"] == "11.5.3" + assert hashlib.sha256((package_root / "dist/pnpm.mjs").read_bytes()).hexdigest() == ( + "db24b8f44a6438e100eab2e690464b971d35ee3c9af4638dcab3af7ee90c8cdd" + ) + native_env = {**env, "PATH": f"{Path(node).parent}:{env['PATH']}"} + version = subprocess.run( + [pnpm, "--version"], cwd=cwd, env=native_env, + capture_output=True, text=True, timeout=10, + ) + assert version.returncode == 0 and version.stdout.strip() == "11.5.3", version.stderr + result = subprocess.run( + [pnpm, "store", "path", "--silent"], cwd=cwd, env=native_env, + capture_output=True, text=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + assert not result.stderr + return Path(result.stdout.strip()) + + +@pytest.mark.parametrize("key,selected", [ + ("PNPM_CONFIG_STORE_DIR", True), ("pnpm_config_store_dir", True), + ("npm_config_store_dir", False), +]) +def test_pnpm11_native_store_env_prefix_controls(tmp_path, key, selected): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env.pop("PNPM_CONFIG_STORE_DIR", None) + env.pop("npm_config_store_dir", None) + expected = checkout / ".naruon-pnpm-store" + env[key] = str(expected) + store = pinned_pnpm_store(env, checkout) + assert (expected in store.parents) is selected + if not selected: + assert root / "home" in store.parents + + +def test_corepack_shell_checks_native_pnpm_store_before_setup_node(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + native_pnpm = shutil.which("pnpm") + node = shutil.which("node") + assert native_pnpm and node + env["PATH"] = f"{tools}:{Path(node).parent}:/usr/bin:/bin" + env["NATIVE_PNPM"] = native_pnpm + # Provisioning is inert/offline; the workflow's pnpm commands are the real pinned CLI. + executable(tools / "corepack", '''if [ "$1" = enable ]; then + ln -s "$NATIVE_PNPM" "$NARUON_JOB_ROOT/bin/pnpm" +elif [ "$1" = prepare ]; then + [ "$2" = pnpm@11.5.3 ] +else + echo offline-provisioning-adapter +fi +''') + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == 0, result.stderr + store = pinned_pnpm_store(env, checkout) + assert checkout / ".naruon-pnpm-store" in store.parents + # Removing the supported key reproduces the old HOME store and the real shell refuses it. + env.pop("PNPM_CONFIG_STORE_DIR") + env["npm_config_store_dir"] = str(checkout / ".naruon-pnpm-store") + (root / "bin/pnpm").unlink() + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == 1 + assert "Refuse pnpm store outside checkout-local cache root" in result.stderr + + +def test_frontend_cache_store_survives_home_cleanup_for_setup_node_post(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + store = pinned_pnpm_store(env, checkout) + assert checkout / ".naruon-pnpm-store" in store.parents + assert root not in store.parents + assert pinned_pnpm_store(env, checkout / "frontend") == store + store.mkdir(parents=True, exist_ok=True) + (store / "sentinel").write_text("cache-post-input") + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0 + assert not root.exists() + assert (store / "sentinel").read_text() == "cache-post-input" + + +@pytest.mark.parametrize("name,title", [ + ("backend", "Verify backend in a job-private environment"), + ("bandit", "Run Bandit in a job-private environment"), + ("frontend", "Prepare job-private runtime"), +]) +def test_private_runtime_rejects_symlink_runner_temp(tmp_path, name, title): + outside = tmp_path / "outside"; outside.mkdir() + (outside / "sentinel").write_text("keep") + linked = tmp_path / "linked"; linked.symlink_to(outside, target_is_directory=True) + tools = tmp_path / "tools"; tools.mkdir() + executable(tools / "python", 'echo invoked >> "$TRACE"\nexit 0\n') + executable(tools / "bandit", 'echo bandit-invoked >> "$TRACE"\nexit 0\n') + (tmp_path / "backend").mkdir() + env = {"PATH": f"{tools}:/usr/bin:/bin", "RUNNER_TEMP": str(linked), + "TRACE": str(tmp_path / "trace"), "GITHUB_ENV": str(tmp_path / "env"), + "GITHUB_OUTPUT": str(tmp_path / "output")} + result = shell(step_named(name, title)["run"], tmp_path, env) + assert result.returncode == 1, result.stderr + assert "Refuse untrusted runner temporary root" in result.stderr + assert not (tmp_path / "trace").exists() + assert sorted(p.name for p in outside.iterdir()) == ["sentinel"] + + +@pytest.mark.parametrize("title", [ + "Install frontend dependencies", "Run frontend tests", "Run frontend lint", + "Build frontend", "Install Playwright Chromium", "Run full product smoke", +]) +def test_frontend_workload_failures_remain_gating(tmp_path, title): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + executable(tools / "pnpm", 'exit 17\n') + result = shell(step_named("frontend", title)["run"], checkout, env) + assert result.returncode == 17 + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0 + assert not root.exists() + assert (shared / "package-lock.json").read_text() == "keep" From e32ae0ac4a671564a2a30503abf23656b171cd69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 15 Oct 2026 03:44:36 -0700 Subject: [PATCH 2/3] fix(ci): preserve Naruon pull-request merge revision --- .github/workflows/naruon-backend-ci.yml | 2 +- .github/workflows/naruon-bandit-ci.yml | 2 +- .github/workflows/naruon-frontend-ci.yml | 2 +- docs/doctoring/naruon-reusable-ci.md | 27 +++++++++++++++++++++-- tests/test_naruon_reusable_ci_contract.py | 4 +++- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/workflows/naruon-backend-ci.yml b/.github/workflows/naruon-backend-ci.yml index 117ea4c939..5f5b6f0e57 100644 --- a/.github/workflows/naruon-backend-ci.yml +++ b/.github/workflows/naruon-backend-ci.yml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.sha }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/.github/workflows/naruon-bandit-ci.yml b/.github/workflows/naruon-bandit-ci.yml index 0f2c5ad100..c9df60fd98 100644 --- a/.github/workflows/naruon-bandit-ci.yml +++ b/.github/workflows/naruon-bandit-ci.yml @@ -55,7 +55,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.sha }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/.github/workflows/naruon-frontend-ci.yml b/.github/workflows/naruon-frontend-ci.yml index 6fd3e614cb..cab8670315 100644 --- a/.github/workflows/naruon-frontend-ci.yml +++ b/.github/workflows/naruon-frontend-ci.yml @@ -75,7 +75,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.sha }} # setup-node's pnpm cache resolves pnpm during the action, not afterwards. - name: Configure checkout-local pnpm store for setup-node post shell: bash --noprofile --norc -e -o pipefail {0} diff --git a/docs/doctoring/naruon-reusable-ci.md b/docs/doctoring/naruon-reusable-ci.md index cb1445c61c..60bcb02b43 100644 --- a/docs/doctoring/naruon-reusable-ci.md +++ b/docs/doctoring/naruon-reusable-ci.md @@ -16,8 +16,8 @@ with `[self-hosted, linux, x64, cwlab-ci-isolated]`; no privileged control pool GitHub-hosted fallback is present. The first step follows `pg-erd-cloud-quality.yml`'s inline pre-checkout fixed-caller pattern. It rejects foreign repositories, forks, foreign PR bases and `pull_request_target` before -any action or checkout. Checkout remains full-SHA pinned, selects the caller's PR -head or event SHA and sets `persist-credentials: false`. All action pins retain +any action or checkout. Checkout remains full-SHA pinned, selects the caller's event SHA (the synthetic PR +merge commit for pull-request validation) and sets `persist-credentials: false`. All action pins retain the consumer's exact revisions and trailing version comments. ### Admission and concurrency @@ -182,3 +182,26 @@ operational gates. `src/cache-utils.ts`, `src/cache-save.ts` and `action.yml` provide the cache discovery/post lifecycle and Node24 action-runtime contract. Exact copies are retained in the implementation packet. + +## 2026-10-15 checkout parity repair + +The original supplier explicitly selected the PR head while the product's +`app-ci.yml` and `bandit.yml` retained checkout's event default. That silently +changed integration validation to head-only validation. All three suppliers now +select `github.sha`, preserving the PR merge commit, push commits, and Bandit-only manual event commits. +The parsed-workflow regression failed for all three predecessors before repair. +No caller activation, runner admission, credential or required-check change is +included. Runtime parity and exact-head checks remain independent acceptance +requirements. + +Source: GitHub, [Events that trigger workflows: pull_request](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request). + +Validation: the changed checkout/admission cohort passes 36 cases with +`GITHUB_ACTIONS=true PYTHONWARNINGS=error python -m pytest +tests/test_naruon_reusable_ci_contract.py -q -k +'fixed_isolated_runner or actual_precheckout_admission'`. actionlint passes all +three supplier workflows. The complete supplier cohort reports 88 passed and +5 failed both before and after this repair in the same environment: five native +pnpm tests require the unavailable exact pnpm 11.5.3 installation layout. This +is not a full-suite pass; the existing pnpm provisioning and pip-cache review +findings remain open. diff --git a/tests/test_naruon_reusable_ci_contract.py b/tests/test_naruon_reusable_ci_contract.py index 3b2333e15c..5bb4587124 100644 --- a/tests/test_naruon_reusable_ci_contract.py +++ b/tests/test_naruon_reusable_ci_contract.py @@ -66,7 +66,9 @@ def test_fixed_isolated_runner_and_no_executable_inputs(name: str) -> None: checkout = next(step for step in job["steps"] if step.get("name") == "Checkout repository") assert CHECKOUT_RE.fullmatch(checkout["uses"]) assert checkout["with"]["persist-credentials"] == "false" - assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha || github.sha }}" + # Product workflows use checkout's event revision: the merge commit for PRs. + # Head-only checkout misses regressions introduced by the target branch. + assert checkout["with"]["ref"] == "${{ github.sha }}" def _admission_script(name: str) -> str: From 88640772068c4e8bb774a55530891936fd246f61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 15 Oct 2026 03:52:56 -0700 Subject: [PATCH 3/3] fix(ci): provision Naruon native supplier contracts explicitly --- .github/workflows/naruon-backend-ci.yml | 2 - .../workflows/naruon-supplier-quality-ci.yml | 85 ++++++++ docs/doctoring/naruon-reusable-ci.md | 44 +++- integration/naruon/test_native_toolchain.py | 199 ++++++++++++++++++ integration/naruon/toolchain/.gitignore | 1 + .../naruon/toolchain/package-lock.json | 33 +++ integration/naruon/toolchain/package.json | 1 + tests/test_naruon_reusable_ci_contract.py | 178 ---------------- .../test_naruon_supplier_quality_workflow.py | 62 ++++++ 9 files changed, 422 insertions(+), 183 deletions(-) create mode 100644 .github/workflows/naruon-supplier-quality-ci.yml create mode 100644 integration/naruon/test_native_toolchain.py create mode 100644 integration/naruon/toolchain/.gitignore create mode 100644 integration/naruon/toolchain/package-lock.json create mode 100644 integration/naruon/toolchain/package.json create mode 100644 tests/test_naruon_supplier_quality_workflow.py diff --git a/.github/workflows/naruon-backend-ci.yml b/.github/workflows/naruon-backend-ci.yml index 5f5b6f0e57..7916bfcf71 100644 --- a/.github/workflows/naruon-backend-ci.yml +++ b/.github/workflows/naruon-backend-ci.yml @@ -61,8 +61,6 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - cache: pip - cache-dependency-path: backend/requirements-hashes.txt - name: Verify backend in a job-private environment shell: bash --noprofile --norc -e -o pipefail {0} run: | diff --git a/.github/workflows/naruon-supplier-quality-ci.yml b/.github/workflows/naruon-supplier-quality-ci.yml new file mode 100644 index 0000000000..663c2a5b50 --- /dev/null +++ b/.github/workflows/naruon-supplier-quality-ci.yml @@ -0,0 +1,85 @@ +name: Naruon supplier quality + +on: + pull_request: + paths: + - '.github/workflows/naruon-*-ci.yml' + - 'tests/test_naruon*.py' + - 'integration/naruon/**' + - 'requirements-opencode-review-ci-hashes.txt' + push: + branches: [main] + paths: + - '.github/workflows/naruon-*-ci.yml' + - 'tests/test_naruon*.py' + - 'integration/naruon/**' + - 'requirements-opencode-review-ci-hashes.txt' + +permissions: + contents: read + +concurrency: + group: naruon-supplier-quality-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + supplier-contracts: + timeout-minutes: 20 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + - name: Prepare private quality environment + id: runtime + shell: bash + run: | + set -euo pipefail + umask 077 + root="$(mktemp -d "${RUNNER_TEMP:?}/naruon-quality.XXXXXXXX")" + printf 'root=%s\n' "$root" >> "$GITHUB_OUTPUT" + python -m venv "$root/venv" + printf '%s/venv/bin\n' "$root" >> "$GITHUB_PATH" + printf 'npm_config_cache=%s/npm-cache\n' "$root" >> "$GITHUB_ENV" + - name: Install hash-locked Python tooling + run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt + - name: Install locked pnpm without lifecycle hooks + shell: bash + run: | + set -euo pipefail + npm ci --ignore-scripts --no-audit --no-fund --prefix integration/naruon/toolchain + printf '%s/integration/naruon/toolchain/node_modules/.bin\n' "$GITHUB_WORKSPACE" >> "$GITHUB_PATH" + - name: Run complete supplier contracts and native integration + env: + PYTHONWARNINGS: error + run: | + python -m pytest -q \ + tests/test_naruon_reusable_ci_contract.py \ + tests/test_naruon_supplier_quality_workflow.py \ + integration/naruon/test_native_toolchain.py + - name: Clean private quality environment + if: always() + env: + JOB_ROOT: ${{ steps.runtime.outputs.root }} + shell: bash + run: | + if [ -n "$JOB_ROOT" ]; then + case "$JOB_ROOT" in + "${RUNNER_TEMP:?}"/naruon-quality.????????) rm -rf -- "$JOB_ROOT" ;; + *) echo 'Refuse nonowned quality cleanup' >&2; exit 1 ;; + esac + fi diff --git a/docs/doctoring/naruon-reusable-ci.md b/docs/doctoring/naruon-reusable-ci.md index 60bcb02b43..28ea3c1df0 100644 --- a/docs/doctoring/naruon-reusable-ci.md +++ b/docs/doctoring/naruon-reusable-ci.md @@ -38,7 +38,7 @@ mutable-branch supplier ref. ## Preserved workloads and thresholds -- **Backend:** Python 3.14, existing pip cache/dependency path, core hash-locked +- **Backend:** Python 3.14, private uncached installs, core hash-locked dependency install, second combined core+optional agent hash-locked install, `python -m ruff check .`, and `python -m pytest -q` through `tee` with pipefail. `PYTHONWARNINGS=error`, `DISABLE_BACKGROUND_WORKERS=1`, and the case-insensitive @@ -65,8 +65,8 @@ and terminal failures rather than installing into shared Python. 1. **Private Python:** backend and Bandit allocate unique mode-private RUNNER_TEMP roots, venvs, HOME and TMP/TMPDIR/TEMP, unset inherited Python path overrides, disable pip configuration/cache reuse for installs, and clean their root on - EXIT, including nonzero tool exits. The existing setup-python pip cache - declaration remains, but candidate dependency installs never use shared Python. + EXIT, including nonzero tool exits. The unused setup-python pip cache + declaration is removed; dependency installs never use shared Python. 2. **Owned HOME and browser output:** frontend exports a unique HOME, temporary, npm cache, Corepack prefix and Playwright browser directory. It selects `NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE=/tmp/naruon-full-product-smoke`, a @@ -205,3 +205,41 @@ three supplier workflows. The complete supplier cohort reports 88 passed and pnpm tests require the unavailable exact pnpm 11.5.3 installation layout. This is not a full-suite pass; the existing pnpm provisioning and pip-cache review findings remain open. + +## 2026-10-15 native toolchain and cache repair + +The native Node/pnpm integration cases previously ran implicitly inside every +Python suite despite requiring pnpm 11.5.3's exact package layout and distribution +hash. They now live in `integration/naruon/test_native_toolchain.py`. Every +original case and assertion is retained: 82 tool-independent supplier cases plus +11 native cases still total 93. Four additional workflow/cache contracts make +the focused gate 97 cases. No test is skipped or replaced with a stub. + +`.github/workflows/naruon-supplier-quality-ci.yml` supplies Node 24, installs the +lockfile-pinned MIT pnpm package with npm integrity verification and lifecycle +hooks disabled, then runs all 97 cases. PR and protected-main path triggers cover +the suppliers, tests, integration lock, and Python lock. Native binary/package +files stay untracked. The gate uses the isolated Linux/x64 pool, read-only +permissions, no supplied secrets, same-repository PR admission, and private +Python/npm runtime directories. Product callers remain unchanged. + +The backend also stops restoring/saving a pip cache that its deliberate private +HOME and `PIP_NO_CACHE_DIR=1` policy never consumed. Dependency hash checks and +workload failure thresholds are unchanged. This removes useless cache traffic +without sharing dependency caches between trust boundaries. + +Local validation: all 97 cases pass with `GITHUB_ACTIONS=true` and warnings fatal +using the real lock-installed pnpm 11.5.3 and Node 24. The ordinary 86-case +Python-only cohort also runs without Node or pnpm on PATH. Actionlint passes +all four Naruon workflows. These receipts do not establish runner canary, +current-head remote gate success, approval, or protected integration. + +All seven moved native functions, including decorators and parametrizations, +were byte-compared against predecessor `e32ae0ac`; a source-hash contract retains +that comparison. The native case count is 11. The new native job excludes fork +PRs to preserve self-hosted admission; generic Python tests no longer imply +native integration evidence on any event. Effective required-check registration +and a successful execution of this new job remain unverified, so this change +is source preparation rather than completed runtime remediation. Its temporary +directory cleanup is not a security boundary against code executing in the same +guest: disposable isolated runners and verified reclamation remain prerequisites. diff --git a/integration/naruon/test_native_toolchain.py b/integration/naruon/test_native_toolchain.py new file mode 100644 index 0000000000..ecf6919bcd --- /dev/null +++ b/integration/naruon/test_native_toolchain.py @@ -0,0 +1,199 @@ +"""Native Node/pnpm integration, executed by naruon-supplier-quality-ci.""" + +import hashlib +import importlib.util +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +SPEC = importlib.util.spec_from_file_location( + "naruon_supplier_contract", ROOT / "tests/test_naruon_reusable_ci_contract.py" +) +CONTRACT = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(CONTRACT) +frontend_fixture = CONTRACT.frontend_fixture +shell = CONTRACT.shell +step_named = CONTRACT.step_named +executable = CONTRACT.executable + + +# Exact declarations from naruon@042b0c70531b229af3acbd0421a2f23098d848b3, +# frontend/scripts/full-product-ui-smoke.mjs (blob 008d612678f9497b865a296a79038c3e875d864c). +# The outer evidence runner verifies these byte slices against the complete Git blob. +FULL_PRODUCT_ARTIFACT_SOURCE = '''const DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-smoke"; +const RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-responsive-qa"; + +export function resolveFullProductScreenshotProfile(environment = process.env) { + return ( + environment.NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE ?? + environment.NARUON_FULL_PRODUCT_SCREENSHOT_DIR + ); +} + +function resolveFullProductArtifactPrefix(rawProfile) { + switch (rawProfile) { + case undefined: + case DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE: + return "naruon-full-product-smoke-"; + case RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE: + return "naruon-full-product-responsive-qa-"; + default: + throw new Error( + "NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE (or legacy NARUON_FULL_PRODUCT_SCREENSHOT_DIR) must select an approved artifact profile", + ); + } +} + +export async function createFullProductArtifactDirectory(rawProfile) { + const prefix = resolveFullProductArtifactPrefix(rawProfile); + return mkdtemp(path.join(tmpdir(), prefix)); +} +''' + + +def artifact_directory(env, cwd): + node = shutil.which("node") + assert node, "Node is required for exact-consumer characterization" + script = ( + 'import { mkdtemp } from "node:fs/promises";\n' + 'import { tmpdir } from "node:os";\n' + 'import path from "node:path";\n' + + FULL_PRODUCT_ARTIFACT_SOURCE + + 'console.log(await createFullProductArtifactDirectory(resolveFullProductScreenshotProfile()));\n' + ) + return subprocess.run( + [node, "--input-type=module", "-e", script], cwd=cwd, env=env, + capture_output=True, text=True, timeout=10, + ) + + +def test_frontend_emitted_profile_reaches_exact_consumer_private_mkdtemp(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + result = artifact_directory(env, checkout) + assert result.returncode == 0, result.stderr + artifact = Path(result.stdout.strip()) + assert artifact.parent == root / "tmp" + assert artifact.name.startswith("naruon-full-product-smoke-") + assert artifact.stat().st_mode & 0o777 == 0o700 + sentinel = artifact / "home.png" + sentinel.write_bytes(b"owned-artifact-control") + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0, result.stderr + assert not artifact.exists() + assert (shared / "package-lock.json").read_text() == "keep" + assert (runner / "unrelated").read_text() == "keep" + + +@pytest.mark.parametrize("profile,prefix", [ + (None, "naruon-full-product-smoke-"), + ("/tmp/naruon-full-product-smoke", "naruon-full-product-smoke-"), + ("/tmp/naruon-full-product-responsive-qa", "naruon-full-product-responsive-qa-"), + ("arbitrary-owned-path", None), ("", None), +]) +def test_exact_consumer_artifact_profile_controls(tmp_path, profile, prefix): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE", None) + env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_DIR", None) + if profile is not None: + env["NARUON_FULL_PRODUCT_SCREENSHOT_DIR"] = ( + str(root / "screenshots") if profile == "arbitrary-owned-path" else profile + ) + result = artifact_directory(env, checkout) + if prefix is None: + assert result.returncode == 1 + assert "must select an approved artifact profile" in result.stderr + assert list((root / "tmp").iterdir()) == [] + else: + assert result.returncode == 0, result.stderr + artifact = Path(result.stdout.strip()) + assert artifact.parent == root / "tmp" + assert artifact.name.startswith(prefix) + + +def pinned_pnpm_store(env, cwd): + pnpm = shutil.which("pnpm") + node = shutil.which("node") + assert pnpm and node, "Node and locally provisioned pnpm 11.5.3 are required (no download/skip)" + package_root = Path(pnpm).resolve().parents[1] + assert json.loads((package_root / "package.json").read_text())["version"] == "11.5.3" + assert hashlib.sha256((package_root / "dist/pnpm.mjs").read_bytes()).hexdigest() == ( + "db24b8f44a6438e100eab2e690464b971d35ee3c9af4638dcab3af7ee90c8cdd" + ) + native_env = {**env, "PATH": f"{Path(node).parent}:{env['PATH']}"} + version = subprocess.run( + [pnpm, "--version"], cwd=cwd, env=native_env, + capture_output=True, text=True, timeout=10, + ) + assert version.returncode == 0 and version.stdout.strip() == "11.5.3", version.stderr + result = subprocess.run( + [pnpm, "store", "path", "--silent"], cwd=cwd, env=native_env, + capture_output=True, text=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + assert not result.stderr + return Path(result.stdout.strip()) + + +@pytest.mark.parametrize("key,selected", [ + ("PNPM_CONFIG_STORE_DIR", True), ("pnpm_config_store_dir", True), + ("npm_config_store_dir", False), +]) +def test_pnpm11_native_store_env_prefix_controls(tmp_path, key, selected): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + env.pop("PNPM_CONFIG_STORE_DIR", None) + env.pop("npm_config_store_dir", None) + expected = checkout / ".naruon-pnpm-store" + env[key] = str(expected) + store = pinned_pnpm_store(env, checkout) + assert (expected in store.parents) is selected + if not selected: + assert root / "home" in store.parents + + +def test_corepack_shell_checks_native_pnpm_store_before_setup_node(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + native_pnpm = shutil.which("pnpm") + node = shutil.which("node") + assert native_pnpm and node + env["PATH"] = f"{tools}:{Path(node).parent}:/usr/bin:/bin" + env["NATIVE_PNPM"] = native_pnpm + # Provisioning is inert/offline; the workflow's pnpm commands are the real pinned CLI. + executable(tools / "corepack", '''if [ "$1" = enable ]; then + ln -s "$NATIVE_PNPM" "$NARUON_JOB_ROOT/bin/pnpm" +elif [ "$1" = prepare ]; then + [ "$2" = pnpm@11.5.3 ] +else + echo offline-provisioning-adapter +fi +''') + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == 0, result.stderr + store = pinned_pnpm_store(env, checkout) + assert checkout / ".naruon-pnpm-store" in store.parents + # Removing the supported key reproduces the old HOME store and the real shell refuses it. + env.pop("PNPM_CONFIG_STORE_DIR") + env["npm_config_store_dir"] = str(checkout / ".naruon-pnpm-store") + (root / "bin/pnpm").unlink() + result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) + assert result.returncode == 1 + assert "Refuse pnpm store outside checkout-local cache root" in result.stderr + + +def test_frontend_cache_store_survives_home_cleanup_for_setup_node_post(tmp_path): + checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) + store = pinned_pnpm_store(env, checkout) + assert checkout / ".naruon-pnpm-store" in store.parents + assert root not in store.parents + assert pinned_pnpm_store(env, checkout / "frontend") == store + store.mkdir(parents=True, exist_ok=True) + (store / "sentinel").write_text("cache-post-input") + result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) + assert result.returncode == 0 + assert not root.exists() + assert (store / "sentinel").read_text() == "cache-post-input" + + diff --git a/integration/naruon/toolchain/.gitignore b/integration/naruon/toolchain/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/integration/naruon/toolchain/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/integration/naruon/toolchain/package-lock.json b/integration/naruon/toolchain/package-lock.json new file mode 100644 index 0000000000..154f5a4d12 --- /dev/null +++ b/integration/naruon/toolchain/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "naruon-supplier-integration-toolchain", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "naruon-supplier-integration-toolchain", + "version": "1.0.0", + "dependencies": { + "pnpm": "11.5.3" + } + }, + "node_modules/pnpm": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.5.3.tgz", + "integrity": "sha512-esHJGTQcITo03A0Cr7cUPFwmrCbujEeC3uqCG4rGTSE0oIH9iUHa5uKbu0j1jfwrf7zuzMB8svCdIZ00Kklp7Q==", + "license": "MIT", + "bin": { + "pn": "bin/pnpm.mjs", + "pnpm": "bin/pnpm.mjs", + "pnpx": "bin/pnpx.mjs", + "pnx": "bin/pnpx.mjs" + }, + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + } + } +} diff --git a/integration/naruon/toolchain/package.json b/integration/naruon/toolchain/package.json new file mode 100644 index 0000000000..e1003e43fb --- /dev/null +++ b/integration/naruon/toolchain/package.json @@ -0,0 +1 @@ +{"name":"naruon-supplier-integration-toolchain","private":true,"version":"1.0.0","dependencies":{"pnpm":"11.5.3"}} diff --git a/tests/test_naruon_reusable_ci_contract.py b/tests/test_naruon_reusable_ci_contract.py index 5bb4587124..a13a0074a6 100644 --- a/tests/test_naruon_reusable_ci_contract.py +++ b/tests/test_naruon_reusable_ci_contract.py @@ -2,8 +2,6 @@ from __future__ import annotations -import hashlib -import json import re import shutil import subprocess @@ -298,99 +296,6 @@ def frontend_fixture(tmp_path): return checkout, env, tools, root, shared, runner -# Exact declarations from naruon@042b0c70531b229af3acbd0421a2f23098d848b3, -# frontend/scripts/full-product-ui-smoke.mjs (blob 008d612678f9497b865a296a79038c3e875d864c). -# The outer evidence runner verifies these byte slices against the complete Git blob. -FULL_PRODUCT_ARTIFACT_SOURCE = '''const DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-smoke"; -const RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE = "/tmp/naruon-full-product-responsive-qa"; - -export function resolveFullProductScreenshotProfile(environment = process.env) { - return ( - environment.NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE ?? - environment.NARUON_FULL_PRODUCT_SCREENSHOT_DIR - ); -} - -function resolveFullProductArtifactPrefix(rawProfile) { - switch (rawProfile) { - case undefined: - case DEFAULT_FULL_PRODUCT_SCREENSHOT_PROFILE: - return "naruon-full-product-smoke-"; - case RESPONSIVE_FULL_PRODUCT_SCREENSHOT_PROFILE: - return "naruon-full-product-responsive-qa-"; - default: - throw new Error( - "NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE (or legacy NARUON_FULL_PRODUCT_SCREENSHOT_DIR) must select an approved artifact profile", - ); - } -} - -export async function createFullProductArtifactDirectory(rawProfile) { - const prefix = resolveFullProductArtifactPrefix(rawProfile); - return mkdtemp(path.join(tmpdir(), prefix)); -} -''' - - -def artifact_directory(env, cwd): - node = shutil.which("node") - assert node, "Node is required for exact-consumer characterization" - script = ( - 'import { mkdtemp } from "node:fs/promises";\n' - 'import { tmpdir } from "node:os";\n' - 'import path from "node:path";\n' - + FULL_PRODUCT_ARTIFACT_SOURCE - + 'console.log(await createFullProductArtifactDirectory(resolveFullProductScreenshotProfile()));\n' - ) - return subprocess.run( - [node, "--input-type=module", "-e", script], cwd=cwd, env=env, - capture_output=True, text=True, timeout=10, - ) - - -def test_frontend_emitted_profile_reaches_exact_consumer_private_mkdtemp(tmp_path): - checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) - result = artifact_directory(env, checkout) - assert result.returncode == 0, result.stderr - artifact = Path(result.stdout.strip()) - assert artifact.parent == root / "tmp" - assert artifact.name.startswith("naruon-full-product-smoke-") - assert artifact.stat().st_mode & 0o777 == 0o700 - sentinel = artifact / "home.png" - sentinel.write_bytes(b"owned-artifact-control") - result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) - assert result.returncode == 0, result.stderr - assert not artifact.exists() - assert (shared / "package-lock.json").read_text() == "keep" - assert (runner / "unrelated").read_text() == "keep" - - -@pytest.mark.parametrize("profile,prefix", [ - (None, "naruon-full-product-smoke-"), - ("/tmp/naruon-full-product-smoke", "naruon-full-product-smoke-"), - ("/tmp/naruon-full-product-responsive-qa", "naruon-full-product-responsive-qa-"), - ("arbitrary-owned-path", None), ("", None), -]) -def test_exact_consumer_artifact_profile_controls(tmp_path, profile, prefix): - checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) - env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE", None) - env.pop("NARUON_FULL_PRODUCT_SCREENSHOT_DIR", None) - if profile is not None: - env["NARUON_FULL_PRODUCT_SCREENSHOT_DIR"] = ( - str(root / "screenshots") if profile == "arbitrary-owned-path" else profile - ) - result = artifact_directory(env, checkout) - if prefix is None: - assert result.returncode == 1 - assert "must select an approved artifact profile" in result.stderr - assert list((root / "tmp").iterdir()) == [] - else: - assert result.returncode == 0, result.stderr - artifact = Path(result.stdout.strip()) - assert artifact.parent == root / "tmp" - assert artifact.name.startswith(prefix) - - def test_frontend_owned_home_build_screenshot_and_cleanup(tmp_path): checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) executable(tools / "pnpm", 'printf "%s\\n" "$HOME $TMPDIR $NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE"\n') @@ -487,89 +392,6 @@ def test_node_cache_prerequisites_are_causally_ordered(): assert all(not s.get("continue-on-error") for s in steps) -def pinned_pnpm_store(env, cwd): - pnpm = shutil.which("pnpm") - node = shutil.which("node") - assert pnpm and node, "Node and locally provisioned pnpm 11.5.3 are required (no download/skip)" - package_root = Path(pnpm).resolve().parents[1] - assert json.loads((package_root / "package.json").read_text())["version"] == "11.5.3" - assert hashlib.sha256((package_root / "dist/pnpm.mjs").read_bytes()).hexdigest() == ( - "db24b8f44a6438e100eab2e690464b971d35ee3c9af4638dcab3af7ee90c8cdd" - ) - native_env = {**env, "PATH": f"{Path(node).parent}:{env['PATH']}"} - version = subprocess.run( - [pnpm, "--version"], cwd=cwd, env=native_env, - capture_output=True, text=True, timeout=10, - ) - assert version.returncode == 0 and version.stdout.strip() == "11.5.3", version.stderr - result = subprocess.run( - [pnpm, "store", "path", "--silent"], cwd=cwd, env=native_env, - capture_output=True, text=True, timeout=10, - ) - assert result.returncode == 0, result.stderr - assert not result.stderr - return Path(result.stdout.strip()) - - -@pytest.mark.parametrize("key,selected", [ - ("PNPM_CONFIG_STORE_DIR", True), ("pnpm_config_store_dir", True), - ("npm_config_store_dir", False), -]) -def test_pnpm11_native_store_env_prefix_controls(tmp_path, key, selected): - checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) - env.pop("PNPM_CONFIG_STORE_DIR", None) - env.pop("npm_config_store_dir", None) - expected = checkout / ".naruon-pnpm-store" - env[key] = str(expected) - store = pinned_pnpm_store(env, checkout) - assert (expected in store.parents) is selected - if not selected: - assert root / "home" in store.parents - - -def test_corepack_shell_checks_native_pnpm_store_before_setup_node(tmp_path): - checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) - native_pnpm = shutil.which("pnpm") - node = shutil.which("node") - assert native_pnpm and node - env["PATH"] = f"{tools}:{Path(node).parent}:/usr/bin:/bin" - env["NATIVE_PNPM"] = native_pnpm - # Provisioning is inert/offline; the workflow's pnpm commands are the real pinned CLI. - executable(tools / "corepack", '''if [ "$1" = enable ]; then - ln -s "$NATIVE_PNPM" "$NARUON_JOB_ROOT/bin/pnpm" -elif [ "$1" = prepare ]; then - [ "$2" = pnpm@11.5.3 ] -else - echo offline-provisioning-adapter -fi -''') - result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) - assert result.returncode == 0, result.stderr - store = pinned_pnpm_store(env, checkout) - assert checkout / ".naruon-pnpm-store" in store.parents - # Removing the supported key reproduces the old HOME store and the real shell refuses it. - env.pop("PNPM_CONFIG_STORE_DIR") - env["npm_config_store_dir"] = str(checkout / ".naruon-pnpm-store") - (root / "bin/pnpm").unlink() - result = shell(step_named("frontend", "Enable Corepack pnpm")["run"], checkout / "frontend", env) - assert result.returncode == 1 - assert "Refuse pnpm store outside checkout-local cache root" in result.stderr - - -def test_frontend_cache_store_survives_home_cleanup_for_setup_node_post(tmp_path): - checkout, env, tools, root, shared, runner = frontend_fixture(tmp_path) - store = pinned_pnpm_store(env, checkout) - assert checkout / ".naruon-pnpm-store" in store.parents - assert root not in store.parents - assert pinned_pnpm_store(env, checkout / "frontend") == store - store.mkdir(parents=True, exist_ok=True) - (store / "sentinel").write_text("cache-post-input") - result = shell(step_named("frontend", "Clean job-private runtime")["run"], checkout, env) - assert result.returncode == 0 - assert not root.exists() - assert (store / "sentinel").read_text() == "cache-post-input" - - @pytest.mark.parametrize("name,title", [ ("backend", "Verify backend in a job-private environment"), ("bandit", "Run Bandit in a job-private environment"), diff --git a/tests/test_naruon_supplier_quality_workflow.py b/tests/test_naruon_supplier_quality_workflow.py new file mode 100644 index 0000000000..316609bd6b --- /dev/null +++ b/tests/test_naruon_supplier_quality_workflow.py @@ -0,0 +1,62 @@ +"""Native toolchain provisioning and isolated backend cache contracts.""" + +import ast +import hashlib +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_backend_does_not_restore_an_unused_shared_pip_cache(): + workflow = yaml.load((ROOT / '.github/workflows/naruon-backend-ci.yml').read_text(), Loader=yaml.BaseLoader) + setup = next(s for s in workflow['jobs']['backend']['steps'] if s.get('name') == 'Set up Python') + assert setup['with'] == {'python-version': '3.14'} + assert 'PIP_NO_CACHE_DIR=1' in (ROOT / '.github/workflows/naruon-backend-ci.yml').read_text() + + +def test_native_contracts_have_a_triggered_pinned_toolchain_gate(): + path = ROOT / '.github/workflows/naruon-supplier-quality-ci.yml' + workflow = yaml.load(path.read_text(), Loader=yaml.BaseLoader) + for event in ('push', 'pull_request'): + assert '.github/workflows/naruon-*-ci.yml' in workflow['on'][event]['paths'] + assert 'integration/naruon/**' in workflow['on'][event]['paths'] + assert 'tests/test_naruon*.py' in workflow['on'][event]['paths'] + assert workflow['permissions'] == {'contents': 'read'} + job = workflow['jobs']['supplier-contracts'] + assert job['runs-on'] == {'group': 'CWL CI isolated', 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated']} + steps = job['steps'] + names = [s['name'] for s in steps] + assert names.index('Set up Node') < names.index('Install locked pnpm without lifecycle hooks') < names.index('Run complete supplier contracts and native integration') + node = next(s for s in steps if s['name'] == 'Set up Node') + assert node['with'] == {'node-version': '24', 'package-manager-cache': 'false'} + install = next(s for s in steps if s['name'] == 'Install locked pnpm without lifecycle hooks')['run'] + assert 'npm ci --ignore-scripts' in install + command = next(s for s in steps if s['name'] == 'Run complete supplier contracts and native integration')['run'] + assert 'tests/test_naruon_reusable_ci_contract.py' in command + assert 'integration/naruon/test_native_toolchain.py' in command + assert 'tests/test_naruon_supplier_quality_workflow.py' in command + assert not any('continue-on-error' in s for s in steps) + + +def test_native_toolchain_keeps_all_original_integration_cases(): + native = (ROOT / 'integration/naruon/test_native_toolchain.py').read_text() + for name in ('frontend_emitted_profile_reaches_exact_consumer_private_mkdtemp', 'exact_consumer_artifact_profile_controls', 'pnpm11_native_store_env_prefix_controls', 'corepack_shell_checks_native_pnpm_store_before_setup_node', 'frontend_cache_store_survives_home_cleanup_for_setup_node_post'): + assert 'def test_' + name in native + assert 'pytest.skip' not in native + assert 'db24b8f44a6438e100eab2e690464b971d35ee3c9af4638dcab3af7ee90c8cdd' in native + + +def test_native_functions_and_parametrizations_match_predecessor_bytes(): + source = (ROOT / 'integration/naruon/test_native_toolchain.py').read_text() + lines = source.splitlines(keepends=True) + expected = {'artifact_directory': '49c57e87a528effaffaf1c2db129c959403a70233a124219974ec52ca93e397e', 'test_frontend_emitted_profile_reaches_exact_consumer_private_mkdtemp': '2e478ff8285cb39cfb2f62d2cdb9fb53009f944edeba6049f44c445f130bb75c', 'test_exact_consumer_artifact_profile_controls': '44c72077017bbcf0e688cba16f6bf8974c1a876c8e7121b1e519c6c1e73fb380', 'pinned_pnpm_store': '2318e0489995c1fafe22bbefd3576322150e33bd9225be63d5a036ae1004f884', 'test_pnpm11_native_store_env_prefix_controls': '4f6c9afe608b7b9cd7ae0359194936705e5c50d049c8d93d67c6c2d0990d1096', 'test_corepack_shell_checks_native_pnpm_store_before_setup_node': 'dbb4c9b00d5928b015c7d09f35de4eb7568a81f2e84be4f8465c3fdf1ef75d7b', 'test_frontend_cache_store_survives_home_cleanup_for_setup_node_post': '6fd85074f0d5e272d5a704afc41b63a23f3172e0252ad901b614b193e900083f'} + actual = {} + for node in ast.parse(source).body: + if isinstance(node, ast.FunctionDef): + start = min([node.lineno] + [d.lineno for d in node.decorator_list]) - 1 + original = ''.join(lines[start:node.end_lineno]) + actual[node.name] = hashlib.sha256(original.encode()).hexdigest() + assert actual == expected