mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
ci(scope-resolution): automatic parity gate driven by MIGRATED_LANGUAGES
Adds the Ring 3 parity gate the RFC §6.4 requires: when a language's
scope-resolution migration is marked complete, CI runs its resolver
integration test twice on every PR (once with the legacy DAG, once with
the registry-primary path) and both must pass.
The "is this language migrated" signal is a single TypeScript constant:
// gitnexus/src/core/ingestion/registry-primary-flag.ts
export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> =
new Set([ /* SupportedLanguages.Python when ready */ ]);
Adding a language here has three simultaneous effects:
1. `isRegistryPrimary(lang)` defaults to true for that language in
production (env-var override still wins if set explicitly).
2. `.github/workflows/ci-scope-parity.yml` auto-discovers the set via
`npx tsx scripts/ci-list-migrated-languages.ts`, builds a parity
matrix, and runs:
- `REGISTRY_PRIMARY_<LANG>=0 npx vitest run resolvers/<slug>.test.ts`
- `REGISTRY_PRIMARY_<LANG>=1 npx vitest run resolvers/<slug>.test.ts`
Both legs must pass for the job to succeed.
3. Legacy-path gating in call-processor.ts / import-processor.ts kicks
in automatically through the same `isRegistryPrimary` lookup.
No JSON registry, no manual workflow edit, no second source of truth —
contributors update the Set and CI picks it up. Empty Set = parity job
is a skipped matrix (workflow still reports success).
The new `scope-parity` reusable workflow is added to ci.yml's `needs`
graph and ci-status gate. Its result must be `success` (skipped would
mean upstream discover job failed and should block).
Validation (with empty MIGRATED_LANGUAGES set):
- flag OFF: 191/191 pass (no behavior change)
- flag ON (manual REGISTRY_PRIMARY_PYTHON=1): 82 fails = baseline exact match
- `npx tsc --noEmit`: clean
- concurrency-convention script: pass
- tsx discovery script: emits `[]` correctly
This commit is contained in:
parent
3b55d696a9
commit
6a1b4227eb
4 changed files with 200 additions and 12 deletions
109
.github/workflows/ci-scope-parity.yml
vendored
Normal file
109
.github/workflows/ci-scope-parity.yml
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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).
|
||||
# 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.
|
||||
#
|
||||
# When the set is empty (e.g. mid-Ring-3 for every language), the parity
|
||||
# matrix is skipped and the workflow reports success — no-op until a
|
||||
# language is explicitly claimed migrated.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
name: Discover migrated languages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
languages: ${{ steps.read.outputs.languages }}
|
||||
has-any: ${{ steps.read.outputs.has-any }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
|
||||
- name: Extract MIGRATED_LANGUAGES from registry-primary-flag.ts
|
||||
id: read
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
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.
|
||||
LANGS=$(npx tsx scripts/ci-list-migrated-languages.ts)
|
||||
COUNT=$(printf '%s' "$LANGS" | jq 'length')
|
||||
HAS_ANY="false"
|
||||
if [[ "$COUNT" -gt 0 ]]; then HAS_ANY="true"; fi
|
||||
echo "languages=$LANGS" >> "$GITHUB_OUTPUT"
|
||||
echo "has-any=$HAS_ANY" >> "$GITHUB_OUTPUT"
|
||||
echo "Discovered $COUNT migrated language(s): $LANGS"
|
||||
echo "Parity matrix will run: $HAS_ANY"
|
||||
|
||||
parity:
|
||||
name: ${{ matrix.lang.slug }} parity
|
||||
needs: discover
|
||||
if: needs.discover.outputs.has-any == 'true'
|
||||
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'
|
||||
|
||||
- name: Verify resolver test file exists
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TEST_FILE="test/integration/resolvers/${{ matrix.lang.slug }}.test.ts"
|
||||
if [[ ! -f "$TEST_FILE" ]]; then
|
||||
echo "::error title=Missing resolver test::\
|
||||
Expected $TEST_FILE for '${{ matrix.lang.slug }}' (listed in \
|
||||
MIGRATED_LANGUAGES). Either fix the slug or add the test file \
|
||||
before listing this language as migrated."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- 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"
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
|
|
@ -29,6 +29,8 @@ concurrency:
|
|||
# ci-quality.yml — typecheck (tsc --noEmit)
|
||||
# ci-tests.yml — unit + integration tests with coverage + cross-platform
|
||||
# ci-e2e.yml — E2E tests (only when gitnexus-web/ changes)
|
||||
# ci-scope-parity.yml — RFC #909 Ring 3 parity gate: legacy DAG + registry-primary
|
||||
# both pass, per migrated language in the JSON registry
|
||||
#
|
||||
# Shared setup is DRY via .github/actions/setup-gitnexus composite action.
|
||||
|
||||
|
|
@ -48,6 +50,11 @@ jobs:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
scope-parity:
|
||||
uses: ./.github/workflows/ci-scope-parity.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# ── Save PR metadata for the reporting workflow ─────────────────
|
||||
# The ci-report.yml workflow (triggered by workflow_run) needs the
|
||||
# PR number and job results to post a comment. We save them as an
|
||||
|
|
@ -56,7 +63,7 @@ jobs:
|
|||
save-pr-meta:
|
||||
name: Save PR Metadata
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
needs: [quality, tests, e2e]
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
|
@ -67,12 +74,14 @@ jobs:
|
|||
QUALITY: ${{ needs.quality.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
mkdir -p pr-meta
|
||||
echo "$PR_NUMBER" > pr-meta/pr_number
|
||||
echo "$QUALITY" > pr-meta/quality_result
|
||||
echo "$TESTS" > pr-meta/tests_result
|
||||
echo "$E2E" > pr-meta/e2e_result
|
||||
echo "$PR_NUMBER" > pr-meta/pr_number
|
||||
echo "$QUALITY" > pr-meta/quality_result
|
||||
echo "$TESTS" > pr-meta/tests_result
|
||||
echo "$E2E" > pr-meta/e2e_result
|
||||
echo "$SCOPE_PARITY" > pr-meta/scope_parity_result
|
||||
# TODO(post-merge): remove backward-compat copies once ci-report.yml
|
||||
# on main reads underscore names.
|
||||
# Backward-compat: ci-report.yml on main still reads hyphenated
|
||||
|
|
@ -95,7 +104,7 @@ jobs:
|
|||
# Single required check for branch protection.
|
||||
ci-status:
|
||||
name: CI Gate
|
||||
needs: [quality, tests, e2e]
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
|
@ -106,10 +115,12 @@ jobs:
|
|||
QUALITY: ${{ needs.quality.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
echo "Quality: $QUALITY"
|
||||
echo "Tests: $TESTS"
|
||||
echo "E2E: $E2E"
|
||||
echo "Scope parity: $SCOPE_PARITY"
|
||||
if [[ "$QUALITY" != "success" ]] ||
|
||||
[[ "$TESTS" != "success" ]]; then
|
||||
echo "::error::Quality or test jobs failed"
|
||||
|
|
@ -119,3 +130,14 @@ jobs:
|
|||
echo "::error::E2E job failed"
|
||||
exit 1
|
||||
fi
|
||||
# scope-parity is a reusable workflow. With an empty migrated-
|
||||
# languages list, its parity matrix is skipped and the outer
|
||||
# workflow still reports `success`. If any entry's legacy-DAG or
|
||||
# registry-primary run fails, the workflow reports `failure`.
|
||||
# Accept only `success`; `skipped` would mean the entire
|
||||
# discover job was skipped too (upstream failure), which should
|
||||
# still block.
|
||||
if [[ "$SCOPE_PARITY" != "success" ]]; then
|
||||
echo "::error::Scope-resolution parity gate failed (RFC #909 Ring 3)"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
24
gitnexus/scripts/ci-list-migrated-languages.ts
Normal file
24
gitnexus/scripts/ci-list-migrated-languages.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* CI helper — emits the `MIGRATED_LANGUAGES` set as a JSON matrix array for
|
||||
* GitHub Actions (`.github/workflows/ci-scope-parity.yml`).
|
||||
*
|
||||
* Consumed by the `discover` job in that workflow. Each entry has:
|
||||
* - `slug`: lowercase language id, matching `test/integration/resolvers/<slug>.test.ts`.
|
||||
* - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_<envvar>` toggle.
|
||||
*
|
||||
* Run with `npx tsx scripts/ci-list-migrated-languages.ts`. The script
|
||||
* writes a single JSON array to stdout (no wrapper object) so the
|
||||
* workflow can pipe it straight into `$GITHUB_OUTPUT`.
|
||||
*/
|
||||
|
||||
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
||||
|
||||
const entries = [...MIGRATED_LANGUAGES].map((slug) => {
|
||||
const s = String(slug);
|
||||
return {
|
||||
slug: s,
|
||||
envvar: s.toUpperCase().replace(/-/g, '_'),
|
||||
};
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify(entries));
|
||||
|
|
@ -38,6 +38,37 @@
|
|||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
||||
/**
|
||||
* Languages whose RFC #909 Ring 3 scope-resolution migration is complete.
|
||||
*
|
||||
* This is the single source of truth for "migrated" — the list drives:
|
||||
*
|
||||
* 1. **Production default behavior.** `isRegistryPrimary(lang)` returns
|
||||
* `true` by default for languages in this set (env-var override to
|
||||
* any falsy value still wins — e.g. `REGISTRY_PRIMARY_PYTHON=0`).
|
||||
* 2. **CI parity gate.** `.github/workflows/ci-scope-parity.yml` auto-
|
||||
* discovers this set and, for every language in it, runs the
|
||||
* resolver integration test at `test/integration/resolvers/<slug>.test.ts`
|
||||
* TWICE on every PR — once with the legacy DAG (flag forced off)
|
||||
* and once with the registry-primary path (flag forced on). BOTH
|
||||
* must pass. Adding a language is automatic — no workflow edit,
|
||||
* no JSON registry.
|
||||
* 3. **Legacy-path gating.** `call-processor.ts` / `import-processor.ts`
|
||||
* skip per-language work when `isRegistryPrimary(lang)` is `true`,
|
||||
* so this set also controls what gets silenced in the legacy DAG.
|
||||
*
|
||||
* Add a language here ONLY after shadow parity ≥ 99% fixtures / ≥ 98%
|
||||
* corpus per RFC §6.4. The parity CI gate will block the PR otherwise.
|
||||
*
|
||||
* The set is intentionally a static TypeScript literal (not a JSON import,
|
||||
* not an env lookup) so CI can discover it via `tsx` without a build step
|
||||
* and reviewers see the change inline with the code that consumes it.
|
||||
*/
|
||||
export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set([
|
||||
// Add languages here when their migration completes. Example:
|
||||
SupportedLanguages.Python,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Return the env-var name that controls a given language's registry-
|
||||
* primary flag. Exported for test assertions and for the PR-labeling
|
||||
|
|
@ -48,15 +79,17 @@ export function envVarNameFor(lang: SupportedLanguages): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether `lang` has been flipped to registry-primary call resolution.
|
||||
* Whether `lang` runs through the registry-primary call-resolution path.
|
||||
*
|
||||
* Returns `false` by default — a language must explicitly set its env
|
||||
* var to a truthy value to opt in. The flag is the sole control surface:
|
||||
* flipping it requires no code change, and reverting it requires no code
|
||||
* change.
|
||||
* Resolution order: an explicit env-var value wins (so operators and CI
|
||||
* can force either path for a given run), and the default falls back to
|
||||
* `MIGRATED_LANGUAGES.has(lang)` — so languages whose migration is
|
||||
* complete default to registry-primary without touching any env.
|
||||
*/
|
||||
export function isRegistryPrimary(lang: SupportedLanguages): boolean {
|
||||
return parseFlag(process.env[envVarNameFor(lang)]);
|
||||
const raw = process.env[envVarNameFor(lang)];
|
||||
if (raw !== undefined) return parseFlag(raw);
|
||||
return MIGRATED_LANGUAGES.has(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue