Skip to content

CI/CD Pipeline Security Controls Reference

Security controls applied across the Jerry Framework CI/CD pipeline workflows.

Document Sections

Section Purpose
Overview Workflows covered and control taxonomy
Pipeline Job Structure Jobs in ci.yml with purpose and trigger conditions
Permission Model Top-level and job-level GitHub Actions permissions
Push Trigger Scope Branch filter applied to push and pull_request events
SHA Pinning GitHub Actions pinned to commit SHAs
Pre-Commit Hook Pinning External pre-commit hooks pinned to full SHAs
UV Binary Pinning astral-sh/setup-uv version pinning
Frozen Lockfile Enforcement Lockfile enforcement across workflows
Dependency Audit pip-audit scan via exported lockfile
Skip-Bump Guard Infinite-loop and double-bump prevention in version-bump.yml
SLSA Build Provenance Artifact attestation via actions/attest-build-provenance
SBOM Generation CycloneDX JSON software bill of materials attached to GitHub Releases
Dependabot Configuration Risk-tiered dependency update management
Scheduled Security Scan Daily pip-audit for transitive CVE detection
CODEOWNERS Required-review paths for security-sensitive files
H-05 Compliance UV-only Python environment enforcement in CI

Overview

The Jerry Framework CI/CD pipeline spans five GitHub Actions workflow files and one Dependabot configuration. The security controls documented here apply to supply chain integrity, reproducible builds, permission scoping, and workflow loop prevention.

Workflow files covered:

File Purpose
.github/workflows/ci.yml Main CI pipeline: static analysis, security, validation, plugin checks, CLI integration, test matrix
.github/workflows/version-bump.yml Automated version bump on push to main
.github/workflows/release.yml GitHub Release creation on v* tag push
.github/workflows/docs.yml MkDocs deployment to GitHub Pages
.github/workflows/pat-monitor.yml Weekly VERSION_BUMP_PAT liveness check
.github/dependabot.yml Automated dependency update configuration

Pipeline Job Structure

All jobs are defined in .github/workflows/ci.yml.

Job summary:

Job Trigger Runs on Purpose
static-analysis push, pull_request ubuntu-latest ruff check, ruff format --check, pyright
security push, pull_request ubuntu-latest pip-audit full lockfile scan, banned YAML API check
validation push, pull_request ubuntu-latest lockfile freshness, HARD rule ceiling, version sync, SPDX headers, adversarial templates, agent frontmatter
plugin-validation push, pull_request ubuntu-latest plugin manifests, hook wrapper syntax, hook script syntax, plugin.json agent sync
cli-integration push, pull_request ubuntu-latest subprocess CLI tests, MkDocs e2e validation, jerry --help, jerry --version
test-uv push, pull_request ubuntu/windows/macos x Python 3.11–3.14 (8 cells) pytest suite with coverage
changelog-check pull_request only ubuntu-latest Validates CHANGELOG.md updated in PR
ci-success always (gate) ubuntu-latest Aggregates all required job results; branch protection target

ci-success required jobs:

static-analysis, security, validation, plugin-validation, cli-integration, test-uv, changelog-check

changelog-check result of skipped is treated as passing (it only runs on pull_request events; push events skip it).

static-analysis job

Merged replacement for the former lint and type-check jobs.

Step Command
Check linting uv run ruff check . --config=pyproject.toml
Check formatting uv run ruff format --check . --config=pyproject.toml
Run type check uv run pyright src/

Dependencies installed via: uv sync --frozen --extra dev

security job

