Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/actions/classify-changes/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: "Classify changed files"

description: "Classifies a pull request change range as documentation-only or full validation. The classification fails safe: every uncertain input selects full validation."

inputs:
event-name:
description: "GitHub event name that drives skip eligibility"
required: true
head-sha:
description: "Head commit SHA of the classification range"
required: true
force-full-label:
description: "When true, force full validation because the ci:full-validation label is present"
required: false
default: "false"
force-full-variable:
description: "When true, force full validation because the ZOO_CODE_REQUIRE_FULL_VALIDATION repository variable is true"
required: false
default: "false"

outputs:
category:
description: "docs-only or full"
value: ${{ steps.classify.outputs.category }}
reason:
description: "Human-readable classification reason"
value: ${{ steps.classify.outputs.reason }}
reason-code:
description: "Stable classification reason code"
value: ${{ steps.classify.outputs.reason-code }}
changed-files:
description: "Number of changed files in the classification range"
value: ${{ steps.classify.outputs.changed-files }}
allowlist-fingerprint:
description: "Fingerprint of the active documentation-only allowlist"
value: ${{ steps.classify.outputs.allowlist-fingerprint }}

runs:
using: "composite"
steps:
- id: classify
shell: bash
run: |
HEAD_SHA="${{ inputs.head-sha }}"
FLAGS=()
if [[ "${{ inputs.force-full-label }}" == "true" ]]; then
FLAGS+=("--force-full-label")
fi
if [[ "${{ inputs.force-full-variable }}" == "true" ]]; then
FLAGS+=("--force-full-variable")
fi
# The pull_request checkout is the synthetic merge commit, so its
# first parent is the base branch tip that was actually merged
# into. A failed resolution fails the step and fails safe.
BASE_SHA="$(git rev-parse "${HEAD_SHA}^1")"
node scripts/change-classification.mjs ci --event "${{ inputs.event-name }}" --head "$HEAD_SHA" --base "$BASE_SHA" "${FLAGS[@]}"
182 changes: 160 additions & 22 deletions .github/workflows/code-qa.yml

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches: ["main"]
pull_request:
types: [opened, reopened, ready_for_review, synchronize, labeled, unlabeled]
schedule:
- cron: "24 19 * * 3"

Expand All @@ -19,8 +20,44 @@ permissions:
contents: read

jobs:
classify:
name: classify (codeql)
# Classifies the pull request change range before any validation
# runs. Only an explicit documentation-only allowlist match can
# skip expensive jobs, and only for pull_request events; a failed
# or missing classification fails safe to full validation. The
# validation-gate job below audits this decision on every run.
runs-on: ubuntu-latest
outputs:
category: ${{ steps.classify.outputs.category }}
reason: ${{ steps.classify.outputs.reason }}
reason-code: ${{ steps.classify.outputs.reason-code }}
changed-files: ${{ steps.classify.outputs.changed-files }}
allowlist-fingerprint: ${{ steps.classify.outputs.allowlist-fingerprint }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Classification resolves the merge base from history.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Classify changed files
id: classify
uses: ./.github/actions/classify-changes
with:
event-name: ${{ github.event_name }}
head-sha: ${{ github.sha }}
force-full-label: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
force-full-variable: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}

analyze:
name: Analyze (${{ matrix.language }})
needs: classify
# Fail-safe: only a successful documentation-only classification on
# a pull request skips this job. Merge-queue, push, and scheduled
# analysis always runs.
if: ${{ !cancelled() && (needs.classify.result != 'success' || github.event_name != 'pull_request' || needs.classify.outputs.category != 'docs-only') }}
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
Expand Down Expand Up @@ -89,3 +126,43 @@ jobs:
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
category: "/language:${{matrix.language}}"

validation-gate:
name: validation-gate (codeql)
# Always-running required gate for change-based validation. It
# rejects a missing or failed classification, validation jobs that
# did not run when full validation was required, jobs that ran when
# a documentation-only skip was expected, and a classifier replay
# that disagrees with the recorded classification. It also records
# the classification, the skipped work, runner minutes, and the
# required-check completion time in the job summary.
if: always()
needs:
- classify
- analyze
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The gate replays the classifier against the same range.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Run validation gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEEDS_JSON: ${{ toJSON(needs) }}
EVENT_NAME: ${{ github.event_name }}
PR_DRAFT: ${{ github.event.pull_request.draft }}
RUN_ID: ${{ github.run_id }}
HEAD_SHA: ${{ github.sha }}
FORCE_FULL_LABEL: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
FORCE_FULL_VARIABLE: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}
WATCHED_JOBS: analyze:always:gated
run: |
BASE_SHA="$(git rev-parse "${HEAD_SHA}^1")"
node scripts/change-gate.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
77 changes: 76 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: E2E Tests (Mocked)
on:
workflow_dispatch:
pull_request:
types: [opened, reopened, ready_for_review, synchronize]
types: [opened, reopened, ready_for_review, synchronize, labeled, unlabeled]
merge_group:
types: [checks_requested]

Expand All @@ -17,7 +17,42 @@ permissions:
contents: read

