diff --git a/.github/CI_POLICY.md b/.github/CI_POLICY.md new file mode 100644 index 0000000000..c7c7b8c56c --- /dev/null +++ b/.github/CI_POLICY.md @@ -0,0 +1,25 @@ +# GitHub Actions policy — Jenkins is the CI authority + +This fork deliberately delegates automatic builds, tests, lint, security audits, +soak tests, benchmarks, and release qualification to **Jenkins**. + +GitHub-hosted workflows must use **only** `workflow_dispatch` (operator-initiated) +or `workflow_call` (reusable by a manually dispatched workflow) as events. +No `push`, `pull_request`, `pull_request_target`, `schedule`, `issues`, +`workflow_run`, or other automatic Actions events are allowed. + +* Jenkins' `Jenkinsfile` runs `scripts/ci/check-github-actions-policy.py`. +* The GitHub release/promotion workflows are retained as **manual-only** + fallback/publishing operations. They can consume Actions minutes when started. +* CodeQL, GitHub Pages, smoke/soak and repro workflows remain manual-only; + GitHub Pages will **not** update automatically after merges. +* The cost-intensive PR matrix and weekly test-impact artifact producer have + been removed. Use Jenkins for automatic PR checks and qualification evidence. +* Upstream merges may reintroduce workflows: adapt or remove their triggers + before accepting a synchronization PR. This policy is part of the release gate. +* No GitHub Actions workflow should be used for automatic upstream synchronization. + Monitoring may be external; any proposed integration remains a reviewable PR. + +To eliminate even manual GitHub Actions usage, disable Actions in the repository's +**Settings → Actions → General**; that also disables the manual release and +Pages deployment workflows. diff --git a/.github/workflows/cache-warm.yml b/.github/workflows/cache-warm.yml deleted file mode 100644 index ff15fbb1a7..0000000000 --- a/.github/workflows/cache-warm.yml +++ /dev/null @@ -1,123 +0,0 @@ -# Nightly compiler-cache warming for main's scope. GitHub cache isolation -# lets a PR restore only caches written by its own ref or the base branch — -# and nothing used to write test caches in main's scope (the test workflow -# runs on pull_request and workflow_dispatch only), so the cross-ref -# fallback restore keys in _test.yml had no warm source and every fresh PR -# built cold. This build-only job (no tests) keeps main-scoped caches at -# most a day stale; CCACHE_COMPILERCHECK=content turns staleness into -# partial misses, never wrong output. The build commands and ccache -# environment mirror scripts/test.sh's build phase exactly — a warm entry -# only hits when flags and compiler content match, so fidelity here is what -# makes the cache useful. -name: Cache warm - -on: - schedule: - - cron: '17 2 * * *' - workflow_dispatch: {} - -permissions: - contents: read - -jobs: - warm-unix: - strategy: - fail-fast: false - matrix: - include: - - { os: ubuntu-latest, cc: gcc, cxx: g++ } - - { os: ubuntu-24.04-arm, cc: gcc, cxx: g++ } - - { os: macos-14, cc: cc, cxx: c++ } - - { os: macos-15-intel, cc: cc, cxx: c++ } - runs-on: ${{ matrix.os }} - timeout-minutes: 60 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install deps (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache - - - name: Install ccache (macOS) - if: startsWith(matrix.os, 'macos') - run: command -v ccache >/dev/null 2>&1 || brew install ccache - - - name: Compiler cache (restore) - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ github.workspace }}/.ccache - key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }} - restore-keys: | - ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}- - ccache-test-${{ matrix.os }}-${{ matrix.cc }}- - - - name: Build (test runner + product, no tests) - run: | - source scripts/env.sh - make -j"$(getconf _NPROCESSORS_ONLN)" -f Makefile.cbm \ - build/c/test-runner cbm CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1500M - - - name: ccache stats - run: ccache -sv || ccache -s - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - - - name: Compiler cache (save) - if: always() - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ github.workspace }}/.ccache - key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }} - - warm-windows: - runs-on: windows-latest - timeout-minutes: 90 - defaults: - run: - shell: msys2 {0} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 - with: - msystem: CLANG64 - path-type: inherit - install: >- - mingw-w64-clang-x86_64-clang - mingw-w64-clang-x86_64-compiler-rt - mingw-w64-clang-x86_64-zlib - mingw-w64-clang-x86_64-ccache - make - git - - - name: Compiler cache (restore) - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ github.workspace }}/.ccache - key: ccache-test-windows-latest-CLANG64-${{ github.ref }}-${{ github.sha }} - restore-keys: | - ccache-test-windows-latest-CLANG64-${{ github.ref }}- - ccache-test-windows-latest-CLANG64- - - - name: Build (test runner + product, no tests) - run: | - source scripts/env.sh - make -j"$(nproc)" -f Makefile.cbm build/c/test-runner cbm CC=clang CXX=clang++ - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1500M - - - name: ccache stats - run: ccache -sv || ccache -s - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - - - name: Compiler cache (save) - if: always() - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ github.workspace }}/.ccache - key: ccache-test-windows-latest-CLANG64-${{ github.ref }}-${{ github.sha }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 621d407fc0..3f7a93d729 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,10 +1,9 @@ name: CodeQL SAST +# Security scanning is available only on explicit operator request. +# Jenkins owns automatic builds, tests and security checks. on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/fast-repro.yml b/.github/workflows/fast-repro.yml index 5f38c17729..852241790b 100644 --- a/.github/workflows/fast-repro.yml +++ b/.github/workflows/fast-repro.yml @@ -3,7 +3,7 @@ # 5-platform ASan board. The full bug-repro.yml board remains the comprehensive # all-platform check; this is just the fast inner loop. # -# Trigger: workflow_dispatch, or push to a qa/fast-** branch. Non-gating. +# Trigger: manual workflow_dispatch only. Jenkins owns automated testing. name: Fast Repro on: @@ -13,8 +13,6 @@ on: description: 'Comma list of suite-name substrings to run (empty = all)' type: string default: '' - push: - branches: ['qa/fast-**'] permissions: contents: read diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml deleted file mode 100644 index 60fc62ae97..0000000000 --- a/.github/workflows/issue-labeler.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Issue area labeler - -# Adds area labels to new/edited issues based on keyword regexes in -# .github/issue-labeler.yml. Additive only (addLabels never removes): it -# never touches a label a maintainer set by hand. Base bug/enhancement -# labels already come from the issue forms. -# -# Implemented with first-party actions/github-script (not a third-party -# labeler action) so every pattern is compiled as a JavaScript RegExp with -# the `i` flag applied centrally — inline `(?i)` groups are PCRE-only and -# threw `SyntaxError: Invalid group` on every issue (#764). A pattern that -# fails to compile now fails the run loudly instead of silently no-oping. - -on: - issues: - types: [opened, edited] - -permissions: - contents: read - -jobs: - triage: - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const fs = require('fs'); - const text = [context.payload.issue.title, context.payload.issue.body] - .filter(Boolean).join('\n'); - const rules = []; - const bad = []; - // Config format: one rule per line — "label": 'regex' (see the - // header comment in .github/issue-labeler.yml). - for (const line of fs.readFileSync('.github/issue-labeler.yml', 'utf8').split('\n')) { - const m = line.match(/^"([^"]+)":\s*'(.*)'\s*$/); - if (!m) continue; - try { rules.push({ label: m[1], re: new RegExp(m[2], 'i') }); } - catch (e) { bad.push(`${m[1]}: ${e.message}`); } - } - if (rules.length === 0 && bad.length === 0) { - core.setFailed('no labeling rules parsed from .github/issue-labeler.yml'); - return; - } - const labels = rules.filter(r => r.re.test(text)).map(r => r.label); - if (labels.length) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels, - }); - } - core.info(`matched: ${labels.join(', ') || '(none)'}`); - if (bad.length) core.setFailed(`invalid label regex(es): ${bad.join('; ')}`); diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml deleted file mode 100644 index d60d2efae2..0000000000 --- a/.github/workflows/label-actions.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Label actions - -# Posts a templated comment when a maintainer adds certain labels. -# Behavior is configured in .github/label-actions.yml. Issues only. - -on: - issues: - types: [labeled, unlabeled] - -permissions: - contents: read - -jobs: - action: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - uses: dessant/label-actions@65225c179d3b2502f6eda7b3d15101a3f412366b # v5.0.3 - with: - process-only: issues diff --git a/.github/workflows/nightly-soak.yml b/.github/workflows/nightly-soak.yml index 2bd5bc7e86..5bebdc43f0 100644 --- a/.github/workflows/nightly-soak.yml +++ b/.github/workflows/nightly-soak.yml @@ -1,16 +1,14 @@ -# Soak dispatcher — the weekly scheduled soak AND every on-demand soak run. +# Soak dispatcher — explicitly requested runs only (no scheduled runs). # # Absorbs the retired soak.yml (the standalone "multi-hour #581" workflow): its # reasons to exist — a job budget that actually covers a 4h run, and the # query-leak mode that never reindexes so the #581 leak can accumulate — are # now _soak.yml facts (timeout 560, legs input). A long #581-only run is # `legs: query-leak` here, not a separate workflow with its own copy of the -# soak procedure. NON-GATING: never a required check, never blocks a merge. +# soak procedure. NON-GATING: manually dispatched only; Jenkins owns automated soak. name: Nightly Soak on: - schedule: - - cron: '0 2 * * 0' # Every Sunday at 2am UTC workflow_dispatch: inputs: duration_minutes: @@ -26,10 +24,6 @@ on: description: 'Also run the 15-min ASan soak legs' type: boolean default: true - # Iteration convenience carried over from soak.yml: pushing a qa/soak-** - # branch starts a real run from that branch's own copy of the workflows. - push: - branches: ['qa/soak-**'] permissions: contents: read @@ -39,7 +33,7 @@ jobs: # budget instead of a fixed count. NON-GATING like the soak; a crash uploads # its reproducer, which becomes a fixed-seed regression input once fixed. fuzz: - if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 120 steps: @@ -59,8 +53,7 @@ jobs: soak: uses: ./.github/workflows/_soak.yml with: - # On schedule/push events the inputs context is empty — fall back to the - # weekly defaults (4h, both legs, ASan on). + # Values come only from manual workflow_dispatch input. duration_minutes: ${{ inputs.duration_minutes || 240 }} legs: ${{ inputs.legs || 'quick,query-leak' }} - run_asan: ${{ github.event_name != 'workflow_dispatch' || inputs.run_asan }} + run_asan: ${{ inputs.run_asan }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8c337ad3ef..ab0c9f72aa 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,11 +11,6 @@ name: Deploy Pages on: - push: - branches: [main] - paths: - - 'docs/**' - - '.github/workflows/pages.yml' workflow_dispatch: permissions: diff --git a/.github/workflows/pr-acknowledgement.yml b/.github/workflows/pr-acknowledgement.yml deleted file mode 100644 index ef36a9fb0f..0000000000 --- a/.github/workflows/pr-acknowledgement.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: PR acknowledgement - -# Posts one acknowledgement comment when a pull request is opened, so a -# contributor learns the current review status immediately instead of inferring -# it from silence. -# -# The message text lives in `.github/pr-acknowledgement.md`. Edit that file to -# change what is said; blank it (or delete it) to turn acknowledgements off -# without touching this workflow. No code change is needed to switch it. -# -# SECURITY — this uses `pull_request_target`, which runs against the BASE repo -# with a token that can write. That is required: a `pull_request` trigger gives -# fork PRs a read-only token, so commenting on exactly the contributions we most -# want to acknowledge would fail. -# -# The safety rule that makes it sound: this job NEVER checks out, builds, or -# executes pull-request code. `actions/checkout` here resolves to the base -# commit (the default under `pull_request_target`), and it is used only to read -# the message file out of our own tree. Do not add a `ref:` pointing at the PR -# head, and do not add a build step. Nothing from the PR is interpolated into a -# shell command either — the body comes from a file, and the PR number is passed -# through the environment rather than expanded inline. - -on: - pull_request_target: - types: [opened] - -permissions: - contents: read - -jobs: - acknowledge: - runs-on: ubuntu-latest - # Bots do not read comments, and acknowledging our own PRs is noise. - if: >- - github.event.pull_request.user.type != 'Bot' && - github.event.pull_request.author_association != 'OWNER' - permissions: - pull-requests: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Base commit only — never the PR head. See the security note above. - persist-credentials: false - - - name: Post acknowledgement - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - MESSAGE_FILE=.github/pr-acknowledgement.md - - if [ ! -f "$MESSAGE_FILE" ]; then - echo "No $MESSAGE_FILE in the base tree — acknowledgements are off." - exit 0 - fi - - # An empty or whitespace-only file is the documented off switch. - if [ -z "$(tr -d '[:space:]' < "$MESSAGE_FILE")" ]; then - echo "$MESSAGE_FILE is blank — acknowledgements are off." - exit 0 - fi - - gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$MESSAGE_FILE" - echo "Acknowledged PR #${PR_NUMBER}." diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index 2bfa492a21..0000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,373 +0,0 @@ -# PR validation: security gates + lint + the test legs this PR's changes need. -# `changes` runs scripts/ci/select-lanes.sh on the PR's file list; every lane -# job below runs only when selected (unknown paths, workflows, the harness, -# Makefile.cbm and vendored code select everything). Builds, smoke and soak at -# full breadth stay maintainer-driven via the dry-run workflow_dispatch. Branch -# protection requires `dco` + `ci-ok` — a single stable summary context that -# fails unless every PR stage succeeded and every selected lane ran green. -name: PR - -on: - pull_request: - branches: [main] - -permissions: - contents: read - pull-requests: read - -# A new push to the PR supersedes the running validation — cancel it -# instead of stacking zombie pipelines. -concurrency: - group: pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - - -jobs: - security: - needs: [changes] - uses: ./.github/workflows/_security.yml - # The called job requests security-events:read to count code-scanning - # alerts; a reusable workflow cannot request more than its caller grants, - # so withholding this here fails the whole run at startup. - permissions: - contents: read - security-events: read - actions: read - secrets: inherit - with: - lanes: ${{ needs.changes.outputs.lanes }} - - lint: - needs: [changes] - if: ${{ contains(fromJSON(needs.changes.outputs.lanes), 'lint') || contains(fromJSON(needs.changes.outputs.lanes), 'lint-mem') }} - uses: ./.github/workflows/_lint.yml - with: - lanes: ${{ needs.changes.outputs.lanes }} - - # ── Smart CI, PR CI only: run only the tests this change can reach. - # Active only while the repository variable TEST_IMPACT_MODE is `gate`; - # unset (shadow) every test runs and test-impact-shadow only reports. - # test-impact-select.sh --gate narrows only on a well-formed answer: a - # failure of this job, a run-all answer or a lane that selects nothing - # leaves `test` without a selection, and every test runs. ci-ok does not - # need this job: when it breaks, PRs cost more minutes, never coverage. - # Dry runs and releases never pass a selection. - select-tests: - needs: [changes] - if: ${{ vars.TEST_IMPACT_MODE == 'gate' }} - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml) - outputs: - selection: ${{ steps.select.outputs.selection }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Full history: the engine certifies the history it diffs and refuses - # a shallow clone, so a depth-2 checkout always answered `all`. - fetch-depth: 0 - persist-credentials: false - - name: Build this checkout's engine - run: | - sudo apt-get update && sudo apt-get install -y zlib1g-dev - scripts/build.sh CC=gcc CXX=g++ - - name: Select the tests - id: select - env: - GH_TOKEN: ${{ github.token }} - run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-cli --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection" - - name: Upload the selection - if: ${{ steps.select.outputs.selection == 'narrowed' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: test-selection - path: ${{ runner.temp }}/test-selection - if-no-files-found: error - overwrite: true - - test: - needs: [changes, lint, select-tests] - if: ${{ !cancelled() && needs.lint.result == 'success' }} - uses: ./.github/workflows/_test.yml - with: - lanes: ${{ needs.changes.outputs.lanes }} - # Smart CI: only a successful select-tests run that narrowed passes a - # selection; skipped (shadow), failed, run-all and nothing-selected - # answers all run every test. - test_selection: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.selection == 'narrowed' && 'test-selection' || '' }} - # Perf assertions are timing-sensitive on shared runners; they stay in - # dry runs and releases where a flaky red does not block a merge. - skip_perf: true - # Iteration speed: split C-suite legs across parallel shard jobs - # (coverage re-proven every run by the shard-completeness job). The PR - # profile shards only arm64 (x2); x86 and Windows run whole, since each - # shard re-pays its setup. Dry runs and releases keep x3 / x2. - shard_suites: true - shard_profile: pr - - # ── Which lanes does this PR need? The changed files go through - # scripts/ci/select-lanes.sh (tier, lanes, full, reasons); docs / CI / - # test-only PRs stay fast, anything it cannot place runs everything. ── - changes: - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - tier: ${{ steps.select.outputs.tier }} - full: ${{ steps.select.outputs.full }} - lanes: ${{ steps.select.outputs.lanes }} - smoke: ${{ steps.select.outputs.smoke }} - steps: - - name: List the changed files - env: - GH_TOKEN: ${{ github.token }} - PR: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - run: | - # The full .diff endpoint rejects large-but-valid PRs at 20k lines. - # The paginated files endpoint remains filename-only for this gate. - # A rename lists its old path too, as the fallback's --no-renames - # diff does: moving a file away is a change to where it was. - if FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[] | .filename, (.previous_filename // empty)'); then - echo "file list: pulls/files API" - else - # The files endpoint renders the diff server-side too, and answers - # 422 "this diff is taking too long to generate" for a PR that - # regenerates a vendored parser (#2246: a 42 MB sql/parser.c) -- - # so does the compare endpoint. The PR merge ref's first parent is - # the base, so a name-only diff across it is the same file list. - # depth 2 + blob:none fetches commits and trees only (well under - # 1 MB, under a second); nothing from the PR is checked out or run. - echo "::notice::pulls/files API failed; file list taken from refs/pull/$PR/merge" - CHANGES_GIT="$RUNNER_TEMP/changes.git" - git init -q --bare "$CHANGES_GIT" - git --git-dir="$CHANGES_GIT" fetch -q --depth=2 --filter=blob:none \ - "https://github.com/$REPO" "refs/pull/$PR/merge" - FILES=$(git --git-dir="$CHANGES_GIT" diff --name-only --no-renames FETCH_HEAD^1 FETCH_HEAD) - fi - printf '%s\n' "$FILES" > "$RUNNER_TEMP/changed-files.txt" - cat "$RUNNER_TEMP/changed-files.txt" - - # The selector is PR code: it runs in its own step, after the - # token-bearing one, from a checkout that keeps no credentials. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - # The pulls/files API lists at most 3000 files: the PR's own count lets - # the selector notice a cut list and select everything instead. - - name: Select lanes - id: select - env: - CHANGED_FILES: ${{ github.event.pull_request.changed_files }} - run: | - scripts/ci/select-lanes.sh --format github-output --expect-files "$CHANGED_FILES" "$RUNNER_TEMP/changed-files.txt" > "$RUNNER_TEMP/selection.txt" - cat "$RUNNER_TEMP/selection.txt" - cat "$RUNNER_TEMP/selection.txt" >> "$GITHUB_OUTPUT" - # pr-smoke's legs, each tagged with its lane (kept in the job name, - # where ci-ok reads it back); the job gets the selected ones. - SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-14","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]' - LANES=$(sed -n 's/^lanes=//p' "$RUNNER_TEMP/selection.txt") - echo "smoke={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$SMOKE")}" >> "$GITHUB_OUTPUT" - - # ── Light smoke: build the PRODUCTION binary and run the core smoke on the - # three RELIABLE NATIVE platforms. Catches built-binary regressions at PR - # time (e.g. the Windows CreateProcess argv-quoting class that previously - # only surfaced in the release dry run). The full broad/emulated smoke stays - # in the dry run. Only the selected legs run (ubuntu from the extraction - # tier up, all three for platform/packaging changes); every leg gates. - # The maintained local wrappers add a race-free release fixture so the - # download/checksum/install/update phases run here as well. ── - pr-smoke: - needs: [changes] - if: ${{ !cancelled() && needs.changes.result == 'success' && (contains(fromJSON(needs.changes.outputs.lanes), 'smoke-ubuntu') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-mac') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-win')) }} - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.changes.outputs.smoke) }} - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install deps (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache - - - name: Install ccache (macOS) - if: matrix.os == 'macos-14' - run: command -v ccache >/dev/null 2>&1 || brew install ccache - - - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 - if: matrix.os == 'windows-latest' - with: - msystem: CLANG64 - path-type: inherit - install: >- - mingw-w64-clang-x86_64-clang - mingw-w64-clang-x86_64-zlib - mingw-w64-clang-x86_64-python3 - mingw-w64-clang-x86_64-ccache - make - coreutils - curl - zip - unzip - - # Verified compiler cache — content-keyed, stale hits impossible by - # construction (see scripts/env.sh). - - name: Compiler cache (content-verified) - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ github.workspace }}/.ccache - key: ccache-smoke-${{ matrix.os }}-${{ github.ref }}-${{ github.sha }} - restore-keys: | - ccache-smoke-${{ matrix.os }}-${{ github.ref }}- - - - name: Build prod + smoke (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: | - scripts/build.sh CC=gcc CXX=g++ - scripts/smoke-local.sh "$(pwd)/build/c/codebase-memory-cli" - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1000M - - - name: Build prod + smoke (macOS) - if: matrix.os == 'macos-14' - run: | - scripts/build.sh CC=cc CXX=c++ - codesign --sign - --force build/c/codebase-memory-cli - scripts/smoke-local.sh "$(pwd)/build/c/codebase-memory-cli" - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1000M - - - name: Build prod + smoke (Windows) - if: matrix.os == 'windows-latest' - shell: msys2 {0} - run: | - scripts/build.sh CC=clang CXX=clang++ - SMOKE_ARCH=amd64 bash test-infrastructure/vm/vm-smoke.sh - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1000M - - # ── Waste sanitizer, REPORT-ONLY: memory + CPU waste on this repository and - # the per-function scaling lane (Linux). Deliberately NOT in ci-ok's needs: - # it uploads evidence and prints the ratchet, it never blocks a merge. Only - # PRs whose selection holds the memwaste lane (memory core, memwaste - # tooling, full runs) pay for it. ── - memwaste: - needs: [changes] - if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'memwaste') }} - uses: ./.github/workflows/_memwaste.yml - with: - mode: pr - - # ── Test-impact SHADOW, REPORT-ONLY: predicts from the code graph which C - # suites this change can affect (the latest released binary indexes the - # PR checkout; detect_changes at depth 15 against the base, mapped to - # suites, with the selector's run-all triggers) and uploads the - # prediction, so graph-based selection can be judged against what really - # failed before anything gates on it. continue-on-error and NOT in - # ci-ok's needs: it can never block or pass a merge. ── - test-impact-shadow: - runs-on: ubuntu-latest - timeout-minutes: 45 - continue-on-error: true - permissions: - contents: read - actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml) - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Full history: the engine (test-impact select) certifies the history - # it diffs and refuses a shallow clone ("Graft or shallow state - # prevents history certification"), which made every shadow answer - # run_all (GIT_UNAVAILABLE). About 400 MiB, this job only. - fetch-depth: 0 - persist-credentials: false - - name: Fetch the latest released binary - id: release - continue-on-error: true - env: - GH_TOKEN: ${{ github.token }} - run: scripts/ci/test-impact-shadow.sh --fetch-release "$RUNNER_TEMP/cbm-bin" - - name: Build the binary from this checkout (download failed) - if: ${{ steps.release.outcome == 'failure' }} - run: | - sudo apt-get update && sudo apt-get install -y zlib1g-dev - scripts/build.sh CC=gcc CXX=g++ - mkdir -p "$RUNNER_TEMP/cbm-bin" - cp build/c/codebase-memory-cli "$RUNNER_TEMP/cbm-bin/" - - name: Predict the impacted suites - run: scripts/ci/test-impact-shadow.sh --binary "$RUNNER_TEMP/cbm-bin/codebase-memory-cli" --out "$RUNNER_TEMP/test-impact" - # The engine (test-impact select) is this checkout's own code: the - # released binary predates it, so the checkout is built either way. - - name: Build this checkout's engine - if: ${{ steps.release.outcome != 'failure' }} - run: | - sudo apt-get update && sudo apt-get install -y zlib1g-dev - scripts/build.sh CC=gcc CXX=g++ - - name: Select with the team artifact (engine shadow) - env: - GH_TOKEN: ${{ github.token }} - run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-cli --out "$RUNNER_TEMP/test-impact" - - name: Upload the prediction - if: ${{ always() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: test-impact-prediction - path: | - ${{ runner.temp }}/test-impact/prediction.json - ${{ runner.temp }}/test-impact/selection.json - ${{ runner.temp }}/test-impact/changed-files.txt - ${{ runner.temp }}/test-impact/selection-engine.json - ${{ runner.temp }}/test-impact/summary-engine.md - if-no-files-found: warn - - # ── Docs-only and non-product PRs (tiers T0a, T0b) run the static contract - # steps of the canonical test leg on their own: they police docs - # surfaces (language counts, version metadata), test-infrastructure/ and - # lint config too. Every test leg runs the same steps. ── - contracts: - needs: [changes] - if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'contracts') }} - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Contract steps only - run: scripts/test.sh --contracts-only - - ci-ok: - # The one required context (besides dco) — fails unless every PR stage - # succeeded, so matrix renames can never silently deadlock merges, and - # every lane the selector chose ran green: `skipped` is OK only for a lane - # nobody selected (a selected lane that skipped is a silent gate loss). - # memwaste and the test-impact shadow are report-only and not needed here. - needs: [changes, security, lint, test, pr-smoke, contracts] - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - # actions:read lists this run's jobs, the lane-level truth behind `needs`. - permissions: - contents: read - actions: read - steps: - # Needs a checkout (unlike before): the gate logic lives in the canonical - # scripts/ci entry, not inline YAML (venue-parity contract). - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: List this run's jobs - env: - GH_TOKEN: ${{ github.token }} - run: | - gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=latest&per_page=100" \ - --jq '.jobs[] | {name, status, conclusion}' > "$RUNNER_TEMP/jobs.jsonl" - - name: All PR stages and every selected lane must have succeeded - env: - RESULTS: ${{ toJSON(needs) }} - LANES: ${{ needs.changes.outputs.lanes }} - JOBS: ${{ runner.temp }}/jobs.jsonl - run: scripts/ci/require-all-green.sh diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index fb0a52bbc7..0000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: OpenSSF Scorecard - -on: - push: - branches: [main] - schedule: - - cron: '0 6 * * 1' # Every Monday at 06:00 UTC - -permissions: read-all - -# Only the newest main commit is scored: a newer push (or the weekly run) -# cancels a pending or running score of an older commit. -concurrency: - group: scorecard-${{ github.ref }} - cancel-in-progress: true - -jobs: - scorecard: - runs-on: ubuntu-latest - permissions: - security-events: write - id-token: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run OpenSSF Scorecard - uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - - name: Upload SARIF results - uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4 - with: - sarif_file: results.sarif diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 832f21ab49..05edc2dd36 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -8,14 +8,12 @@ # class), all arm64 variants + windows-11-arm (arch portability), multiple macOS # and Windows versions. A FAIL on any platform is a binary users would receive. # -# NON-GATING: workflow_dispatch + push to qa/smoke-** only (the full ~10-platform -# build is heavy, so it is opt-in rather than on every qa push). +# NON-GATING: manual workflow_dispatch only (the full ~10-platform +# build is expensive; Jenkins owns automatic builds and smoke tests). name: Smoke (all platforms) on: workflow_dispatch: - push: - branches: ['qa/smoke-**'] permissions: contents: read diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 05a95e9e24..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Stale awaiting-reporter - -# Marks inactive `awaiting-reporter` issues as `stale`. It NEVER closes them: -# closing an issue as "not planned" is a maintainer judgement, not an automated -# one, so this workflow only surfaces a review queue and a human decides. -# -# Only touches issues that a maintainer has explicitly marked `awaiting-reporter`. -# Never touches pull requests, and never touches issues without that label. -# A reporter comment removes `stale` and resets the clock. -# -# WHY never close (2026-07-26): `awaiting-reporter` is applied by hand and means -# "the reporter owes the next step". It was also being used to mean "we will come -# back to this", which is a different thing. In that second case the automation was -# closing issues the project itself owed work on — #56 was a confirmed bug with an -# in-tree reproduction (tests/repro/repro_issue56.c, landed via #667) queued to close -# as `not_planned` purely because a maintainer had the last word. A mislabel should -# cost a stale tag, never a silently discarded bug. -# -# The `stale` label is therefore a review queue, not a countdown. Sweep it during -# triage: clear the label where we owe the next step, and close by hand where the -# thread is genuinely dead. - -on: - schedule: - - cron: '17 2 * * *' # daily 02:17 UTC - workflow_dispatch: - -permissions: - contents: read - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 - with: - only-labels: 'awaiting-reporter' - stale-issue-label: 'stale' - days-before-issue-stale: 21 - # -1 disables automated closing entirely. Marking stale is the whole job. - days-before-issue-close: -1 - # Never act on pull requests with this workflow. - days-before-pr-stale: -1 - days-before-pr-close: -1 - # Belt-and-braces: these categories must never be auto-flagged even if - # `awaiting-reporter` is applied to them by mistake. `task` covers the - # umbrella/epic issues (incl. the pinned roadmap index #595), and - # `maintainer-notes` is the explicit "we owe the next step" escape hatch. - exempt-issue-labels: 'security,task,maintainer-notes' - remove-stale-when-updated: true - operations-per-run: 60 - ascending: true - stale-issue-message: > - This issue has been waiting on more information for 21 days (a - version, exact steps, or a public repro), so it's now marked - `stale`. **It will not be closed automatically** — a maintainer - reviews stale issues by hand. Adding a comment with the details - clears the label and puts it straight back in the queue. We're happy - to pick it back up the moment we can reproduce it. diff --git a/.github/workflows/test-impact-artifact.yml b/.github/workflows/test-impact-artifact.yml deleted file mode 100644 index d827f7fdfa..0000000000 --- a/.github/workflows/test-impact-artifact.yml +++ /dev/null @@ -1,87 +0,0 @@ -# Team artifact for test selection (smart CI, report-only until the oracles -# and the shadow comparison are green). Every push to main publishes the -# commit's bundle — its frozen graph, built incrementally from the previous -# bundle, and its per-test coverage map, re-run only for the suites the push -# selects — as the Actions artifact test-impact-. A weekly run rebuilds -# both from scratch and fails when the incremental graph differs from the full -# one. PR selection fetches the bundle of its merge base -# (scripts/ci/test-impact-bundle.sh) and never narrows on anything else. -name: test-impact-artifact - -on: - push: - branches: [main] - schedule: - - cron: "23 3 * * 0" # weekly full refresh, Sunday 03:23 UTC - workflow_dispatch: - inputs: - mode: - description: incremental or full - type: choice - options: [incremental, full] - default: full - -permissions: - contents: read - actions: read # fetch the previous bundle - -concurrency: - # Bundles chain: each push builds on the previous one, so runs never overlap - # and are never cancelled half-way. - group: test-impact-artifact - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-24.04 - timeout-minutes: 120 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Full history: `test-impact select/publish` certify the history they - # read and refuse a shallow clone. - fetch-depth: 0 - persist-credentials: false - - - name: Install build deps and LLVM 21 (coverage toolchain) - run: | - sudo apt-get update && sudo apt-get install -y zlib1g-dev - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list - sudo apt-get update - sudo apt-get install -y clang-21 llvm-21 libclang-rt-21-dev - - - name: Build the production binary and the coverage runner - run: | - scripts/build.sh CC=gcc CXX=g++ - make -f Makefile.cbm test-runner-cov COV_LLVM_BIN=/usr/lib/llvm-21/bin -j"$(nproc)" - - - name: Publish the bundle - env: - GH_TOKEN: ${{ github.token }} - EVENT: ${{ github.event_name }} - BEFORE: ${{ github.event.before }} - REQUESTED: ${{ inputs.mode }} - # The selection lane excludes @perf and PR CI skips perf, so their - # (slowest, longest under coverage) suites are neither mapped nor - # listed: the runner then neither lists nor runs them. - CBM_SKIP_PERF: "1" - run: | - mode=incremental - if [ "$EVENT" = schedule ] || [ "$REQUESTED" = full ]; then mode=full; fi - before="$(git rev-parse HEAD^1)" - if [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then - before="$BEFORE" - fi - scripts/ci/test-impact-publish.sh --binary build/c/codebase-memory-cli \ - --runner build/cov/test-runner --llvm-bin /usr/lib/llvm-21/bin \ - --out "$RUNNER_TEMP/bundle" --mode "$mode" --before "$before" - - - name: Upload the bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: test-impact-${{ github.sha }} - path: ${{ runner.temp }}/bundle - include-hidden-files: true # the graph lives in .codebase-memory/ - retention-days: 30 - if-no-files-found: error diff --git a/Jenkinsfile b/Jenkinsfile index 28c8c1c2c8..868f0b0ed1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,6 +22,7 @@ arena hash_table dyn_array str_intern log str_util index_policy workspace platfo sh ''' set -eu rm -f .jenkins-installed-cli + python3 scripts/ci/check-github-actions-policy.py python3 tests/test_jenkins_daemon_recovery.py scripts/lint.sh --ci CLANG_FORMAT=clang-format-20 ''' diff --git a/scripts/ci/check-github-actions-policy.py b/scripts/ci/check-github-actions-policy.py new file mode 100644 index 0000000000..7ecdf2fbda --- /dev/null +++ b/scripts/ci/check-github-actions-policy.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Jenkins guard: forbid billable, automatically triggered GitHub Actions. + +Only manually dispatched or reusable workflows are permitted in this fork. +When importing upstream changes, keep this policy even if upstream adds CI. +""" +from pathlib import Path +import re +import sys + +ROOT = Path(__file__).resolve().parents[2] +WORKFLOWS = ROOT / ".github" / "workflows" +ALLOWED = {"workflow_dispatch", "workflow_call"} +ON = re.compile(r"""^(?:on|"on"|'on'):\s*(?:#.*)?$""") +EVENT = re.compile(r"^ ([A-Za-z_][A-Za-z0-9_-]*):") + +def inspect(path: Path) -> list[str]: + lines = path.read_text(encoding="utf-8").splitlines() + triggers = [] + found = False + for idx, line in enumerate(lines): + if not ON.fullmatch(line): + continue + if found: + return [f"{path.name}: multiple on sections"] + found = True + for sub in lines[idx + 1:]: + if sub and not sub.startswith((" ", "#")): + break + match = EVENT.match(sub) + if match: + triggers.append(match.group(1)) + if not found or not triggers: + return [f"{path.name}: missing or non-block YAML on declaration"] + return [f"{path.name}: forbidden GitHub Actions trigger {event!r}" + for event in triggers if event not in ALLOWED] + +def main() -> int: + errors = [] + if not WORKFLOWS.is_dir(): + errors.append("missing GitHub workflows directory") + else: + for path in sorted((*WORKFLOWS.glob("*.yml"), *WORKFLOWS.glob("*.yaml"))): + errors.extend(inspect(path)) + if errors: + for error in errors: + print("CI policy violation:", error, file=sys.stderr) + return 1 + print("GitHub Actions policy: manual/reusable only; Jenkins owns automatic CI") + return 0 + +if __name__ == "__main__": + sys.exit(main())