Skip to content

Commit b97460f

Browse files
authored
Merge pull request #18 from generalui/feature/tests-and-documentation
feat: add unit tests and documentation
2 parents b21e3d2 + 9cb9fe7 commit b97460f

12 files changed

Lines changed: 1283 additions & 7 deletions

File tree

.github/workflows/code-quality.yml

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
name: Github Action - Code Quality
1+
name: Code Quality
2+
23
on:
34
pull_request:
45
branches: [main]
56
types: [opened, reopened, synchronize]
67
workflow_dispatch:
8+
79
jobs:
810
quality:
911
name: Quality
10-
# The quality pipeline should not take more that 2 minutes.
11-
timeout-minutes: 2
12+
# The quality pipeline should not take more than 10 minutes.
13+
timeout-minutes: 10
1214
runs-on: ubuntu-latest
1315
steps:
1416
- name: Checkout code
@@ -18,15 +20,51 @@ jobs:
1820

1921
- name: Get changed markdown files
2022
id: changed-markdown
21-
uses: tj-actions/changed-files@v41
23+
uses: tj-actions/changed-files@v45
2224
with:
2325
files: |
2426
**/*.md
25-
.github/workflows/github-action-code-quality.yml
27+
.github/workflows/code-quality.yml
28+
29+
- name: Get changed action and test paths
30+
id: changed-actions
31+
uses: tj-actions/changed-files@v45
32+
with:
33+
files: |
34+
.github/actions/**
35+
tests/**
2636
27-
- name: Lint all Documentation
37+
- name: Lint all documentation
2838
if: steps.changed-markdown.outputs.any_modified == 'true'
2939
uses: DavidAnson/markdownlint-cli2-action@v14
3040
with:
3141
globs: |
3242
**/*.md
43+
44+
- name: Install bats
45+
if: steps.changed-actions.outputs.any_changed == 'true'
46+
run: sudo npm install -g bats
47+
48+
- name: Test - promote-ecr-image
49+
if: >
50+
contains(steps.changed-actions.outputs.all_changed_files, '.github/actions/promote-ecr-image') ||
51+
contains(steps.changed-actions.outputs.all_changed_files, 'tests/unit/promote-ecr-image')
52+
run: bats --verbose-run tests/unit/promote-ecr-image/
53+
54+
- name: Test - test-python
55+
if: >
56+
contains(steps.changed-actions.outputs.all_changed_files, '.github/actions/test-python') ||
57+
contains(steps.changed-actions.outputs.all_changed_files, 'tests/unit/test-python')
58+
run: bats --verbose-run tests/unit/test-python/
59+
60+
- name: Test - update-aws-ecs
61+
if: >
62+
contains(steps.changed-actions.outputs.all_changed_files, '.github/actions/update-aws-ecs') ||
63+
contains(steps.changed-actions.outputs.all_changed_files, 'tests/unit/update-aws-ecs')
64+
run: bats --verbose-run tests/unit/update-aws-ecs/
65+
66+
- name: Test - update-aws-lambda
67+
if: >
68+
contains(steps.changed-actions.outputs.all_changed_files, '.github/actions/update-aws-lambda') ||
69+
contains(steps.changed-actions.outputs.all_changed_files, 'tests/unit/update-aws-lambda')
70+
run: bats --verbose-run tests/unit/update-aws-lambda/

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ This repo hold various accelerators for Github workflows (Actions) as well as re
2525

2626
### Linting and Formatting
2727

28-
See [LINTING.md](./LINTING.md) for more information.
28+
See [documentation/LINTING.md](./documentation/LINTING.md) for more information.
29+
30+
### Testing
31+
32+
See [documentation/TESTING.md](./documentation/TESTING.md) for more information.
File renamed without changes.

