From ef380d2874a4846c7b272ff596b89db7c210c08c Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Tue, 16 Jun 2026 14:13:43 +0530 Subject: [PATCH 01/13] infra[notask]: add qvac-ci pending-approval workflow and CODEOWNERS --- .github/CODEOWNERS | 1 + .github/workflows/check-pending-approval.yml | 47 ++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/check-pending-approval.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..86ec9227ce --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @tetherto/qvac-internal-merge @tetherto/qvac-internal-mgmt diff --git a/.github/workflows/check-pending-approval.yml b/.github/workflows/check-pending-approval.yml new file mode 100644 index 0000000000..b19b53d242 --- /dev/null +++ b/.github/workflows/check-pending-approval.yml @@ -0,0 +1,47 @@ +name: Check Approvals + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, labeled, synchronize] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + check-approvals: + name: Check Approvals + runs-on: ubuntu-latest + continue-on-error: true + if: >- + ( + github.event_name == 'issue_comment' && + github.event.comment.body == 'pending_reviews' && + github.event.issue.pull_request != null + ) || ( + github.event_name == 'pull_request_target' && + github.event.action == 'opened' + ) || ( + github.event_name == 'pull_request_target' && + (github.event.action == 'labeled' || github.event.action == 'synchronize') && + contains(github.event.pull_request.labels.*.name, 'verified') + ) + steps: + - name: Install @qvac/ci + run: npm install -g @qvac/ci@0.1.0 + + - name: Check Approvals + continue-on-error: true + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_APP_ID: ${{ secrets.APP_ID }} + GITHUB_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + run: | + qvac-ci pending-approvals \ + --pr-number "${{ github.event.pull_request.number || github.event.issue.number }}" \ + --maintainers-team 'qvac-internal-mgmt' \ + --team-leads-team 'qvac-internal-merge' \ + --min-approvals '2' From b16e2baab5eb92783b558f6caedca669cd7d4c17 Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Tue, 16 Jun 2026 15:59:35 +0530 Subject: [PATCH 02/13] infra[notask]: rename workflow file to check-approvals.yml --- .../workflows/{check-pending-approval.yml => check-approvals.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{check-pending-approval.yml => check-approvals.yml} (100%) diff --git a/.github/workflows/check-pending-approval.yml b/.github/workflows/check-approvals.yml similarity index 100% rename from .github/workflows/check-pending-approval.yml rename to .github/workflows/check-approvals.yml From f7b8d2c5e4244998861e8bc1f8f934d71b49f6ea Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Tue, 16 Jun 2026 19:04:03 +0530 Subject: [PATCH 03/13] chore(ci): remove labeled trigger and verified label condition Remove the `labeled` trigger type from pull_request_target and the associated `verified` label guard from the if condition. The check now fires on opened and synchronize only, without requiring a label. --- .github/workflows/check-approvals.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-approvals.yml b/.github/workflows/check-approvals.yml index b19b53d242..0077a9745a 100644 --- a/.github/workflows/check-approvals.yml +++ b/.github/workflows/check-approvals.yml @@ -4,7 +4,7 @@ on: issue_comment: types: [created] pull_request_target: - types: [opened, labeled, synchronize] + types: [opened, synchronize] permissions: contents: read @@ -23,11 +23,7 @@ jobs: github.event.issue.pull_request != null ) || ( github.event_name == 'pull_request_target' && - github.event.action == 'opened' - ) || ( - github.event_name == 'pull_request_target' && - (github.event.action == 'labeled' || github.event.action == 'synchronize') && - contains(github.event.pull_request.labels.*.name, 'verified') + (github.event.action == 'opened' || github.event.action == 'synchronize') ) steps: - name: Install @qvac/ci From 1cb653b99d1ed7d84fc6cc0f701e1226865f3fff Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Wed, 17 Jun 2026 13:18:12 +0530 Subject: [PATCH 04/13] chore(ci): restrict check-approvals trigger to main and master branches --- .github/workflows/check-approvals.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-approvals.yml b/.github/workflows/check-approvals.yml index 0077a9745a..913f17aec0 100644 --- a/.github/workflows/check-approvals.yml +++ b/.github/workflows/check-approvals.yml @@ -5,6 +5,7 @@ on: types: [created] pull_request_target: types: [opened, synchronize] + branches: [main, master] permissions: contents: read From 088ff4332e50c5b957cd4f45d9083342c5f83ad6 Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Tue, 23 Jun 2026 18:55:40 +0530 Subject: [PATCH 05/13] infra[notask]: add concurrency, timeout, and fix issue-comment trigger in check-approvals (#15) --- .github/workflows/check-approvals.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-approvals.yml b/.github/workflows/check-approvals.yml index 913f17aec0..a55ddc1618 100644 --- a/.github/workflows/check-approvals.yml +++ b/.github/workflows/check-approvals.yml @@ -12,15 +12,20 @@ permissions: pull-requests: write issues: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} + cancel-in-progress: false + jobs: check-approvals: name: Check Approvals runs-on: ubuntu-latest + timeout-minutes: 30 continue-on-error: true if: >- ( github.event_name == 'issue_comment' && - github.event.comment.body == 'pending_reviews' && + contains(github.event.comment.body, 'review') && github.event.issue.pull_request != null ) || ( github.event_name == 'pull_request_target' && From 5e27830fbd26913533e9a45843b48f80cc559eeb Mon Sep 17 00:00:00 2001 From: sidj-thr Date: Thu, 25 Jun 2026 18:11:54 +0530 Subject: [PATCH 06/13] QVAC-21374 chore: bump @qvac/ci to 0.2.0 (#16) --- .github/workflows/check-approvals.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-approvals.yml b/.github/workflows/check-approvals.yml index a55ddc1618..c093d4f47e 100644 --- a/.github/workflows/check-approvals.yml +++ b/.github/workflows/check-approvals.yml @@ -33,7 +33,7 @@ jobs: ) steps: - name: Install @qvac/ci - run: npm install -g @qvac/ci@0.1.0 + run: npm install -g @qvac/ci@0.2.0 - name: Check Approvals continue-on-error: true From 705b7a4233c262e8367d469eb54b6fd41c4b2215 Mon Sep 17 00:00:00 2001 From: GSServita Date: Tue, 7 Jul 2026 12:09:33 +0100 Subject: [PATCH 07/13] QVAC-21550 infra: roll out canonical security baseline (TruffleHog + CodeQL) to qvac-ext-stable-diffusion.cpp (#17) * QVAC-21550 infra: add canonical security baseline caller (TruffleHog + CodeQL) * QVAC-21550 fix: drop paths-exclude (unsupported by reusable security v0) * QVAC-21550 fix: buildless CodeQL (codeql-build-mode: none) via qvac-actions#10 * QVAC-21550 fix: drop secrets: inherit (baseline needs no repo secrets; github.token suffices) * QVAC-21550 fix: repin security baseline to qvac-actions 0.2.0 (buildless c-cpp) --- .github/workflows/security-baseline.yml | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/security-baseline.yml diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml new file mode 100644 index 0000000000..4b78950191 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,49 @@ +name: Security baseline + +# Canonical security baseline (TruffleHog secret scan + CodeQL static analysis) +# rolled out to Tier-1 repos per QVAC-21550. This is a thin caller that +# delegates to the org's reusable workflow (authored under QVAC-19055); its +# inputs and behavior are documented in +# tetherto/qvac-actions/docs/security-baseline.md. +# +# Uses plain `pull_request` (never `pull_request_target`): fork PRs run with a +# read-only token and no secrets, and this baseline needs none of the +# privileged context (`codeql-upload: never` below), so the "pwn request" +# surface of `pull_request_target` buys nothing here. + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + baseline: + # Pinned to the qvac-actions 0.2.0 release commit (immutable SHA + version + # comment, per the org freeze-and-pin convention). + uses: tetherto/qvac-actions/.github/workflows/public-reusable-security.yml@be1d22629cc48b6dcc36645acc9e6d89cfaf54d8 # 0.2.0 + permissions: + contents: read + security-events: write + pull-requests: write + actions: read + with: + # C/C++ fork: set languages explicitly (CodeQL autodetect misses C/C++). + languages: c-cpp + severity-threshold: high + # These repos use GitHub "Default setup" for code scanning, which rejects + # SARIF from advanced workflows. Skip the upload; the severity gate and + # TruffleHog still run (SARIF is written locally for the gate to read). + codeql-upload: never + # Buildless C/C++ extraction (no compilation / submodule checkout). + # Uniform across every Tier-1 C/C++ caller and robust vs autobuild on + # native/submodule projects. Requires qvac-actions >= 0.2.0. + codeql-build-mode: none From e417a9ead43ca86d6e8822670ace0629bf9ff88f Mon Sep 17 00:00:00 2001 From: GSServita Date: Thu, 30 Jul 2026 13:14:23 +0100 Subject: [PATCH 08/13] QVAC-22747 infra: add weekly schedule to CodeScan caller (#23) The canonical CodeScan baseline currently runs on push/PR only, so the QVAC-19056 commitment to a weekly scan is unmet. Add a scheduled cron (staggered per repo across the fleet) alongside the existing push / pull_request / workflow_dispatch triggers. No other change. --- .github/workflows/security-baseline.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml index 4b78950191..335bb805c7 100644 --- a/.github/workflows/security-baseline.yml +++ b/.github/workflows/security-baseline.yml @@ -16,6 +16,9 @@ on: branches: - master pull_request: + schedule: + # Weekly full scan (QVAC-19056 rollout). Staggered per repo across weekdays. + - cron: "0 7 * * 0" workflow_dispatch: permissions: From 48361f9f1fd21e2d595f7b683d64189aa17d772a Mon Sep 17 00:00:00 2001 From: GSServita Date: Fri, 31 Jul 2026 12:00:10 +0100 Subject: [PATCH 09/13] QVAC-22740 infra: bump CodeScan caller to qvac-actions 0.3.0 (#26) Bump the reusable security workflow pin 0.2.0 -> 0.3.0 (SHA bbb0740e). 0.3.0 adds the findings-export artifact (export-report default on), so each run now publishes a downloadable security-scan-report (findings.json/md + SARIF). --- .github/workflows/security-baseline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml index 335bb805c7..aa5c8a96f4 100644 --- a/.github/workflows/security-baseline.yml +++ b/.github/workflows/security-baseline.yml @@ -32,7 +32,7 @@ jobs: baseline: # Pinned to the qvac-actions 0.2.0 release commit (immutable SHA + version # comment, per the org freeze-and-pin convention). - uses: tetherto/qvac-actions/.github/workflows/public-reusable-security.yml@be1d22629cc48b6dcc36645acc9e6d89cfaf54d8 # 0.2.0 + uses: tetherto/qvac-actions/.github/workflows/public-reusable-security.yml@bbb0740e2a16b94371c7439e0e06945c5b68e759 # 0.3.0 permissions: contents: read security-events: write From 88c1e8b7466cb85b41f7ab52bc9b52853f072b7f Mon Sep 17 00:00:00 2001 From: GSServita Date: Fri, 4 Sep 2026 09:59:06 +0100 Subject: [PATCH 10/13] QVAC-14347 infra: centralize CI runner labels in a shared catalog (#36) Add .github/runners.yaml as the single source of truth for specialized CI runner labels, a generated reusable-runner-names workflow that exports them as job outputs, and sync/validate scripts with unit tests. Wire build.yml's Windows jobs to consume the output instead of hardcoding windows-2022. Rolling -latest aliases and matrix.os identities stay hardcoded. --- .github/RUNNERS.md | 51 ++++ .github/runners.yaml | 17 ++ .github/scripts/lib/runner-names.mjs | 255 ++++++++++++++++++++ .github/scripts/sync-runner-names.mjs | 15 ++ .github/scripts/validate-runner-names.mjs | 47 ++++ .github/workflows/build.yml | 14 +- .github/workflows/reusable-runner-names.yml | 27 +++ .github/workflows/runner-names-validate.yml | 32 +++ 8 files changed, 454 insertions(+), 4 deletions(-) create mode 100644 .github/RUNNERS.md create mode 100644 .github/runners.yaml create mode 100644 .github/scripts/lib/runner-names.mjs create mode 100644 .github/scripts/sync-runner-names.mjs create mode 100644 .github/scripts/validate-runner-names.mjs create mode 100644 .github/workflows/reusable-runner-names.yml create mode 100644 .github/workflows/runner-names-validate.yml diff --git a/.github/RUNNERS.md b/.github/RUNNERS.md new file mode 100644 index 0000000000..1d1da77a9a --- /dev/null +++ b/.github/RUNNERS.md @@ -0,0 +1,51 @@ +# Runner label catalog + +CI runner labels live in one place: [`.github/runners.yaml`](./runners.yaml). +This lets a runner-image migration (OS bump, hosted-image retirement) update a +single file instead of grepping every workflow. + +## How it works + +`runners.yaml` is the source of truth. A generated reusable workflow, +[`.github/workflows/reusable-runner-names.yml`](./workflows/reusable-runner-names.yml), +exports each catalog entry as a job output. Callers pull the label from that +output instead of hardcoding it, because `runs-on:` is evaluated before any step +runs, so a composite action cannot supply the label — a reusable workflow's +outputs can. + +A catalog value is one of: + +- a **scalar** label (`windows-2022`) — consumed as + `runs-on: ${{ needs.runner_names.outputs. }}` +- a **composite label set** (`[self-hosted, Linux, X64]`) — exported as a JSON + array string and consumed as + `runs-on: ${{ fromJSON(needs.runner_names.outputs.) }}` + +Rolling `-latest` aliases (`ubuntu-latest`, `macos-latest`, `windows-latest`) +are intentionally left hardcoded — they are GitHub aliases, not fleet labels. +`matrix.os` values (and `matrix.os == '...'` conditionals) are frozen logical +identities and are not managed by the catalog. + +## Wiring a job + +```yaml +jobs: + runner_names: + permissions: + contents: read + uses: ./.github/workflows/reusable-runner-names.yml + + my-job: + needs: runner_names + runs-on: ${{ needs.runner_names.outputs.windows_2022 }} + steps: ... +``` + +## Changing a label + +1. Edit `.github/runners.yaml`. +2. Regenerate: `node .github/scripts/sync-runner-names.mjs` +3. Test: `node --test .github/scripts/test/runner-names.test.mjs` + +CI enforces both invariants via +[`runner-names-validate.yml`](./workflows/runner-names-validate.yml). diff --git a/.github/runners.yaml b/.github/runners.yaml new file mode 100644 index 0000000000..ab4613d62b --- /dev/null +++ b/.github/runners.yaml @@ -0,0 +1,17 @@ +# Specialized GitHub Actions runner labels for qvac-ext-stable-diffusion.cpp CI. +# Edit this file, then run: node .github/scripts/sync-runner-names.mjs +# +# ubuntu-latest / macos-latest / windows-latest are intentionally omitted; keep +# them hardcoded as GitHub's rolling aliases. +# +# FORMAT CONTRACT: a flat `key: value # comment` list, one entry per line. The +# parser (.github/scripts/lib/runner-names.mjs) is line-based, not YAML. A value +# is EITHER a bare single label (`windows-2022`) OR a flow array of bare tokens +# (`[self-hosted, Linux, X64]`). Do not quote values or nest maps (the parser +# fails loudly). +# +# SCOPE: this catalog governs only where jobs RUN (runs-on / matrix.runner). It +# does NOT manage `matrix.os` identities or `matrix.os == '...'` conditionals. + +# GitHub-hosted (pinned images) +windows_2022: windows-2022 # Windows Server 2022 hosted x64 diff --git a/.github/scripts/lib/runner-names.mjs b/.github/scripts/lib/runner-names.mjs new file mode 100644 index 0000000000..a9a3c0fce2 --- /dev/null +++ b/.github/scripts/lib/runner-names.mjs @@ -0,0 +1,255 @@ +/** + * Shared helpers for the qvac-ext-stable-diffusion.cpp runner-label catalog. + * + * Source of truth: .github/runners.yaml + * Generated consumer: .github/workflows/reusable-runner-names.yml + * + * A catalog entry is either a scalar label or an ordered array of labels + * (composite self-hosted set). Array entries are exported as a JSON-array + * string and consumed with `fromJSON(...)` in runs-on. + */ +import { readdirSync, readFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +export const repoRoot = resolve(here, '../../..') + +export const RUNNERS_YAML = '.github/runners.yaml' +export const REUSABLE_WORKFLOW = '.github/workflows/reusable-runner-names.yml' +export const REUSABLE_USES = './.github/workflows/reusable-runner-names.yml' + +// Workflows wired to the catalog and validated. Orchestration-only files that +// use nothing but ubuntu-latest / the org reusable (check-approvals, +// security-baseline) stay out of scope. +const ADDON_WORKFLOWS = new Set(['build.yml']) + +const KEY = '[a-z][a-z0-9_]*' +const COMMENT = '(?:#\\s*(.*\\S))?' +const SCALAR_RE = new RegExp(`^(${KEY}):\\s+([^#\\[\\s'"][^#]*?)\\s*${COMMENT}\\s*$`) +const ARRAY_RE = new RegExp(`^(${KEY}):\\s+\\[([^\\]]+)\\]\\s*${COMMENT}\\s*$`) + +export function parseRunnersYaml(source) { + const runners = [] + const seenKeys = new Set() + const seenTargets = new Set() + + for (const line of source.split(/\r?\n/)) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + + const arrayMatch = line.match(ARRAY_RE) + const scalarMatch = arrayMatch ? null : line.match(SCALAR_RE) + if (!arrayMatch && !scalarMatch) { + throw new Error(`invalid runners.yaml line (expected "key: label" or "key: [a, b]"): ${JSON.stringify(line)}`) + } + + let entry + if (arrayMatch) { + const [, key, body, description = ''] = arrayMatch + const labels = body.split(',').map((token) => token.trim()).filter(Boolean) + if (labels.length === 0) throw new Error(`empty runner array: ${JSON.stringify(line)}`) + for (const label of labels) { + if (/["'{}[\]]/.test(label)) { + throw new Error(`runner label must be bare, not quoted/structured: ${JSON.stringify(line)}`) + } + } + entry = { key, kind: 'array', labels, description } + } else { + const [, key, rawLabel, description = ''] = scalarMatch + const label = rawLabel.trim() + if (/["'{}[\]]/.test(label)) { + throw new Error(`runner label must be bare, not quoted/structured: ${JSON.stringify(line)}`) + } + entry = { key, kind: 'scalar', label, description } + } + + const target = targetKeyString(entry) + if (seenKeys.has(entry.key)) throw new Error(`duplicate runner key: ${entry.key}`) + if (seenTargets.has(target)) throw new Error(`duplicate runner target: ${target}`) + seenKeys.add(entry.key) + seenTargets.add(target) + runners.push(entry) + } + + if (runners.length === 0) throw new Error('runners.yaml has no entries') + return runners +} + +/** Canonical string for a target, used for duplicate detection + matching. */ +export function targetKeyString(entry) { + return entry.kind === 'array' ? `[${entry.labels.join(',')}]` : entry.label +} + +/** The output value the reusable exports: scalar label, or compact JSON array. */ +export function outputValue(entry) { + return entry.kind === 'array' ? JSON.stringify(entry.labels) : entry.label +} + +/** How a caller references the output in `runs-on:`. */ +export function runsOnExpression(entry) { + const ref = `needs.runner_names.outputs.${entry.key}` + return entry.kind === 'array' ? `\${{ fromJSON(${ref}) }}` : `\${{ ${ref} }}` +} + +export function loadRunners() { + return parseRunnersYaml(readFileSync(join(repoRoot, RUNNERS_YAML), 'utf8')) +} + +export function listAddonWorkflows() { + const directory = join(repoRoot, '.github/workflows') + return readdirSync(directory) + .filter((name) => ADDON_WORKFLOWS.has(name)) + .map((name) => `.github/workflows/${name}`) + .sort() +} + +export function renderReusableWorkflow(runners) { + const outputsBlock = runners + .map((entry) => { + const description = entry.description || `Runner target ${targetKeyString(entry)}` + return [ + ` ${entry.key}:`, + ` description: ${yamlDoubleQuoted(description)}`, + ` value: \${{ jobs.export.outputs.${entry.key} }}`, + ].join('\n') + }) + .join('\n') + + const jobOutputs = runners + .map((entry) => ` ${entry.key}: \${{ steps.export.outputs.${entry.key} }}`) + .join('\n') + + const exportLines = runners + .map((entry) => ` echo '${entry.key}=${outputValue(entry)}' >> "$GITHUB_OUTPUT"`) + .join('\n') + + return `# AUTO-GENERATED by .github/scripts/sync-runner-names.mjs +# Source of truth: ${RUNNERS_YAML} +# Do not edit this file by hand. + +name: Runner names + +on: + workflow_call: + outputs: +${outputsBlock} + +permissions: + contents: read + +jobs: + export: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: +${jobOutputs} + steps: + - name: Export runner labels + id: export + run: | +${exportLines} +` +} + +export function assertReusableMatchesCatalog(runners, reusableSource) { + const expected = renderReusableWorkflow(runners) + if (normalizeNewlines(reusableSource) !== normalizeNewlines(expected)) { + throw new Error(`${REUSABLE_WORKFLOW} is out of date. Run: node .github/scripts/sync-runner-names.mjs`) + } +} + +const LABEL_BOUNDARY_BEFORE = `(^|[\\s"'\\[,])` +const LABEL_BOUNDARY_AFTER = `([\\s"'\\],]|$)` + +function escapeLabel(label) { + return label.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function normalizeArrayTokens(body) { + return body.split(',').map((token) => token.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean) +} + +function isCommentLine(line) { + return /^\s*#/.test(line) +} + +/** + * Flags catalog targets hardcoded in `runs-on:` where they select the machine. + * Handles scalar forms (bare / quoted / inside a `${{ }}` expression) and the + * composite flow-array form. Frozen logical identities are intentionally NOT + * flagged: `matrix.os` values and `matrix. == '