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..af66d6e --- /dev/null +++ b/documentation/ADDING_AN_ACTION.md @@ -0,0 +1,87 @@ +# 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` + +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. + +Two conventions specific to this repo: + +- 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` + +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..63e2376 --- /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..1c7ff19 --- /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..565d52b --- /dev/null +++ b/documentation/CREATE_RELEASE.md @@ -0,0 +1,55 @@ +# 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}`: + +```text +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: + +```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). + +## 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..26f5a4e --- /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`). +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..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` | @@ -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..0ab71f8 --- /dev/null +++ b/documentation/UPDATING_AN_ACTION.md @@ -0,0 +1,77 @@ +# 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..30f15a1 --- /dev/null +++ b/documentation/WRITING_TESTS.md @@ -0,0 +1,91 @@ +# 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). + +## 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 +`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`. See existing tests for reference. + +```text +tests/ +└── unit/ + └── update-aws-ecs/ + └── test_update_ecs.bats +``` + +## Repo-Specific Conventions + +### `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`. See any existing test file for the full mock setup pattern. + +`tests/helpers/mock_helpers.bash` provides `setup_mocks`, `assert_mock_called_with`, and +`assert_mock_not_called` utilities. + +**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`. + +### Testing Exit Codes + +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 + +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. 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) |