Reusable GitHub Actions job snippets for code quality checks. Each snippet is a self-contained job definition that projects compose into their own workflows.
The project uses a three-layer architecture:
scripts/shell/<category>/<tool>.sh ← bash logic (tested, standalone)
↓ assembled by scripts/assemble-ci.sh
scripts/CI/<category>/<tool>.yml ← source YAML (references the script)
↓
CI/<category>/<tool>.yml ← assembled output (script inlined, ready to use)
Shell scripts (scripts/shell/) contain the actual linting/testing logic.
They run standalone in CI without needing anything from this repo's structure.
Source YAMLs (scripts/CI/) define the GitHub Actions job: install the tool,
then call the script with run: bash scripts/shell/<category>/<tool>.sh.
Assembled YAMLs (CI/) are generated by the assembler. The run: bash ... line is replaced with
run: | followed by the full script body inlined. These are the files downstream projects import.
Workflows/
├── scripts/
│ ├── assemble-ci.sh # assembles source YAMLs → CI/
│ ├── CI/ # source YAML templates
│ │ ├── linters/
│ │ │ ├── clippy.yml
│ │ │ ├── eslint.yml
│ │ │ ├── golangci-lint.yml
│ │ │ ├── hadolint.yml
│ │ │ ├── htmlhint.yml
│ │ │ ├── ktlint.yml
│ │ │ ├── markdownlint.yml
│ │ │ ├── mermaid.yml
│ │ │ ├── phpcs.yml
│ │ │ ├── rubocop.yml
│ │ │ ├── ruff.yml
│ │ │ ├── shellcheck.yml
│ │ │ ├── sqlfluff.yml
│ │ │ ├── stylelint.yml
│ │ │ ├── swiftlint.yml
│ │ │ ├── tflint.yml
│ │ │ └── yamllint.yml
│ │ ├── security/
│ │ │ ├── bundler-audit.yml
│ │ │ ├── gitleaks.yml
│ │ │ ├── pip-audit.yml
│ │ │ ├── semgrep.yml
│ │ │ └── trivy.yml
│ │ ├── static_analysis/
│ │ │ ├── mypy.yml
│ │ │ ├── phpstan.yml
│ │ │ └── spotbugs.yml
│ │ └── tests/
│ │ ├── bats.yml
│ │ ├── cargo_test.yml
│ │ ├── go_test.yml
│ │ ├── jest.yml
│ │ ├── laravel_tests.yml
│ │ ├── pytest.yml
│ │ ├── rspec.yml
│ │ └── xcodebuild_test.yml
│ └── shell/ # bash scripts (one per tool)
│ ├── linters/
│ │ ├── clippy.sh
│ │ ├── eslint.sh
│ │ ├── golangci-lint.sh
│ │ ├── hadolint.sh
│ │ ├── htmlhint.sh
│ │ ├── ktlint.sh
│ │ ├── markdownlint.sh
│ │ ├── mermaid.sh
│ │ ├── phpcs.sh
│ │ ├── rubocop.sh
│ │ ├── ruff.sh
│ │ ├── shellcheck.sh
│ │ ├── sqlfluff.sh
│ │ ├── stylelint.sh
│ │ ├── swiftlint.sh
│ │ ├── tflint.sh
│ │ └── yamllint.sh
│ ├── security/
│ │ ├── bundler-audit.sh
│ │ ├── gitleaks.sh
│ │ ├── pip-audit.sh
│ │ ├── semgrep.sh
│ │ └── trivy.sh
│ └── tests/
│ ├── cargo_test.sh
│ ├── jest.sh
│ └── xcodebuild_test.sh
│
├── CI/ # assembled output (ready to use)
│ ├── linters/
│ ├── security/
│ ├── static_analysis/
│ └── tests/
│
├── tests/
│ ├── assemble-ci.bats # tests for the assembler
│ ├── linters/ # unit tests for each shell script
│ │ ├── clippy.bats
│ │ ├── hadolint.bats
│ │ ├── htmlhint.bats
│ │ ├── markdownlint.bats
│ │ ├── phpcs.bats
│ │ ├── shellcheck.bats
│ │ ├── sqlfluff.bats
│ │ ├── stylelint.bats
│ │ ├── tflint.bats
│ │ └── yamllint.bats
│ ├── security/
│ │ ├── bundler-audit.bats
│ │ ├── gitleaks.bats
│ │ ├── pip-audit.bats
│ │ ├── semgrep.bats
│ │ └── trivy.bats
│ ├── tests/
│ │ ├── cargo_test.bats
│ │ ├── jest.bats
│ │ └── xcodebuild_test.bats
│ ├── dependabot/
│ │ └── dependabot.bats # validates the Dependabot template
│ └── helpers/
│ └── common.bash # shared test utilities (mocks, temp dirs)
│
├── dependabot/
│ └── dependabot.yml # Dependabot config template (copy to .github/)
│
└── rules/ # living documentation for contributors
├── README.md
├── _meta/how-to-write-rules.md
└── process/
├── architecture-design.md
└── ci-cd.md
Run the assembler for each category:
bash scripts/assemble-ci.sh scripts/CI/linters scripts/shell/linters CI/linters
bash scripts/assemble-ci.sh scripts/CI/security scripts/shell/security CI/security
bash scripts/assemble-ci.sh scripts/CI/static_analysis scripts/shell/static_analysis CI/static_analysis
bash scripts/assemble-ci.sh scripts/CI/tests scripts/shell/tests CI/testsThe assembler takes three arguments: <source-yamls-dir> <scripts-dir> <output-dir>.
For each .yml in the source dir it looks for a matching .sh in the scripts dir. If found, the run: bash ...
line is replaced with run: | and the script body is inlined (shebang stripped, indentation preserved).
If not found, the YAML is copied as-is.
Example transformation:
Source (scripts/CI/linters/shellcheck.yml):
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: bash scripts/shell/linters/shellcheck.shAssembled (CI/linters/shellcheck.yml):
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: |
ERROR_FOUND=0
DIRS=("scripts" "docker/scripts")
for DIR in "${DIRS[@]}"; do
...
done
...| Snippet | Tool | What it checks |
|---|---|---|
CI/security/bundler-audit.yml |
bundler-audit | CVEs in Ruby gem dependencies via the Ruby Advisory Database |
CI/security/gitleaks.yml |
gitleaks | Hardcoded secrets, tokens, and API keys |
CI/security/pip-audit.yml |
pip-audit | CVEs in Python dependencies via the PyPI Advisory Database |
CI/security/semgrep.yml |
semgrep | OWASP Top 10 patterns and insecure coding patterns across Python, JS/TS, Go, Java, Ruby, and more |
CI/security/trivy.yml |
trivy | CVEs in OS packages, container images, and dependency manifests |
| Snippet | Tool | What it checks |
|---|---|---|
CI/linters/clippy.yml |
clippy | Rust |
CI/linters/eslint.yml |
eslint | JavaScript / TypeScript |
CI/linters/golangci-lint.yml |
golangci-lint | Go |
CI/linters/hadolint.yml |
hadolint | Dockerfiles |
CI/linters/htmlhint.yml |
htmlhint | HTML files |
CI/linters/ktlint.yml |
ktlint | Kotlin |
CI/linters/markdownlint.yml |
markdownlint | Markdown files |
CI/linters/mermaid.yml |
mermaid-cli | Mermaid diagrams |
CI/linters/phpcs.yml |
phpcs | PHP (PSR-12 and custom rulesets) |
CI/linters/rubocop.yml |
rubocop | Ruby |
CI/linters/ruff.yml |
ruff | Python |
CI/linters/shellcheck.yml |
shellcheck | Shell scripts |
CI/linters/sqlfluff.yml |
sqlfluff | SQL files |
CI/linters/stylelint.yml |
stylelint | CSS / SCSS / LESS |
CI/linters/swiftlint.yml |
swiftlint | Swift |
CI/linters/tflint.yml |
tflint | Terraform |
CI/linters/yamllint.yml |
yamllint | YAML files |
| Snippet | Tool | What it checks |
|---|---|---|
CI/static_analysis/mypy.yml |
mypy | Python (type checking) |
CI/static_analysis/phpstan.yml |
PHPStan | PHP (level from phpstan.neon) |
CI/static_analysis/spotbugs.yml |
SpotBugs | Java (via Maven) |
| Snippet | What it runs |
|---|---|
CI/tests/bats.yml |
BATS tests (tests/ directory) |
CI/tests/cargo_test.yml |
Rust test suite (cargo test --all-features --verbose) |
CI/tests/go_test.yml |
Go test suite (go test ./...) |
CI/tests/jest.yml |
JavaScript/TypeScript test suite (Jest) |
CI/tests/laravel_tests.yml |
Laravel test suite (PHP 8.2, SQLite, parallel) |
CI/tests/pytest.yml |
Python test suite (pytest) |
CI/tests/rspec.yml |
Ruby test suite (RSpec via Bundler) |
CI/tests/xcodebuild_test.yml |
Swift/iOS XCTest suite (xcodebuild test via xcpretty) — macOS only |
Each assembled YAML defines a single top-level job key. Copy the content into your workflow under jobs::
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
shellcheck: # paste content of CI/linters/shellcheck.yml here
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck on scripts
run: |
ERROR_FOUND=0
...
phpstan: # paste content of CI/static_analysis/phpstan.yml here
...dependabot/dependabot.yml is a ready-to-copy Dependabot
configuration template. Unlike the CI snippets in CI/, this file is not a GitHub Actions job — it is a
repository-level configuration that GitHub reads natively from .github/dependabot.yml.
This artefact does not follow the three-layer pattern (shell → source YAML → assembled YAML). There is
no shell script or assembler step. Copy the file directly into your project's .github/ directory.
cp dependabot/dependabot.yml <your-project>/.github/dependabot.ymlThe template enables weekly automated dependency-update PRs for seven ecosystems:
github-actions, npm, pip, bundler, composer, cargo, and gomod.
Each entry uses open-pull-requests-limit: 5 and a stable commit-message prefix so the
resulting PRs are easy to filter and review.
Adjust the directory field per entry if your dependency manifests live in a subdirectory
rather than the repository root.
Every script in scripts/shell/ follows the same patterns defined in rules/process/ci-cd.md:
Error accumulator — check all files before exiting, so one failure doesn't hide others:
ERROR_FOUND=0
for file in "${files[@]}"; do
some-tool "$file" || ERROR_FOUND=1
done
if [[ $ERROR_FOUND -eq 0 ]]; then
echo "✅ All files passed!"
else
echo "❌ Issues found!"
exit 1
fiConfig validation — hard fail early if a required config is missing:
if [ ! -f ".yamllint.yml" ]; then
echo "::error::Config file .yamllint.yml not found"
exit 1
fiGraceful skip — exit 0 when there are no files to check:
if [ ${#files[@]} -eq 0 ]; then
echo "⚠️ No .yml files found. Skipping."
exit 0
fiOutput conventions:
✅— all checks passed❌— issues found⚠️— skipped or non-critical warningℹ️— currently processing a file::error::— GitHub Actions error annotation (hard failures)
Tests use BATS. Each shell script has a corresponding .bats file
that mocks external tools and tests success, failure, and edge cases in isolation.
# Install (macOS)
brew install bats-core
# Run all tests
bats --recursive tests/Test helpers (tests/helpers/common.bash) provide:
setup_test_dir/teardown_test_dir— create and clean up a temp working directorymock_tool <name> <exit_code>— stub an external binary to always exit with a given codemock_tool_conditional <name>— stub that exits 1 only when its argument matches$MOCK_FAIL_PATTERN
-
Write the bash script in
scripts/shell/<category>/<tool>.shFollow the error accumulator pattern and output conventions above. -
Write tests in
tests/<category>/<tool>.batsCover: missing config, no files found, all pass, one fails, multiple files with one failing. -
Create the source YAML in
scripts/CI/<category>/<tool>.ymlInstall the tool, then call the script:run: bash scripts/shell/<category>/<tool>.sh -
Run the assembler to generate the output file in
CI/. -
Run tests to verify everything works.
The rules/ directory is the authoritative guide for contributors:
rules/process/architecture-design.md— directory layout, file naming, snippet scoperules/process/ci-cd.md— mandatory patterns for every job (error accumulator, config validation, action versions, etc.)rules/_meta/how-to-write-rules.md— how to write and format rules files