Step Command
Export dependencies uv export --no-hashes --frozen --all-extras --no-emit-project > /tmp/requirements.txt
Audit dependencies uv run pip-audit --requirement /tmp/requirements.txt --strict --desc
Check for banned YAML APIs grep -rn 'yaml\.load\(' and yaml\.unsafe_load\( in src/

The banned YAML API check enforces M-04b: yaml.load() and yaml.unsafe_load() are prohibited; yaml.safe_load() is the required alternative. Exit code 1 on any match.

validation job

Consolidates six formerly independent single-script jobs into one runner. Each former job is a named step.

Step Former job Validation
Verify uv.lock is fresh lockfile-check uv lock --check
Check HARD rule ceiling hard-rule-ceiling uv run python scripts/check_hard_rule_ceiling.py
Validate version consistency version-sync uv run python scripts/sync_versions.py --check
Validate SPDX license headers license-headers uv run python scripts/check_spdx_headers.py
Validate adversarial strategy templates template-validation uv run python scripts/validate_templates.py --verbose
Validate agent and skill frontmatter frontmatter-validation uv run jerry agents validate-frontmatter

Dependencies installed via: uv sync --frozen

plugin-validation job

Step Command
Validate plugin manifests uv run python scripts/validate_plugin_manifests.py
Validate hook wrappers syntax python3 -m py_compile hooks/session-start.py hooks/pre-compact.py hooks/pre-tool-use.py hooks/user-prompt-submit.py
Validate hook scripts syntax uv run python -m py_compile for each .py in hooks/ and scripts/
Validate plugin.json agent sync uv run python scripts/check_plugin_agent_sync.py

Dependencies installed via: uv sync --frozen --extra dev

cli-integration job

Step Command
Run CLI subprocess tests uv run pytest tests/integration/cli/test_jerry_cli_subprocess.py -v --tb=short
Run MkDocs e2e validation tests uv run pytest tests/e2e/test_mkdocs_research_validation.py -v --tb=short
Verify jerry --help works PYTHONPATH="." uv run jerry --help
Verify jerry --version works PYTHONPATH="." uv run jerry --version

Dependencies installed via: uv sync --frozen --extra dev --extra test

test-uv job

Parameter Value
OS matrix ubuntu-latest, windows-latest, macos-latest
Python matrix 3.11, 3.12, 3.13, 3.14
Excluded cells windows+3.11, windows+3.12, macos+3.11, macos+3.12
Active cells 8
fail-fast false
Coverage threshold --cov-fail-under=80
Skip marker [skip-coverage] in commit message disables --cov-fail-under
Excluded test markers llm, subprocess

Coverage artifact upload and Codecov upload occur only for the ubuntu-latest + 3.14 cell.

changelog-check job

Parameter Value
Trigger pull_request events only
Exempt actors dependabot[bot], github-actions[bot]
Exempt marker [skip-changelog] in PR title
Validation CHANGELOG.md must appear in git diff --name-only BASE...HEAD and contain new content under [Unreleased]
Shell injection prevention PR title passed via env: block (CLCHK-001), not inline ${{ }} expansion

ci-success job

Parameter Value
if condition always()
Required results success for all jobs except changelog-check
changelog-check accepted results success or skipped

Permission Model

Top-level permissions in ci.yml:

permissions:
  contents: read

All jobs in ci.yml inherit contents: read unless overridden at the job level.

Job-level permission overrides in ci.yml:

No job in ci.yml overrides the top-level contents: read permission. No job receives pull-requests: write or any other elevated permission.

Workflow-level permissions in release.yml:

permissions:
  contents: write

Job-level permissions on release job:

release:
  permissions:
    contents: write
    id-token: write
    attestations: write
Permission Scope Value Rationale
contents workflow write Required to create the GitHub Release and upload release artifacts via gh release create
id-token release job only write Required by actions/attest-build-provenance to request an OIDC token from GitHub's identity provider for signing the attestation
attestations release job only write Required by actions/attest-build-provenance to write the signed attestation to the repository's attestation store

The id-token: write and attestations: write permissions are scoped to the release job only. The validate, ci, and build jobs inherit only contents: write from the workflow-level block.


Push Trigger Scope

ci.yml trigger configuration:

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master, "claude/**"]

Push events trigger CI only on main and master. The wildcard "**" is not used. This prevents CI from running on every developer branch push.

