mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Collapse release-candidate.yml into publish.yml so there is exactly one workflow that publishes gitnexus to npm, creates GitHub Releases, and triggers Docker builds — for both release candidates and stable releases. Closes #1609 architecturally.
A first-stage `route` job classifies push-to-main / push-tag / workflow_dispatch into `rc` / `stable` modes and fails closed on malformed shapes. RC path runs rc-guard → ci.yml → publish (mint GitHub App token → checkout with persist-credentials:false → resolve next rc version → atomic v-tag + rc/<SHA> marker push → vtag integrity gate → npm publish via OIDC → GitHub prerelease → if: failure() cleanup) → docker.yml. Stable path verifies package.json matches the tag and publishes to `latest` via OIDC (no docker).
Hardening:
• Self-trigger prevention via negative-glob `tags: ['v*', '!v*-rc.*']` — the bug class behind #1609 cannot recur.
• Two distinct actions/checkout steps per mode (no conditional `token:` expression footgun).
• Workflow-level `permissions: {}` deny-all + per-job grants; `id-token: write` only where OIDC is used.
• npm Trusted Publishing replaces NPM_TOKEN (delete the secret after the first successful publish).
• GitHub App installation token (actions/create-github-app-token@v3.2.0) replaces the long-lived RELEASE_PUSH_TOKEN PAT (delete after first successful RC).
• vtag integrity gate fails closed on empty / mode-mismatched output (prevents Release named `main` from a github.ref fallback).
• Annotation-injection sanitization on every logged ref.
• Explicit `secrets:` passthrough on docker.yml (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN); ci.yml no longer inherits anything.
• `if: failure()` cleanup auto-deletes v-tag + rc-marker on partial failure (eliminates the external-consumer phantom-version ingestion window).
• ACTIONS_STEP_DEBUG window closed via `set +x` wrap on the inline auth-header compute.
• Curated retry-loud error handling on `gh api` bot-user-id lookup and `npx semver`.
Pre-merge validation:
• 10-reviewer multi-agent code-review pass; 14 findings fixed inline (commit 820cefae), 6 deferred to follow-ups.
• End-to-end dry-run rehearsal via workflow_dispatch (run 25919563064) validated route classification, rc-guard, App token mint, RC checkout, version resolver, vtag synthetic-regex check, and faithful tarball pack at the bumped version.
• All zizmor findings on the unification commits closed.
• Branch-protection required checks all green.
Post-merge actions:
• After the first successful RC, delete the `NPM_TOKEN` and `RELEASE_PUSH_TOKEN` secrets — they are no longer used.
• The first real RC after merge is the live-fire test for steps dry-run could not exercise (atomic tag push, real npm OIDC handshake, GitHub Release creation, docker.yml under explicit secrets passthrough). The if: failure() cleanup step handles the partial-failure recovery automatically; the Rollback Runbook in CONTRIBUTING.md covers the rare cases auto-cleanup can't reach.
142 lines
5.8 KiB
YAML
142 lines
5.8 KiB
YAML
name: CI
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
paths-ignore: ['**.md', 'docs/**', 'LICENSE']
|
|
workflow_call:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
|
|
# Hardcoded `CI-` prefix (not `${{ github.workflow }}`) because this workflow is
|
|
# invoked as a reusable workflow from publish.yml. In called-workflow context
|
|
# `github.workflow` evaluation is ambiguous across GitHub Actions versions, and a
|
|
# prefix that could resolve to the caller's name would share a concurrency group
|
|
# with the caller → deadlock. A literal prefix is immune. Direct `pull_request`
|
|
# invocations use `CI-<ref>`; invocations from a reusable-workflow caller fall
|
|
# into a per-run-unique group that never serializes with the caller. `push` to
|
|
# main is handled by publish.yml (RC mode), which calls this workflow once
|
|
# before publishing.
|
|
concurrency:
|
|
group: ${{ github.event_name == 'pull_request' && format('CI-{0}', github.ref) || format('CI-nested-{0}', github.run_id) }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
# ── Reusable workflow orchestration ─────────────────────────────────
|
|
# Each concern lives in its own workflow file for maintainability:
|
|
# 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.
|
|
|
|
jobs:
|
|
quality:
|
|
uses: ./.github/workflows/ci-quality.yml
|
|
permissions:
|
|
contents: read
|
|
|
|
tests:
|
|
uses: ./.github/workflows/ci-tests.yml
|
|
permissions:
|
|
contents: read
|
|
|
|
e2e:
|
|
uses: ./.github/workflows/ci-e2e.yml
|
|
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
|
|
# artifact because workflow_run context doesn't reliably carry PR
|
|
# info for fork PRs.
|
|
save-pr-meta:
|
|
name: Save PR Metadata
|
|
if: always() && github.event_name == 'pull_request'
|
|
needs: [quality, tests, e2e, scope-parity]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Write metadata
|
|
shell: bash
|
|
env:
|
|
PR_NUMBER: ${{ github.event.number }}
|
|
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 "$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
|
|
# names. workflow_run always executes from the default branch, so
|
|
# the main-branch reader won't find the underscore variants until
|
|
# this PR is merged. Write both until then.
|
|
cp pr-meta/pr_number pr-meta/pr-number
|
|
cp pr-meta/quality_result pr-meta/quality-result
|
|
cp pr-meta/tests_result pr-meta/tests-result
|
|
cp pr-meta/e2e_result pr-meta/e2e-result
|
|
|
|
- name: Upload PR metadata
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: pr-meta
|
|
path: pr-meta/
|
|
retention-days: 1
|
|
|
|
# ── Unified CI gate ──────────────────────────────────────────────
|
|
# Single required check for branch protection.
|
|
ci-status:
|
|
name: CI Gate
|
|
needs: [quality, tests, e2e, scope-parity]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Check all jobs passed
|
|
shell: bash
|
|
env:
|
|
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"
|
|
exit 1
|
|
fi
|
|
if [[ "$E2E" != "success" && "$E2E" != "skipped" ]]; then
|
|
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
|