GitNexus/.github/workflows/ci-scope-parity.yml
Gergo Magyar 1688ef2ab3 fix(review): address all residual ce-code-review findings
Five reviewers (maintainability, reliability, testing, agent-native,
adversarial) flagged that ci-report.yml's sticky PR comment ignores
scope-parity entirely. Add scope_parity_result to the artifact read
step, render it in the Pipeline Status table, and include it in the
OVERALL pass/fail condition. Skipped is now visualised distinctly from
cancelled (skipped → ⏭, cancelled → 🛑).

Add `.github/scripts/check-language-filters.py` and wire it into
ci-quality.yml's workflow-convention job. The script parses
MIGRATED_LANGUAGES from registry-primary-flag.ts and asserts:

  1. ci.yml has a per-language filter block for every migrated slug
  2. ci.yml's FULL_LANG_LIST env var matches
  3. ci-scope-parity.yml's changed-languages default matches

This catches the most common drift mode: adding a language to
MIGRATED_LANGUAGES without updating the two YAML hardcoded lists,
which would silently make scope-parity skip the new language. Negative-
tested with a synthetic flag.ts that adds Kotlin without updating the
filter blocks — script correctly fails with all three location-
specific errors.

Move the resolver test-file existence check from the parity matrix
job into the discover job, iterating the full MIGRATED_LANGUAGES set
rather than just the filtered matrix. A missing test file now fails
the gate even on docs-only PRs that prune the matrix to empty,
holding the invariant regardless of which slugs were filtered.

Dismissed (verified out-of-scope): the performance reviewer's
suggestion to replace setup-gitnexus with a lighter setup in the
discover job. gitnexus-shared/package.json exports only ./dist/, so
`tsx` cannot resolve `import 'gitnexus-shared'` without the build
step. The optimisation would require modifying gitnexus-shared's
exports field to expose ./src/, which is a separate refactor.
2026-05-14 17:34:52 +01:00

206 lines
9.9 KiB
YAML