documentation/TESTING.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Testing
2+
3+
Unit tests for the shell scripts that power the GitHub Actions in this repository.
4+
5+
## Framework
6+
7+
Tests are written using [bats-core](https://github.com/bats-core/bats-core) — the Bash Automated Testing System.
8+
9+
## Structure
10+
11+
Tests are organised per-action, mirroring the mono-repo structure of the actions themselves.
12+
Each action that contains testable shell scripts has a corresponding directory under `tests/unit/`.
13+
14+
```text
15+
tests/
16+
├── unit/
17+
│ ├── promote-ecr-image/
18+
│ │ ├── test_aws_unset.bats # Tests for the shared aws_unset.sh utility
19+
│ │ ├── test_options_helpers.bats # Tests for the shared options_helpers.sh utility
20+
│ │ └── test_promote_image.bats # Tests for promote_image.sh
21+
│ ├── test-python/
22+
│ │ └── test_configure_pip.bats # Tests for configure_pip.sh
23+
│ ├── update-aws-ecs/
24+
│ │ └── test_update_ecs.bats # Tests for update_ecs.sh
25+
│ └── update-aws-lambda/
26+
│ └── test_update_lambda.bats # Tests for update_lambda.sh
27+
└── helpers/
28+
└── mock_helpers.bash # Shared mock creation and assertion utilities
29+
```
30+
31+
## What Is Tested
32+
33+
| Action | Script | Tests | What's covered |
34+
|--------|--------|-------|----------------|
35+
| `promote-ecr-image` | `options_helpers.sh` | 15 | `has_argument()` and `extract_argument()` parsing logic |
36+
| `promote-ecr-image` | `aws_unset.sh` | 7 | All 4 AWS credential env vars are cleared; no-op when already unset |
37+
| `promote-ecr-image` | `promote_image.sh` | 13 | Every required env var validation (exits 1 for each missing var); `--help` |
38+
| `test-python` | `configure_pip.sh` | 10 | Correct `pip config set` calls per env var; no-op when unset; `--help` |
39+
| `update-aws-ecs` | `update_ecs.sh` | 8 | `--help`, `aws ecs update-service` invocation, `--force-new-deployment`, failure path |
40+
| `update-aws-lambda` | `update_lambda.sh` | 7 | `--help`, `aws lambda update-function-code` invocation, failure path |
41+
42+
### What Is NOT Tested Here
43+
44+
- **Composite action YAML** — action `.yml` files use GitHub Actions expression syntax
45+
(`${{ inputs.xxx }}`) that cannot run outside of a GitHub Actions runner.
46+
- **Live AWS calls** — tests that require actual AWS credentials are integration tests
47+
and must run in a real CI environment with OIDC or stored secrets.
48+
49+
## Mocking Strategy
50+
51+
External commands (`aws`, `pip`, `tput`) are replaced with lightweight mock binaries
52+
that record every invocation to a log file (`$MOCK_DIR/<command>_calls.log`).
53+
Tests assert the correct arguments were passed without hitting real cloud APIs.
54+
55+
`tests/helpers/mock_helpers.bash` provides shared utilities for creating mocks and
56+
making assertions against them.
57+
58+
## Running Locally
59+
60+
### Install bats
61+
62+
```sh
63+
# via npm (recommended — matches the CI install)
64+
npm install -g bats
65+
66+
# via Homebrew
67+
brew install bats-core
68+
```
69+
70+
### Run all tests for a specific action
71+
72+
```sh
73+
bats tests/unit/update-aws-ecs/
74+
```
75+
76+
### Run tests for all actions
77+
78+
```sh
79+
for dir in tests/unit/*/; do bats --verbose-run "$dir"; done
80+
```
81+
82+
### Run with verbose output
83+
84+
```sh
85+
bats --verbose-run tests/unit/promote-ecr-image/
86+
```
87+
88+
## CI
89+
90+
The `code-quality.yml` workflow runs automatically on every PR to `main`.
91+
It uses `tj-actions/changed-files` to detect which action directories have changed
92+
and runs tests only for those actions — each in its own isolated job.
93+
94+
## Writing New Tests
95+
96+
1. Create `tests/unit/<action-name>/test_<script_name>.bats`.
97+
2. Set `REPO_ROOT` relative to `BATS_TEST_DIRNAME` — tests are three levels deep,
98+
so use: `REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)"`
99+
3. In `setup()`, create a `MOCK_DIR`, add mocks for any external commands, and prepend
100+
`$MOCK_DIR` to `PATH` — subshells spawned by `run bash -c "..."` inherit the PATH
101+
automatically, so do not re-export `PATH` inside the subshell.
102+
4. Use `run bash -c "source '...script.sh'"` for tests that need to capture a non-zero
103+
exit code from the script under test.
104+
105+
See existing test files for patterns.

tests/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Tests
2+
3+
Unit tests for the shell scripts that power the GitHub Actions in this repository.
4+
5+
## Framework
6+
7+
Tests are written using [bats-core](https://github.com/bats-core/bats-core) — the Bash Automated Testing System.
8+
9+
## Structure
10+
11+
Tests are organised per-action, mirroring the mono-repo structure of the actions themselves.
12+
Each action that contains testable shell scripts has a corresponding directory under `tests/unit/`.
13+
14+
```text
15+
tests/
16+
├── unit/
17+
│ ├── promote-ecr-image/
18+
│ │ ├── test_aws_unset.bats # Tests for the shared aws_unset.sh utility
19+
│ │ ├── test_options_helpers.bats # Tests for the shared options_helpers.sh utility
20+
│ │ └── test_promote_image.bats # Tests for promote_image.sh
21+
│ ├── test-python/
22+
│ │ └── test_configure_pip.bats # Tests for configure_pip.sh
23+
│ ├── update-aws-ecs/
24+
│ │ └── test_update_ecs.bats # Tests for update_ecs.sh
25+
│ └── update-aws-lambda/
26+
│ └── test_update_lambda.bats # Tests for update_lambda.sh
27+
└── helpers/
28+
└── mock_helpers.bash # Shared mock creation and assertion utilities
29+
```
30+
31+
## What Is Tested
32+
33+
| Action | Script | Tests |
34+
|--------|--------|-------|
35+
| `promote-ecr-image` | `options_helpers.sh` | `has_argument()` and `extract_argument()` parsing logic |
36+
| `promote-ecr-image` | `aws_unset.sh` | All four AWS credential env vars are cleared |
37+
| `promote-ecr-image` | `promote_image.sh` | Env var validation (exits 1 for each missing required var) |
38+
| `test-python` | `configure_pip.sh` | Correct `pip config set` calls for each env var; no-op when unset |
39+
| `update-aws-ecs` | `update_ecs.sh` | AWS CLI invocation, `--force-new-deployment`, empty-response failure |
40+
| `update-aws-lambda` | `update_lambda.sh` | AWS CLI invocation, function name + image URL propagation, failure |
41+
42+
### What Is NOT Tested Here
43+
44+
- **Composite action YAML** — action `.yml` files use GitHub Actions expression syntax
45+
(`${{ inputs.xxx }}`) that cannot run outside of a GitHub Actions runner.
46+
- **Live AWS calls** — tests that require actual AWS credentials are integration tests
47+
and must run in a real CI environment with OIDC or stored secrets.
48+
49+
## Running Locally
50+
51+
### Install bats
52+
53+
```bash
54+
# via npm (recommended)
55+
npm install -g bats
56+
57+
# via Homebrew
58+
brew install bats-core
59+
```
60+
61+
### Run all tests for a specific action
62+
63+
```bash
64+
# From the repo root
65+
bats tests/unit/update-aws-ecs/
66+
```
67+
68+
### Run tests for all actions
69+
70+
```bash
71+
for dir in tests/unit/*/; do bats --verbose-run "$dir"; done
72+
```
73+
74+
### Run with verbose output
75+
76+
```bash
77+
bats --verbose-run tests/unit/promote-ecr-image/
78+
```
79+
80+
## Writing New Tests
81+
82+
1. Create `tests/unit/<action-name>/test_<script_name>.bats`
83+
2. Set `REPO_ROOT` relative to `BATS_TEST_DIRNAME` — tests are three levels deep,
84+
so use: `REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)"`
85+
3. Mock external commands (`aws`, `docker`, `pip`) using `MOCK_DIR` in PATH
86+
4. Use `run bash -c "..."` for tests that expect `exit 1` from the script under test
87+
88+
See existing test files and `tests/helpers/mock_helpers.bash` for patterns.

tests/helpers/mock_helpers.bash

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# mock_helpers.bash
4+
# Shared helper utilities for bats unit tests.
5+
# Provides mock command creation and assertion utilities.
6+
# =============================================================================
7+
8+
# Set up a temporary directory and create mock executables for the given
9+
# commands. Each mock records every call (arguments) to
10+
# "$MOCK_DIR/<command>_calls.log" and exits 0 by default.
11+
#
12+
# Usage:
13+
# source tests/helpers/mock_helpers.bash
14+
# setup_mocks pip tput aws docker
15+
#
16+
# After setup_mocks, $MOCK_DIR is in PATH, so the fakes shadow real binaries.
17+
setup_mocks() {
18+
MOCK_DIR="$(mktemp -d)"
19+
export MOCK_DIR
20+
21+
for cmd in "$@"; do
22+
# Write the mock script
23+
cat > "$MOCK_DIR/$cmd" << 'MOCK_SCRIPT'
24+
#!/usr/bin/env bash
25+
# Record the call
26+
echo "$@" >> "${MOCK_DIR}/${0##*/}_calls.log"
27+
exit 0
28+
MOCK_SCRIPT
29+
30+
# Substitute the actual command name (heredoc can't expand $cmd)
31+
sed -i.bak "s|\${0##\*/}|${cmd}|g" "$MOCK_DIR/$cmd"
32+
rm -f "$MOCK_DIR/$cmd.bak"
33+
chmod +x "$MOCK_DIR/$cmd"
34+
done
35+
36+
# Prepend mock dir to PATH so mocks shadow real binaries
37+
export PATH="$MOCK_DIR:$PATH"
38+
}
39+
40+
# Remove the mock directory created by setup_mocks.
41+
teardown_mocks() {
42+
if [[ -n "${MOCK_DIR:-}" && -d "$MOCK_DIR" ]]; then
43+
rm -rf "$MOCK_DIR"
44+
fi
45+
}
46+
47+
# Assert that a mock was called with the given argument string.
48+
#
49+
# Usage:
50+
# assert_mock_called_with "pip" "config set global.index-url https://example.com"
51+
assert_mock_called_with() {
52+
local cmd="$1"
53+
local expected_args="$2"
54+
local log_file="$MOCK_DIR/${cmd}_calls.log"
55+
56+
if [[ ! -f "$log_file" ]]; then
57+
echo "FAIL: mock '$cmd' was never called (no call log found)" >&2
58+
return 1
59+
fi
60+
61+
if ! grep -qF "$expected_args" "$log_file"; then
62+
echo "FAIL: mock '$cmd' was not called with args: $expected_args" >&2
63+
echo "Actual calls recorded in $log_file:" >&2
64+
cat "$log_file" >&2
65+
return 1
66+
fi
67+
68+
return 0
69+
}
70+
71+
# Assert that a mock was NOT called at all.
72+
#
73+
# Usage:
74+
# assert_mock_not_called "pip"
75+
assert_mock_not_called() {
76+
local cmd="$1"
77+
local log_file="$MOCK_DIR/${cmd}_calls.log"
78+
79+
if [[ -f "$log_file" ]]; then
80+
echo "FAIL: mock '$cmd' was called but should not have been" >&2
81+
echo "Calls recorded:" >&2
82+
cat "$log_file" >&2
83+
return 1
84+
fi
85+
86+
return 0
87+
}
88+
89+
# Return the call count for a given mock command.
90+
mock_call_count() {
91+
local cmd="$1"
92+
local log_file="$MOCK_DIR/${cmd}_calls.log"
93+
if [[ -f "$log_file" ]]; then
94+
wc -l < "$log_file" | tr -d ' '
95+
else
96+
echo "0"
97+
fi
98+
}

0 commit comments

Comments
 (0)