From 57670e98911f085a6eabb464d17b30dc696bb231 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 7 Nov 2026 16:14:09 -0600 Subject: [PATCH 1/2] ci: build on the shared ci-workflows node.yaml, remove GitHub workflows Gitea CI runs the uv script-quality and source-header checks, typecheck, lint, check and bundle through KofTwentyTwo/ci-workflows node.yaml (Node 24, pinned uv). Removes the retired GitHub Actions workflows (ci, codeql, codeql-analysis, pr-gates, security) and the CodeQL-only scripts. The EX-0015 uri-js guard keeps its lock/integrity/license pins; its check of the pr-gates.yml allowlist entry goes with that file. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build.yaml | 23 +++++ .github/scripts/check-codeql-sarif.py | 108 -------------------- .github/scripts/check-script-quality.py | 9 +- .github/scripts/test-scan-gates.py | 107 -------------------- .github/scripts/test-script-quality.py | 32 ++---- .github/source-headers.json | 1 + .github/workflows/ci.yml | 100 ------------------ .github/workflows/codeql-analysis.yml | 128 ------------------------ .github/workflows/codeql.yml | 21 ---- .github/workflows/pr-gates.yml | 29 ------ .github/workflows/security.yml | 26 ----- CONTRIBUTING.md | 4 +- 12 files changed, 35 insertions(+), 553 deletions(-) create mode 100644 .gitea/workflows/build.yaml delete mode 100644 .github/scripts/check-codeql-sarif.py delete mode 100644 .github/scripts/test-scan-gates.py delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/pr-gates.yml delete mode 100644 .github/workflows/security.yml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..0e7503d --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,23 @@ +# Copyright 2026 Kingsrook, LLC +# SPDX-License-Identifier: Apache-2.0 + +name: build +on: + push: +jobs: + node: + uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@main + with: + node-version: '24' + install: npm ci --ignore-scripts + uv: true + commands: | + uv sync --locked + uv run --locked --no-sync python .github/scripts/test-script-quality.py + uv run --locked --no-sync python .github/scripts/check-script-quality.py + uv run --locked --no-sync python .github/scripts/test-source-headers.py + uv run --locked --no-sync python .github/scripts/check-source-headers.py + npm run typecheck + npm run lint + npm run check + npm run bundle diff --git a/.github/scripts/check-codeql-sarif.py b/.github/scripts/check-codeql-sarif.py deleted file mode 100644 index 7177466..0000000 --- a/.github/scripts/check-codeql-sarif.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -"""Reject missing/malformed CodeQL evidence before enforcing the severity gate.""" - -import json -import math -import os -from pathlib import Path -from typing import TypedDict - -RuleProperties = TypedDict("RuleProperties", {"security-severity": str | float | None}, total=False) - - -class Rule(TypedDict): - """Keep the rule identity and optional severity metadata used by the gate.""" - - id: str - properties: RuleProperties - - -class Component(TypedDict, total=False): - """Describe CodeQL driver or extension rule metadata.""" - - name: str - rules: list[Rule] - - -class Tool(TypedDict, total=False): - """Group the driver and optional extension rule catalogs.""" - - driver: Component - extensions: list[Component] - - -class Result(TypedDict, total=False): - """Describe only the result fields consumed by the severity gate.""" - - ruleId: str - rule: dict[str, str] - suppressions: list[object] - - -class Run(TypedDict, total=False): - """Retain invocation success and findings for one analyzed language.""" - - tool: Tool - results: list[Result] - invocations: list[dict[str, bool]] - - -class Report(TypedDict, total=False): - """Represent SARIF fields while retaining the explicit runtime validation.""" - - version: str - runs: list[Run] - - -MAX_SEVERITY = 10 - - -def check(directory: str | Path, language: str, threshold: float) -> bool: # noqa: PLR0912 -- Keep the ordered evidence checks together; none may be skipped. - """Validate complete CodeQL evidence and report findings at the threshold.""" - if not math.isfinite(threshold) or not 0 <= threshold <= MAX_SEVERITY: - raise ValueError("Invalid security severity threshold") - canonical = {"java-kotlin": "java", "javascript-typescript": "javascript", "c-cpp": "cpp"}.get(language, language) - files = sorted(Path(directory).glob("*.sarif")) - if not canonical or not any(path.name == canonical + ".sarif" for path in files): - raise ValueError("Missing expected CodeQL SARIF report for " + language) - blocking: list[tuple[float, str]] = [] - for path in files: - report: Report = json.loads(path.read_text(encoding="utf-8")) - runs = report.get("runs") - if report.get("version") != "2.1.0" or not isinstance(runs, list) or not runs: - raise ValueError("Invalid or empty SARIF runs: " + path.name) - for run in runs: - driver = run.get("tool", {}).get("driver", {}) - results = run.get("results") - if driver.get("name") != "CodeQL" or not isinstance(driver.get("rules"), list) or not isinstance(results, list): - raise ValueError("Missing CodeQL tool/rules/results evidence: " + path.name) - rules: dict[str, Rule] = {} - for component in [driver, *run.get("tool", {}).get("extensions", [])]: - for rule in component.get("rules", []): - rules[rule["id"]] = rule - if not rules: - raise ValueError("CodeQL report has no analyzed rule metadata: " + path.name) - for invocation in run.get("invocations", []): - if invocation.get("executionSuccessful") is False: - raise ValueError("CodeQL invocation failed: " + path.name) - for result in results: - rule_id = result.get("ruleId") or result.get("rule", {}).get("id") - if rule_id not in rules: - raise ValueError("Result references missing rule metadata: " + path.name) - if result.get("suppressions"): - continue - severity = float(rules[rule_id].get("properties", {}).get("security-severity", 0) or 0) - if not math.isfinite(severity) or not 0 <= severity <= MAX_SEVERITY: - raise ValueError("Invalid rule severity: " + rule_id) - if severity >= threshold: - blocking.append((severity, rule_id)) - for severity, rule_id in sorted(blocking, reverse=True): - print(f"::error title=CodeQL security finding::{rule_id} ({severity})") - print(f"Validated {len(files)} SARIF report(s); {len(blocking)} blocking result(s)") - return bool(blocking) - - -if __name__ == "__main__": - raise SystemExit(1 if check(os.environ["SARIF_DIR"], os.environ["EXPECTED_LANGUAGE"], float(os.environ["THRESHOLD"])) else 0) diff --git a/.github/scripts/check-script-quality.py b/.github/scripts/check-script-quality.py index fa3b4df..b765829 100644 --- a/.github/scripts/check-script-quality.py +++ b/.github/scripts/check-script-quality.py @@ -5,7 +5,6 @@ import hashlib import json -import re import shutil import subprocess import sys @@ -16,12 +15,10 @@ PYTHON_SUFFIXES = (".py", ".pyi", ".pyw") SCRIPT_PATTERN = ".github/scripts/*.py" PYRIGHT_CONFIG = ".github" JAVASCRIPT_SUFFIXES = (".js", ".cjs", ".mjs", ".jsx", ".ts", ".mts", ".cts", ".tsx") -# EX-0015: the only npm package whose license classification the PR gate may exempt (the matcher ignores versions, so this guard pins it). -URI_JS_PURL = "pkg:npm/uri-js" +# EX-0015: the only npm package with a license exception; this guard pins the reviewed copy. URI_JS_VERSION = "4.4.1" URI_JS_INTEGRITY = "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" URI_JS_LICENSE_SHA256 = "0af366eff4c01ec147c9c61ea9e8ffad64a4294754c9d79355f3fd1b97cb2fb9" -PR_GATES = ".github/workflows/pr-gates.yml" # Microsoft's official npm Pyright, restored from package-lock.json by `npm ci --ignore-scripts`. PYRIGHT_ENTRY = Path(__file__).resolve().parents[2] / "node_modules" / "pyright" / "index.js" @@ -116,10 +113,6 @@ def check_uri_js_exception(root: Path) -> None: digest = hashlib.sha256((root / "node_modules" / "uri-js" / "LICENSE").read_bytes()).hexdigest() if digest != URI_JS_LICENSE_SHA256: raise ValueError(f"Installed uri-js LICENSE bytes differ from the reviewed EX-0015 copy ({digest})") - caller = [line for line in (root / PR_GATES).read_text(encoding="utf-8").splitlines() if not line.lstrip().startswith("#")] - listed = re.findall(r"pkg:npm/[^\s,]+", "\n".join(caller)) - if listed != [URI_JS_PURL]: - raise ValueError(f"EX-0015 permits only {URI_JS_PURL} in allow-dependencies-licenses, found {listed}") def main() -> None: diff --git a/.github/scripts/test-scan-gates.py b/.github/scripts/test-scan-gates.py deleted file mode 100644 index 2b173dd..0000000 --- a/.github/scripts/test-scan-gates.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -"""Exercise the exact CI guards using synthetic evidence only.""" - -import contextlib -import copy -import importlib.util -import io -import json -import sys -import tempfile -import unittest -from pathlib import Path -from types import ModuleType -from typing import Any, override - -sys.dont_write_bytecode = True - - -def load(name: str) -> ModuleType: - """Load the real sibling guard for synthetic evidence tests.""" - spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + ".py")) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -codeql = load("check-codeql-sarif") - - -class SarifTests(unittest.TestCase): - """Reject incomplete SARIF evidence and preserve severity enforcement.""" - - @override - def setUp(self) -> None: - """Create isolated synthetic evidence with cleanup after each test.""" - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.directory = Path(self.temporary.name) - self.report: dict[str, Any] = {"version": "2.1.0", "runs": [{"tool": {"driver": {"name": "CodeQL", "rules": [{"id": "synthetic", "properties": {"security-severity": "8.0"}}]}}, "results": []}]} - - def save(self, report: object) -> None: - """Write the test body as SARIF without validating it first.""" - (self.directory / "java.sarif").write_text(json.dumps(report), encoding="utf-8") - - def test_clean_report_with_zero_findings_passes(self) -> None: - """Verify clean report with zero findings passes.""" - self.save(self.report) - self.assertFalse(codeql.check(self.directory, "java-kotlin", 7)) - - def test_missing_report_or_wrong_language_fails(self) -> None: - """Verify missing report or wrong language fails.""" - with self.assertRaises(ValueError): - codeql.check(self.directory, "java-kotlin", 7) - self.save(self.report) - with self.assertRaises(ValueError): - codeql.check(self.directory, "csharp", 7) - - def test_empty_or_malformed_report_fails(self) -> None: - """Verify empty or malformed report fails.""" - for text in ["", "{", "null", "[]"]: - with self.subTest(text=text): - (self.directory / "java.sarif").write_text(text) - with self.assertRaises((ValueError, AttributeError)): - codeql.check(self.directory, "java-kotlin", 7) - - def test_missing_runs_tool_rules_or_results_fails(self) -> None: - """Verify missing runs tool rules or results fails.""" - variants: list[dict[str, Any]] = [{"version": "2.1.0"}, {"version": "2.1.0", "runs": []}] - for field in ["tool", "results"]: - bad = copy.deepcopy(self.report) - del bad["runs"][0][field] - variants.append(bad) - bad = copy.deepcopy(self.report) - bad["runs"][0]["tool"]["driver"]["rules"] = [] - variants.append(bad) - for bad in variants: - with self.subTest(report=bad): - self.save(bad) - with self.assertRaises(ValueError): - codeql.check(self.directory, "java-kotlin", 7) - - def test_high_finding_blocks_and_unknown_rule_fails(self) -> None: - """Verify high finding blocks and unknown rule fails.""" - self.report["runs"][0]["results"] = [{"ruleId": "synthetic"}] - self.save(self.report) - with contextlib.redirect_stdout(io.StringIO()): - self.assertTrue(codeql.check(self.directory, "java-kotlin", 7)) - self.report["runs"][0]["results"] = [{"ruleId": "missing"}] - self.save(self.report) - with self.assertRaises(ValueError): - codeql.check(self.directory, "java-kotlin", 7) - - def test_failed_invocation_and_invalid_threshold_fail(self) -> None: - """Verify failed invocation and invalid threshold fail.""" - self.report["runs"][0]["invocations"] = [{"executionSuccessful": False}] - self.save(self.report) - with self.assertRaises(ValueError): - codeql.check(self.directory, "java-kotlin", 7) - with self.assertRaises(ValueError): - codeql.check(self.directory, "java-kotlin", float("nan")) - - -if __name__ == "__main__": - unittest.main() diff --git a/.github/scripts/test-script-quality.py b/.github/scripts/test-script-quality.py index 48673bb..031a483 100644 --- a/.github/scripts/test-script-quality.py +++ b/.github/scripts/test-script-quality.py @@ -234,37 +234,29 @@ class JavaScriptProbeTests(unittest.TestCase): self.assertIn("TS2322", bad.stdout) -URI_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/uri-js\n" -URI_MIXED_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: >-\n pkg:nuget/Microsoft.WindowsAppSDK,\n pkg:npm/uri-js\n" - - def uri_lock(version: str = "4.4.1", integrity: str | None = None, dev: bool = True, direct: bool = False, consumer_dev: bool = True) -> dict[str, Any]: """Build a minimal package-lock shape with uri-js under an ESLint-like consumer.""" root_entry: dict[str, Any] = {"devDependencies": {"uri-js": version} if direct else {"eslint": "10.14.0"}} return {"packages": {"": root_entry, "node_modules/uri-js": {"version": version, "integrity": integrity or guard.URI_JS_INTEGRITY, "dev": dev}, "node_modules/eslint": {"dev": consumer_dev, "dependencies": {"uri-js": "^4.2.2"}}}} -def uri_project(root: Path, lock: dict[str, Any], gates: str = URI_GATES, license_bytes: bytes | None = None) -> None: - """Write a private project with the lock, installed LICENSE and PR caller that the guard inspects.""" +def uri_project(root: Path, lock: dict[str, Any], license_bytes: bytes | None = None) -> None: + """Write a private project with the lock and installed LICENSE that the guard inspects.""" (root / "package-lock.json").write_text(json.dumps(lock), encoding="utf-8") installed = root / "node_modules" / "uri-js" installed.mkdir(parents=True) installed.joinpath("LICENSE").write_bytes((ROOT / "node_modules" / "uri-js" / "LICENSE").read_bytes() if license_bytes is None else license_bytes) - workflows = root / ".github" / "workflows" - workflows.mkdir(parents=True) - (workflows / "pr-gates.yml").write_text("# EX-0015 pkg:npm/uri-js is cited here\n" + gates, encoding="utf-8") class UriJsExceptionTests(unittest.TestCase): """Allow the EX-0015 license exception only for the reviewed, unmodified, development-only uri-js.""" - def test_reviewed_uri_js_with_exact_caller_is_accepted(self) -> None: - """Accept the reviewed package, installed license bytes and an exact caller list, mixed with NuGet entries.""" - for gates in (URI_GATES, URI_MIXED_GATES): - with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates): - root = Path(name) - uri_project(root, uri_lock(), gates) - guard.check_uri_js_exception(root) + def test_reviewed_uri_js_is_accepted(self) -> None: + """Accept the reviewed package with the reviewed installed license bytes.""" + with tempfile.TemporaryDirectory() as name: + root = Path(name) + uri_project(root, uri_lock()) + guard.check_uri_js_exception(root) def test_other_version_integrity_scope_or_consumer_is_rejected(self) -> None: """Reject another version, integrity, non-dev or direct use, a non-dev consumer and a second copy.""" @@ -284,14 +276,6 @@ class UriJsExceptionTests(unittest.TestCase): with self.assertRaisesRegex(ValueError, "LICENSE bytes differ"): guard.check_uri_js_exception(root) - def test_caller_must_list_only_uri_js(self) -> None: - """Reject a caller that lists no npm exception, another package, a versioned form or a duplicate.""" - for gates in ("jobs:\n pr:\n uses: shared\n", URI_GATES.rstrip() + ", pkg:npm/other\n", "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/other\n", URI_GATES.rstrip() + "@4.4.1\n", URI_GATES.rstrip() + ", pkg:npm/uri-js\n"): - with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates), self.assertRaisesRegex(ValueError, "permits only"): - root = Path(name) - uri_project(root, uri_lock(), gates) - guard.check_uri_js_exception(root) - if __name__ == "__main__": unittest.main() diff --git a/.github/source-headers.json b/.github/source-headers.json index b9eefb5..d7b774e 100644 --- a/.github/source-headers.json +++ b/.github/source-headers.json @@ -3,6 +3,7 @@ "includes": [ ".github/*.yml", ".github/*.yaml", + ".gitea/*.yaml", ".github/scripts/*.py", "tools/*.mjs" ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 3c3e578..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -name: Protocol CI and release - -on: - pull_request: - push: - branches: [main] - tags: ['v*'] - workflow_dispatch: - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: '24' - package-manager-cache: false - - run: npm ci --ignore-scripts - - name: Set up uv - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - with: - version: '0.13.6' - enable-cache: false - - name: Restore locked Python tools - run: uv sync --locked - - name: Verify script quality gates and rejection paths - run: | - uv run --locked --no-sync python .github/scripts/test-script-quality.py - uv run --locked --no-sync python .github/scripts/check-script-quality.py - npm run typecheck - npm run lint - - name: Verify owned source notices and rejection paths - run: | - python3 .github/scripts/test-source-headers.py - python3 .github/scripts/check-source-headers.py - - run: npm run check - - run: npm run bundle - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: protocol-bundle - path: dist/* - if-no-files-found: error - - release-security: - if: startsWith(github.ref, 'refs/tags/v') - uses: ./.github/workflows/security.yml - permissions: - contents: read - security-events: write - - release-codeql: - if: startsWith(github.ref, 'refs/tags/v') - uses: ./.github/workflows/codeql.yml - permissions: - contents: read - security-events: write - - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: [verify, release-security, release-codeql] - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: write - id-token: write - attestations: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: protocol-bundle - path: dist - - name: Verify packaged checksums - working-directory: dist - run: sha256sum --check --strict SHA256SUMS - - name: Attest verified release artifacts - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: dist/* - - name: Publish versioned GitHub release assets - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - prerelease=() - if [[ "$GITHUB_REF_NAME" == *-* ]]; then prerelease+=(--prerelease); fi - gh release create "$GITHUB_REF_NAME" dist/*.tar.gz dist/SHA256SUMS \ - --verify-tag --title "$GITHUB_REF_NAME" --notes-file CHANGELOG.md "${prerelease[@]}" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 06ebaca..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -# Local copy of KofTwentyTwo/standards at fdf5ed029603c938615a32b1beca3628b8787945 (v0.1.2). -# Pins, inputs and analysis checks preserved; report validation now fails closed. -# Static analysis (K22-CI-22): CodeQL with the security-extended suite, failing on -# alerts of high severity or above. Called as: -# -# jobs: -# codeql: -# uses: KofTwentyTwo/standards/.github/workflows/codeql.yml@ # vX.Y.Z -# permissions: -# contents: read -# security-events: write -# with: -# languages: '["csharp", "actions"]' -# build-mode: manual -# build-command: dotnet build MySolution.slnx -p:Platform=x64 -# runs-on: windows-latest -# -# producing `codeql / analyze ()` for each language. The CodeQL action -# uploads results but never fails a build on them, so the last step reads the SARIF -# and fails on any result whose rule has security-severity >= the threshold. -# Suppress a false positive with a query filter in a CodeQL config file passed as -# `config-file`, citing the exception record in a comment. -name: codeql - -on: - workflow_call: - inputs: - languages: - description: JSON array of CodeQL languages, e.g. '["csharp","actions"]'. - type: string - required: true - build-mode: - description: CodeQL build mode for compiled languages (none, autobuild, manual). Interpreted languages always use none. - type: string - default: autobuild - build-command: - description: Command run for build-mode manual. - type: string - default: "" - runs-on: - description: Runner label; Windows-only builds (WinUI, WPF) need windows-latest. - type: string - default: ubuntu-24.04 - setup-dotnet: - description: Install the .NET SDK from global.json before building. - type: boolean - default: false - config-file: - description: Optional path to a CodeQL configuration file in the calling repository. - type: string - default: "" - fail-on-security-severity: - description: Minimum CVSS-style security-severity that fails the job (7.0 = high). - type: string - default: "7.0" - -permissions: {} - -jobs: - analyze: - name: analyze (${{ matrix.language }}) - runs-on: ${{ inputs.runs-on }} - timeout-minutes: 60 - permissions: - contents: read - security-events: write # upload SARIF results to code scanning - strategy: - fail-fast: false - matrix: - language: ${{ fromJSON(inputs.languages) }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Test scan guard rejection paths - shell: python - run: | - import runpy - runpy.run_path(".github/scripts/test-scan-gates.py", run_name="__main__") - - - name: Set up .NET SDK - if: inputs.setup-dotnet - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json - - - name: Initialize CodeQL - uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language) && 'none' || inputs.build-mode }} - queries: security-extended - config-file: ${{ inputs.config-file }} - - - name: Build (manual mode) - if: inputs.build-mode == 'manual' && !contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language) - shell: bash - env: - BUILD_COMMAND: ${{ inputs.build-command }} - run: | - if [[ -z "$BUILD_COMMAND" ]]; then - echo "::error::build-mode is manual but build-command is empty" - exit 1 - fi - eval "$BUILD_COMMAND" - - - name: Analyze - uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 - with: - category: /language:${{ matrix.language }} - output: codeql-sarif - # Fork pull requests have a read-only token; analyze still runs and gates. - upload: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'never' || 'always' }} - - - name: Gate on security severity - shell: python - env: - SARIF_DIR: codeql-sarif - THRESHOLD: ${{ inputs.fail-on-security-severity }} - EXPECTED_LANGUAGE: ${{ matrix.language }} - run: | - import runpy - runpy.run_path(".github/scripts/check-codeql-sarif.py", run_name="__main__") diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 49cd59e..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -name: codeql -on: - pull_request: - push: - branches: [main] - schedule: - - cron: '41 6 * * 1' - workflow_call: -permissions: {} -jobs: - codeql: - uses: ./.github/workflows/codeql-analysis.yml - permissions: - contents: read - security-events: write - with: - languages: '["javascript-typescript", "actions"]' - build-mode: none diff --git a/.github/workflows/pr-gates.yml b/.github/workflows/pr-gates.yml deleted file mode 100644 index cc10d2e..0000000 --- a/.github/workflows/pr-gates.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -# Pull request gates (K22-CI-01): Conventional Commit title, DCO sign-off, and -# dependency review, from the shared KofTwentyTwo/standards workflow pinned by SHA -# (K22-CI-11). Dependabot's github-actions ecosystem keeps the pin current. -name: pr-gates - -on: - pull_request: - -permissions: {} - -concurrency: - group: pr-gates-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - pr: - uses: KofTwentyTwo/standards/.github/workflows/pr.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2 - with: - # EX-0015: development-only uri-js 4.4.1 (ESLint transitive; BSD-2-Clause plus an authors' views sentence). - # https://git.kof22.com/KofTwentyTwo/standards/src/commit/a18179e90f8e6d35ba9f5e583249885c066e52b8/exceptions/register.md#ex-0015 - # The matcher ignores versions; check-script-quality.py pins the version, lock integrity and installed - # license bytes and rejects any other pkg:npm entry here. - allow-dependencies-licenses: pkg:npm/uri-js - permissions: - contents: read # read the commits being merged - pull-requests: read # read the pull request title and dependency diff diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 020c4b7..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2026 Kingsrook, LLC -# SPDX-License-Identifier: Apache-2.0 - -name: security - -on: - pull_request: - push: - branches: [main] - schedule: - - cron: '23 6 * * 1' - workflow_dispatch: - workflow_call: - -permissions: {} - -concurrency: - group: security-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - security: - uses: KofTwentyTwo/standards/.github/workflows/security.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2 - permissions: - contents: read - security-events: write diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c01e921..7e2abb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,6 @@ npm run typecheck npm run lint ``` -`npm run quality` runs the repository's Python sequence in one step (`uv sync --locked`, both script-quality commands and both source-header commands); the shared Node CI calls it as its `quality-script`. It needs `uv` on `PATH` and `npm ci` first. +`npm run quality` runs the repository's Python sequence in one step (`uv sync --locked`, both script-quality commands and both source-header commands); Gitea CI (`.gitea/workflows/build.yaml`, the shared ci-workflows `node.yaml`) runs the same commands individually, then typecheck, lint, check and bundle. It needs `uv` on `PATH` and `npm ci` first. -`check-script-quality.py` fails for any tracked Python outside `.github/scripts/` or JavaScript outside the `tsconfig.json` include list, and requires Pyright's analyzed-file count to equal the selected scripts. Pyright must run as `pyright -p .github`: it silently skips dot directories from a root config. Documented exceptions: CLI `print`/`console.log` only in `check-*.py` and `tools/`; synthetic test assertions and boundary constants; narrow inline `noqa` comments for fixed no-shell Git/interpreter calls and ordered fail-closed validators. Do not loosen a rule or add a type waiver to pass. `package.json` overrides `minimatch` to 11.0.1, the patched release for [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (affected before 11.0.1); keep it until ESLint's own dependency range includes a patched release. The only npm license exception, [EX-0015](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0015) for development-only `uri-js` 4.4.1, is enforced by `check-script-quality.py` (version, lock integrity, installed license bytes and the exact `pkg:npm/uri-js` caller entry); changing that package or the entry needs renewed approval. +`check-script-quality.py` fails for any tracked Python outside `.github/scripts/` or JavaScript outside the `tsconfig.json` include list, and requires Pyright's analyzed-file count to equal the selected scripts. Pyright must run as `pyright -p .github`: it silently skips dot directories from a root config. Documented exceptions: CLI `print`/`console.log` only in `check-*.py` and `tools/`; synthetic test assertions and boundary constants; narrow inline `noqa` comments for fixed no-shell Git/interpreter calls and ordered fail-closed validators. Do not loosen a rule or add a type waiver to pass. `package.json` overrides `minimatch` to 11.0.1, the patched release for [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (affected before 11.0.1); keep it until ESLint's own dependency range includes a patched release. The only npm license exception, [EX-0015](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0015) for development-only `uri-js` 4.4.1, is enforced by `check-script-quality.py` (version, lock integrity and installed license bytes); changing that package needs renewed approval. -- 2.54.0 From 177db4b06b09626aac20cd412debc800272a69e4 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 7 Nov 2026 16:15:36 -0600 Subject: [PATCH 2/2] ci: pin ci-workflows node.yaml to @v1 Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 0e7503d..04e0376 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -6,7 +6,7 @@ on: push: jobs: node: - uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@main + uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1 with: node-version: '24' install: npm ci --ignore-scripts -- 2.54.0