name: Scope Resolution Parity
# Reusable workflow — called from ci.yml. Does NOT declare concurrency;
# it inherits the caller's concurrency group per the convention documented
# in CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
#
# ── Purpose (RFC #909 Ring 3, §6.4 "Observability gates") ──────────────
# For every language in `MIGRATED_LANGUAGES` (exported from
# `gitnexus/src/core/ingestion/registry-primary-flag.ts`), run the
# resolver integration test at `test/integration/resolvers/<slug>.test.ts`
# TWICE on every PR:
#
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
# broken the old path while migrating). Known legacy gaps may be skipped
# through the resolver test helper's expected-failure list.
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
# new path carries the same behavior — the parity gate).
#
# BOTH must pass. The source of truth is the TypeScript constant — adding
# a language to that `Set` is the ONLY contributor action; CI auto-
# discovers it, runs parity, and the language's default production path
# flips to registry-primary in the same change.
#
# ── Path-aware matrix pruning ─────────────────────────────────────────
# `ci.yml` computes change detection ONCE per run and passes:
# - `shared-resolution-changed` — when 'true', shared ingestion code
# changed and ALL migrated languages must run (any could regress).
# - `changed-languages` — JSON array of language slugs whose source or
# resolver-test files changed (e.g. `["python","go"]`).
#
# The discover job intersects MIGRATED_LANGUAGES with `changed-languages`
# unless `shared-resolution-changed=true`, in which case the full matrix
# runs. On non-PR callers (release-candidate.yml) both inputs default to
# the full set, so the full matrix runs there too.
#
# When the filtered matrix is empty, the matrix job is skipped via the
# `if: needs.discover.outputs.languages != '[]'` guard. The outer workflow
# conclusion stays `success` so the `ci-status` aggregator in ci.yml
# passes the scope-parity gate.
on:
workflow_call:
inputs:
shared-resolution-changed:
description: >-
When `'true'`, shared ingestion/resolution code changed and the
full migrated-languages matrix must run. Conservatively defaults
to `'true'` so any caller that omits the input gets the full
battery.
required: false
type: string
default: 'true'
changed-languages:
description: >-
JSON array of migrated language slugs whose source or resolver
test files changed on this PR (e.g. `["python","go"]`). Used to
prune the parity matrix when `shared-resolution-changed` is
`'false'`. Defaults to the full set so omitting the input runs
everything.
required: false
type: string
default: '["python","csharp","typescript","go","c","cpp","php"]'
permissions:
contents: read
jobs:
discover:
name: Discover migrated languages
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
languages: ${{ steps.read.outputs.languages }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-gitnexus
- name: Extract MIGRATED_LANGUAGES and filter by changed paths
id: read
shell: bash
working-directory: gitnexus
env:
SHARED_CHANGED: ${{ inputs.shared-resolution-changed }}
CHANGED_LANGS: ${{ inputs.changed-languages }}
run: |
set -euo pipefail
# `tsx` evaluates the TS source directly (no build step), imports
# the exported `Set`, and emits a GH-Actions-friendly JSON matrix
# array: [{slug, envvar}, ...].
ALL_LANGS=$(npx tsx scripts/ci-list-migrated-languages.ts)
# Validate the discovery script's output is a well-formed JSON
# array. A crash or schema regression here would otherwise
# propagate to `fromJSON('')` at matrix expansion time, where
# the failure surface is much harder to read.
if ! printf '%s' "$ALL_LANGS" | jq -e 'type == "array"' >/dev/null; then
echo "::error::ci-list-migrated-languages.ts produced non-array output: $ALL_LANGS"
exit 1
fi
ALL_COUNT=$(printf '%s' "$ALL_LANGS" | jq 'length')
echo "MIGRATED_LANGUAGES: $ALL_LANGS ($ALL_COUNT entries)"
# Shared-resolution change forces the full matrix — any change
# to shared ingestion code could regress any language's parity.
if [ "${SHARED_CHANGED:-true}" = "true" ]; then
LANGS="$ALL_LANGS"
REASON="shared-resolution changed → running full matrix"
else
# Validate the caller-supplied CHANGED_LANGS is a JSON array
# of strings. Malformed input would otherwise crash `jq
# --argjson` with a hard-to-diagnose parse error inside the
# filter pipeline.
if ! printf '%s' "${CHANGED_LANGS:-[]}" | jq -e 'type == "array" and all(type == "string")' >/dev/null; then
echo "::error::changed-languages input is not a JSON array of strings: ${CHANGED_LANGS:-<unset>}"
exit 1
fi
# Intersect MIGRATED_LANGUAGES with the changed-languages list.
# `IN(.slug; $changed[])` is strict equality membership — using
# `inside()` here would substring-match (`"c"` would match
# `"cpp"` because the `c` string is a prefix), which would
# over-trigger the matrix. The empty-list case is handled by
# the parity job's `if: != '[]'` guard.
LANGS=$(jq -c --argjson changed "${CHANGED_LANGS:-[]}" \
'[.[] | select(IN(.slug; $changed[]))]' <<< "$ALL_LANGS")
REASON="pruned by changed-languages: $CHANGED_LANGS"
fi
echo "languages=$LANGS" >> "$GITHUB_OUTPUT"
COUNT=$(printf '%s' "$LANGS" | jq 'length')
echo "Parity matrix entries: $COUNT ($REASON)"
# Resolver test-file existence is verified against the FULL
# MIGRATED_LANGUAGES set (ALL_LANGS), not just the filtered
# matrix. This catches a missing test file even on a docs-only
# PR that prunes the matrix to empty — otherwise the invariant
# could only fail on a PR that happens to touch the affected
# language. The check runs from repo root because the test
# paths are relative to `gitnexus/`.
while IFS= read -r slug; do
TEST_FILE="test/integration/resolvers/${slug}.test.ts"
if [[ ! -f "$TEST_FILE" ]]; then
echo "::error title=Missing resolver test::Expected gitnexus/$TEST_FILE for migrated language '$slug'. Either fix the slug, add the test file, or remove the language from MIGRATED_LANGUAGES."
exit 1
fi
done < <(printf '%s' "$ALL_LANGS" | jq -r '.[].slug')
# Job-summary line so reviewers and agents can see which
# languages actually ran without scraping logs.
{
echo "### Scope-parity matrix"
echo ""
echo "- **Entries:** $COUNT of $ALL_COUNT migrated languages"
echo "- **Reason:** $REASON"
if [ "$COUNT" -gt 0 ]; then
SLUGS=$(printf '%s' "$LANGS" | jq -r 'map(.slug) | join(", ")')
echo "- **Slugs:** $SLUGS"
fi
} >> "$GITHUB_STEP_SUMMARY"
parity:
name: ${{ matrix.lang.slug }} parity
needs: discover
# Empty-matrix guard. When the filter produced no entries, the matrix
# job MUST skip — `fromJSON('[]')` in `strategy.matrix` would otherwise
# crash with "Matrix vector does not contain any values"
# (community#27096). Comparing the JSON string against `'[]'` is the
# idiomatic, documented pattern for this guard.
if: needs.discover.outputs.languages != '[]'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
# One language failing must not abort the others — we want the full
# parity matrix result on a single CI run so a reviewer sees every
# regression at once rather than one-at-a-time.
fail-fast: false
matrix:
lang: ${{ fromJSON(needs.discover.outputs.languages) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
# Resolver test-file existence is now validated up front in the
# `discover` job against the full MIGRATED_LANGUAGES set, so a
# missing file fails before any matrix runner spins up — and the
# invariant holds even on docs-only PRs that prune the matrix to
# empty.
- name: Resolver tests — legacy DAG (REGISTRY_PRIMARY_${{ matrix.lang.envvar }}=0)
shell: bash
working-directory: gitnexus
env:
FLAG_NAME: REGISTRY_PRIMARY_${{ matrix.lang.envvar }}
# Explicitly force the flag to `0` even though it also defaults to
# `MIGRATED_LANGUAGES.has(lang)` — once a language is in the set,
# the default flips to registry-primary, so an unset env var would
# silently re-run the same path as step #2. `env FOO=0 cmd` spawns
# `cmd` with the override scoped to just this invocation.
run: env "$FLAG_NAME=0" npx vitest run "test/integration/resolvers/${{ matrix.lang.slug }}.test.ts"
- name: Resolver tests — registry-primary (REGISTRY_PRIMARY_${{ matrix.lang.envvar }}=1)
shell: bash
working-directory: gitnexus
env:
FLAG_NAME: REGISTRY_PRIMARY_${{ matrix.lang.envvar }}
run: env "$FLAG_NAME=1" npx vitest run "test/integration/resolvers/${{ matrix.lang.slug }}.test.ts"