fix: require canonical governance and normal-merge authority #8811
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| verify: | |
| name: verify | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| env: | |
| GIT_CONFIG_COUNT: "1" | |
| GIT_CONFIG_KEY_0: init.defaultBranch | |
| GIT_CONFIG_VALUE_0: main | |
| steps: | |
| - name: checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: verify exact checkout | |
| shell: bash | |
| env: | |
| NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Invalid expected head SHA.\n' | |
| exit 1 | |
| fi | |
| test "$(git rev-parse HEAD)" = "$NOEMA_EXPECTED_HEAD_SHA" | |
| - name: setup node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "24.19.0" | |
| cache: npm | |
| - name: verify package-manager toolchain | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test "$(node --version)" = "v24.19.0" | |
| test "$(npm --version)" = "11.17.0" | |
| - name: verify live pull-request base before lockfile control | |
| if: github.event_name == 'pull_request' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NOEMA_PR_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Invalid expected head SHA.\n' | |
| exit 1 | |
| fi | |
| if [ -z "$NOEMA_PR_BASE_REF" ]; then | |
| printf '::error::Pull-request base ref is unavailable.\n' | |
| exit 1 | |
| fi | |
| repository_name="${GITHUB_REPOSITORY#*/}" | |
| resolve_live_base_sha() { | |
| local output="" | |
| local graphql_transient_exhausted="false" | |
| for attempt in 1 2 3; do | |
| if output="$( | |
| gh api graphql \ | |
| -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ | |
| -F owner="$GITHUB_REPOSITORY_OWNER" \ | |
| -F name="$repository_name" \ | |
| -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ | |
| --jq '.data.repository.ref.target.oid' \ | |
| 2>&1 | |
| )"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$'; then | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$attempt" | |
| continue | |
| fi | |
| graphql_transient_exhausted="true" | |
| break | |
| fi | |
| printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2 | |
| return 1 | |
| done | |
| if [ "$graphql_transient_exhausted" != "true" ]; then | |
| return 1 | |
| fi | |
| if output="$( | |
| gh api --method GET "repos/${GITHUB_REPOSITORY}/git/ref/heads/${NOEMA_PR_BASE_REF}" \ | |
| --jq '.object.sha' \ | |
| 2>&1 | |
| )"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| printf '::error::Live pull-request base REST fallback failed.\n' >&2 | |
| return 1 | |
| } | |
| live_base_sha="$(resolve_live_base_sha)" | |
| if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n' | |
| exit 1 | |
| fi | |
| if ! git merge-base --is-ancestor "$live_base_sha" "$NOEMA_EXPECTED_HEAD_SHA"; then | |
| printf '::error::Pull-request head does not contain the current live base %s.\n' "$live_base_sha" | |
| exit 1 | |
| fi | |
| printf 'NOEMA_LIVE_BASE_SHA=%s\n' "$live_base_sha" >> "$GITHUB_ENV" | |
| - name: verify lockfile change control | |
| if: github.event_name == 'pull_request' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$NOEMA_LIVE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Invalid live pull-request base SHA.\n' | |
| exit 1 | |
| fi | |
| base_lock="$RUNNER_TEMP/noema-package-lock-base.json" | |
| git show "${NOEMA_LIVE_BASE_SHA}:package-lock.json" >"$base_lock" | |
| NOEMA_LOCKFILE_BASE_PATH="$base_lock" \ | |
| NOEMA_LOCKFILE_BASE_SHA="$NOEMA_LIVE_BASE_SHA" \ | |
| node --input-type=module <<'NODE' | |
| import { runLockfileChangeControl } from "./scripts/lockfile-change-control.mjs"; | |
| const result = runLockfileChangeControl(); | |
| if (!result.passed) { | |
| for (const failure of result.failures) { | |
| console.error(`::error::${failure}`); | |
| } | |
| process.exit(1); | |
| } | |
| console.log(`Lockfile change control passed for ${result.changedPackages.length} changed package node(s).`); | |
| NODE | |
| - name: regenerate canonical lockfile in disposable workspace | |
| id: regenerate_lockfile | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" | |
| rm -rf "$regeneration_root" | |
| mkdir -p "$regeneration_root" | |
| cp package.json package-lock.json .npmrc "$regeneration_root/" | |
| ( | |
| cd "$regeneration_root" | |
| npm install \ | |
| --package-lock-only \ | |
| --ignore-scripts \ | |
| --no-audit \ | |
| --no-fund \ | |
| --legacy-peer-deps=false \ | |
| --install-links=false | |
| ) | |
| cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" | |
| if cmp --silent package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then | |
| printf 'match=true\n' >> "$GITHUB_OUTPUT" | |
| else | |
| printf 'match=false\n' >> "$GITHUB_OUTPUT" | |
| diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ | |
| > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true | |
| fi | |
| - name: upload regenerated lockfile evidence | |
| if: ${{ !cancelled() && steps.regenerate_lockfile.outcome != 'skipped' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.sha }} | |
| path: | | |
| ${{ runner.temp }}/noema-package-lock-regenerated.json | |
| ${{ runner.temp }}/noema-package-lock-regeneration.diff | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: require committed lockfile reproducibility | |
| if: steps.regenerate_lockfile.outputs.match != 'true' | |
| shell: bash | |
| run: | | |
| printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' | |
| exit 1 | |
| - name: install | |
| run: npm ci --legacy-peer-deps=false --install-links=false | |
| - name: release typecheck | |
| run: npm run typecheck | |
| - name: release tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| log="$RUNNER_TEMP/noema-release-tests.log" | |
| rm -f coverage/coverage-final.json | |
| if npm run test -- --reporter=dot --coverage.reporter=json --coverage.reporter=text >"$log" 2>&1; then | |
| exit 0 | |
| fi | |
| tail -c 32768 "$log" | tail -n 160 | |
| if [ -f coverage/coverage-final.json ]; then | |
| node --input-type=module <<'NODE' | |
| import { readFileSync } from "node:fs"; | |
| import { relative } from "node:path"; | |
| const report = JSON.parse(readFileSync("coverage/coverage-final.json", "utf8")); | |
| let diagnosticCount = 0; | |
| outer: for (const file of Object.keys(report).sort()) { | |
| const coverage = report[file]; | |
| const statementMap = coverage?.statementMap ?? {}; | |
| for (const [statementId, hits] of Object.entries(coverage?.s ?? {})) { | |
| if (hits !== 0) continue; | |
| const location = statementMap[statementId]; | |
| if (!location?.start) continue; | |
| const path = relative(process.cwd(), file) || file; | |
| console.error( | |
| `Uncovered statement ${path}:${location.start.line}:${location.start.column}`, | |
| ); | |
| diagnosticCount += 1; | |
| if (diagnosticCount >= 64) break outer; | |
| } | |
| const branchMap = coverage?.branchMap ?? {}; | |
| for (const [branchId, hits] of Object.entries(coverage?.b ?? {})) { | |
| if (!Array.isArray(hits)) continue; | |
| const branch = branchMap[branchId]; | |
| const locations = branch?.locations ?? []; | |
| for (const [armIndex, armHits] of hits.entries()) { | |
| if (armHits !== 0) continue; | |
| const location = locations[armIndex] ?? branch?.loc; | |
| const line = branch?.line ?? branch?.loc?.start?.line ?? location?.line ?? location?.start?.line; | |
| if (!Number.isInteger(line)) continue; | |
| const column = branch?.loc?.start?.column ?? location?.start?.column ?? 0; | |
| const path = relative(process.cwd(), file) || file; | |
| console.error( | |
| `Uncovered branch ${path}:${line}:${column} branch=${branchId} arm=${armIndex}`, | |
| ); | |
| diagnosticCount += 1; | |
| if (diagnosticCount >= 64) break outer; | |
| } | |
| } | |
| } | |
| NODE | |
| fi | |
| exit 1 | |
| - name: release security scan | |
| run: npm run security:scan | |
| - name: release KPI verification | |
| run: npm run kpi:verify | |
| - name: release dependency license inventory | |
| run: npm run release:dependency-license-inventory | |
| - name: release acquisition manifest | |
| run: npm run acquisition:manifest | |
| - name: release acquisition integrity | |
| run: npm run acquisition:integrity | |
| - name: refuse pull-request base drift after verification | |
| if: github.event_name == 'pull_request' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NOEMA_PR_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$NOEMA_LIVE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Initial live pull-request base SHA is unavailable.\n' | |
| exit 1 | |
| fi | |
| if [ -z "$NOEMA_PR_BASE_REF" ]; then | |
| printf '::error::Pull-request base ref is unavailable.\n' | |
| exit 1 | |
| fi | |
| repository_name="${GITHUB_REPOSITORY#*/}" | |
| resolve_live_base_sha() { | |
| local output="" | |
| local graphql_transient_exhausted="false" | |
| for attempt in 1 2 3; do | |
| if output="$( | |
| gh api graphql \ | |
| -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ | |
| -F owner="$GITHUB_REPOSITORY_OWNER" \ | |
| -F name="$repository_name" \ | |
| -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ | |
| --jq '.data.repository.ref.target.oid' \ | |
| 2>&1 | |
| )"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$'; then | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$attempt" | |
| continue | |
| fi | |
| graphql_transient_exhausted="true" | |
| break | |
| fi | |
| printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2 | |
| return 1 | |
| done | |
| if [ "$graphql_transient_exhausted" != "true" ]; then | |
| return 1 | |
| fi | |
| if output="$( | |
| gh api --method GET "repos/${GITHUB_REPOSITORY}/git/ref/heads/${NOEMA_PR_BASE_REF}" \ | |
| --jq '.object.sha' \ | |
| 2>&1 | |
| )"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| printf '::error::Live pull-request base REST fallback failed.\n' >&2 | |
| return 1 | |
| } | |
| live_base_sha="$(resolve_live_base_sha)" | |
| if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n' | |
| exit 1 | |
| fi | |
| if [ "$live_base_sha" != "$NOEMA_LIVE_BASE_SHA" ]; then | |
| printf '::error::Pull-request base branch advanced during verification from %s to %s.\n' \ | |
| "$NOEMA_LIVE_BASE_SHA" "$live_base_sha" | |
| exit 1 | |
| fi | |
| test "$live_base_sha" = "$NOEMA_LIVE_BASE_SHA" |