From e0db272468b9b0174a35edcb15562ce48c2e37d0 Mon Sep 17 00:00:00 2001 From: "genui-scotty[bot]" Date: Thu, 12 Mar 2026 14:42:55 -0700 Subject: [PATCH 1/3] docs: add AI rules files and contributor documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md (root) — AI assistant context: repo purpose, structure, versioning contract, CI/release triggers, key conventions - .cursorrules — symlink → CLAUDE.md (Cursor support) - .github/copilot-instructions.md — symlink → ../CLAUDE.md (Copilot support) - documentation/CONTRIBUTING.md — contributor entry point with links to guides - documentation/ADDING_AN_ACTION.md — step-by-step: new action checklist - documentation/UPDATING_AN_ACTION.md — versioning rules, patch/minor/major guidance - documentation/WRITING_TESTS.md — bats patterns, mock strategy, CI registration - documentation/DEVOPS.md — CI/CD overview linking to workflow docs - documentation/CODE_QUALITY.md — code-quality.yml explained - documentation/CREATE_RELEASE.md — create-release.yml explained - documentation/TESTING.md — add link to WRITING_TESTS.md - README.md — add link to CONTRIBUTING.md --- .cursorrules | 1 + .github/copilot-instructions.md | 1 + CLAUDE.md | 82 ++++++++++++++++ README.md | 3 + documentation/ADDING_AN_ACTION.md | 106 +++++++++++++++++++++ documentation/CODE_QUALITY.md | 44 +++++++++ documentation/CONTRIBUTING.md | 43 +++++++++ documentation/CREATE_RELEASE.md | 54 +++++++++++ documentation/DEVOPS.md | 34 +++++++ documentation/TESTING.md | 5 + documentation/UPDATING_AN_ACTION.md | 75 +++++++++++++++ documentation/WRITING_TESTS.md | 139 ++++++++++++++++++++++++++++ 12 files changed, 587 insertions(+) create mode 120000 .cursorrules create mode 120000 .github/copilot-instructions.md create mode 100644 CLAUDE.md create mode 100644 documentation/ADDING_AN_ACTION.md create mode 100644 documentation/CODE_QUALITY.md create mode 100644 documentation/CONTRIBUTING.md create mode 100644 documentation/CREATE_RELEASE.md create mode 100644 documentation/DEVOPS.md create mode 100644 documentation/UPDATING_AN_ACTION.md create mode 100644 documentation/WRITING_TESTS.md diff --git a/.cursorrules b/.cursorrules new file mode 120000 index 0000000..681311e --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 0000000..949a29f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +../CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..436de27 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md — AI Assistant Context + +This file provides context for AI coding assistants (Claude, Cursor, GitHub Copilot). +For full documentation, see the [`documentation/`](./documentation/) directory. + +## What This Repository Is + +A mono-repo of reusable [composite GitHub Actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) +used across GenUI's CI/CD pipelines. All actions are composite actions (YAML + shell scripts) — +not Docker or JavaScript actions. + +Consumers reference actions via: + +```yaml +uses: generalui/github-workflow-accelerators/.github/actions/{action-name}@{version-tag} +``` + +## Repository Structure + +```text +.github/ + actions/ + {action-name}/ + action.yml # Composite action definition + project.json # Version — MUST be bumped on every change to this action + README.md # Action documentation + scripts/ # Shell scripts invoked by action.yml + workflows/ + code-quality.yml # PR gate: markdownlint + per-action bats tests + create-release.yml # Auto-releases on merge to main +tests/ + unit/ + {action-name}/ # bats tests for that action's shell scripts + helpers/ + mock_helpers.bash # Shared mock utilities +documentation/ # Contributor and DevOps guides +``` + +## Critical: Versioning Contract + +**Every change to an action requires a `project.json` version bump. No exceptions.** + +- Patch (1.0.0 → 1.0.1): bug fixes, dependency updates, documentation, refactors +- Minor (1.0.0 → 1.1.0): new optional inputs, backwards-compatible features +- Major (1.0.0 → 2.0.0): breaking changes to inputs or outputs + +Skipping the version bump causes `create-release.yml` to fail on merge to `main` with a +"tag already exists" error. + +See [UPDATING_AN_ACTION.md](./documentation/UPDATING_AN_ACTION.md). + +## CI vs Release Triggers + +**`code-quality.yml`** (PR gate) — runs on PRs to `main`: + +- Markdownlint: fires when any `**/*.md` file changes +- bats tests: fires per-action when files in `.github/actions/{name}/` or `tests/unit/{name}/` change +- To add a new testable action, add its name to the `actions_with_tests` array in `code-quality.yml` + +**`create-release.yml`** (release) — runs on push to `main`: + +- Reads `project.json` from each changed action directory +- Ignores: `documentation/`, `tests/`, `*.md`, `.github/workflows/`, config files +- Creates tag `{version}-{action-name}` and GitHub Release per changed action + +See [DEVOPS.md](./documentation/DEVOPS.md), [CODE_QUALITY.md](./documentation/CODE_QUALITY.md), +and [CREATE_RELEASE.md](./documentation/CREATE_RELEASE.md). + +## Adding or Modifying Actions + +- **Adding**: [ADDING_AN_ACTION.md](./documentation/ADDING_AN_ACTION.md) +- **Modifying**: [UPDATING_AN_ACTION.md](./documentation/UPDATING_AN_ACTION.md) +- **Testing**: [WRITING_TESTS.md](./documentation/WRITING_TESTS.md) +- **Contributing**: [CONTRIBUTING.md](./documentation/CONTRIBUTING.md) + +## Key Conventions + +- `main` is protected — all changes via PR; direct commits are blocked +- Shell scripts: `#!/usr/bin/env bash`, inputs passed via env vars not positional args +- Tests: bats, `REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)"` (3 levels deep) +- Internal action references are pinned to a specific release tag +- New actions start at version `1.0.0` diff --git a/README.md b/README.md index 64a31d2..ec35186 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ This repo hold various accelerators for Github workflows (Actions) as well as re ## Contributing +See [documentation/CONTRIBUTING.md](./documentation/CONTRIBUTING.md) for the full contributor guide, including +how to add or update actions, write tests, and understand the CI/CD workflows. + ### Linting and Formatting See [documentation/LINTING.md](./documentation/LINTING.md) for more information. diff --git a/documentation/ADDING_AN_ACTION.md b/documentation/ADDING_AN_ACTION.md new file mode 100644 index 0000000..49b5154 --- /dev/null +++ b/documentation/ADDING_AN_ACTION.md @@ -0,0 +1,106 @@ +# Adding an Action + +This guide covers how to add a new reusable composite action to this repository. + +## Directory Structure + +Each action lives in its own directory under `.github/actions/`: + +```text +.github/actions/{action-name}/ +├── action.yml # Composite action definition (required) +├── project.json # Version metadata (required) +├── README.md # Action documentation (required) +└── scripts/ # Shell scripts invoked by action.yml (if needed) + └── {script}.sh +``` + +## Step-by-Step + +### 1. Create the action directory + +Use kebab-case for the directory name. It becomes part of the version tag and the consumer's `uses:` reference. + +```sh +mkdir -p .github/actions/my-new-action/scripts +``` + +### 2. Create `action.yml` + +Define the action as a composite action. All actions in this repo use `using: composite`. + +```yaml +name: My New Action + +description: A brief description of what this action does. + +inputs: + my_input: + description: Description of the input. + required: true + +runs: + using: composite + steps: + - name: Run script + env: + MY_INPUT: ${{ inputs.my_input }} + run: ${{ github.action_path }}/scripts/my_script.sh + shell: bash +``` + +Key conventions: + +- Pass inputs to shell scripts via environment variables, not positional arguments +- Use `${{ github.action_path }}` to reference scripts relative to the action +- Reference internal actions (e.g. `configure-aws`) by pinning to a release tag: + `uses: generalui/github-workflow-accelerators/.github/actions/configure-aws@{tag}` + +### 3. Create `project.json` + +Start at version `1.0.0`. This file is read by the release workflow to create a version tag on merge to `main`. + +```json +{ + "name": "my-new-action", + "version": "1.0.0" +} +``` + +The `name` field must match the directory name exactly. + +### 4. Create `README.md` + +Document the action's inputs, outputs, and a usage example. See any existing action README for the expected format. + +### 5. Write shell scripts (if applicable) + +Place scripts in the `scripts/` directory. Follow the patterns established in existing scripts: + +- Use `#!/usr/bin/env bash` +- Guard required environment variables at the top before doing any work +- Source `scripts/general/options_helpers.sh` if the script accepts CLI flags +- Exit with code `1` on validation failure (makes testing straightforward) + +### 6. Write tests (if the action contains testable shell scripts) + +See [WRITING_TESTS.md](./WRITING_TESTS.md) for the full guide. + +Then register the action in the `actions_with_tests` array in `.github/workflows/code-quality.yml`: + +```yaml +actions_with_tests=( + "my-new-action" + ... +) +``` + +### 7. Open a pull request + +The `code-quality.yml` PR gate will run markdownlint and, if registered, the bats test suite for your new action. + +On merge to `main`, `create-release.yml` will automatically create a git tag `1.0.0-my-new-action` and a GitHub Release. + +## Versioning + +New actions always start at `1.0.0`. See [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md) for version bump rules when making subsequent changes. diff --git a/documentation/CODE_QUALITY.md b/documentation/CODE_QUALITY.md new file mode 100644 index 0000000..bd1d927 --- /dev/null +++ b/documentation/CODE_QUALITY.md @@ -0,0 +1,44 @@ +# Code Quality Workflow + +The `code-quality.yml` workflow is the PR gate for this repository. It runs automatically on every pull request targeting `main`. + +## What It Does + +The workflow runs a single `quality` job with conditional steps: + +1. **Detect changes** — uses `tj-actions/changed-files` to determine which files changed +2. **Lint Markdown** — runs `markdownlint-cli2` if any `.md` files or the workflow file itself changed +3. **Install bats** — installs the test runner if any action or test files changed +4. **Per-action tests** — runs `bats tests/unit/{action-name}/` for each action whose files changed + +Only the steps relevant to the PR's changes are executed, keeping runner time minimal. + +## Triggering Conditions + +| Step | Triggers when... | +|------|-----------------| +| Lint Markdown | Any `**/*.md` file or `code-quality.yml` itself changed | +| bats tests | Files changed in `.github/actions/{action-name}/` or `tests/unit/{action-name}/` | + +## Per-Action Test Detection + +The workflow maintains an explicit list of actions that have bats test suites: + +```yaml +actions_with_tests=( + "promote-ecr-image" + "test-python" + "update-aws-ecs" + "update-aws-lambda" +) +``` + +When adding a new action with testable shell scripts, add its name to this array. See [ADDING_AN_ACTION.md](./ADDING_AN_ACTION.md). + +## Single-Job Design + +All steps run sequentially in one job rather than as parallel matrix jobs. For fast-running bats suites, the overhead of spinning up multiple runners exceeds any parallelism benefit. + +## Required Status Check + +The `Quality` check produced by this workflow is a required status check on `main`. A PR cannot be merged until it passes. diff --git a/documentation/CONTRIBUTING.md b/documentation/CONTRIBUTING.md new file mode 100644 index 0000000..698cfaf --- /dev/null +++ b/documentation/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing + +Thank you for contributing to GitHub Workflow Accelerators. This document is the starting point for contributors — human or AI. + +## Prerequisites + +- [bats-core](https://github.com/bats-core/bats-core) — for running shell script tests locally +- [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) — for linting markdown +- [shellcheck](https://www.shellcheck.net/) — recommended for shell script authoring + +See [LINTING.md](./LINTING.md) and [TESTING.md](./TESTING.md) for setup details. + +## Guides + +| Task | Document | +|------|----------| +| Add a new action | [ADDING_AN_ACTION.md](./ADDING_AN_ACTION.md) | +| Modify an existing action | [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md) | +| Write tests for shell scripts | [WRITING_TESTS.md](./WRITING_TESTS.md) | +| Understand CI/CD workflows | [DEVOPS.md](./DEVOPS.md) | + +## Branch and PR Conventions + +- `main` is a protected branch — all changes must go through a pull request +- Branch naming: `feat/`, `fix/`, `chore/` prefixes (e.g. `feat/add-notify-slack`) +- Each PR should change only the action(s) it intends to change +- PRs that change an action's files **must** include a `project.json` version bump — see [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md) + +## Commit Messages + +Use conventional commit prefixes: + +- `feat:` — new action or new feature in an existing action +- `fix:` — bug fix +- `chore:` — dependency updates, version bumps, tooling changes +- `docs:` — documentation only changes +- `refactor:` — code changes that don't affect behaviour +- `test:` — adding or updating tests + +## Code Review + +- The `code-quality.yml` PR gate must pass before merging +- Markdownlint and bats tests run automatically for changed files — see [DEVOPS.md](./DEVOPS.md) diff --git a/documentation/CREATE_RELEASE.md b/documentation/CREATE_RELEASE.md new file mode 100644 index 0000000..3146dbe --- /dev/null +++ b/documentation/CREATE_RELEASE.md @@ -0,0 +1,54 @@ +# Create Release Workflow + +The `create-release.yml` workflow automatically creates versioned git tags and GitHub Releases when changes are merged to `main`. + +## How It Works + +1. **Detect changed files** — uses `tj-actions/changed-files` to identify which files changed in the push +2. **Extract action paths** — parses changed file paths to identify which action directories were modified +3. **Validate versions** — reads `project.json` from each changed action and verifies the version tag does not already exist +4. **Tag and release** — for each changed action, creates a git tag and GitHub Release with an auto-generated changelog + +## Version Tag Format + +Tags follow the format `{version}-{action-name}`: + +``` +1.0.1-update-aws-ecs +1.2.0-lint-test-yarn +``` + +The changelog is generated from commit messages between the previous tag and the new one. + +## What Triggers a Release + +A push to `main` triggers a release for any action whose files changed **and** whose `project.json` version is higher than the most recent tag for that action. + +If the version has not been bumped, the workflow fails with: + +``` +The tag {version}-{action-name} already exists, ensure you have incremented the version in project.json. +``` + +Always bump `project.json` before merging. See [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md). + +## What Does NOT Trigger a Release + +The following paths are excluded from change detection: + +| Path | Reason | +|------|--------| +| `.github/workflows/create-release.yml` | Self-referential | +| `.github/workflows/code-quality.yml` | Workflow-only change | +| `.github/**/*.md` | Workflow documentation | +| `documentation/**` | Documentation only | +| `tests/**` | Test files only | +| `*.md` | Root markdown files | +| `.vscode/**` | Editor config | +| `.gitignore`, `.markdownlint*`, `*.code-workspace` | Config files | + +If you need to add a new top-level directory that should not trigger releases, add it to the `files_ignore` list in `create-release.yml`. + +## Matrix Releases + +If multiple actions change in a single merge, the workflow releases each one independently in parallel via a matrix strategy. Each action gets its own tag, changelog, and GitHub Release. diff --git a/documentation/DEVOPS.md b/documentation/DEVOPS.md new file mode 100644 index 0000000..8b6e812 --- /dev/null +++ b/documentation/DEVOPS.md @@ -0,0 +1,34 @@ +# DevOps + +This document covers the CI/CD infrastructure for this repository. + +## CI/CD Workflows + +All workflows are defined in `.github/workflows/`. + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| [`code-quality.yml`](./../.github/workflows/code-quality.yml) | PR to `main` | Linting and unit tests | +| [`create-release.yml`](./../.github/workflows/create-release.yml) | Push to `main` | Version tagging and GitHub Releases | + +### Code Quality + +See [CODE_QUALITY.md](./CODE_QUALITY.md) for details on the PR gate workflow. + +### Create Release + +See [CREATE_RELEASE.md](./CREATE_RELEASE.md) for details on the release workflow. + +## Branch Protection + +The `main` branch is protected: + +- All changes must be submitted via pull request +- The `Quality` check (from `code-quality.yml`) must pass before merging +- Direct commits to `main` are blocked + +## Dependency Management + +Actions in this repo reference specific versions of external GitHub Actions (e.g. `actions/checkout@v6`). +When new versions are released — particularly those resolving runtime deprecation warnings — the `action.yml` +files and corresponding `project.json` versions should be updated. See [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md). diff --git a/documentation/TESTING.md b/documentation/TESTING.md index e4af132..fcf8e04 100644 --- a/documentation/TESTING.md +++ b/documentation/TESTING.md @@ -93,6 +93,11 @@ and runs tests only for those actions — each in its own isolated job. ## Writing New Tests +For a full guide on writing new tests — including the mock pattern, exit code testing, and how to register +a new action with CI — see [WRITING_TESTS.md](./WRITING_TESTS.md). + +Quick reference: + 1. Create `tests/unit//test_.bats`. 2. Set `REPO_ROOT` relative to `BATS_TEST_DIRNAME` — tests are three levels deep, so use: `REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)"` diff --git a/documentation/UPDATING_AN_ACTION.md b/documentation/UPDATING_AN_ACTION.md new file mode 100644 index 0000000..019da47 --- /dev/null +++ b/documentation/UPDATING_AN_ACTION.md @@ -0,0 +1,75 @@ +# Updating an Action + +Every change to an action's files **requires a `project.json` version bump**. This is not optional — the release workflow reads `project.json` on every push to `main` and will fail with a "tag already exists" error if the version has not been incremented. + +## Versioning Rules + +This repository follows [Semantic Versioning](https://semver.org/): + +| Change type | Version bump | Example | +|-------------|-------------|---------| +| Bug fix, dependency update, refactor, documentation | **Patch** | `1.0.0` → `1.0.1` | +| New optional input, backwards-compatible enhancement | **Minor** | `1.0.0` → `1.1.0` | +| Removed input, changed input name, changed behaviour | **Major** | `1.0.0` → `2.0.0` | + +When in doubt, bump the patch version. A version bump is always cheaper than a failed release. + +## How to Update an Action + +### 1. Make your changes + +Edit the relevant files in `.github/actions/{action-name}/` — `action.yml`, scripts, `README.md`, etc. + +### 2. Bump `project.json` + +```json +{ + "name": "my-action", + "version": "1.0.1" +} +``` + +### 3. Update tests if needed + +If you changed a shell script's behaviour or added new logic, update or add tests in `tests/unit/{action-name}/`. See [WRITING_TESTS.md](./WRITING_TESTS.md). + +### 4. Open a pull request + +The PR gate validates markdown and runs bats tests for the changed action. On merge to `main`, the release workflow creates the new version tag and GitHub Release automatically. + +## Updating Dependencies + +When a GitHub Action used inside an action's `action.yml` releases a new version, update the `uses:` reference and bump the patch version in `project.json`. + +```yaml +# Before +uses: actions/checkout@v5 + +# After +uses: actions/checkout@v6 +``` + +Check for Node.js deprecation warnings in GitHub Actions run logs — these indicate an action's runtime is approaching end-of-life. + +## Updating Internal Action References + +Actions that reference other actions in this repo (e.g. `configure-aws`) pin to a specific release tag: + +```yaml +uses: generalui/github-workflow-accelerators/.github/actions/configure-aws@1.0.0-configure-aws +``` + +If `configure-aws` is updated, consumers should update their pinned tag in a separate PR with a corresponding version bump. + +## What the Release Workflow Does + +On every push to `main`, `create-release.yml`: + +1. Detects which action directories changed (ignoring docs, tests, and workflow files) +2. Reads `project.json` from each changed action directory +3. Checks that the new version tag does not already exist (fails if it does) +4. Creates a git tag in the format `{version}-{action-name}` (e.g. `1.0.1-update-aws-ecs`) +5. Generates a changelog from commit messages +6. Creates a GitHub Release + +See [DEVOPS.md](./DEVOPS.md) for more detail on the CI/CD workflows. diff --git a/documentation/WRITING_TESTS.md b/documentation/WRITING_TESTS.md new file mode 100644 index 0000000..1c62fe2 --- /dev/null +++ b/documentation/WRITING_TESTS.md @@ -0,0 +1,139 @@ +# Writing Tests + +This guide covers how to write bats unit tests for shell scripts in this repository. +For running existing tests locally, see [TESTING.md](./TESTING.md). + +## What to Test + +Tests cover the **shell scripts** inside each action's `scripts/` directory. Composite action `action.yml` files use GitHub Actions expression syntax (`${{ inputs.xxx }}`) that cannot run outside a real runner and are therefore out of scope for unit tests. + +Good candidates for testing: + +- Input validation (does the script exit 1 when a required env var is missing?) +- Core logic (does the script call the right CLI with the right arguments?) +- Edge cases (what happens with empty strings, missing optional vars, `--help`?) + +## File Location + +Tests live in `tests/unit/{action-name}/`, one directory per action. Each test file is named `test_{script_name}.bats`. + +```text +tests/ +└── unit/ + └── update-aws-ecs/ + └── test_update_ecs.bats +``` + +## Anatomy of a Test File + +```bash +#!/usr/bin/env bats +# Brief description of what this file tests. + +# REPO_ROOT must use this exact depth — tests are 3 levels deep. +REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" +SCRIPT_UNDER_TEST="$REPO_ROOT/.github/actions/my-action/scripts/my_script.sh" + +setup() { + # Create mock binaries, set env vars, etc. +} + +teardown() { + # Clean up temp files, unset env vars. +} + +@test "my_script: does something expected" { + # Arrange, act, assert +} +``` + +### Critical: `BATS_TEST_DIRNAME` Depth + +Tests are located at `tests/unit/{action-name}/test_*.bats` — three directory levels below the repo root. Always use: + +```bash +REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" +``` + +## Mocking External Commands + +External commands (`aws`, `pip`, `docker`, `tput`) must be mocked — tests must not make real network calls. + +The standard pattern is to create lightweight mock executables in a temp directory and prepend it to `PATH`: + +```bash +setup() { + MOCK_DIR="$(mktemp -d)" + export MOCK_DIR + + # Mock aws: records all calls to a log file + cat > "$MOCK_DIR/aws" << MOCK +#!/bin/bash +echo "\$@" >> "${MOCK_DIR}/aws_calls.log" +exit 0 +MOCK + chmod +x "$MOCK_DIR/aws" + + # Prepend mock dir — subshells inherit this PATH automatically + export PATH="$MOCK_DIR:$PATH" +} + +teardown() { + [ -n "${MOCK_DIR:-}" ] && rm -rf "$MOCK_DIR" +} +``` + +Then assert against the call log: + +```bash +@test "script: calls aws ecs update-service" { + bash -c "source '$SCRIPT_UNDER_TEST'" + grep -q "update-service" "$MOCK_DIR/aws_calls.log" +} +``` + +**Do not re-export `PATH` inside `run bash -c "..."` subshells** — they inherit `PATH` from `setup()` automatically. Overriding it in the subshell can break system tools like `dirname`. + +### Shared Helpers + +`tests/helpers/mock_helpers.bash` provides `setup_mocks`, `assert_mock_called_with`, and `assert_mock_not_called` utilities. See the file for usage. + +## Testing Exit Codes + +To assert that a script exits with a non-zero code, use `run bash -c "..."`: + +```bash +@test "script: exits 1 when required var is missing" { + run bash -c "source '$SCRIPT_UNDER_TEST'" + [ "$status" -eq 1 ] +} +``` + +`run` captures the exit code in `$status` and stdout/stderr in `$output`. Without `run`, a non-zero exit would cause the test itself to fail before you can assert. + +## Adding Tests for a New Action + +1. Create the directory: `tests/unit/{action-name}/` +2. Create `tests/unit/{action-name}/test_{script_name}.bats` +3. Register the action in `.github/workflows/code-quality.yml`: + + ```yaml + actions_with_tests=( + "my-new-action" + ... + ) + ``` + + Without this, CI will not run your tests on PRs. + +## Running Tests Locally + +```sh +# Run tests for one action +bats tests/unit/update-aws-ecs/ + +# Run all action test suites +for dir in tests/unit/*/; do bats --verbose-run "$dir"; done +``` + +See [TESTING.md](./TESTING.md) for installation instructions. From 3d238796f9beb4bdd32b69443038e8f176aadab3 Mon Sep 17 00:00:00 2001 From: "genui-scotty[bot]" Date: Thu, 12 Mar 2026 14:55:06 -0700 Subject: [PATCH 2/3] fix: resolve markdownlint errors in new documentation - MD040: add language tag to fenced code blocks in CREATE_RELEASE.md - MD013: wrap long lines in UPDATING_AN_ACTION.md and WRITING_TESTS.md - MD060: add spaces to table separator rows across all new docs Run markdownlint locally before committing markdown. --- documentation/CODE_QUALITY.md | 2 +- documentation/CONTRIBUTING.md | 2 +- documentation/CREATE_RELEASE.md | 9 +++++---- documentation/DEVOPS.md | 2 +- documentation/TESTING.md | 2 +- documentation/UPDATING_AN_ACTION.md | 6 ++++-- documentation/WRITING_TESTS.md | 4 +++- tests/README.md | 2 +- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/documentation/CODE_QUALITY.md b/documentation/CODE_QUALITY.md index bd1d927..63e2376 100644 --- a/documentation/CODE_QUALITY.md +++ b/documentation/CODE_QUALITY.md @@ -16,7 +16,7 @@ Only the steps relevant to the PR's changes are executed, keeping runner time mi ## Triggering Conditions | Step | Triggers when... | -|------|-----------------| +| ------ | ----------------- | | Lint Markdown | Any `**/*.md` file or `code-quality.yml` itself changed | | bats tests | Files changed in `.github/actions/{action-name}/` or `tests/unit/{action-name}/` | diff --git a/documentation/CONTRIBUTING.md b/documentation/CONTRIBUTING.md index 698cfaf..1c7ff19 100644 --- a/documentation/CONTRIBUTING.md +++ b/documentation/CONTRIBUTING.md @@ -13,7 +13,7 @@ See [LINTING.md](./LINTING.md) and [TESTING.md](./TESTING.md) for setup details. ## Guides | Task | Document | -|------|----------| +| ------ | ---------- | | Add a new action | [ADDING_AN_ACTION.md](./ADDING_AN_ACTION.md) | | Modify an existing action | [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md) | | Write tests for shell scripts | [WRITING_TESTS.md](./WRITING_TESTS.md) | diff --git a/documentation/CREATE_RELEASE.md b/documentation/CREATE_RELEASE.md index 3146dbe..565d52b 100644 --- a/documentation/CREATE_RELEASE.md +++ b/documentation/CREATE_RELEASE.md @@ -13,7 +13,7 @@ The `create-release.yml` workflow automatically creates versioned git tags and G Tags follow the format `{version}-{action-name}`: -``` +```text 1.0.1-update-aws-ecs 1.2.0-lint-test-yarn ``` @@ -26,8 +26,9 @@ A push to `main` triggers a release for any action whose files changed **and** w If the version has not been bumped, the workflow fails with: -``` -The tag {version}-{action-name} already exists, ensure you have incremented the version in project.json. +```text +The tag {version}-{action-name} already exists, +ensure you have incremented the version in project.json. ``` Always bump `project.json` before merging. See [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md). @@ -37,7 +38,7 @@ Always bump `project.json` before merging. See [UPDATING_AN_ACTION.md](./UPDATIN The following paths are excluded from change detection: | Path | Reason | -|------|--------| +| ------ | -------- | | `.github/workflows/create-release.yml` | Self-referential | | `.github/workflows/code-quality.yml` | Workflow-only change | | `.github/**/*.md` | Workflow documentation | diff --git a/documentation/DEVOPS.md b/documentation/DEVOPS.md index 8b6e812..d7b4257 100644 --- a/documentation/DEVOPS.md +++ b/documentation/DEVOPS.md @@ -7,7 +7,7 @@ This document covers the CI/CD infrastructure for this repository. All workflows are defined in `.github/workflows/`. | Workflow | Trigger | Purpose | -|----------|---------|---------| +| ---------- | --------- | --------- | | [`code-quality.yml`](./../.github/workflows/code-quality.yml) | PR to `main` | Linting and unit tests | | [`create-release.yml`](./../.github/workflows/create-release.yml) | Push to `main` | Version tagging and GitHub Releases | diff --git a/documentation/TESTING.md b/documentation/TESTING.md index fcf8e04..2f526b2 100644 --- a/documentation/TESTING.md +++ b/documentation/TESTING.md @@ -31,7 +31,7 @@ tests/ ## What Is Tested | Action | Script | Tests | What's covered | -|--------|--------|-------|----------------| +| -------- | -------- | ------- | ---------------- | | `promote-ecr-image` | `options_helpers.sh` | 15 | `has_argument()` and `extract_argument()` parsing logic | | `promote-ecr-image` | `aws_unset.sh` | 7 | All 4 AWS credential env vars are cleared; no-op when already unset | | `promote-ecr-image` | `promote_image.sh` | 13 | Every required env var validation (exits 1 for each missing var); `--help` | diff --git a/documentation/UPDATING_AN_ACTION.md b/documentation/UPDATING_AN_ACTION.md index 019da47..0ab71f8 100644 --- a/documentation/UPDATING_AN_ACTION.md +++ b/documentation/UPDATING_AN_ACTION.md @@ -1,13 +1,15 @@ # Updating an Action -Every change to an action's files **requires a `project.json` version bump**. This is not optional — the release workflow reads `project.json` on every push to `main` and will fail with a "tag already exists" error if the version has not been incremented. +Every change to an action's files **requires a `project.json` version bump**. This is not optional — +the release workflow reads `project.json` on every push to `main` and will fail with a +"tag already exists" error if the version has not been incremented. ## Versioning Rules This repository follows [Semantic Versioning](https://semver.org/): | Change type | Version bump | Example | -|-------------|-------------|---------| +| ------------- | ------------- | --------- | | Bug fix, dependency update, refactor, documentation | **Patch** | `1.0.0` → `1.0.1` | | New optional input, backwards-compatible enhancement | **Minor** | `1.0.0` → `1.1.0` | | Removed input, changed input name, changed behaviour | **Major** | `1.0.0` → `2.0.0` | diff --git a/documentation/WRITING_TESTS.md b/documentation/WRITING_TESTS.md index 1c62fe2..63171ab 100644 --- a/documentation/WRITING_TESTS.md +++ b/documentation/WRITING_TESTS.md @@ -5,7 +5,9 @@ For running existing tests locally, see [TESTING.md](./TESTING.md). ## What to Test -Tests cover the **shell scripts** inside each action's `scripts/` directory. Composite action `action.yml` files use GitHub Actions expression syntax (`${{ inputs.xxx }}`) that cannot run outside a real runner and are therefore out of scope for unit tests. +Tests cover the **shell scripts** inside each action's `scripts/` directory. Composite action +`action.yml` files use GitHub Actions expression syntax (`${{ inputs.xxx }}`) that cannot run +outside a real runner and are therefore out of scope for unit tests. Good candidates for testing: diff --git a/tests/README.md b/tests/README.md index 604ff32..452ed88 100644 --- a/tests/README.md +++ b/tests/README.md @@ -31,7 +31,7 @@ tests/ ## What Is Tested | Action | Script | Tests | -|--------|--------|-------| +| -------- | -------- | ------- | | `promote-ecr-image` | `options_helpers.sh` | `has_argument()` and `extract_argument()` parsing logic | | `promote-ecr-image` | `aws_unset.sh` | All four AWS credential env vars are cleared | | `promote-ecr-image` | `promote_image.sh` | Env var validation (exits 1 for each missing required var) | From 59c3970b8631e683baf52b9d6a7e2562adf5b94a Mon Sep 17 00:00:00 2001 From: "genui-scotty[bot]" Date: Thu, 12 Mar 2026 15:42:02 -0700 Subject: [PATCH 3/3] docs: trim repo-specific docs, link out to upstream documentation - WRITING_TESTS.md: replace bats syntax/examples with links to bats-core docs; keep only repo-specific conventions (BATS_TEST_DIRNAME depth, mock pattern, CI registration) - ADDING_AN_ACTION.md: replace prescriptive action.yml example with link to GitHub Actions docs; keep only the two repo-specific conventions - DEVOPS.md: remove version from actions/checkout reference (avoids going stale) --- documentation/ADDING_AN_ACTION.md | 29 ++------- documentation/DEVOPS.md | 2 +- documentation/WRITING_TESTS.md | 100 ++++++++---------------------- 3 files changed, 31 insertions(+), 100 deletions(-) diff --git a/documentation/ADDING_AN_ACTION.md b/documentation/ADDING_AN_ACTION.md index 49b5154..af66d6e 100644 --- a/documentation/ADDING_AN_ACTION.md +++ b/documentation/ADDING_AN_ACTION.md @@ -27,34 +27,15 @@ mkdir -p .github/actions/my-new-action/scripts ### 2. Create `action.yml` -Define the action as a composite action. All actions in this repo use `using: composite`. +Refer to the [GitHub documentation on creating actions](https://docs.github.com/en/actions/sharing-automations/creating-actions) +for the full `action.yml` syntax. Any action type is supported. -```yaml -name: My New Action - -description: A brief description of what this action does. - -inputs: - my_input: - description: Description of the input. - required: true - -runs: - using: composite - steps: - - name: Run script - env: - MY_INPUT: ${{ inputs.my_input }} - run: ${{ github.action_path }}/scripts/my_script.sh - shell: bash -``` - -Key conventions: +Two conventions specific to this repo: -- Pass inputs to shell scripts via environment variables, not positional arguments -- Use `${{ github.action_path }}` to reference scripts relative to the action - Reference internal actions (e.g. `configure-aws`) by pinning to a release tag: `uses: generalui/github-workflow-accelerators/.github/actions/configure-aws@{tag}` +- When invoking shell scripts, pass inputs via environment variables rather than + positional arguments — it keeps the script interface explicit and testable ### 3. Create `project.json` diff --git a/documentation/DEVOPS.md b/documentation/DEVOPS.md index d7b4257..26f5a4e 100644 --- a/documentation/DEVOPS.md +++ b/documentation/DEVOPS.md @@ -29,6 +29,6 @@ The `main` branch is protected: ## Dependency Management -Actions in this repo reference specific versions of external GitHub Actions (e.g. `actions/checkout@v6`). +Actions in this repo reference specific versions of external GitHub Actions (e.g. `actions/checkout`). When new versions are released — particularly those resolving runtime deprecation warnings — the `action.yml` files and corresponding `project.json` versions should be updated. See [UPDATING_AN_ACTION.md](./UPDATING_AN_ACTION.md). diff --git a/documentation/WRITING_TESTS.md b/documentation/WRITING_TESTS.md index 63171ab..30f15a1 100644 --- a/documentation/WRITING_TESTS.md +++ b/documentation/WRITING_TESTS.md @@ -3,6 +3,12 @@ This guide covers how to write bats unit tests for shell scripts in this repository. For running existing tests locally, see [TESTING.md](./TESTING.md). +## Framework + +Tests use [bats-core](https://github.com/bats-core/bats-core) (Bash Automated Testing System). +The [bats-core documentation](https://bats-core.readthedocs.io/en/stable/) covers the full +test syntax, assertions, and built-in variables. + ## What to Test Tests cover the **shell scripts** inside each action's `scripts/` directory. Composite action @@ -17,7 +23,8 @@ Good candidates for testing: ## File Location -Tests live in `tests/unit/{action-name}/`, one directory per action. Each test file is named `test_{script_name}.bats`. +Tests live in `tests/unit/{action-name}/`, one directory per action. Each test file is named +`test_{script_name}.bats`. See existing tests for reference. ```text tests/ @@ -26,92 +33,35 @@ tests/ └── test_update_ecs.bats ``` -## Anatomy of a Test File - -```bash -#!/usr/bin/env bats -# Brief description of what this file tests. - -# REPO_ROOT must use this exact depth — tests are 3 levels deep. -REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" -SCRIPT_UNDER_TEST="$REPO_ROOT/.github/actions/my-action/scripts/my_script.sh" - -setup() { - # Create mock binaries, set env vars, etc. -} - -teardown() { - # Clean up temp files, unset env vars. -} - -@test "my_script: does something expected" { - # Arrange, act, assert -} -``` +## Repo-Specific Conventions -### Critical: `BATS_TEST_DIRNAME` Depth +### `BATS_TEST_DIRNAME` Depth -Tests are located at `tests/unit/{action-name}/test_*.bats` — three directory levels below the repo root. Always use: +Tests are located at `tests/unit/{action-name}/test_*.bats` — three directory levels below +the repo root. Always use: ```bash REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" ``` -## Mocking External Commands +### Mocking External Commands -External commands (`aws`, `pip`, `docker`, `tput`) must be mocked — tests must not make real network calls. +External commands (`aws`, `pip`, `docker`, `tput`) must be mocked — tests must not make real +network calls. The standard pattern is to create lightweight mock executables in a temp +directory and prepend it to `PATH`. See any existing test file for the full mock setup pattern. -The standard pattern is to create lightweight mock executables in a temp directory and prepend it to `PATH`: +`tests/helpers/mock_helpers.bash` provides `setup_mocks`, `assert_mock_called_with`, and +`assert_mock_not_called` utilities. -```bash -setup() { - MOCK_DIR="$(mktemp -d)" - export MOCK_DIR - - # Mock aws: records all calls to a log file - cat > "$MOCK_DIR/aws" << MOCK -#!/bin/bash -echo "\$@" >> "${MOCK_DIR}/aws_calls.log" -exit 0 -MOCK - chmod +x "$MOCK_DIR/aws" - - # Prepend mock dir — subshells inherit this PATH automatically - export PATH="$MOCK_DIR:$PATH" -} - -teardown() { - [ -n "${MOCK_DIR:-}" ] && rm -rf "$MOCK_DIR" -} -``` +**Do not re-export `PATH` inside `run bash -c "..."` subshells** — they inherit `PATH` from +`setup()` automatically. Overriding it in the subshell can break system tools like `dirname`. -Then assert against the call log: - -```bash -@test "script: calls aws ecs update-service" { - bash -c "source '$SCRIPT_UNDER_TEST'" - grep -q "update-service" "$MOCK_DIR/aws_calls.log" -} -``` - -**Do not re-export `PATH` inside `run bash -c "..."` subshells** — they inherit `PATH` from `setup()` automatically. Overriding it in the subshell can break system tools like `dirname`. - -### Shared Helpers - -`tests/helpers/mock_helpers.bash` provides `setup_mocks`, `assert_mock_called_with`, and `assert_mock_not_called` utilities. See the file for usage. - -## Testing Exit Codes - -To assert that a script exits with a non-zero code, use `run bash -c "..."`: - -```bash -@test "script: exits 1 when required var is missing" { - run bash -c "source '$SCRIPT_UNDER_TEST'" - [ "$status" -eq 1 ] -} -``` +### Testing Exit Codes -`run` captures the exit code in `$status` and stdout/stderr in `$output`. Without `run`, a non-zero exit would cause the test itself to fail before you can assert. +To assert a script exits with a non-zero code, use `run bash -c "..."`. `run` captures the +exit code in `$status` without causing the test to fail. See the +[bats-core docs](https://bats-core.readthedocs.io/en/stable/writing-tests.html#run-test-other-commands) +for details. ## Adding Tests for a New Action