jobs:
classify:
name: classify (e2e)
# Classifies the pull request change range before any validation
# runs. Only an explicit documentation-only allowlist match can
# skip expensive jobs, and only for pull_request events; a failed
# or missing classification fails safe to full validation. The
# validation-gate job below audits this decision on every run.
runs-on: ubuntu-latest
outputs:
category: ${{ steps.classify.outputs.category }}
reason: ${{ steps.classify.outputs.reason }}
reason-code: ${{ steps.classify.outputs.reason-code }}
changed-files: ${{ steps.classify.outputs.changed-files }}
allowlist-fingerprint: ${{ steps.classify.outputs.allowlist-fingerprint }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Classification resolves the merge base from history.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Classify changed files
id: classify
uses: ./.github/actions/classify-changes
with:
event-name: ${{ github.event_name }}
head-sha: ${{ github.sha }}
force-full-label: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
force-full-variable: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}

e2e-mock:
needs: classify
# Fail-safe: only a successful documentation-only classification on
# a pull request skips this job. Merge-queue validation always runs.
if: ${{ !cancelled() && (needs.classify.result != 'success' || github.event_name != 'pull_request' || needs.classify.outputs.category != 'docs-only') }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -191,3 +226,43 @@ jobs:
with:
path: .cache/e2e-pass
key: ${{ steps.e2e-marker.outputs.cache-primary-key }}

validation-gate:
name: validation-gate (e2e)
# Always-running required gate for change-based validation. It
# rejects a missing or failed classification, validation jobs that
# did not run when full validation was required, jobs that ran when
# a documentation-only skip was expected, and a classifier replay
# that disagrees with the recorded classification. It also records
# the classification, the skipped work, runner minutes, and the
# required-check completion time in the job summary.
if: always()
needs:
- classify
- e2e-mock
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The gate replays the classifier against the same range.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Run validation gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEEDS_JSON: ${{ toJSON(needs) }}
EVENT_NAME: ${{ github.event_name }}
PR_DRAFT: ${{ github.event.pull_request.draft }}
RUN_ID: ${{ github.run_id }}
HEAD_SHA: ${{ github.sha }}
FORCE_FULL_LABEL: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
FORCE_FULL_VARIABLE: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}
WATCHED_JOBS: e2e-mock:always:gated
run: |
BASE_SHA="$(git rev-parse "${HEAD_SHA}^1")"
node scripts/change-gate.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
78 changes: 76 additions & 2 deletions .github/workflows/mutation-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Changed-code mutation testing

on:
pull_request:
types: [edited, opened, reopened, ready_for_review, synchronize]
types: [edited, opened, reopened, ready_for_review, synchronize, labeled, unlabeled]
merge_group:
types: [checks_requested]

Expand All @@ -14,9 +14,43 @@ concurrency:
cancel-in-progress: true

jobs:
classify:
name: classify (mutation-testing)
# Classifies the pull request change range before any validation
# runs. Only an explicit documentation-only allowlist match can
# skip expensive jobs, and only for pull_request events; a failed
# or missing classification fails safe to full validation. The
# validation-gate job below audits this decision on every run.
runs-on: ubuntu-latest
outputs:
category: ${{ steps.classify.outputs.category }}
reason: ${{ steps.classify.outputs.reason }}
reason-code: ${{ steps.classify.outputs.reason-code }}
changed-files: ${{ steps.classify.outputs.changed-files }}
allowlist-fingerprint: ${{ steps.classify.outputs.allowlist-fingerprint }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Classification resolves the merge base from history.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Classify changed files
id: classify
uses: ./.github/actions/classify-changes
with:
event-name: ${{ github.event_name }}
head-sha: ${{ github.sha }}
force-full-label: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
force-full-variable: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}

mutation-diff:
name: mutation-diff
if: github.event_name == 'merge_group' || github.event.pull_request.draft == false
needs: classify
# Fail-safe: only a successful documentation-only classification on
# a pull request skips this job. Merge-queue validation always runs.
if: ${{ !cancelled() && (github.event_name == 'merge_group' || github.event.pull_request.draft == false) && (needs.classify.result != 'success' || github.event_name != 'pull_request' || needs.classify.outputs.category != 'docs-only') }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -76,3 +110,43 @@ jobs:
echo "### Download mutation reports"
echo "[Open the changed-code-mutation-report artifact]($ARTIFACT_URL), then open the package's mutation.html file."
} >> "$GITHUB_STEP_SUMMARY" || echo "::warning title=Mutation test advisory::Could not write the job summary"

validation-gate:
name: validation-gate (mutation-testing)
# Always-running required gate for change-based validation. It
# rejects a missing or failed classification, validation jobs that
# did not run when full validation was required, jobs that ran when
# a documentation-only skip was expected, and a classifier replay
# that disagrees with the recorded classification. It also records
# the classification, the skipped work, runner minutes, and the
# required-check completion time in the job summary.
if: always()
needs:
- classify
- mutation-diff
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The gate replays the classifier against the same range.
fetch-depth: 0
# This job never pushes — don't persist the GITHUB_TOKEN.
persist-credentials: false
- name: Run validation gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEEDS_JSON: ${{ toJSON(needs) }}
EVENT_NAME: ${{ github.event_name }}
PR_DRAFT: ${{ github.event.pull_request.draft }}
RUN_ID: ${{ github.run_id }}
HEAD_SHA: ${{ github.sha }}
FORCE_FULL_LABEL: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:full-validation') }}
FORCE_FULL_VARIABLE: ${{ vars.ZOO_CODE_REQUIRE_FULL_VALIDATION == 'true' }}
WATCHED_JOBS: mutation-diff:pr-non-draft:gated
run: |
BASE_SHA="$(git rev-parse "${HEAD_SHA}^1")"
node scripts/change-gate.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
Loading
Loading