Pull request events trigger CI for PRs targeting main, master, or any claude/** branch.


SHA Pinning

GitHub Actions referenced by immutable commit SHA rather than floating version tag. A human-readable version comment accompanies each SHA.

SHA-to-version mapping (current):

Action SHA Version
actions/checkout de0fac2e4500dabe0009e67214ff5f5447ce83dd v6.0.2
astral-sh/setup-uv cec208311dfd045dd5311c1add060b2062131d57 v8.0.0
actions/upload-artifact 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a v7.0.1
actions/download-artifact 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c v8.0.1
codecov/codecov-action 57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 v6.0.0
actions/github-script 3a2844b7e9c422d3c10d287c895573f7108da1b3 v9.0.0
actions/attest-build-provenance e8998f949152b193b063cb0ec769d69d929409be v2

Workflows where SHA-pinned actions appear:

Action ci.yml version-bump.yml release.yml docs.yml pat-monitor.yml
actions/checkout Yes Yes Yes Yes No
astral-sh/setup-uv Yes (all uv jobs) No Yes Yes No
actions/upload-artifact Yes (test-uv) No Yes No No
actions/download-artifact No No Yes (release) No No
codecov/codecov-action Yes (test-uv) No No No No
actions/github-script No No No No Yes
actions/attest-build-provenance No No Yes (release) No No

Maintenance: Dependabot monitors the github-actions ecosystem and opens pull requests when new versions of pinned actions are available. See Dependabot Configuration.

Syntax example:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Pre-Commit Hook Pinning

External hooks declared in .pre-commit-config.yaml are pinned to full commit SHAs in the rev field. Local hooks (repo: local) execute scripts from the repository and are not subject to this pinning requirement.

External hook SHA-to-version mapping (current):

Hook repository SHA Version
pre-commit/pre-commit-hooks cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b v5.0.0
astral-sh/ruff-pre-commit 73413df07b4ab0bf103ca1ae73c7cec5c0ace593 v0.9.2
commitizen-tools/commitizen b494c556437473519f8ab69020c7256ba84714c1 v4.4.1

A human-readable version comment accompanies each SHA in .pre-commit-config.yaml.

Syntax example:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b  # v5.0.0

Maintenance: Dependabot monitors .pre-commit-config.yaml via the pre-commit ecosystem entry. See Dependabot Configuration. The .pre-commit-config.yaml file is protected by CODEOWNERS (see CODEOWNERS), requiring @geekatron review before any change is merged.


UV Binary Pinning

The astral-sh/setup-uv action accepts a version parameter. All workflows that install uv specify version: "0.10.9".

Value: "0.10.9"

Workflows that apply this pin:

Workflow Job(s)
ci.yml static-analysis, security, validation, plugin-validation, cli-integration, test-uv
version-bump.yml bump
release.yml validate, ci
docs.yml deploy

Configuration example:

- name: Install uv
  uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
  with:
    version: "0.10.9"

Frozen Lockfile Enforcement

uv sync --frozen reads the existing uv.lock without modification. If the lockfile is inconsistent with pyproject.toml, --frozen exits with an error rather than silently updating the lockfile.

Effect of --frozen in CI:

Behavior Without --frozen With --frozen
Resolution mode Full re-resolution against CI Python version Reads existing lockfile verbatim
uv.lock modification Possible (platform marker or bound changes) Not possible
Working tree state May become dirty Remains clean
bump-my-version compatibility Fails if working tree is dirty Passes

Workflows and jobs using uv sync --frozen:

Workflow Job Command
ci.yml static-analysis uv sync --frozen --extra dev
ci.yml security uv sync --frozen (dependency export only; see Dependency Audit)
ci.yml validation uv sync --frozen
ci.yml plugin-validation uv sync --frozen --extra dev
ci.yml cli-integration uv sync --frozen --extra dev --extra test
ci.yml test-uv uv sync --frozen --extra test
version-bump.yml bump (Install project dependencies) uv sync --frozen
version-bump.yml bump (Validate version sync) uv sync --frozen
release.yml validate uv sync --frozen
release.yml ci uv sync --frozen --extra dev --extra test
docs.yml deploy uv sync --frozen --extra dev
security-scan.yml pip-audit uv sync --frozen --all-extras

Clean working tree guard (version-bump.yml):

After uv sync --frozen but before applying the version bump, version-bump.yml runs an explicit check:

- name: Ensure clean working tree
  if: steps.bump.outputs.type != 'none'
  run: |
    if [[ -n "$(git status --porcelain)" ]]; then
      echo "::error::Working tree unexpectedly dirty before bump (uv sync --frozen should prevent this)"
      git diff --name-only
      exit 1
    fi

validation job lockfile freshness check:

The validation job runs uv lock --check (the lockfile-check step) before installing dependencies. This verifies the lockfile is consistent with pyproject.toml without modifying it.


Dependency Audit

The security job scans the full locked dependency tree for known CVEs via pip-audit.

Export command:

uv export --no-hashes --frozen --all-extras --no-emit-project > /tmp/requirements.txt
Flag Effect
--no-hashes Produces pip-compatible requirements format without hash directives
--frozen Reads the existing lockfile; does not re-resolve
--all-extras Includes all optional dependency groups (dev, test, etc.)
--no-emit-project Excludes the project package itself from the output

Audit command:

uv run pip-audit --requirement /tmp/requirements.txt --strict --desc
Flag Effect
--requirement Audits the specified requirements file rather than the active environment
--strict Exits non-zero if any package cannot be audited
--desc Includes vulnerability descriptions in output

The security job does not install standalone tools via pip install. All execution is via uv run within the project virtual environment.


Skip-Bump Guard

The version-bump workflow (version-bump.yml) runs on every push to main. The job-level if expression prevents infinite loops and double-bumping.

Guard conditions:

Trigger Condition for job to run
workflow_dispatch !contains(github.event.head_commit.message, '[skip-bump]')
All other triggers (push to main) !contains(github.event.head_commit.message, '[skip-bump]') AND github.actor != 'github-actions[bot]'

[skip-bump] marker:

When a commit message contains the string [skip-bump], the version-bump job does not run for that commit, regardless of how the workflow was triggered.

github.actor check:

github.actor is the authenticated identity set by GitHub from the token used to push the commit. It is not the git config user.name value. The version-bump commit is pushed using github-actions[bot] as the actor. Checking github.actor rather than the commit author name prevents spoofing via git config user.name "github-actions[bot]".

Complete if expression:

if: >-
  (
    github.event_name == 'workflow_dispatch' &&
    !contains(github.event.head_commit.message, '[skip-bump]')
  ) ||
  (
    github.event_name != 'workflow_dispatch' &&
    !contains(github.event.head_commit.message, '[skip-bump]') &&
    github.actor != 'github-actions[bot]'
  )

Prerelease input validation:

When workflow_dispatch is used with a prerelease label, the label is validated as alphanumeric-only before being passed to shell commands:

if [[ -n "$PRERELEASE" && ! "$PRERELEASE" =~ ^[a-zA-Z0-9]+$ ]]; then
  echo "::error::Invalid prerelease label '$PRERELEASE'. Must be alphanumeric (e.g., alpha, beta, rc)."
  exit 1
fi

SLSA Build Provenance

SLSA Level: 2

Workflow: release.yml, release job

Action: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be (v2)

The release job generates a signed SLSA provenance attestation for all release artifacts before creating the GitHub Release. The attestation is written to GitHub's attestation store and associates each artifact with the exact workflow run, repository, and commit SHA that produced it.

Attested artifacts:

Artifact pattern Description
dist/*.tar.gz Plugin archive (tar format)
dist/*.zip Plugin archive (zip format)
dist/checksums.sha256 SHA-256 checksums for both archives
dist/sbom.cyclonedx.json CycloneDX JSON SBOM (see SBOM Generation)

Action configuration:

- name: Generate SLSA provenance attestation
  uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
  with:
    subject-path: |
      dist/*.tar.gz
      dist/*.zip
      dist/checksums.sha256
      dist/sbom.cyclonedx.json

Required permissions: id-token: write and attestations: write are scoped to the release job level. See Permission Model.

Verification: Consumers can verify release artifact attestations using the GitHub CLI:

gh attestation verify <artifact-file> --repo geekatron/jerry

SBOM Generation

Workflow: release.yml, release job

Format: CycloneDX JSON

Generator: cyclonedx-bom via uv run --with cyclonedx-bom

The release job generates a CycloneDX JSON SBOM for the project's locked dependency tree before creating the GitHub Release.

Generation command:

uv run --with cyclonedx-bom cyclonedx-py environment --of JSON -o dist/sbom.cyclonedx.json

Attested artifact:

Artifact Description
dist/sbom.cyclonedx.json CycloneDX JSON SBOM for the release dependency tree

The SBOM is included in the subject-path glob passed to actions/attest-build-provenance, associating it with the same SLSA provenance attestation as the plugin archives and checksums file. See SLSA Build Provenance.

The SBOM is attached to the GitHub Release alongside the plugin archives and checksums file via gh release create.

Required permissions: No additional permissions beyond those declared for the release job. See Permission Model.


Dependabot Configuration

File: .github/dependabot.yml

Dependabot monitors three package ecosystems with risk-tiered grouping.

Risk-tiered update handling:

Update Type uv GitHub Actions Review Level
Patch + Minor Grouped (1 PR) Grouped (1 PR) CI green = merge
Major Individual PR Individual PR Manual review required
Security Individual PR Individual PR Priority review
Transitive Excluded (allow: direct) N/A Updates via parent dep

Transitive dependency policy: Dependabot only opens PRs for direct dependencies declared in pyproject.toml. Transitive dependencies are excluded to prevent incompatible standalone bumps.

Compensating control: The scheduled security scan runs pip-audit daily against the locked dependency tree, catching transitive CVEs that Dependabot does not surface.

Ecosystems configured:

Ecosystem Directory Schedule Day Commit prefix PR limit Grouping
github-actions / weekly Monday ci 10 minor+patch grouped
uv / weekly Monday deps 10 minor+patch grouped, direct only
pre-commit / weekly Monday chore 10 none (individual PRs)

Note: bump-my-version is declared as a dev dependency in pyproject.toml and tracked in uv.lock. Dependabot monitors it via the uv ecosystem alongside all other direct dependencies.

Note: ruff uses 0.x versioning where 0.x to 0.(x+1) is semantically equivalent to a major release (new default-enabled lint rules). Dependabot classifies these as "minor."


Scheduled Security Scan

File: .github/workflows/security-scan.yml

Schedule: Daily at 06:00 UTC.

Permissions:

Permission Value Rationale
contents read Checkout only; no write operations

Job steps:

Step Command
Install dependencies uv sync --frozen --all-extras
Run pip-audit uv run pip-audit --strict --desc
Verify pip-audit executed Checks /tmp/pip-audit-output.txt for non-empty output

--all-extras installs all optional dependency groups (dev, test, transcript) before scanning. This matches the coverage of the security job in ci.yml, which exports with --all-extras before auditing. Without --all-extras, optional dependencies declared only in extras groups would be absent from the scheduled scan's virtual environment, leaving their transitive chains unaudited.

Failure behavior: Vulnerabilities found causes the workflow to fail with exit code 1 and posts results to the job summary. A separate verification step catches pip-audit silent failures (empty output).


CODEOWNERS

File: .github/CODEOWNERS

Defines required reviewers for paths containing security-sensitive files. GitHub enforces at least one approval from a listed owner before a pull request targeting those paths can be merged, subject to branch protection rules.

Pattern evaluation: GitHub evaluates CODEOWNERS entries top-to-bottom; the last matching pattern wins.

Protected paths:

Path pattern Owner Scope
.github/workflows/ @geekatron All CI/CD workflow files
.github/dependabot.yml @geekatron Dependabot configuration
.github/CODEOWNERS @geekatron The CODEOWNERS file itself
.pre-commit-config.yaml @geekatron Pre-commit hook configuration
.context/rules/ @geekatron Framework constraint rule files
docs/governance/ @geekatron Governance documents

H-05 Compliance

H-05 requires that all Python execution uses uv run and that dependencies are managed with uv add. Direct invocation of python, pip, or pip3 is prohibited.

Application in CI:

All jobs in ci.yml use uv sync --frozen to install dependencies and uv run to execute Python. No job uses pip install to install tools into the runner's system Python environment. The security job previously used standalone pip-installed tools; after EPIC-003, it uses uv run pip-audit exclusively.

Compliant pattern (uv-managed environment):

- name: Install uv
  uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
  with:
    version: "0.10.9"
- name: Set up Python
  run: uv python install 3.14
- name: Install dependencies
  run: uv sync --frozen
- name: Run script
  run: uv run python scripts/example.py

release.yml H-05 compliance note:

release.yml uses uv sync --frozen and uv run exclusively. The comment # EN-001/F-001: Use uv directly (H-05 compliance). No pip fallback. marks this migration.

VERSION_BUMP_PAT (pat-monitor.yml):

The PAT monitor workflow performs only curl API calls and JavaScript execution via actions/github-script. It does not install Python dependencies and has no H-05 applicability.

version-bump.yml PAT usage:

The version-bump workflow checks out with a personal access token (VERSION_BUMP_PAT) rather than the default GITHUB_TOKEN to allow the bump commit to push through branch protection rules. The PAT is a fine-grained token scoped to the geekatron/jerry repository with Contents: Read and write permission and a 90-day expiration. The PAT Monitor workflow (pat-monitor.yml) runs weekly to detect expiration before it causes a version-bump failure.


  • Explanation: docs/explanation/ci-cd-supply-chain-security.md — Supply chain security model
  • Reference: pyproject.toml — Python dependency declarations and tool configuration
  • Source: .github/workflows/ci.yml — Authoritative pipeline definition