
Enterprise Readiness
- 56 installs
- 4 repo stars
- Updated August 3, 2026
- netresearch/enterprise-readiness-skill
Helps with ai & agent building tasks.
About
enterprise-readiness is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- enterprise-readiness
- AI & Agent Building
- AI-coding skill
Enterprise Readiness by the numbers
- 56 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,668 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netresearch/enterprise-readiness-skill --skill enterprise-readinessAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 4 |
| Last updated | August 3, 2026 |
| Repository | netresearch/enterprise-readiness-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Enterprise Readiness Assessment
Production/enterprise tier only — see references/tier-framing.md.When to Use
- Production/enterprise readiness evaluations
- Supply chain security: SLSA provenance, cosign signing, SBOMs
- CI/CD hardening, workflow permissions
- OpenSSF Best Practices (Passing/Silver/Gold), OSPS Baseline (L1/2/3)
- Scorecard optimization (Token-Permissions, Branch-Protection, Pinned-Deps)
- Code review, ADRs, changelogs, SECURITY.md
Assessment Workflow
1. Discovery: Identify platform, languages, existing CI/CD, dependabot.yml 2. Scoring: Apply checklists; check Scorecard, badge criteria, coverage 3. Gap Analysis: List missing controls by severity 4. Implementation: Apply fixes (SHA-pin actions, harden permissions, add workflows) 5. Verification: Re-score and compare
Mandatory Workflows & Badges
Required coverage: CI, CodeQL, Scorecard, dependency review, composer audit, SBOM — as dedicated workflows or jobs calling the netresearch reusable. Badges: CI, Codecov, Scorecard, Best Practices, Baseline. See references/badges-and-workflows.md.
Key Hardening Patterns
- Permissions: Declare
permissions: contents: readat workflow-level; grant write only per-job - SHA pinning: Third-party actions pinned to SHA with version comment (
# v4.2.0). Org-internal reusable workflows use@main - Harden-Runner:
step-security/harden-runneras first step in every job; preferegress-policy: blockwith allowed-endpoints - Dependabot: Configure
dependabot.ymlwith all ecosystems (composer,npm,github-actions,docker); set up auto-merge workflow for dependency PRs usingpull_request_target - Coverage: Upload via
codecov-action; configurecodecov.ymlwith patch coverage threshold - Duplicate CI prevention: Scope
push:trigger tobranches: [main]whenpull_request:is also present - SLSA provenance: Use
actions/attest-build-provenancewithid-token: writeandattestations: writepermissions; verify withgh attestation verify - Security policy: Create
SECURITY.mdwith vulnerability disclosure process and response SLA (Critical: 7 days, High: 30 days)
Critical Rules
- NEVER interpolate
${{ github.event.* }}or${{ inputs.* }}inrun:blocks (script injection) - NEVER guess action versions -- fetch from GitHub API and verify SHA against tags
- ALWAYS include
https://URLs in badge justifications - ALWAYS configure auto-merge for repos with Dependabot/Renovate
References
| Reference | Use |
|---|---|
references/general.md | Always |
references/scorecard-playbook.md | Scorecard optimization |
references/badges-and-workflows.md | Badge URLs, workflows |
references/mandatory-requirements.md | Checklist |
references/ci-patterns.md | CI/CD, hooks |
references/code-review.md | PR quality |
references/documentation.md | ADRs, changelogs |
references/slsa-provenance.md | SLSA Level 3 |
references/signed-releases.md | Cosign/GPG |
references/openssf-badge-silver.md | Silver |
references/openssf-badge-gold.md | Gold |
references/openssf-badge-baseline.md | OSPS Baseline |
references/harden-runner-guide.md | Harden-Runner |
references/solo-maintainer-guide.md | N/A criteria |
references/npm-pnpm-supply-chain.md | pnpm |
Related skills: go-development, github-project, security-audit, git-workflow.
# Example checkpoints.yaml for enterprise-readiness skill
# Copy this to your skill repo root as checkpoints.yaml
version: 1
skill_id: enterprise-readiness
mechanical:
# === SECURITY WORKFLOW CHECKS ===
# Security coverage is satisfied by either:
# (a) a dedicated .github/workflows/security.yml file (the historical
# layout that the previous checkpoints required); or
# (b) a job in any workflow that delegates to the netresearch reusable
# workflow (uses: netresearch/{typo3-ci-workflows,.github}/.github/
# workflows/security.yml). Quoting after `uses:` is optional in YAML.
# `command` is used so we can express the OR cleanly across file existence
# and content patterns. Empty stderr is suppressed; exit 0 = pass.
# Patterns use POSIX char classes ([[:space:]]) and `[.]` instead of \. to
# avoid YAML+shell+regex escape stacking. The runner's YAML line parser
# strips outer quotes but doesn't decode YAML escape sequences, so we use
# double-quoted YAML scalars and single-quoted shell args (which don't need
# YAML escaping).
#
# ER-01: pure-pipeline rewrite for the runner allowlist (no ; && || $() `).
# Two evidence streams concatenated via `cat` + process substitution:
# 1. find emits .github/workflows/security.yml if it exists
# 2. grep -RlE emits paths of any workflow delegating to the netresearch
# reusable security workflow
# If either stream produces a path, the final `grep -q .` succeeds (pass).
# Process substitution `<(...)` is allowlist-safe: it doesn't contain `$(`
# or any rejected metachar; the pipeline's first token is `cat` (allowed).
- id: ER-01
type: command
pattern: "cat <(find .github/workflows/security.yml -type f -print 2>/dev/null) <(grep -RlE 'uses:[[:space:]]+\"?netresearch/(typo3-ci-workflows|[.]github)/[.]github/workflows/security[.]yml' .github/workflows/ 2>/dev/null) | grep -q ."
severity: error
desc: "A security workflow must exist — either as .github/workflows/security.yml or as a reusable-workflow job"
- id: ER-02
type: command
pattern: "grep -RqE '(composer[[:space:]]+audit|uses:[[:space:]]+\"?netresearch/(typo3-ci-workflows|[.]github)/[.]github/workflows/security[.]yml)' .github/workflows/ 2>/dev/null"
severity: error
desc: "A workflow should run composer audit (directly or by delegating to a security reusable workflow)"
- id: ER-03
type: command
pattern: "grep -RqE '(sbom|cyclonedx|syft|uses:[[:space:]]+\"?netresearch/(typo3-ci-workflows|[.]github)/[.]github/workflows/security[.]yml)' .github/workflows/ 2>/dev/null"
severity: warning
desc: "A workflow should generate SBOM (directly via cyclonedx/syft or by delegating to a security reusable workflow)"
# === BADGE CHECKS ===
- id: ER-04
type: regex
target: README.md
pattern: 'scorecard\.dev|securityscorecards\.dev|api\.scorecard\.dev'
severity: warning
desc: "README should have OpenSSF Scorecard badge"
- id: ER-05
type: contains
target: README.md
pattern: "bestpractices.dev"
severity: warning
desc: "README should have OpenSSF Best Practices badge"
- id: ER-06
type: regex
target: README.md
pattern: 'slsa\.dev|slsa-github-generator|attestation'
severity: info
desc: "README should have SLSA badge or mention attestation (if provenance enabled)"
# === DEPENDENCY MANAGEMENT ===
- id: ER-07
type: file_exists
target: .github/dependabot.yml
severity: warning
desc: "Dependabot should be configured"
- id: ER-08
type: regex
target: .github/dependabot.yml
pattern: 'package-ecosystem:\s*"?(composer|gomod|npm|pip)"?'
severity: warning
desc: "Dependabot should monitor language-specific dependencies (composer, gomod, npm, pip)"
- id: ER-09
type: regex
target: .github/dependabot.yml
pattern: 'package-ecosystem:\s*"?github-actions"?'
severity: warning
desc: "Dependabot should monitor GitHub Actions"
# === ACTION PINNING ===
# IMPORTANT: Only THIRD-PARTY actions need SHA-pinning.
# Org-internal reusable workflows (org/repo/.github/workflows/*.yml@main)
# MUST use @main or @tag — SHA-pinning them degrades security by breaking
# centralized update propagation. A pinned org-internal reusable workflow
# is itself a finding (anti-pattern).
#
# Pure-delegation repos (only org reusable workflows, no direct third-party
# action uses) trivially satisfy the SHA-pinning rule because the reusable
# workflows pin their own deps. Accept that pattern.
- id: ER-10
type: regex
target: .github/workflows/*.yml
pattern: 'uses:[[:space:]]+[^@]+@[a-f0-9]{40}|uses:[[:space:]]+netresearch/(typo3-ci-workflows|[.]github)/[.]github/workflows/'
severity: warning
desc: "Third-party GitHub Actions must be pinned to SHA, OR all CI delegated to netresearch reusable workflows (which pin their own deps centrally)"
# === WORKFLOW PERMISSIONS ===
- id: ER-11
type: contains
target: .github/workflows/ci.yml
pattern: "permissions:"
severity: warning
desc: "CI workflow should declare explicit permissions"
- id: ER-12
type: regex
target: .github/workflows/ci.yml
pattern: 'contents:\s*read'
severity: warning
desc: "CI workflow should use minimal permissions (contents: read)"
# === COVERAGE ===
- id: ER-13
type: regex
target: .github/workflows/ci.yml
pattern: '\b(codecov|upload-coverage)\b'
severity: warning
desc: "CI should upload coverage to Codecov (or use upload-coverage input for typo3-ci-workflows)"
- id: ER-14
type: file_exists
target: codecov.yml
severity: info
desc: "Codecov config should exist for coverage thresholds"
# === SLSA PROVENANCE ===
# follow_uses: also inspect any reusable workflow referenced via `uses:` so
# delegation to netresearch/typo3-ci-workflows (release.yml,
# release-typo3-extension.yml) or skill-repo-skill release.yml that
# internally calls attest-build-provenance counts as a pass.
- id: ER-15
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'attest-build-provenance|attest@|build-go-attest|slsa-github-generator|uses:[[:space:]]+netresearch/typo3-ci-workflows/[.]github/workflows/(release|release-typo3-extension)[.]yml|uses:[[:space:]]+netresearch/skill-repo-skill/[.]github/workflows/release[.]yml'
severity: info
desc: "Provenance attestation: actions/attest-build-provenance (preferred), build-go-attest, slsa-github-generator, OR delegation to a netresearch reusable release workflow (typo3-ci-workflows, skill-repo-skill)"
# === REQUIRED WORKFLOWS ===
- id: ER-18
type: file_exists
target: .github/workflows/ci.yml
severity: error
desc: "CI workflow (ci.yml) must exist"
# CodeQL / Scorecard / dependency-review may be a dedicated workflow file OR
# a job in another workflow that delegates to the netresearch reusable
# workflow (netresearch/.github/.github/workflows/{codeql,scorecard,dependency-review}.yml).
# follow_uses: walks one hop into any reusable workflow referenced via
# `uses: owner/repo/.github/workflows/file.yml@ref` so an indirect chain
# (e.g. typo3-ci-workflows/security.yml → netresearch/.github/codeql.yml)
# still satisfies the inline-action pattern.
- id: ER-19
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'uses:[[:space:]]*github/codeql-action|uses:[[:space:]]+netresearch/[.]github/[.]github/workflows/codeql[.]yml|uses:[[:space:]]+[^[:space:]]*/[.]github/workflows/codeql[.]yml@'
severity: warning
desc: "CodeQL must be wired up (dedicated workflow, reusable-workflow job, or transitive delegation via netresearch/.github)"
- id: ER-20
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'uses:[[:space:]]*ossf/scorecard-action|uses:[[:space:]]+netresearch/[.]github/[.]github/workflows/scorecard[.]yml|uses:[[:space:]]+[^[:space:]]*/[.]github/workflows/scorecard[.]yml@'
severity: warning
desc: "OpenSSF Scorecard must be wired up (dedicated workflow, reusable-workflow job, or transitive delegation via netresearch/.github)"
- id: ER-21
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'uses:[[:space:]]*actions/dependency-review-action|uses:[[:space:]]+netresearch/[.]github/[.]github/workflows/dependency-review[.]yml|uses:[[:space:]]+[^[:space:]]*/[.]github/workflows/dependency-review[.]yml@'
severity: warning
desc: "Dependency review must be wired up (dedicated workflow, reusable-workflow job, or transitive delegation via netresearch/.github)"
# === REQUIRED BADGES ===
- id: ER-22
type: regex
target: README.md
pattern: "github.com/.*/actions/workflows"
severity: error
desc: "README must have CI status badge"
- id: ER-23
type: contains
target: README.md
pattern: "codecov.io"
severity: warning
desc: "README must have Codecov badge"
- id: ER-24
type: regex
target: README.md
pattern: "baseline\\.openssf\\.org|openssf\\.org.*baseline"
severity: info
desc: "README should have OpenSSF Baseline badge"
# === SCRIPT INJECTION PREVENTION ===
- id: ER-25
type: regex_not
target: .github/workflows/*.yml
pattern: 'run:.*\$\{\{\s*github\.event\.'
severity: error
desc: "Workflows must not interpolate github.event.* in run: blocks (script injection)"
# === WORKFLOW INPUT INJECTION PREVENTION ===
- id: ER-49
type: regex_not
target: .github/workflows/*.yml
pattern: 'run:.*\$\{\{\s*inputs\.'
severity: error
desc: "Workflow run: blocks must not interpolate ${{ inputs.* }} directly (code injection). Pass through env: block instead"
# === GITHUB ATTESTATION ===
# follow_uses: also inspect upstream content of any reusable workflow
# referenced via `uses:` so a release workflow that delegates entirely to
# an org reusable workflow (which itself runs attest-build-provenance) is
# detected.
- id: ER-26
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'attest-build-provenance|attest@|slsa-github-generator|uses:[[:space:]]+netresearch/typo3-ci-workflows/[.]github/workflows/(release|release-typo3-extension)[.]yml|uses:[[:space:]]+netresearch/skill-repo-skill/[.]github/workflows/release[.]yml|uses:[[:space:]]+[^[:space:]]*/[.]github/workflows/release[a-z0-9-]*[.]yml@'
severity: info
desc: "Release workflow should generate provenance attestation: actions/attest-build-provenance (preferred), slsa-github-generator, OR delegation to a netresearch reusable release workflow (typo3-ci-workflows, skill-repo-skill)"
# === SLSA-GENERATOR DEPRECATION CHECK ===
- id: ER-29
type: regex_not
target: .github/workflows/*.yml
pattern: 'slsa-framework/slsa-github-generator'
severity: warning
desc: "Should migrate from slsa-github-generator to actions/attest-build-provenance (slsa-github-generator internal actions use tag refs that conflict with SHA-pinning rulesets, see slsa-framework/slsa-github-generator#4440)"
# === SUPPLY CHAIN SECURITY ===
# harden-runner can live in caller workflows OR be added by the org
# reusable workflow (which standardises egress policy). Accept either.
- id: ER-27
type: regex
target: .github/workflows/*.yml
follow_uses: true
pattern: 'uses:[[:space:]]*step-security/harden-runner@[a-f0-9]{40}|uses:[[:space:]]+netresearch/(typo3-ci-workflows|[.]github)/[.]github/workflows/'
severity: warning
desc: "Workflows should include step-security/harden-runner for egress monitoring (pinned to SHA), OR delegate to a netresearch reusable workflow (which applies harden-runner centrally — detected transitively via follow_uses)"
# === SHA-PINNING VIOLATION DETECTION ===
- id: ER-28
type: regex_not
target: .github/workflows/*.yml
pattern: 'uses:\s+[^#\n]+@v\d+(?!\S*[a-f0-9]{40})\s'
severity: warning
desc: "GitHub Actions must not use tag-only references (@v4, @v6); pin to SHA with version comment (uses: owner/action@<sha> # vX.Y.Z) for supply chain security"
# === CI DUPLICATE RUN PREVENTION ===
# ER-30 moved to llm_reviews — mechanical regex cannot reliably detect duplicate CI triggers
# === PHPSTAN BASELINE ===
# A PHPStan baseline file is a *liability*, not a requirement: it
# suppresses known errors so they no longer fail the build (technical
# debt). The healthy state is to have NO baseline OR an EMPTY one
# (project passes PHPStan cleanly at its declared level).
#
# Pass when:
# - no baseline file exists, OR
# - baseline exists but contains zero `message:` entries (empty
# placeholder for tooling compatibility)
# Fail when baseline exists and has any `message:` entries — surface
# the technical debt for active reduction.
#
# Pure-pipeline form for the runner allowlist (no ; && || $() `):
# `cat` reads every candidate path silently (missing files vanish via
# 2>/dev/null), grep tests for `message:`, and the leading `!` inverts
# the exit so "no message: found" becomes pass. Checking all candidates
# at once is stricter than the original (which stopped at the first
# existing file), but functionally equivalent — any baseline with
# suppressed errors fails regardless of which path it lives at.
- id: ER-31
type: command
pattern: "! cat phpstan-baseline.neon Build/phpstan-baseline.neon Build/phpstan/phpstan-baseline.neon 2>/dev/null | grep -qF 'message:'"
severity: info
desc: "PHPStan baseline should be absent or empty (no suppressed errors). Non-empty baselines are tech debt — actively shrink them."
# === CODECOV CONFIG ===
- id: ER-32
type: regex
target: codecov.yml
pattern: 'patch:'
severity: info
desc: "Codecov config should define patch coverage target for realistic new-code coverage thresholds"
# === LABELER CONFIG ===
# Pass when no workflow references "labeler" OR .github/labeler.yml exists.
# Implementation: list workflows that mention "labeler"; for each, assert
# .github/labeler.yml exists. xargs -r skips when grep finds nothing
# (vacuous pass); xargs propagates non-zero when test -f fails.
# Avoids ; && || $() to satisfy the assessment runner allowlist.
- id: ER-43
type: command
pattern: "grep -rlF 'labeler' .github/workflows/ 2>/dev/null | xargs -r -I {} test -f .github/labeler.yml"
severity: warning
desc: "If CI workflows use a labeler job, .github/labeler.yml must exist to define label rules"
# === CHANGELOG COMPLETENESS ===
- id: ER-44
type: file_exists
target: CHANGELOG.md
severity: warning
desc: "CHANGELOG.md must exist to document release history"
# === RELEASE WORKFLOW PERMISSIONS ===
- id: ER-54
type: contains
target: .github/workflows/release.yml
pattern: "permissions:"
severity: warning
desc: "Release workflow should declare explicit permissions (principle of least privilege)"
# === REUSABLE WORKFLOW SHA-PINNING ===
# ER-33 moved to llm_reviews — mechanical regex cannot distinguish org-internal vs third-party workflows
# === MUTATION TESTING QUALITY ===
- id: ER-70
type: command
command: "test -f infection.json5 -o -f infection.json"
severity: warning
desc: "Infection config should exist for mutation testing"
# ER-71/ER-72: extract numeric value from infection config and assert >=90.
# Pure-pipeline form (no ; && || $()) so the assessment-runner allowlist
# accepts it. Pattern semantics:
# 1. cat both candidate files (only one usually exists)
# 2. grep for the key:value occurrence with a numeric value
# 3. extract the trailing integer
# 4. grep -qE matches when the integer is 90-99 or 100+ (i.e. >=90)
# Missing file or missing key => empty pipeline => grep -q fails => fail.
# Note: the previous `expected:` field was silently ignored by the runner
# for type=command checkpoints; the threshold is now enforced inline.
- id: ER-71
type: command
pattern: "cat infection.json5 infection.json 2>/dev/null | grep -oE '\"minMsi\"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | grep -qE '^(9[0-9]|100)$'"
severity: warning
desc: "Enterprise projects should have minMsi >= 90% for mutation testing"
- id: ER-72
type: command
pattern: "cat infection.json5 infection.json 2>/dev/null | grep -oE '\"minCoveredMsi\"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | grep -qE '^(9[0-9]|100)$'"
severity: warning
desc: "Enterprise projects should have minCoveredMsi >= 90%"
# === COVERAGE CONFIGURATION ===
# ER-73: pass unless codecov.yml exists AND its patch.target is exactly 100%.
# Pure-pipeline form (no ; && || $()) for the allowlist. Pattern semantics:
# 1. cat codecov.yml (silent if absent)
# 2. sed extracts the value(s) following `target:` inside the patch block
# 3. head -1 keeps the first occurrence
# 4. ! grep -qF '100%' negates: pass when 100% is NOT present
# No file => empty pipeline => grep finds nothing => ! makes it pass.
- id: ER-73
type: command
pattern: "! cat codecov.yml 2>/dev/null | sed -n '/patch:/,/target:/{ s/.*target:[[:space:]]*//p }' | head -1 | grep -qF '100%'"
severity: warning
desc: "codecov patch target should not be 100% — achievable only with full functional test coverage upload"
llm_reviews:
# === ACTION PINNING (companion to ER-10) ===
- id: ER-10b
domain: security
prompt: |
Check GitHub Actions pinning in workflow files:
1. Third-party actions (e.g., actions/checkout, codecov/codecov-action) MUST be pinned to SHA
2. Org-internal reusable workflows (uses: org/repo/.github/workflows/*.yml@ref) MUST use @main or @tag — this is CORRECT and REQUIRED, NOT a finding
3. Flag any third-party action using a tag (e.g., @v4) or branch instead of SHA
4. Do NOT flag org-internal reusable workflows using @main — SHA-pinning them is an ANTI-PATTERN that breaks centralized security update propagation
5. If an org-internal reusable workflow IS SHA-pinned, flag THAT as a warning (defeats the purpose of centralized workflow management)
severity: warning
desc: "Verify third-party actions are SHA-pinned. Org-internal reusable workflows MUST use @main or @tag (pinning them is an anti-pattern)."
# === SIGNED COMMITS ===
- id: ER-16
domain: security
prompt: |
Check if the repository requires signed commits:
1. Look for branch protection settings mentioning signatures
2. Check if .github/workflows mention commit signing
3. Check CONTRIBUTING.md for signing requirements. CONTRIBUTING.md is
a community-health file that GitHub auto-inherits from the org-wide
<owner>/.github repo ONLY when the project has NO local
CONTRIBUTING.md. The fallback does NOT supplement a present-but-
incomplete local file. Apply this rule:
- If the project has NO local CONTRIBUTING.md and the org-wide
<owner>/.github/CONTRIBUTING.md (e.g., netresearch/.github)
documents the required signing/commit conventions, treat the
requirement as satisfied. Do NOT flag absence of a project-local
CONTRIBUTING.md on its own in this case.
- If the project HAS a local CONTRIBUTING.md, evaluate it on its
own merits. The org-wide file is NOT consulted by GitHub in this
case and MUST NOT be used to suppress a finding. If the local
CONTRIBUTING.md does not document signing requirements, flag it
regardless of org-wide content.
- Only flag absence outright when neither the project nor (when
applicable per the fallback rule above) the org-wide repo
addresses signing requirements.
Note: CODEOWNERS does NOT inherit from org-wide .github — it must
live in the repo itself if needed.
severity: warning
desc: "Repository should require signed commits"
# === HARDEN-RUNNER REVIEW ===
- id: ER-27b
domain: security
prompt: |
Verify Harden-Runner configuration in workflow files:
1. Every job should have step-security/harden-runner as its FIRST step
2. Check egress-policy: prefer 'block' with allowed-endpoints over 'audit'
3. Verify harden-runner is SHA-pinned (not tag-referenced)
4. If block mode: verify allowed-endpoints are appropriate for the job type
severity: warning
desc: "Verify Harden-Runner is correctly configured in all workflow jobs with appropriate egress policy"
# === PHPUNIT VERSION COMPATIBILITY ===
- id: ER-34
domain: testing
prompt: |
Check for PHPUnit version compatibility issues:
1. Review composer.json for PHPUnit version constraints — broad ranges like "^11.0 || ^12.0" risk cross-version attribute failures
2. Check test files for PHPUnit 12-only attributes (e.g., #[AllowMockObjectsWithoutExpectations]) that break on PHPUnit 11
3. Verify CI matrix includes the lowest supported PHP version, which may pull an older PHPUnit
4. Flag any PHPUnit attribute not available in the minimum supported PHPUnit version
severity: warning
desc: "PHPUnit attributes and features must be compatible with all PHPUnit versions resolved across the CI PHP version matrix"
# === SLSA PROVENANCE REVIEW ===
- id: ER-17
domain: security
prompt: |
Verify SLSA provenance setup if present:
1. Preferred: actions/attest-build-provenance in a reusable workflow
hosted in the org .github repo (SLSA Build Level 3)
2. Legacy: slsa-github-generator — should be migrated away due to
SHA-pinning incompatibility (slsa-framework/slsa-github-generator#4440)
3. If using actions/attest: verify it's SHA-pinned and has
id-token: write + attestations: write permissions
4. Verification should use `gh attestation verify` (not slsa-verifier)
severity: info
desc: "SLSA provenance should use actions/attest-build-provenance (preferred) or slsa-github-generator"
# === PHPSTAN BASELINE CONTENT QUALITY ===
- id: ER-35
domain: quality
prompt: |
If PHPStan is not configured in the project (no phpstan.neon, no phpstan in composer.json require-dev), report N/A and skip this check.
Review PHPStan baseline file for quality:
1. Check if phpstan-baseline.neon exists and contains ignoreErrors entries
2. An empty baseline (ignoreErrors: []) is the ideal state — all issues fixed
3. Flag any baseline with >0 entries as needing review
4. Check if baseline entries are legitimate (complex generics, third-party types) vs. suppressions of real bugs
5. Flag any entry that suppresses error-level issues (should be fixed, not suppressed)
6. Verify the project is not adding NEW baseline entries (baseline should only shrink over time)
severity: warning
desc: "PHPStan baseline should be empty (ignoreErrors: []). All static analysis issues should be fixed, not suppressed. Review entries and plan to eliminate them"
# === REUSABLE WORKFLOW SHA PINNING (complements ER-10b for reusable workflows) ===
- id: ER-36
domain: security
prompt: |
Review reusable workflow references (pattern: uses: org/repo/.github/workflows/file.yml@ref) in .github/workflows/:
This checkpoint complements ER-10b (which covers GitHub Actions) by specifically targeting reusable workflow references.
1. Third-party reusable workflows (external organizations) MUST be SHA-pinned for supply chain security
2. Org-internal reusable workflows (same GitHub org) MUST use @main or @tag, NOT SHA-pinned
3. SHA-pinning org-internal workflows is an anti-pattern — it breaks centralized security update propagation
4. Flag: third-party workflow using @v tag without SHA (e.g., uses: external/repo/.github/workflows/ci.yml@v2)
5. Flag: org-internal workflow using SHA pin (e.g., uses: myorg/workflows/.github/workflows/ci.yml@abc123)
6. Correct patterns:
- Third-party: uses: external/repo/.github/workflows/ci.yml@abc123def456 # v2.1.0
- Org-internal: uses: myorg/workflows/.github/workflows/ci.yml@main
severity: warning
desc: "Third-party reusable workflows MUST use SHA pins. Org-internal reusable workflows MUST use @main/@tag for centralized security updates"
# === CI DUPLICATE RUN PREVENTION ===
- id: ER-30
domain: ci
prompt: |
Check GitHub Actions workflow files for duplicate CI run issues:
1. Find workflows that trigger on BOTH push: and pull_request:
2. If push: is present WITHOUT branch restrictions (e.g., branches: [main]),
pushing to a PR branch causes every job to run TWICE
3. Flag workflows where push: is unrestricted while pull_request: is also present
4. Do NOT flag workflows where push: is scoped to specific branches (e.g., push: branches: [main])
5. Do NOT flag workflows that only have one trigger
severity: warning
desc: "Workflows with both push and pull_request triggers must scope push to specific branches to prevent duplicate CI runs"
# === REUSABLE WORKFLOW SHA-PINNING ===
- id: ER-33
domain: security
prompt: |
Check reusable workflow references (uses: org/repo/.github/workflows/file.yml@ref):
1. Third-party reusable workflows (external organizations) MUST be SHA-pinned
2. Org-internal reusable workflows MUST use @main or @tag (NOT SHA-pinned)
3. A mechanical regex cannot distinguish org-internal from third-party — use context
4. Flag: third-party workflow using @v tag without SHA
5. Flag: org-internal workflow using SHA pin (anti-pattern)
severity: warning
desc: "Third-party reusable workflows must be pinned to commit SHAs, not tags, for supply chain security"
# === NPM DEPENDENCY MONITORING ===
- id: ER-37
name: npm audit runs in CI when package.json exists
type: llm_review
severity: warning
domain: supply-chain
prompt: |
If package.json exists in the project root or Build/ directory:
1. Check that CI workflow runs `npm audit` or equivalent
2. Check that .github/dependabot.yml includes npm ecosystem
3. Check that npm dependencies are not excessively outdated (>2 major versions)
tags: [npm, supply-chain, dependencies, ci]
- id: ER-38
name: Dependabot configured for all dependency ecosystems
type: llm_review
severity: info
domain: supply-chain
prompt: |
Check .github/dependabot.yml covers ALL dependency ecosystems in use:
- composer (if composer.json exists)
- npm (if package.json exists)
- github-actions (if .github/workflows/ exists)
- docker (if Dockerfile exists)
Report any missing ecosystem configurations.
tags: [dependabot, supply-chain, dependencies]
# === CI ROBUSTNESS: DOCKER / WORKTREE ===
- id: ER-39
name: CaptainHook disabled in Docker/CI Composer commands
type: llm_review
severity: warning
domain: ci
prompt: |
If the project uses CaptainHook (check composer.json for captainhook/captainhook
or captainhook/plugin-composer):
1. Check Build/Scripts/runTests.sh and any CI scripts for `composer install`
or `composer update` commands run inside Docker containers
2. Verify each such command passes CAPTAINHOOK_DISABLE=true as an env variable
3. Without this, CaptainHook's hook-installer fails when .git is a file (Docker,
worktrees), aborting all subsequent post-install-cmd plugins
4. Also check docker-compose*.yml / docker-compose*.yaml and compose.yml / compose.yaml
for Composer commands missing the env var
If CaptainHook is not in use, report N/A.
tags: [captainhook, docker, worktree, ci, composer]
- id: ER-40
name: PHPStan extension-installer generates non-empty config
type: llm_review
severity: warning
domain: quality
prompt: |
If the project uses phpstan/extension-installer (check composer.json):
1. Check composer.lock / installed packages for PHPStan extension packages
(e.g., phpstan-doctrine, phpstan-phpunit, phpstan-strict-rules, etc.).
2. If such extension packages are installed, GeneratedConfig.php (typically
vendor/phpstan/extension-installer/src/GeneratedConfig.php) should list
these registered extensions in the EXTENSIONS constant after composer install.
3. If extension packages are present but GeneratedConfig.php shows
EXTENSIONS = [], the installer likely did not run -- usually because a
prior Composer plugin (such as CaptainHook) failed and aborted
post-install-cmd scripts.
4. In that case, check that CI scripts include verification of the generated
config or that CaptainHook is disabled (ER-39) to prevent this failure chain.
If phpstan/extension-installer is not in use, or no supported extension
packages are installed, report N/A.
tags: [phpstan, extension-installer, composer, ci]
- id: ER-41
name: PHP lint scope excludes vendor and build directories
type: llm_review
severity: warning
domain: ci
prompt: |
If the project has a lint test suite or lint CI step:
1. Check that PHP lint (php -l) commands scan only source directories
(e.g., Classes/, Configuration/, Tests/), NOT the entire project tree
2. Flag PHP lint commands that traverse third-party or build directories
(e.g., vendor/, .Build/, var/, node_modules/). A plain `find . -name '*.php'`
is a finding unless it explicitly excludes those directories (via -prune or ! -path).
3. Correct patterns include limiting the start paths to source dirs, e.g.
`find Classes Configuration Tests -name '*.php'`, or using `find .` with
appropriate -prune/! -path exclusions for vendor/, .Build/, var/, etc.
4. Check Build/Scripts/runTests.sh, Makefile, and CI workflow files
If no PHP lint step exists, report N/A.
tags: [lint, php, ci, scope]
- id: ER-42
name: Merge queue thread resolution awareness
type: llm_review
severity: info
domain: ci
prompt: |
If the repository uses GitHub merge queues (check branch protection rules):
1. Verify CONTRIBUTING.md or PR templates mention that all review threads
must be resolved before merge. Both CONTRIBUTING.md and PR templates
(.github/PULL_REQUEST_TEMPLATE.md) are community-health files that
GitHub auto-inherits from the org-wide <owner>/.github repo ONLY
when the project has NO local copy of that specific file. The
fallback does NOT supplement a present-but-incomplete local file.
Apply per-file:
- If the project has NO local CONTRIBUTING.md and the org-wide
<owner>/.github/CONTRIBUTING.md (e.g., netresearch/.github)
covers the thread-resolution requirement, treat that requirement
as satisfied via CONTRIBUTING.md. If the project HAS a local
CONTRIBUTING.md, evaluate it on its own merits — the org-wide
file is NOT consulted by GitHub in that case and MUST NOT be
used to suppress a finding.
- The same rule applies independently to PR templates. GitHub
performs a case-insensitive lookup (the filename's actual
case on disk doesn't matter — GitHub finds it regardless),
and recognises any of these local paths:
* .github/PULL_REQUEST_TEMPLATE.md (or .txt form)
* .github/pull_request_template.md
* pull_request_template.md (repo root)
* docs/pull_request_template.md
* .github/PULL_REQUEST_TEMPLATE/ (directory containing
multiple templates — ANY file here counts as a local
template for the purposes of this fallback rule)
The org-wide template only applies when NONE of these local
paths exist; if any local template exists, evaluate it
directly without consulting the org-wide file.
- Only flag absence outright when neither the project nor (per the
fallback rule above, where applicable) the org-wide repo
addresses the thread-resolution requirement via either file.
CODEOWNERS does NOT inherit from org-wide .github and remains a
project-local concern if needed.
2. Check if auto-merge workflows account for the constraint that commits
cannot be pushed while a branch is in the merge queue
3. Flag if required_review_thread_resolution is enabled but not documented
If merge queues are not in use, report N/A.
tags: [merge-queue, github, branch-protection, ci]
# === ADR COVERAGE ===
- id: ER-45
name: Architecture Decision Records for significant decisions
type: llm_review
severity: warning
domain: documentation
prompt: |
Evaluate whether the project should have Architecture Decision Records (ADRs):
1. Count the number of PHP classes in Classes/ — if >10 classes, the extension
is complex enough to warrant a Documentation/Developer/Adr/ directory
2. Scan for significant architectural decisions that should each have an ADR:
- Encryption or cryptography choices (sodium, openssl, key management)
- Access control mechanisms (RBAC, permission checks, policy enforcement)
- Audit logging or compliance-relevant data handling
- External service integrations or protocol choices
- Caching strategies or performance-critical design decisions
- Data model changes with migration implications
3. Check if Documentation/Developer/Adr/ directory exists
4. If ADRs exist, verify they follow a consistent format (title, status,
context, decision, consequences)
Report missing ADRs for identified significant decisions.
tags: [adr, documentation, architecture, decisions]
# === CHANGELOG COMPLETENESS ===
- id: ER-46
name: Every git tag has a corresponding CHANGELOG entry
type: llm_review
severity: warning
domain: documentation
prompt: |
Compare git tags with CHANGELOG.md entries:
1. Run `git tag -l` to list all version tags, normalizing format by stripping any `v` prefix (e.g., v1.2.0 → 1.2.0)
2. Parse CHANGELOG.md headings (## [x.y.z] or ## x.y.z patterns)
3. Flag any git tag that does not have a corresponding CHANGELOG heading (compare using normalized version without `v` prefix)
4. Flag any CHANGELOG heading that does not correspond to a git tag
(may indicate an unreleased section — acceptable if labeled [Unreleased])
5. Verify CHANGELOG entries are in reverse chronological order
If CHANGELOG.md does not exist, flag that as the primary finding.
tags: [changelog, releases, documentation, tags]
# === UPGRADE WIZARD FOR NEW COLUMNS ===
- id: ER-47
name: UpgradeWizard for new database columns on existing tables
type: llm_review
severity: warning
domain: migration
prompt: |
Check ext_tables.sql for columns added to existing tables and verify
that appropriate data migration exists:
1. Parse ext_tables.sql for CREATE TABLE statements — identify columns
that are additions to core or existing tables (not entirely new tables)
2. For each new column on an existing table, check if an
UpgradeWizardInterface implementation exists in Classes/Updates/ or
Classes/Upgrade/ that handles data migration for that column
3. New columns with sensible defaults (e.g., NOT NULL DEFAULT '') may
not need a wizard — focus on columns that require data transformation
or population from existing data
4. Also check for ext_localconf.php registration of upgrade wizards
Report columns that likely need migration wizards but lack them.
tags: [upgrade-wizard, database, migration, ext-tables, typo3]
# === REUSABLE WORKFLOWS ONLY ===
- id: ER-48
name: CI uses org reusable workflows instead of standalone third-party actions
type: llm_review
severity: warning
domain: ci
prompt: |
Review CI workflow files for use of standalone third-party actions where
org-provided reusable workflows should be used instead:
1. Check if workflows directly use third-party actions with unpinned tags
(e.g., uses: shivammathur/setup-php@v2) instead of calling an org
reusable workflow that wraps and standardizes these
2. Flag workflows that duplicate CI logic already available in org
reusable workflows (e.g., separate PHPStan, PHP-CS-Fixer, PHPUnit
steps instead of a single reusable CI workflow call)
3. Acceptable: using third-party actions that are SHA-pinned AND not
available through org reusable workflows
4. Preferred pattern: uses: org/.github/.github/workflows/ci.yml@main
with inputs, rather than reimplementing CI steps locally
Report specific workflow files and actions that should be replaced
with org reusable workflow calls.
tags: [reusable-workflows, ci, github-actions, standardization]
# === BUILD HEALTH ===
- id: ER-50
domain: ci
prompt: |
Check if the project build produces deprecation warnings or errors:
1. Review CI logs or run the build locally to check for deprecation
warnings that will become errors in future versions
2. For TypeScript projects: check tsconfig.json for deprecated options
(e.g., deprecated tsconfig options that produce TS5101 errors). If found, verify
the option is actually needed — remove if unused, add
ignoreDeprecations if required
3. For Go projects: check for deprecated function usage flagged by
go vet or staticcheck
4. For PHP projects: check for PHP deprecation notices in CI output
5. Build-breaking deprecations in CI indicate the project will fail
on the next major version upgrade
Report specific deprecations found and whether they block CI.
severity: warning
desc: "Build should produce no deprecation warnings — these become errors in future versions and block CI"
# === AUTO-MERGE WORKFLOW PRESENCE ===
- id: ER-51
domain: ci
prompt: |
Check if the repository has auto-merge configured for dependency PRs:
1. Verify .github/workflows/ contains an auto-merge workflow
(auto-merge-deps.yml or auto-merge.yml)
2. If present, verify it uses pull_request_target trigger (not
pull_request) for correct bot PR permissions
3. Verify repo has allow_auto_merge enabled: check via
gh api repos/OWNER/REPO --jq .allow_auto_merge
4. If Dependabot or Renovate is configured but no auto-merge workflow
exists, bot PRs will accumulate without being merged
Report missing or misconfigured auto-merge setup.
severity: warning
desc: "Repos with Dependabot/Renovate should have auto-merge workflow to prevent PR accumulation"
# === REUSABLE WORKFLOW PERMISSION COMPATIBILITY ===
- id: ER-52
name: Caller workflows grant all permissions required by called reusable workflows
type: llm_review
severity: error
domain: ci
prompt: |
For each workflow in .github/workflows/ calling a reusable workflow at
`jobs.<job>.uses` (not step-level `uses`), covering both remote calls
(`uses: <org>/<repo>/.github/workflows/<file>@<ref>`) and local calls
(`uses: ./.github/workflows/<file>.yml`):
1. Identify permissions declared in the caller workflow (top-level and job-level)
2. GitHub validates ALL job-level permissions at startup, even for skipped jobs
3. If the shared workflow declares job-level permissions (e.g., pull-requests: write)
that are not granted by the caller, the entire workflow fails with startup_failure
4. Check: does the caller grant every permission the shared workflow's jobs declare?
5. Common miss: caller grants only `contents: write` but shared workflow also needs
`pull-requests: write` for a bump or changelog job
6. Flag missing permissions as error — this causes startup_failure across all repos
using the shared workflow
tags: [permissions, reusable-workflows, ci, startup-failure]
# === DUPLICATE RELEASE NOTES DETECTION ===
- id: ER-53
name: Release workflow must not produce duplicate content in release notes
type: llm_review
severity: warning
domain: ci
prompt: |
Check for duplicate release note issues.
A common cause is a misconfiguration in softprops/action-gh-release.
Static check (primary):
1. Read .github/workflows/release.yml or any shared release workflow
2. Look for softprops/action-gh-release used with both `body:` (or
`body_path:`) and `generate_release_notes: true` — this is a known
misconfiguration where the explicit body is prepended to
auto-generated notes, producing duplicate content. Flag as warning.
Dynamic confirmation (secondary):
3. Check the 3 most recent releases for actual duplication:
`gh release view <tag> --json body -q .body`
4. Look for repeated sections, duplicate changelog entries, or
duplicated commit lists in release bodies
tags: [release-notes, duplicate-content, ci, softprops]
[
{
"name": "assess_enterprise_readiness",
"prompt": "Assess this project for enterprise readiness and identify gaps",
"assertions": [
{
"type": "content",
"pattern": "(OpenSSF|Scorecard|SLSA|SBOM|supply chain)"
},
{
"type": "content",
"pattern": "(ci\\.yml|codeql\\.yml|scorecard\\.yml|dependency-review)"
}
]
},
{
"name": "setup_slsa_provenance",
"prompt": "Set up SLSA provenance for this GitHub repository",
"assertions": [
{
"type": "content",
"pattern": "(attest-build-provenance|slsa-github-generator|SLSA Level)"
},
{
"type": "content",
"pattern": "(id-token:\\s*write|attestations:\\s*write|permissions)"
},
{
"type": "content",
"pattern": "(gh attestation verify|slsa-verifier)"
}
]
},
{
"name": "openssf_badge_assessment",
"prompt": "Check this project's OpenSSF Best Practices Badge status and what criteria are missing",
"assertions": [
{
"type": "content",
"pattern": "(bestpractices\\.dev|bestpractices\\.coreinfrastructure|OpenSSF)"
},
{
"type": "content",
"pattern": "(Passing|Silver|Gold|criteria)"
},
{
"type": "content",
"pattern": "(https://|URL)"
}
]
},
{
"name": "sha_pin_actions",
"prompt": "Review and fix GitHub Actions pinning in our workflow files",
"assertions": [
{
"type": "content",
"pattern": "(SHA|sha|commit hash|@[a-f0-9]{40})"
},
{
"type": "content",
"pattern": "(version comment|# v\\d)"
},
{
"type": "content",
"pattern": "(third-party|org-internal|reusable workflow)"
}
]
},
{
"name": "script_injection_prevention",
"prompt": "Check our GitHub Actions workflows for script injection vulnerabilities",
"assertions": [
{
"type": "content",
"pattern": "(github\\.event|\\$\\{\\{|interpolat)"
},
{
"type": "content",
"pattern": "(inputs\\.|run:|env:)"
},
{
"type": "content",
"pattern": "(injection|NEVER|environment variable)"
}
]
},
{
"name": "scorecard_optimization",
"prompt": "Our OpenSSF Scorecard is at 6.8. Help us get it above 9.0",
"assertions": [
{
"type": "content",
"pattern": "(Token-Permissions|permissions|contents:\\s*read)"
},
{
"type": "content",
"pattern": "(Branch-Protection|branch protection)"
},
{
"type": "content",
"pattern": "(Security-Policy|SECURITY\\.md)"
},
{
"type": "content",
"pattern": "(Pinned-Dependencies|SHA-pin)"
}
]
},
{
"name": "setup_mandatory_workflows",
"prompt": "Set up all required CI/CD workflows for an enterprise-ready GitHub project",
"assertions": [
{
"type": "content",
"pattern": "ci\\.yml"
},
{
"type": "content",
"pattern": "codeql\\.yml"
},
{
"type": "content",
"pattern": "scorecard\\.yml"
},
{
"type": "content",
"pattern": "dependency-review\\.yml"
}
]
},
{
"name": "setup_mandatory_badges",
"prompt": "Add all required enterprise readiness badges to the project README",
"assertions": [
{
"type": "content",
"pattern": "(CI Status|actions/workflows)"
},
{
"type": "content",
"pattern": "(codecov\\.io|Codecov)"
},
{
"type": "content",
"pattern": "(securityscorecards\\.dev|Scorecard)"
},
{
"type": "content",
"pattern": "(bestpractices\\.dev|Best Practices)"
}
]
},
{
"name": "workflow_permissions_hardening",
"prompt": "Harden GitHub Actions workflow permissions using least-privilege principle",
"assertions": [
{
"type": "content",
"pattern": "(permissions:|contents:\\s*read)"
},
{
"type": "content",
"pattern": "(workflow-level|job-level)"
},
{
"type": "content",
"pattern": "(write|least.privilege|minimal)"
}
]
},
{
"name": "harden_runner_setup",
"prompt": "Add step-security/harden-runner to our CI workflows",
"assertions": [
{
"type": "content",
"pattern": "(harden-runner|step-security)"
},
{
"type": "content",
"pattern": "(egress|audit|block)"
},
{
"type": "content",
"pattern": "(first step|SHA-pin)"
}
]
},
{
"name": "dependabot_configuration",
"prompt": "Configure Dependabot for comprehensive dependency monitoring in a PHP project with GitHub Actions",
"assertions": [
{
"type": "content",
"pattern": "(dependabot\\.yml|dependabot)"
},
{
"type": "content",
"pattern": "(composer|github-actions)"
},
{
"type": "content",
"pattern": "(package-ecosystem|auto-merge)"
}
]
},
{
"name": "code_review_quality",
"prompt": "Review this pull request for enterprise-quality code patterns",
"assertions": [
{
"type": "content",
"pattern": "(test|resource|instance)"
},
{
"type": "content",
"pattern": "(error handling|edge case|boundary)"
},
{
"type": "content",
"pattern": "(review|pattern|quality)"
}
]
},
{
"name": "write_adr",
"prompt": "Write an Architecture Decision Record for choosing JWT authentication over session-based auth",
"assertions": [
{
"type": "content",
"pattern": "(ADR|Architecture Decision Record)"
},
{
"type": "content",
"pattern": "(Status|Context|Decision|Consequences)"
},
{
"type": "content",
"pattern": "(Accepted|Proposed|Superseded)"
}
]
},
{
"name": "duplicate_ci_runs",
"prompt": "Our CI runs twice on every PR push. Diagnose and fix the duplicate runs",
"assertions": [
{
"type": "content",
"pattern": "(push:|pull_request:)"
},
{
"type": "content",
"pattern": "(branches:|branch.restrict|scope)"
},
{
"type": "content",
"pattern": "(duplicate|twice|double)"
}
]
},
{
"name": "coverage_setup",
"prompt": "Set up code coverage reporting with Codecov for a PHP project",
"assertions": [
{
"type": "content",
"pattern": "(codecov|coverage)"
},
{
"type": "content",
"pattern": "(upload|codecov-action)"
},
{
"type": "content",
"pattern": "(patch|threshold|codecov\\.yml)"
}
]
},
{
"name": "security_policy",
"prompt": "Create a SECURITY.md with vulnerability disclosure process for this project",
"assertions": [
{
"type": "content",
"pattern": "(SECURITY\\.md|security policy)"
},
{
"type": "content",
"pattern": "(vulnerabilit|disclosure|reporting)"
},
{
"type": "content",
"pattern": "(SLA|response|timeline|days)"
}
]
},
{
"name": "signed_releases_setup",
"prompt": "Set up signed releases with cosign for our container images",
"assertions": [
{
"type": "content",
"pattern": "(cosign|GPG|signing)"
},
{
"type": "content",
"pattern": "(verif|attest|signature)"
},
{
"type": "content",
"pattern": "(tag|release|artifact)"
}
]
},
{
"name": "osps_baseline_assessment",
"prompt": "Evaluate this project against OpenSSF OSPS Baseline levels 1, 2, and 3",
"assertions": [
{
"type": "content",
"pattern": "(OSPS|Baseline|openssf)"
},
{
"type": "content",
"pattern": "(Level 1|Level 2|Level 3|level)"
},
{
"type": "content",
"pattern": "(criteria|requirement|check)"
}
]
},
{
"name": "auto_merge_setup",
"prompt": "Set up auto-merge for Dependabot and Renovate PRs in our repository",
"assertions": [
{
"type": "content",
"pattern": "(auto.merge|auto-merge)"
},
{
"type": "content",
"pattern": "(Dependabot|Renovate|dependency)"
},
{
"type": "content",
"pattern": "(pull_request_target|allow_auto_merge|workflow)"
}
]
},
{
"name": "changelog_compliance",
"prompt": "Verify our CHANGELOG.md matches git tags and follows best practices",
"assertions": [
{
"type": "content",
"pattern": "(CHANGELOG|changelog)"
},
{
"type": "content",
"pattern": "(tag|version|release)"
},
{
"type": "content",
"pattern": "(chronological|Unreleased|heading)"
}
]
}
]
Two-Factor Authentication Enforcement Guide
OpenSSF Gold Badge requirements:require_2FA,secure_2FA
>
Projects must require 2FA for contributors with commit/accept rights
and encourage secure 2FA methods (hardware tokens, TOTP).
GitHub Organization 2FA
Enable Organization-Wide 2FA Requirement
1. Go to Organization Settings → Security → Authentication security 2. Enable Require two-factor authentication 3. Set grace period for existing members (recommended: 7 days)
Settings → Security → Authentication security
☑️ Require two-factor authentication for everyone in the organizationVerify Member Compliance
# List members without 2FA (requires admin)
gh api orgs/YOUR_ORG/members --jq '.[] | select(.two_factor_disabled == true) | .login'
# Organization security overview
gh api orgs/YOUR_ORG --jq '{
members: .members_with_two_factor_disabled,
total: .members_count
}'Repository-Level Protection
Branch Protection with 2FA
# Recommended branch protection settings
branch_protection:
required_status_checks:
strict: true
contexts: ["ci", "security"]
enforce_admins: true
required_pull_request_reviews:
dismiss_stale_reviews: true
require_code_owner_reviews: true
required_approving_review_count: 1
# Note: GitHub enforces org-level 2FA requirementCommit Signing (Additional Security)
2FA protects account access; commit signing protects commit integrity:
# Configure GPG signing
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_KEY_ID
# Or use SSH signing (requires 2FA to add key)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pubSecure 2FA Methods
Recommended (Gold Badge: secure_2FA)
| Method | Security Level | Notes |
|---|---|---|
| Hardware Security Key | ⭐⭐⭐⭐⭐ | YubiKey, Titan - phishing resistant |
| TOTP Authenticator App | ⭐⭐⭐⭐ | Google Auth, Authy, 1Password |
| GitHub Mobile | ⭐⭐⭐⭐ | Push notifications |
| Passkeys | ⭐⭐⭐⭐⭐ | Platform authenticators, biometric |
Not Recommended
| Method | Issues |
|---|---|
| SMS | Vulnerable to SIM swapping |
| Single point of failure | |
| Backup codes only | Static, easily compromised |
Documentation Requirements
SECURITY.md Section
Add to your SECURITY.md:
## Contributor Security Requirements
### Two-Factor Authentication
All contributors with commit access MUST enable two-factor authentication (2FA).
**Required for:**
- Repository administrators
- Contributors with write access
- Code owners
- Release managers
**Recommended 2FA methods:**
1. Hardware security keys (YubiKey, Titan Key)
2. TOTP authenticator apps (not SMS)
3. Passkeys/Platform authenticators
**Setup instructions:**
- [GitHub 2FA Setup](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa)
- [Hardware Key Setup](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication#configuring-two-factor-authentication-using-a-security-key)
### Commit Signing
We encourage (but do not require) GPG or SSH signed commits.
See [Signing Commits](https://docs.github.com/en/authentication/managing-commit-signature-verification).CONTRIBUTING.md Section
## Security Requirements
Before your first contribution:
1. **Enable 2FA** on your GitHub account
- Required for all contributors with commit access
- Recommended: Hardware security key or authenticator app
- [Setup Guide](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa)
2. **Optional: Sign your commits**
- GPG signing provides non-repudiation
- [Signing Guide](https://docs.github.com/en/authentication/managing-commit-signature-verification)Verification Workflow
Check 2FA Status in CI
# .github/workflows/security-check.yml
name: Security Compliance
on:
pull_request:
types: [opened, synchronize]
jobs:
check-contributor:
runs-on: ubuntu-latest
steps:
- name: Check PR author 2FA (informational)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Note: 2FA status not directly available via API
# Organization-level 2FA requirement is the enforcement mechanism
echo "✅ PR from ${{ github.actor }}"
echo "ℹ️ 2FA enforcement is handled at organization level"Organization Audit
#!/bin/bash
# scripts/audit-2fa.sh
ORG="your-org"
echo "=== 2FA Compliance Audit ==="
# Check org 2FA requirement
REQUIRED=$(gh api orgs/$ORG --jq '.two_factor_requirement_enabled')
echo "2FA Required: $REQUIRED"
if [ "$REQUIRED" = "true" ]; then
echo "✅ Organization requires 2FA"
else
echo "❌ Organization does NOT require 2FA"
echo "Action: Enable at Settings → Security → Authentication security"
fi
# List admins (should all have 2FA if org requires it)
echo ""
echo "Repository Admins (all should have 2FA):"
gh api repos/$ORG/REPO/collaborators --jq '.[] | select(.permissions.admin == true) | .login'Badge Criteria Alignment
| Criterion | Requirement | Implementation |
|---|---|---|
require_2FA | 2FA required for privileged access | Organization 2FA requirement |
secure_2FA | Encourage non-SMS 2FA | Document recommended methods |
access_continuity | Maintain access if contributor unavailable | Multiple admins with 2FA |
Solo Maintainer Considerations
For solo maintainers who cannot enforce organization-level 2FA:
## 2FA Statement (Solo Maintainer)
This project is maintained by a single developer.
**2FA Status:**
- ✅ Maintainer uses hardware security key (YubiKey)
- ✅ Backup codes stored securely offline
- ✅ Recovery email protected with 2FA
**For future contributors:**
- 2FA will be required before granting write access
- Hardware keys or authenticator apps recommendedResources
OpenSSF Badge Display Guide
MANDATORY Badge Requirements
ALL badges in this document are MANDATORY, not optional.
Every project MUST have these badges displayed in README.md:
| Badge | MANDATORY | Action if Missing |
|---|---|---|
| CI Status | YES | Add workflow badge |
| Codecov | YES | Enable codecov, upload coverage in CI |
| OpenSSF Scorecard | YES | Add scorecard.yml workflow |
| OpenSSF Best Practices | YES | Register at bestpractices.dev, get PROJECT_ID |
| OpenSSF Baseline | YES | Same project ID, separate badge URL (/baseline) |
"Unknown" or placeholder badges are NOT acceptable. Fix them immediately.
---
OpenSSF Silver Badge requirement: documentation_achievements>
Projects MUST display their OpenSSF Best Practices Badge prominently in the README.
Badge URLs
Get Your Badge (MANDATORY STEPS)
1. Register at https://www.bestpractices.dev/ 2. Complete criteria for your target level 3. Copy badge URL from your project page
Badge Formats
# Markdown (Recommended for README.md)
[](https://www.bestpractices.dev/projects/YOUR_PROJECT_ID)
# HTML
<a href="https://www.bestpractices.dev/projects/YOUR_PROJECT_ID">
<img src="https://www.bestpractices.dev/projects/YOUR_PROJECT_ID/badge" alt="OpenSSF Best Practices">
</a>
# reStructuredText (for Python docs)
.. image:: https://www.bestpractices.dev/projects/YOUR_PROJECT_ID/badge
:target: https://www.bestpractices.dev/projects/YOUR_PROJECT_ID
:alt: OpenSSF Best PracticesBadge Levels
| Level | Visual | Description |
|---|---|---|
| Passing | !passing | Basic security practices |
| Silver | !silver | Advanced governance & security |
| Gold | !gold | Highest maturity level |
OSPS Baseline Badge (Levels 1/2/3 — separate badge, same project):
[](https://www.bestpractices.dev/projects/PROJECT_ID)The Baseline badge shows the achieved OSPS security level (1, 2, or 3). Both badges should be displayed together.
README Badge Section
Recommended Placement
# Project Name
[](https://www.bestpractices.dev/projects/ID)
[](https://www.bestpractices.dev/projects/ID)
[](https://securityscorecards.dev/viewer/?uri=github.com/ORG/REPO)
[](https://goreportcard.com/report/github.com/ORG/REPO)
[](LICENSE)
Project description here...Complete Badge Row Example
<!-- Recommended badge order: Security → Quality → Build → License -->
[](https://www.bestpractices.dev/projects/ID)
[](https://www.bestpractices.dev/projects/ID)
[](https://securityscorecards.dev/viewer/?uri=github.com/ORG/REPO)
[](SECURITY.md)
[](https://codecov.io/gh/ORG/REPO)
[](https://goreportcard.com/report/github.com/ORG/REPO)
[](https://github.com/ORG/REPO/actions/workflows/ci.yml)
[](LICENSE)Additional Security Badges
OpenSSF Scorecard
[](https://securityscorecards.dev/viewer/?uri=github.com/ORG/REPO)SLSA Provenance Level
[](https://slsa.dev)Sigstore Signed
[](https://search.sigstore.dev/?hash=SHA256_HASH)Security Policy
[](SECURITY.md)Badge Verification
Automated Badge Check
# .github/workflows/verify-badges.yml
name: Verify Badges
on:
schedule:
- cron: '0 0 * * 0' # Weekly
workflow_dispatch:
jobs:
check-badges:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check OpenSSF Badge Status
run: |
# Use cache-buster to avoid stale responses
DATA=$(curl -s "https://www.bestpractices.dev/projects/YOUR_ID.json?_=$(date +%s)")
STATUS=$(echo "$DATA" | jq -r '.badge_level')
echo "Current badge level: $STATUS"
if [ "$STATUS" = "passing" ] || [ "$STATUS" = "silver" ] || [ "$STATUS" = "gold" ]; then
echo "✅ Badge status valid"
else
echo "❌ Badge status issue"
exit 1
fi
- name: Check Scorecard
run: |
SCORE=$(curl -s "https://api.securityscorecards.dev/projects/github.com/ORG/REPO" | jq -r '.score')
echo "Scorecard score: $SCORE"Badge Link Validator
#!/bin/bash
# scripts/verify-badge-links.sh
echo "Checking badge links in README.md..."
# Extract all badge URLs
BADGES=$(grep -oP 'https://[^)]+badge[^)]*' README.md)
for URL in $BADGES; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
if [ "$STATUS" = "200" ]; then
echo "✅ $URL"
else
echo "❌ $URL (HTTP $STATUS)"
EXIT_CODE=1
fi
done
exit ${EXIT_CODE:-0}Tracking Progress
Progress Badge
For projects working toward a badge level:
<!-- Show progress toward Silver -->
[](https://www.bestpractices.dev/projects/ID)Criteria Checklist in README
## OpenSSF Best Practices Progress
- [x] Passing Level (100%)
- [x] Security policy documented
- [x] Signed releases
- [ ] Silver Level (85%)
- [x] DCO enforcement
- [x] 80% test coverage
- [ ] Two-person review (solo maintainer)
- [ ] Gold Level (40%)
- [ ] 90% test coverage
- [ ] Security auditBadge Criteria Alignment
| Criterion | Requirement | How to Display |
|---|---|---|
| documentation_achievements | Badge displayed | Add to README.md header |
| documentation_basics | Project purpose clear | README intro section |
| achieve_passing | Passing badge earned | Badge shows automatically |
| achieve_silver | Silver badge earned | Badge shows automatically |
Resources
Badge Data Submission Guide
Practical guide for programmatically submitting OpenSSF Best Practices Badge data.
Overview
The bestpractices.dev platform uses a Rails web application. There is no official REST API for updating badge criteria. Submissions are done via authenticated HTML form PATCH requests.
Submission Script
A Python script can automate badge data submission using session cookies:
#!/usr/bin/env python3
"""Submit OpenSSF Best Practices Badge data.
Usage:
BADGE_COOKIE='...' python3 submit-badge.py [--dry-run] [--level LEVEL]
"""
import json
import os
import re
import sys
import time
import http.cookiejar
import urllib.parse
import urllib.request
COOKIE_NAME = '_BadgeApp_session'
PROJECT_ID = 12345 # Replace with your project ID
BASE_URL = 'https://www.bestpractices.dev'
AUTH_TOKEN_PATTERN = re.compile(
r'<input type="hidden" name="authenticity_token" value="([^"]+)"'
)
CSRF_TOKEN_PATTERN = re.compile(
r'<meta name="csrf-token" content="([^"]+)"'
)
LEVELS = ['passing', 'silver', 'gold', 'baseline-1', 'baseline-2', 'baseline-3']Authentication
1. Log in to bestpractices.dev in your browser 2. Extract the _BadgeApp_session cookie value from browser DevTools 3. Pass it via environment variable OR file (file recommended due to shell quoting issues):
# RECOMMENDED: Read from file to avoid shell quoting issues with special chars
cookie_file = '/tmp/badge-cookie.txt'
if os.path.exists(cookie_file):
with open(cookie_file) as f:
cookie = f.read().strip()
else:
cookie = os.environ.get('BADGE_COOKIE', '')Submission Flow
For each level:
1. GET the edit page to obtain CSRF tokens and lock version 2. PATCH the level URL with form-encoded data
GET /en/projects/{ID}/{level}/edit → Extract authenticity_token + lock_version
PATCH /en/projects/{ID}/{level} → Submit form data with tokensIMPORTANT: The level path segment is required for ALL levels including passing. Use /en/projects/{ID}/passing/edit, NOT /en/projects/{ID}/edit.
---
Critical Gotchas
1. Cookie Handling - Do NOT URL-Decode, Use File
CRITICAL: The session cookie must be sent exactly as received from the browser. Do NOT URL-decode it.
RECOMMENDED: Write cookie to a file instead of passing via environment variable. Session cookies often contain +, /, =, and other characters that break shell quoting.
# BEST - read from file (no shell quoting issues)
cookie = open('/tmp/badge-cookie.txt').read().strip()
# OK - environment variable (beware of shell quoting)
cookie = os.environ.get('BADGE_COOKIE', '')
# WRONG - URL decoding breaks session authentication
cookie = urllib.parse.unquote(os.environ.get('BADGE_COOKIE', ''))Symptom: Writes appear to succeed (302 redirect) but data is silently not persisted.
2. URL-Required Justifications
Many criteria require https:// URLs in the justification text. Without URLs, criteria show "Warning: URL required, but no URL found" even when status is "Met".
CRITICAL: Even with status "Met", an empty justification or one without URLs causes the criterion to show "Unknown required information, not enough for a badge" on the edit page. The API will still report the status as "Met", but the badge percentage will NOT count it.
Commonly affected criteria (require URLs in justification at ALL levels):
| Level | Criteria Requiring URLs |
|---|---|
| Passing | contribution_requirements, report_process, vulnerability_report_process, vulnerability_report_private, tests_documented_added, warnings_strict, static_analysis_common_vulnerabilities, dynamic_analysis_unsafe |
| Silver | dco, governance, roles_responsibilities, access_continuity, bus_factor, documentation_architecture, documentation_achievements, vulnerability_report_credit, coding_standards, external_dependencies |
| Gold | bus_factor, contributors_unassociated, hardened_site, hardening |
Rule of thumb: Always include at least one https:// URL in every justification text.
{
"dco_justification": "DCO enforced via GitHub Actions: https://github.com/org/repo/blob/main/.github/workflows/dco.yml"
}3. Auto-Detected Fields Cannot Be Set
These fields are auto-detected by the platform and cannot be set via form submission:
| Field | Why |
|---|---|
homepage_url_status | Auto-detected from project URL |
report_url_status | Auto-detected from issue tracker URL |
report_url | Set at project creation, not a form field |
Attempting to submit these fields causes HTTP 400 errors.
Remove them from your badge data files before submission.
Note: These fields show as ? in the API but do NOT count against badge percentages.
4. API Response Caching - Use Correct URL Path
CRITICAL: The /en/projects/{ID}.json path (with locale prefix) returns aggressively cached data that can be stale for hours. The /projects/{ID}.json path (without locale) returns fresh data.
# WRONG - returns stale cached data
curl -s "https://www.bestpractices.dev/en/projects/PROJECT_ID.json"
# CORRECT - returns fresh data
curl -s "https://www.bestpractices.dev/projects/PROJECT_ID.json?_=$(date +%s)"Symptom: After successful submission (302 redirect), the API still shows old values. Switching from /en/projects/ to /projects/ path immediately shows updated data.
5. OSPS Baseline Field Names
OSPS Baseline criteria use a different naming convention in HTML forms:
| Display Format | Form Field Format |
|---|---|
OSPS-AC-01.01 | osps_ac_01_01_status |
OSPS-BR-03.02 | osps_br_03_02_status |
Pattern: All lowercase, hyphens and dots become underscores.
Note: OSPS criteria do NOT affect Passing/Silver/Gold badge percentages. They are a separate badge program (OSPS Baseline).
6. Session Cookie Rotation
The server rotates session cookies via Set-Cookie headers. Your script must use CookieJar to automatically track cookie updates:
def make_opener(cookie):
"""Create urllib opener with cookie jar and no-redirect handling."""
cj = http.cookiejar.CookieJar()
c = http.cookiejar.Cookie(
version=0, name='_BadgeApp_session', value=cookie,
port=None, port_specified=False,
domain='www.bestpractices.dev', domain_specified=True,
domain_initial_dot=False,
path='/', path_specified=True,
secure=True, expires=None, discard=True,
comment=None, comment_url=None, rest={}, rfc2109=False,
)
cj.set_cookie(c)
class NoRedirectHandler(urllib.request.HTTPErrorProcessor):
def http_response(self, request, response):
return response
https_response = http_response
return urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(cj),
NoRedirectHandler,
)7. Rate Limiting
Add delays between level submissions to avoid rate limiting:
time.sleep(3) # Wait 3 seconds between submissions---
Lock Version Extraction
CRITICAL: The lock_version hidden field is required for successful saves. Without it, submissions may silently fail (302 redirect but no data persisted).
The HTML attribute order varies - sometimes name comes before value, sometimes after. Use a regex that handles both orderings:
# CORRECT - handles both attribute orderings
LOCK_VERSION_PATTERN = re.compile(
r'(?:name="project\[lock_version\]"[^>]*value="([^"]*)"|'
r'value="(\d+)"[^>]*name="project\[lock_version\]")'
)
match = LOCK_VERSION_PATTERN.search(html)
lock_version = (match.group(1) or match.group(2)) if match else None
# WRONG - only handles one attribute ordering
re.search(r'name="project\[lock_version\]"[^>]*value="([^"]*)"', html)---
Detecting Insufficient Criteria
The badge edit page contains <img> elements with id="{criterion}_enough" that indicate whether each criterion meets badge requirements. Use this to find what's actually blocking the badge percentage:
# After fetching the edit page HTML:
all_enough = re.findall(r'<img[^>]*id="(\w+)_enough"[^>]*alt="([^"]+)"', html)
not_enough = [(name, alt) for name, alt in all_enough if 'not' in alt.lower() or 'Not' in alt]
for name, alt in not_enough:
print(f" {name}: {alt}")Common "not enough" messages:
"Not enough for a badge."— Status is "Unmet" or criteria not satisfied"Unknown required information, not enough for a badge."— Status is "Met" but justification
is empty or missing required URL
---
Badge Data File Format
Structure badge data as JSON files per level:
badge-data-passing.json
badge-data-silver.json
badge-data-gold.json
badge-data-baseline-1.json
badge-data-baseline-2.json
badge-data-baseline-3.jsonEach file contains field name/value pairs:
{
"criterion_name_status": "Met",
"criterion_name_justification": "Explanation with https://github.com/org/repo/evidence"
}Valid status values: Met, Unmet, N/A, ? (unknown)
Note: Some criteria only accept specific status values:
dynamic_analysis_enable_assertions_status: OnlyMet,Unmet,?(NOTN/A)access_continuity_status: OnlyMet,Unmet,?(NOTN/A)bus_factor_status: OnlyMet,Unmet,?(NOTN/A)contributors_unassociated_status: OnlyMet,Unmet,?(NOTN/A)two_person_review_status: OnlyMet,Unmet,?(NOTN/A)
---
Multi-Project Batch Submission
For submitting badge data across multiple projects, use a configuration dict:
PROJECTS = {
'project-name': {
'id': 12345,
'levels': {
'passing': '/path/to/badge-data-passing.json',
'silver': '/path/to/badge-data-silver.json',
'gold': '/path/to/badge-data-gold.json',
}
},
# ... more projects
}
# Submit all
for name, config in PROJECTS.items():
for level, data_file in config['levels'].items():
if os.path.exists(data_file):
submit_level(opener, config['id'], level, data_file)
time.sleep(3)---
Solo Maintainer Justification Patterns
For projects with a single maintainer, use these justification templates:
`two_person_review` (set to Met, N/A not allowed):
The project uses automated multi-reviewer workflow: GitHub Copilot code review + auto-approve
bot for solo maintainer. Branch protection requires passing CI + review approval.
See: https://github.com/org/repo/blob/main/.github/workflows/pr-quality-gates.yml`bus_factor` (set to Met, N/A not allowed):
Bus factor managed through comprehensive documentation, CI automation, and organizational access.
Organization maintains access to all repositories. Backup maintainers have repository access
via GitHub organization membership: https://github.com/orgs/ORG/people`access_continuity` (set to Met, N/A not allowed):
Access continuity ensured via GitHub organization. Multiple organization members have admin
access. Repository settings and credentials managed at organization level:
https://github.com/orgs/ORG/peopleSee also: references/solo-maintainer-guide.md
---
Verification
After submission, verify via API:
# Check badge level (use /projects/ NOT /en/projects/)
curl -s "https://www.bestpractices.dev/projects/PROJECT_ID.json?_=$(date +%s)" | \
jq '{badge_level, badge_percentage_0, badge_percentage_1, badge_percentage_2}'
# badge_percentage_0 = Passing
# badge_percentage_1 = Silver
# badge_percentage_2 = GoldTo verify specific criteria are truly accepted (not just "Met" in API):
# Fetch edit page and check _enough indicators
html = fetch_edit_page(opener, project_id, level)
not_enough = re.findall(
r'<img[^>]*id="(\w+)_enough"[^>]*alt="([^"]+)"',
html
)
blockers = [(n, a) for n, a in not_enough if 'not' in a.lower()]---
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| 302 redirect but data not saved | URL-decoded cookie | Send cookie as-is, no urllib.parse.unquote() |
| 302 redirect but data not saved | Missing lock_version | Extract lock_version from edit page (handle both attr orderings) |
| HTTP 400 on PATCH | Submitting auto-detected fields | Remove homepage_url_status, report_url_status from data |
Criteria shows ? despite Met status | Missing URL in justification | Add https:// URL to justification text |
| "Met" in API but not counting for % | Empty justification or no URL | Badge edit page shows "Unknown required information" - add justification with URL |
| Stale API response after submit | Using /en/projects/ path | Use /projects/{ID}.json (no locale prefix) with cache-buster |
| GET returns 302 redirect | Cookie expired | Get fresh cookie from browser |
| 200 with "form contains N errors" | Validation errors | Check which criteria allow N/A; some only accept Met/Unmet/? |
| Silver/Gold data silently lost | Cookie rotation lost in redirect | Use NoRedirectHandler + HTTPCookieProcessor with CookieJar |
| N/A rejected for specific criteria | Criterion doesn't allow N/A | Check form radio buttons (see list above) |
| Shell quoting breaks cookie | Special chars (+, /, =) in cookie | Write cookie to file, read in Python |
| Lock version regex returns None | HTML has reversed attribute order | Use alternation regex for both orderings |
N/A Not Allowed on Certain Criteria
Some Silver/Gold criteria do not allow N/A status. Submitting N/A causes validation errors that silently prevent the entire save (form re-renders with 200 status).
Passing criteria that DON'T allow N/A:
dynamic_analysis_enable_assertions(only Met/Unmet/?)
Silver criteria that DON'T allow N/A:
access_continuity(MUST)bus_factor(MUST)
Gold criteria that DON'T allow N/A:
contributors_unassociated(MUST)two_person_review(MUST)
Always check the edit form's radio buttons to verify which values are accepted.
Mandatory Badges & Workflows
Mandatory Badges
| Badge | URL Pattern |
|---|---|
| CI Status | github.com/ORG/REPO/actions/workflows/ci.yml/badge.svg |
| Codecov | codecov.io/gh/ORG/REPO/graph/badge.svg |
| OpenSSF Scorecard | api.securityscorecards.dev/projects/github.com/ORG/REPO/badge |
| OpenSSF Best Practices | www.bestpractices.dev/projects/PROJECT_ID/badge |
| OpenSSF Baseline | www.bestpractices.dev/projects/PROJECT_ID/baseline |
Mandatory Workflows
| Workflow | File | Purpose |
|---|---|---|
| CI | ci.yml | Build, test, lint |
| CodeQL | codeql.yml | Security scanning |
| Scorecard | scorecard.yml | OpenSSF Scorecard |
| Dependency Review | dependency-review.yml | PR CVE check |
Scorecard Quick Wins
| Check | Quick Fix | Impact |
|---|---|---|
| Token-Permissions | permissions: {} at workflow-level, write only per-job | 0->10 |
| Branch-Protection | required_approving_review_count: 1 + auto-approve | 0->8 |
| Security-Policy | SECURITY.md + private vulnerability reporting | 4->10 |
| Pinned-Dependencies | SHA-pin all actions (SLSA generator pinnable but internal actions use tag refs) | 8->10 |
Branch Coverage Guide
OpenSSF Badge Criteria: test_branch_coverage80 (Gold - requires 80% branch coverage)Understanding Branch vs Statement Coverage
Statement Coverage
Measures which lines of code are executed:
func Example(x int) string {
if x > 0 { // Line executed
return "pos" // Line executed OR not
}
return "neg" // Line executed OR not
}100% statement coverage = all lines executed at least once.
Branch Coverage (Decision Coverage)
Measures whether each decision point (if/else, switch) has been tested for all outcomes:
func Example(x int) string {
if x > 0 { // Branch: true AND false
return "pos"
}
return "neg"
}100% branch coverage = both x > 0 (true) AND x <= 0 (false) tested.
Why Branch Coverage Matters
Statement coverage can be 100% while missing critical paths:
func Process(a, b bool) {
if a && b {
// Critical path
}
}
// This test achieves 100% statement coverage
func TestProcess(t *testing.T) {
Process(true, true) // Executes the if block
Process(false, false) // Skips the if block
}
// But misses: (true, false) and (false, true) branches---
Measuring Branch Coverage in Go
Native Go Coverage (Statement Only)
Go's built-in coverage tool measures statement coverage:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out # Shows statement coverageApproach 1: gocover-cobertura + XML Analysis
Convert Go coverage to Cobertura XML format:
# Install
go install github.com/boumenot/gocover-cobertura@latest
# Generate
go test -coverprofile=coverage.out -covermode=count ./...
gocover-cobertura < coverage.out > coverage.xmlThen analyze with tools that support branch metrics.
Approach 2: gocov + gocov-xml
# Install
go install github.com/axw/gocov/gocov@latest
go install github.com/AlekSi/gocov-xml@latest
# Generate
go test -coverprofile=coverage.out ./...
gocov convert coverage.out | gocov-xml > coverage.xmlApproach 3: Codecov Branch Analysis
Codecov can analyze branch coverage:
# .github/workflows/coverage.yml
- name: Run tests
run: go test -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.out
flags: unittests
fail_ci_if_error: trueIn Codecov settings, enable branch coverage reports.
---
Achieving 80% Branch Coverage
Strategy 1: Test All Conditional Outcomes
For every if statement, ensure both true and false branches are tested:
func Validate(s string) error {
if s == "" {
return errors.New("empty")
}
if len(s) > 100 {
return errors.New("too long")
}
return nil
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty string", "", true}, // if s == "" → true
{"valid string", "hello", false}, // if s == "" → false
{"too long", strings.Repeat("x", 101), true}, // if len > 100 → true
{"at limit", strings.Repeat("x", 100), false}, // if len > 100 → false
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Validate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}Strategy 2: Cover Compound Conditions
For && and || conditions, test each combination:
func IsEligible(age int, hasLicense bool) bool {
if age >= 18 && hasLicense {
return true
}
return false
}
func TestIsEligible(t *testing.T) {
tests := []struct {
age int
license bool
want bool
}{
{20, true, true}, // Both true
{20, false, false}, // First true, second false
{16, true, false}, // First false, second true
{16, false, false}, // Both false
}
// Run all test cases...
}Strategy 3: Cover Switch Cases
Test all switch cases including default:
func Categorize(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
default:
return "F"
}
}
func TestCategorize(t *testing.T) {
tests := []struct {
score int
want string
}{
{95, "A"}, // First case
{85, "B"}, // Second case
{75, "C"}, // Third case
{65, "F"}, // Default case
}
// Run all test cases...
}Strategy 4: Test Error Paths
Error handling often has low coverage:
func ReadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file: %w", err) // Often untested
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err) // Often untested
}
return &cfg, nil
}
func TestReadConfig(t *testing.T) {
// Happy path
t.Run("valid config", func(t *testing.T) {
cfg, err := ReadConfig("testdata/valid.json")
require.NoError(t, err)
require.NotNil(t, cfg)
})
// Error path: file not found
t.Run("file not found", func(t *testing.T) {
_, err := ReadConfig("nonexistent.json")
require.Error(t, err)
require.Contains(t, err.Error(), "read file")
})
// Error path: invalid JSON
t.Run("invalid json", func(t *testing.T) {
_, err := ReadConfig("testdata/invalid.json")
require.Error(t, err)
require.Contains(t, err.Error(), "parse config")
})
}---
CI/CD Enforcement
Check and Fail on Low Coverage
# .github/workflows/coverage.yml
- name: Check branch coverage
run: |
go test -coverprofile=coverage.out -covermode=atomic ./...
# Statement coverage as proxy (actual branch coverage requires external tools)
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
# For branch coverage, multiply by 0.85-0.9 factor
# (branch coverage is typically 85-90% of statement coverage)
ESTIMATED_BRANCH=$(awk -v c="$COVERAGE" 'BEGIN { printf "%.1f", c * 0.85 }')
echo "Statement coverage: $COVERAGE%"
echo "Estimated branch coverage: $ESTIMATED_BRANCH%"
if awk -v b="$ESTIMATED_BRANCH" 'BEGIN { exit !(b >= 80) }'; then
echo "✓ Branch coverage meets 80% threshold"
else
echo "✗ Branch coverage below 80% threshold"
exit 1
fiUsing Codecov with Branch Coverage
# codecov.yml
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: 80%
threshold: 1%
branches:
- main
patch:
default:
target: 80%
threshold: 1%
# Comment settings
comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: true
require_base: true
require_head: true
branches:
- main---
Common Pitfalls
1. Testing Only Happy Paths
// Bad: Only tests success
func TestProcess_Success(t *testing.T) {
result, _ := Process(validInput)
assert.Equal(t, expected, result)
}
// Good: Tests all outcomes
func TestProcess(t *testing.T) {
t.Run("success", func(t *testing.T) { ... })
t.Run("invalid input", func(t *testing.T) { ... })
t.Run("timeout", func(t *testing.T) { ... })
}2. Ignoring Default Cases
// Ensure switch default is covered
switch status {
case Active: ...
case Pending: ...
default:
// This path is often missed
return fmt.Errorf("unknown status: %s", status)
}3. Short-Circuit Evaluation
// if a || b: need tests where a=true, and where a=false,b=true, and where both false
// if a && b: need tests where both true, a=false, and a=true,b=false---
Tools Comparison
| Tool | Branch Coverage | Language | Free |
|---|---|---|---|
| Codecov | Yes (with config) | Multi | Freemium |
| Coveralls | Limited | Multi | Freemium |
| SonarQube | Yes | Multi | Freemium |
| gocov | No (statement only) | Go | Yes |
| JaCoCo | Yes | Java | Yes |
| Istanbul | Yes | JS/TS | Yes |
| Coverage.py | Yes (with branch flag) | Python | Yes |
---
Badge Criteria Verification
To verify test_branch_coverage80:
1. Generate coverage report
go test -coverprofile=coverage.out -covermode=atomic ./...2. Convert to analyzable format
gocover-cobertura < coverage.out > coverage.xml3. Check branch coverage
- Upload to Codecov/Coveralls
- Or use local XML analysis tools
- Or estimate from statement coverage (* 0.85)
4. Document in badge application
- Link to coverage reports
- Show coverage percentage
- Explain measurement methodology
---
Resources
CI Robustness in Docker/Worktree Environments
Patterns and fixes for CI pipelines that run inside Docker containers or git worktree checkouts, where common assumptions about the .git directory structure break.
1. CaptainHook in Docker
CaptainHook's hook-installer plugin fails in Docker containers and git worktrees where .git is a file (pointing to the bare repo) instead of a directory. The failure aborts Composer's post-install-cmd scripts, silently preventing subsequent plugins from running.
Fix
Add CAPTAINHOOK_DISABLE=true to every Docker-based Composer command:
docker run ... -e CAPTAINHOOK_DISABLE=true ... composer installIn runTests.sh or similar CI scripts, pass the variable into the container environment:
docker compose run -e CAPTAINHOOK_DISABLE=true php composer install --no-interactionDetection
Find Composer commands that are missing the guard:
grep -n "composer" Build/Scripts/runTests.sh | grep -v "CAPTAINHOOK_DISABLE"Any match indicates a command that will fail when .git is not a directory (Docker, worktrees, CI shallow clones).
2. PHPStan Extension Installer Verification
phpstan/extension-installer registers extensions during Composer's post-install-cmd phase. If an earlier plugin (such as CaptainHook) aborts that phase, extension-installer never runs, and PHPStan operates without the extensions it needs.
Symptom
PHPStan reports false positives or misses errors that extensions should catch (e.g., PHPStan for Doctrine, phpstan-phpunit).
Verification
After composer install, check the generated config:
# Prefer TYPO3-style .Build/vendor if present; otherwise use vendor/ or $VENDOR_DIR
VENDOR_DIR="${VENDOR_DIR:-vendor}"
if [ -d ".Build/vendor" ]; then
SEARCH_PATH=".Build/vendor/phpstan/extension-installer/src/GeneratedConfig.php"
else
SEARCH_PATH="$VENDOR_DIR/phpstan/extension-installer/src/GeneratedConfig.php"
fi
grep -F "EXTENSIONS = []" "$SEARCH_PATH"If the output shows EXTENSIONS = [], the extension-installer plugin did not execute. This is almost always caused by CaptainHook (or another plugin) failing before extension-installer could run.
Fix
Apply the CaptainHook disable fix from Section 1, then re-run composer install. Verify that GeneratedConfig.php now lists the expected extensions.
3. Lint Scope
PHP lint (php -l) test suites must scan only project source directories, not vendor/ or .Build/. Third-party packages may contain intentional syntax error fixtures (e.g., for their own parser tests), which cause false lint failures.
Wrong -- scans vendor too
find . -name '*.php' ! -path './.Build/*' -exec php -l {} \;This still includes vendor/ at the project root (if present) and misses the intent of excluding build artifacts.
Acceptable -- find . with proper exclusions
find . -name '*.php' \
! -path './vendor/*' ! -path './.Build/*' ! -path './var/*' ! -path './node_modules/*' \
-exec php -l {} \;Using find . is fine when it explicitly prunes or excludes third-party and build directories.
Best -- scan only source directories
find Classes Configuration Tests -name '*.php' -exec php -l {} \;Explicitly list the directories that contain project code. This is faster, deterministic, and immune to third-party fixture files.
Detection
grep -n 'find \.' Build/Scripts/runTests.sh | grep -Ei 'php.*-l|lint'If the find starts from . without -prune or ! -path exclusions for vendor/, .Build/, var/, etc., the lint scope is too broad.
4. Merge Queue Awareness
When branch protection uses GitHub merge queues, several constraints apply that can surprise CI workflows:
| Constraint | Effect |
|---|---|
required_review_thread_resolution: true | Blocks merge if any review thread is unresolved |
| Queue locks the branch | Commits cannot be pushed to a branch while it is in the merge queue |
| Auto-merge interaction | Auto-merge must be disabled before pushing fixes to a PR, then re-enabled after |
Workflow
1. Address all review comments and resolve threads 2. If the PR has auto-merge enabled and needs fixes:
- Disable auto-merge
- Push the fix commits
- Wait for CI
- Re-enable auto-merge
3. Ensure all review threads are resolved before the PR enters the queue
Detection
Check whether a repository uses merge queues:
gh api repos/{owner}/{repo}/rules/branches/main \
--jq '.[].parameters.merge_queue_enabled // false'Related References
references/ci-patterns.md-- CI/CD pipeline design and Git hooksreferences/general.md-- Universal enterprise readiness checksreferences/security-hardening.md-- Security scanning in CI
CI/CD Patterns
Comprehensive CI/CD patterns for enterprise-ready projects. Covers Git hooks, pipeline design, and quality gates.
1. Git Hooks Strategy
Pre-commit vs Pre-push Division
Principle: Fast feedback locally, thorough verification before sharing.
| Hook | Purpose | Speed Target | What to Run |
|---|---|---|---|
| pre-commit | Catch obvious issues | < 5 seconds | Format, lint, build, secrets |
| pre-push | Thorough verification | < 2 minutes | Full test suite, security scans |
Lefthook Configuration Example
# .lefthook.yml
pre-commit:
parallel: true
commands:
fmt:
glob: "*.{go,php,ts,js}"
run: |
# Language-specific formatters
gofmt -w {staged_files} 2>/dev/null || true
prettier --write {staged_files} 2>/dev/null || true
lint:
glob: "*.{go,php,ts}"
run: |
golangci-lint run --new-from-rev=HEAD~1 || true
phpstan analyse --no-progress || true
eslint {staged_files} || true
build:
run: |
go build ./... || make build
secrets:
run: gitleaks protect --staged --no-banner
pre-push:
parallel: true
commands:
test:
run: |
go test -race ./...
# Or: phpunit, npm test, etc.
lint-full:
run: golangci-lint run --timeout 5m
security:
run: |
govulncheck ./...
# Or: composer audit, npm audit, etc.Hook Bypass Policy
- Never bypass hooks in normal workflow
- Use
--no-verifyonly for: - Emergency hotfixes (document why)
- WIP commits on feature branches (squash before PR)
- CI must run same checks as hooks (no bypass possible)
2. Comprehensive CI Pipeline
Pipeline Structure
┌─────────────────────────────────────────────────────────────┐
│ CI Pipeline │
├─────────────────────────────────────────────────────────────┤
│ Stage 1: Fast Feedback (parallel) │
│ ├── lint (golangci-lint, phpstan, eslint) │
│ ├── format-check (gofmt, prettier) │
│ ├── build │
│ └── secrets-scan (gitleaks, trufflehog) │
├─────────────────────────────────────────────────────────────┤
│ Stage 2: Testing (parallel by OS/version) │
│ ├── unit-tests (ubuntu, macos, windows) │
│ ├── integration-tests │
│ ├── fuzz-tests (if applicable) │
│ └── benchmarks (optional, for tracking) │
├─────────────────────────────────────────────────────────────┤
│ Stage 3: Security (parallel) │
│ ├── sast (CodeQL, semgrep) │
│ ├── dependency-scan (govulncheck, npm audit) │
│ ├── container-scan (trivy, if applicable) │
│ └── license-check │
├─────────────────────────────────────────────────────────────┤
│ Stage 4: Coverage & Quality (depends on tests) │
│ ├── coverage-report │
│ └── quality-gate (fail if below threshold) │
├─────────────────────────────────────────────────────────────┤
│ Stage 5: Artifacts (depends on all above) │
│ ├── build-artifacts │
│ └── attestations (SLSA provenance) │
└─────────────────────────────────────────────────────────────┘Preventing Duplicate CI Runs
When a workflow triggers on both push: and pull_request:, pushing to a PR branch causes every job to run twice -- once for the push event and once for the pull_request event. This wastes CI minutes and clutters the checks list.
Fix: Restrict push: to protected branches only:
on:
push:
branches: [main] # Only run on pushes to main (merge commits)
pull_request: # Runs on PR open/sync — covers feature branchesAnti-pattern (causes duplicate runs):
on:
push: # BAD: triggers on ALL branch pushes including PR branches
pull_request:This applies to all workflow files, not just ci.yml. Audit every .github/workflows/*.yml for unscoped push: triggers.
GitHub Actions Example
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
# Stage 1: Fast Feedback
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
version: latest
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Stage 2: Testing (matrix)
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go-version: ['1.25.x']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- run: go test -race -coverprofile=coverage.txt ./...
- uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
if: matrix.os == 'ubuntu-latest'
# Stage 3: Security
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
vulnerability-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- run: govulncheck ./...
# Stage 4: Quality Gate
quality-gate:
needs: [lint, test, codeql, vulnerability-scan]
runs-on: ubuntu-latest
steps:
- run: echo "All checks passed"3. Quality Gates
Coverage Thresholds
| Level | Statement Coverage | Branch Coverage | Use Case |
|---|---|---|---|
| Minimum | 60% | - | Legacy projects |
| Standard | 80% | - | Most projects |
| High | 80% | 70% | Critical systems |
| Enterprise | 90% | 80% | Financial, medical |
Multi-Suite Coverage (MANDATORY)
Coverage must be collected from ALL test suites, not just unit tests:
# PHP: Collect coverage from each test suite
- name: Unit Tests
run: php -d pcov.enabled=1 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml --coverage-clover .Build/coverage/unit.xml
- name: Integration Tests
run: php -d pcov.enabled=1 .Build/bin/phpunit -c Build/phpunit/IntegrationTests.xml --coverage-clover .Build/coverage/integration.xml
- name: E2E Tests
run: php -d pcov.enabled=1 .Build/bin/phpunit -c Build/phpunit/E2ETests.xml --coverage-clover .Build/coverage/e2e.xml
# Upload ALL coverage files
- uses: codecov/codecov-action@SHA # vX.Y.Z
with:
files: .Build/coverage/unit.xml,.Build/coverage/integration.xml,.Build/coverage/e2e.xmlPolyglot Coverage (PHP + JavaScript)
Projects with both PHP and JavaScript MUST collect coverage from both:
# PHP Tests
- name: PHP Tests with Coverage
run: |
php -d pcov.enabled=1 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml --coverage-clover .Build/coverage/unit.xml
php -d pcov.enabled=1 .Build/bin/phpunit -c Build/phpunit/IntegrationTests.xml --coverage-clover .Build/coverage/integration.xml
# JavaScript Tests (vitest/jest)
- uses: actions/setup-node@SHA # vX.Y.Z
with:
node-version: '22'
- run: npm install
- name: JavaScript Tests with Coverage
run: npm run test:coverage # Must output coverage/lcov.info
# Upload ALL coverage
- uses: codecov/codecov-action@SHA # vX.Y.Z
with:
files: .Build/coverage/unit.xml,.Build/coverage/integration.xml,coverage/lcov.infovitest.config.js MUST include lcov reporter:
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'], // lcov REQUIRED
reportsDirectory: 'coverage',
}Enforcing Coverage
# In CI workflow
- name: Check coverage threshold
run: |
COVERAGE=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}' | tr -d '%')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
# For patch coverage (new code only)
- name: Check patch coverage
run: |
diff-cover coverage.xml --compare-branch=origin/main --fail-under=90Codecov Patch Coverage Configuration
Codecov's patch target checks coverage on new/changed lines only. Set a realistic threshold -- lines that require specific PHP extensions (GD, Imagick) or a full TYPO3 bootstrap may be uncoverable in some CI matrix entries but covered in others.
# codecov.yml
coverage:
status:
project:
default:
target: 80%
patch:
default:
target: 60% # Realistic for extension code with environment deps
threshold: 5% # Allow 5% drop before failingKey considerations:
- Lines behind
extension_loaded('gd')guards may show as uncovered in matrix
entries without that extension
- TYPO3 integration tests may cover code that unit tests cannot reach
- Set
patch.targetbased on what your CI matrix actually covers, not aspirational goals - Use Codecov flags to separate unit vs integration coverage for clearer reporting
PR Quality Requirements
# Branch protection rules
branch_protection:
required_status_checks:
strict: true
contexts:
- lint
- test (ubuntu-latest)
- test (macos-latest)
- test (windows-latest)
- codeql
- vulnerability-scan
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: true
required_linear_history: true # Enforce rebase
required_signatures: true # Signed commits4. PHPStan Baseline Management
Generating a Baseline
Capture all existing issues so new code is held to a higher standard:
# Generate baseline from current state
vendor/bin/phpstan analyse --generate-baseline
# When codebase is already clean, allow empty baseline
vendor/bin/phpstan analyse --generate-baseline --allow-empty-baselineThis creates phpstan-baseline.neon (or the configured path) containing all currently-known errors, which PHPStan then ignores on subsequent runs.
Baseline CI Strategy
The baseline should shrink over time, never grow. Track this in CI:
- name: PHPStan analysis
run: vendor/bin/phpstan analyse --no-progress
- name: Check baseline size
run: |
if [ -f phpstan-baseline.neon ]; then
IGNORED=$(grep -c 'message:' phpstan-baseline.neon || echo 0)
echo "PHPStan baseline: $IGNORED ignored errors"
# Optional: fail if baseline grew
# Compare against a known count stored in a tracking file
fiBaseline Best Practices
| Practice | Description |
|---|---|
| Goal: empty baseline | ignoreErrors: [] means all issues resolved |
| Never add new entries | Fix new issues immediately; only existing code gets a pass |
| Review baseline in PRs | Any PR that increases baseline size needs justification |
| Track count over time | Log baseline error count as a CI metric |
| Use `--allow-empty-baseline` | Required when generating baseline on a clean codebase |
PHPStan Configuration Example
# phpstan.neon
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- Classes
tmpDir: .Build/phpstan5. Multi-Platform Testing
OS Matrix Strategy
strategy:
fail-fast: false # Don't cancel other jobs on failure
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
# OS-specific configurations
- os: ubuntu-latest
artifact-suffix: linux-amd64
- os: macos-latest
artifact-suffix: darwin-amd64
- os: windows-latest
artifact-suffix: windows-amd64.exeVersion Matrix Strategy
strategy:
matrix:
# Test oldest supported and latest
version: ['1.21', '1.25'] # Or PHP: ['8.1', '8.4']
include:
- version: '1.25'
latest: true # Upload coverage only from latest6. Flaky Test Prevention
CI-Specific Test Configuration
# Increase timeouts in CI
- name: Run tests
run: go test -race -timeout 10m ./...
env:
CI: true
TEST_TIMEOUT_MULTIPLIER: 3Retry Strategy for Known Flaky Tests
- name: Run tests with retry
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: go test -race ./...Detecting Flaky Tests
# Run tests multiple times to detect flakiness
- name: Flaky test detection
if: github.event_name == 'schedule' # Nightly only
run: |
for i in {1..10}; do
go test -race ./... || exit 1
done7. Caching Strategy
Go Caching
- uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true # Caches go mod and build cachePHP Caching
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}Node Caching
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'8. Signed Commits Enforcement
Problem: GitHub Can't Sign Rebase Merges
When repository requires signed commits AND only allows rebase merge:
# This fails because GitHub can't sign rebased commits
gh pr merge 123 --rebase
# Error: Base branch requires signed commitsSolution: Local Merge Workflow
# 1. Fetch and checkout main
git checkout main
git pull origin main
# 2. Fast-forward merge (preserves signatures)
git merge feature-branch --ff-only
# 3. Push to main
git push origin mainAutomating with PR Labels
# Workflow triggered by 'ready-to-merge' label
on:
pull_request:
types: [labeled]
jobs:
merge:
if: github.event.label.name == 'ready-to-merge'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MERGE_TOKEN }} # PAT with push access
- name: Merge PR
run: |
git checkout main
git merge origin/${{ github.head_ref }} --ff-only
git push origin main9. Reusable Workflow Pinning
Supply Chain Security for Reusable Workflows
All uses: references to third-party reusable workflows must be pinned to commit SHAs, just like regular actions:
# BAD: branch reference is mutable (supply chain risk)
jobs:
ci:
uses: org/shared-workflows/.github/workflows/ci.yml@main
# BAD: tag reference is also mutable
jobs:
ci:
uses: org/shared-workflows/.github/workflows/ci.yml@v2
# GOOD: SHA-pinned (immutable reference)
jobs:
ci:
uses: org/shared-workflows/.github/workflows/ci.yml@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 # v2.1.0Exception: Org-Internal Workflows
Org-internal reusable workflows (maintained by the same organization) should use `@main` or `@tag`, not SHAs. SHA-pinning org-internal workflows is an anti-pattern because it breaks centralized security update propagation.
See checkpoints.yaml ER-33 for the enforcement rule.
Auditing Workflow References
# Find all reusable workflow references
grep -rn 'uses:.*\.github/workflows/.*\.yml@' .github/workflows/
# Find unpinned references (tag or branch, no SHA)
grep -rn 'uses:.*\.github/workflows/.*\.yml@' .github/workflows/ | grep -v '@[a-f0-9]\{40\}'Related References
references/ci-docker-worktree.md- Docker/worktree CI robustness (CaptainHook, lint scope, merge queues)references/security-hardening.md- Security scanning detailsreferences/code-review.md- Code quality checklistreferences/signed-releases.md- Release signingassets/workflows/- Ready-to-use workflow templates
Code Review Quality Patterns
Language-agnostic code review checklist and patterns for ensuring production-quality code. These patterns apply to Go, PHP, TypeScript, Python, and other languages.
Code Review Checklist
Before approving any PR, verify these patterns:
1. Test Resource Management
Issue: Tests creating multiple instances of the same resource when only one is needed.
Check:
- [ ] Is there exactly ONE instance of each configurable resource per test?
- [ ] Is the resource fully configured at creation time (not reconfigured later)?
- [ ] Are shared counters/state isolated between test resources?
Bad Example:
func TestFeature(t *testing.T) {
// BAD: Creates two instances, first is unused
c := NewClient(WithTimeout(5))
_, _ = c.AddJob("* * * * *", myJob)
c = NewClient(WithTimeout(5), WithLogger(logger)) // Replaced!
_, _ = c.AddJob("* * * * *", myJob)
c.Start()
}Good Example:
func TestFeature(t *testing.T) {
// GOOD: Single instance, fully configured
c := NewClient(
WithTimeout(5),
WithLogger(logger),
)
_, _ = c.AddJob("* * * * *", myJob)
c.Start()
}2. State Mutation Completeness
Issue: Operations that logically change state but don't update tracking fields.
Check:
- [ ] After operations complete, are ALL related tracking fields updated?
- [ ] On restart/retry, will the state be correct?
- [ ] Are side effects committed atomically with the main operation?
Bad Example:
func (s *Scheduler) handleMissedRuns(entry *Entry, missed []time.Time) {
for _, t := range missed {
s.runJob(entry, t)
}
// BAD: entry.LastRun not updated - will re-run on restart!
}Good Example:
func (s *Scheduler) handleMissedRuns(entry *Entry, missed []time.Time) {
for _, t := range missed {
s.runJob(entry, t)
}
// GOOD: Update tracking state after execution
entry.LastRun = missed[len(missed)-1]
}3. Defensive Enum/Const Handling
Issue: Switch statements on enums without handling unknown values.
Check:
- [ ] Does the enum type have a
Valid()orIsValid()method? - [ ] Do switch statements have a
defaultcase? - [ ] Is the default case tested explicitly?
- [ ] Are unexpected values logged for debugging?
Bad Example:
func (p Policy) Apply() {
switch p {
case PolicyA:
doA()
case PolicyB:
doB()
// BAD: No default case - silently ignores invalid values
}
}Good Example:
// Valid returns true if the policy is a known valid value
func (p Policy) Valid() bool {
return p >= PolicyA && p <= PolicyB
}
func (p Policy) Apply() error {
switch p {
case PolicyA:
doA()
case PolicyB:
doB()
default:
// GOOD: Log and handle unexpected values
log.Warn("unexpected policy value", "policy", int(p))
return fmt.Errorf("invalid policy: %d", p)
}
return nil
}Testing Default Case:
func TestPolicyApply_InvalidPolicy(t *testing.T) {
p := Policy(99) // Invalid value
err := p.Apply()
if err == nil {
t.Error("expected error for invalid policy")
}
}4. Documentation Accuracy
Issue: Documentation claims that don't match actual behavior or benchmarks.
Check:
- [ ] Are performance claims verified against actual benchmark output?
- [ ] Are negative trade-offs documented (memory increase, complexity)?
- [ ] Do code examples include all necessary imports?
- [ ] Do code examples handle errors appropriately?
Bad Example:
## Performance
WithCapacity provides ~6% improvement in bulk operations.(When benchmarks actually show 12% improvement)
Good Example:
## Performance
WithCapacity provides ~12% improvement in bulk addition time (based on
BenchmarkBulkAdd: 128342 ns/op without vs 113338 ns/op with capacity).
Trade-off: ~3% higher memory usage due to pre-allocated capacity.5. Platform-Specific Code
Issue: Code that only works on certain platforms without documentation.
Check:
- [ ] Is platform-specific code clearly marked?
- [ ] Are limitations documented in comments?
- [ ] Are alternatives provided for unsupported platforms?
- [ ] Are build tags used where appropriate?
Bad Example:
func lockFile(f *os.File) error {
return syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
}Good Example:
// lockFile acquires an exclusive lock on the file.
// NOTE: This uses flock() which is Unix-specific (Linux, macOS, BSD).
// On Windows, use LockFileEx from golang.org/x/sys/windows.
//
//go:build !windows
func lockFile(f *os.File) error {
return syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
}6. Defensive Code Coverage
Issue: Defensive code paths (error handling, edge cases) not tested.
Check:
- [ ] Are
defaultswitch cases covered by tests? - [ ] Are error paths tested (not just happy path)?
- [ ] Are boundary conditions tested?
- [ ] Is validation logic tested with invalid inputs?
Strategy for Testing Defensive Code:
// To test defensive code that's normally unreachable,
// bypass normal validation and call internal functions directly
func TestHandleInvalidState(t *testing.T) {
// Create object in invalid state (bypassing constructor validation)
obj := &MyObject{
state: InvalidState(99),
}
// Call method that has defensive handling
err := obj.Process()
// Verify defensive code executed correctly
if err == nil {
t.Error("expected error for invalid state")
}
}Review Process Quality
Required Conversation Resolution (Branch Protection)
Enable required_conversation_resolution in branch protection to enforce that all review threads are resolved before merging. Without this, review feedback can be silently ignored — especially problematic when automated reviewers (e.g., GitHub Copilot) leave actionable comments.
# Check if enabled
gh api repos/{owner}/{repo}/branches/main/protection \
--jq 'if .required_conversation_resolution.enabled then "✅ Enabled" else "❌ NOT enabled" end'See references/github.md → "Review Enforcement" for full setup instructions.
PR Review Thread Resolution
When addressing review feedback:
1. Fix the code - Address the feedback 2. Reply to the thread - Explain what was changed (use GraphQL API to reply to thread, not new comment) 3. Resolve the thread - Mark as resolved after fix is pushed 4. Verify CI - Ensure all checks still pass
Review Comment Quality
Good review comments:
- Explain WHY something is problematic
- Provide a concrete suggestion or example
- Reference documentation or standards when applicable
Language-Specific Implementations
| Pattern | Go | PHP | TypeScript |
|---|---|---|---|
| Enum validation | func (e Enum) Valid() bool | enum_exists() or cases match | Object.values(Enum).includes(v) |
| Time mocking | FakeClock interface | Carbon::setTestNow() | jest.useFakeTimers() |
| Platform detection | Build tags //go:build | PHP_OS_FAMILY | process.platform |
Integration with CI
Add these checks to your CI pipeline:
# Example: Ensure test coverage for new code
- name: Check patch coverage
run: |
# Fail if new code has < 90% coverage
diff-cover coverage.xml --fail-under=90Related References
references/documentation.md- Documentation standardsreferences/ci-patterns.md- CI/CD automationreferences/security-hardening.md- Security patterns
Dependency CVE Workflow
When assessing enterprise readiness, always run dependency audit as part of discovery.
Audit Commands
# PHP/Composer
composer audit
# Node.js
npm audit
# Python
pip-audit
# Go
govulncheck ./...CVE Handling Best Practice
Separate dependency updates from code changes:
| PR Type | Content | Why |
|---|---|---|
| Code changes | Business logic, bug fixes, features | Reviewable, testable in isolation |
| Dependency updates | composer update, version bumps | Clear diff, easy rollback if issues |
Real-world example from t3x-cowriter review:
- Found 4 CVEs during enterprise assessment
- CVE fixes required
composer update typo3/cms-core typo3/cms-backend - Kept separate from code fixes (JS bug, AGENTS.md updates) for clean PR history
CVE Severity Response
| Severity | Response Time | Action |
|---|---|---|
| CRITICAL | Immediate | Hotfix PR, expedited review |
| HIGH | 24-48 hours | Priority PR, security review |
| MEDIUM | 1 week | Normal PR cycle |
| LOW | Next release | Batch with other updates |
CI Integration
Add dependency audit to CI pipeline:
# .github/workflows/ci.yml
- name: Security audit
run: composer audit --format=plainDeveloper Certificate of Origin (DCO) Implementation Guide
The DCO is a lightweight way to certify that contributors have the right to submit their code under the project's license.
What is DCO?
The Developer Certificate of Origin is a legal statement that contributors make when submitting code:
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution is maintained
indefinitely and may be redistributed consistent with this project
or the open source license(s) involved.Implementation Steps
1. Document DCO in CONTRIBUTING.md
Add this section to your CONTRIBUTING.md:
## Developer Certificate of Origin (DCO)
All contributions to this project must be signed off using the DCO.
### Sign Your Commits
Use the `-s` flag when committing:
\`\`\`bash
git commit -s -m "feat: add new feature"
\`\`\`
This adds a `Signed-off-by` line to your commit message:
\`\`\`
feat: add new feature
Signed-off-by: Your Name <your.email@example.com>
\`\`\`
### Retroactive Sign-off
If you forgot to sign a commit:
\`\`\`bash
# For the last commit
git commit --amend -s
# For multiple commits
git rebase --signoff HEAD~N
\`\`\`
### Configure Git for Automatic Sign-off
\`\`\`bash
git config --global alias.cs "commit -s"
\`\`\`2. Add GitHub Action for Enforcement
Create .github/workflows/dco.yml:
name: DCO Check
on:
pull_request:
branches: [main, master]
permissions:
contents: read
pull-requests: read
jobs:
dco:
name: DCO Check
runs-on: ubuntu-latest
steps:
- name: Check DCO
uses: dcoapp/app@v13. Add DCO to PR Template
Add to .github/PULL_REQUEST_TEMPLATE.md:
## DCO Sign-off
- [ ] I have signed off all commits in this PR (`git commit -s`)
By submitting this PR, I certify that my contribution is made under
the terms of the Developer Certificate of Origin.Verification
Check a Commit
git log --format='%h %s%n Signed-off-by: %aN <%aE>' -1Verify All Commits in a PR
git log origin/main..HEAD --format='%h %B' | grep -B1 "Signed-off-by:"Common Issues
1. Missing Sign-off
Error: "DCO check failed"
Fix:
git rebase --signoff HEAD~N
git push --force-with-lease2. Email Mismatch
Error: "Email in sign-off doesn't match commit author"
Fix:
git config user.email "your.email@example.com"
git commit --amend -s3. Corporate Email Required
Some projects require corporate email addresses. Configure:
git config user.email "you@company.com"DCO vs CLA
| Aspect | DCO | CLA |
|---|---|---|
| Legal weight | Per-commit attestation | One-time signed agreement |
| Friction | Low (just -s flag) | Higher (legal review) |
| Implementation | Git hooks/CI | Bot or manual process |
| Common in | Linux kernel, CNCF | Apache, Google projects |
OpenSSF Badge Requirements
For Silver level:
dco: The project MUST have a way to ensure contributors have rights to contribute
This is satisfied by: 1. DCO enforcement (recommended) 2. CLA (Contributor License Agreement) 3. Clear license in CONTRIBUTING.md