Skip to content

prog-time/workflows

Repository files navigation

Workflows

Reusable GitHub Actions job snippets for code quality checks. Each snippet is a self-contained job definition that projects compose into their own workflows.


Snippets

Tool Category File
ESLint linters CI/linters/eslint.yml
Hadolint linters CI/linters/hadolint.yml
HTMLHint linters CI/linters/htmlhint.yml
markdownlint linters CI/linters/markdownlint.yml
mermaid-cli linters CI/linters/mermaid.yml
ShellCheck linters CI/linters/shellcheck.yml
Stylelint linters CI/linters/stylelint.yml
yamllint linters CI/linters/yamllint.yml
PHPStan static_analysis CI/static_analysis/phpstan.yml
mermaid-cli build CI/build/mermaid.yml
BATS tests CI/tests/bats.yml
Laravel tests CI/tests/laravel_tests.yml

How it works

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.


Project structure

Workflows/
├── scripts/
│   ├── assemble-ci.sh              # assembles source YAMLs → CI/
│   ├── CI/                         # source YAML templates
│   │   ├── linters/
│   │   │   ├── hadolint.yml
│   │   │   ├── htmlhint.yml
│   │   │   ├── markdownlint.yml
│   │   │   ├── shellcheck.yml
│   │   │   ├── stylelint.yml
│   │   │   └── yamllint.yml
│   │   ├── static_analysis/
│   │   │   └── phpstan.yml
│   │   └── tests/
│   │       ├── bats.yml
│   │       └── laravel_tests.yml
│   └── shell/                      # bash scripts (one per tool)
│       └── linters/
│           ├── hadolint.sh
│           ├── htmlhint.sh
│           ├── markdownlint.sh
│           ├── shellcheck.sh
│           ├── stylelint.sh
│           └── yamllint.sh
│
├── CI/                             # assembled output (ready to use)
│   ├── linters/
│   ├── static_analysis/
│   └── tests/
│
├── tests/
│   ├── assemble-ci.bats            # tests for the assembler
│   ├── linters/                    # unit tests for each shell script
│   │   ├── hadolint.bats
│   │   ├── htmlhint.bats
│   │   ├── markdownlint.bats
│   │   ├── shellcheck.bats
│   │   ├── stylelint.bats
│   │   └── yamllint.bats
│   └── helpers/
│       └── common.bash             # shared test utilities (mocks, temp dirs)
│
└── rules/                          # living documentation for contributors
    ├── README.md
    ├── _meta/how-to-write-rules.md
    └── process/
        ├── architecture-design.md
        └── ci-cd.md

Assembly

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/static_analysis scripts/shell/static_analysis CI/static_analysis
bash scripts/assemble-ci.sh scripts/CI/tests           scripts/shell/tests           CI/tests

The 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.sh

Assembled (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
        ...

Available snippets

Linters

Snippet Tool What it checks
CI/linters/hadolint.yml hadolint Dockerfiles
CI/linters/htmlhint.yml htmlhint HTML files
CI/linters/markdownlint.yml markdownlint Markdown files
CI/linters/shellcheck.yml shellcheck Shell scripts
CI/linters/stylelint.yml stylelint CSS / SCSS / LESS
CI/linters/yamllint.yml yamllint YAML files

Static analysis

Snippet Tool What it checks
CI/static_analysis/phpstan.yml PHPStan PHP (level from phpstan.neon)

Tests

Snippet What it runs
CI/tests/bats.yml BATS tests (tests/ directory)
CI/tests/laravel_tests.yml Laravel test suite (PHP 8.2, SQLite, parallel)

Using snippets in a project

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
    ...

Shell script conventions

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
fi

Config 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
fi

Graceful skip — exit 0 when there are no files to check:

if [ ${#files[@]} -eq 0 ]; then
  echo "⚠️ No .yml files found. Skipping."
  exit 0
fi

Output conventions:

  • — all checks passed
  • — issues found
  • ⚠️ — skipped or non-critical warning
  • ℹ️ — currently processing a file
  • ::error:: — GitHub Actions error annotation (hard failures)

Running tests locally

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 directory
  • mock_tool <name> <exit_code> — stub an external binary to always exit with a given code
  • mock_tool_conditional <name> — stub that exits 1 only when its argument matches $MOCK_FAIL_PATTERN

Adding a new snippet

  1. Write the bash script in scripts/shell/<category>/<tool>.sh Follow the error accumulator pattern and output conventions above.

  2. Write tests in tests/<category>/<tool>.bats Cover: missing config, no files found, all pass, one fails, multiple files with one failing.

  3. Create the source YAML in scripts/CI/<category>/<tool>.yml Install the tool, then call the script: run: bash scripts/shell/<category>/<tool>.sh

  4. Run the assembler to generate the output file in CI/.

  5. Run tests to verify everything works.


Rules and documentation

The rules/ directory is the authoritative guide for contributors:

  • rules/process/architecture-design.md — directory layout, file naming, snippet scope
  • rules/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

About

Reusable, plug-and-play CI job snippets for GitHub Actions with inlined shell logic, built-in testing, and consistent code quality checks.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors

Languages