diff --git a/.context/docs/development-workflow.md b/.context/docs/development-workflow.md new file mode 100644 index 0000000..bb5d89d --- /dev/null +++ b/.context/docs/development-workflow.md @@ -0,0 +1,52 @@ +--- +type: doc +name: development-workflow +description: Day-to-day engineering processes, branching, and contribution guidelines +category: workflow +generated: 2026-07-29 +status: filled +--- + +# Development Workflow + +## Prerequisites + +- Docker +- Git + +## Local Development + +1. Clone the repository +2. Build the Docker image: + ```bash + docker compose build + ``` +3. Run the full smoke test suite: + ```bash + tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh + ``` +4. Or test a single command: + ```bash + docker run --rm eftechcombr/ansible-cli-github-action "ansible --version" + ``` + +## Making Changes + +- Edit the appropriate file (`Dockerfile`, `entrypoint.sh`, `requirements.txt`, `action.yml`) +- Rebuild and run the smoke test suite locally +- Update `README.md` if the interface changes +- Verify all five smoke tests pass before committing + +## CI/CD + +The repository has two workflows (see `.github/workflows/`): + +- **test.yaml** — Runs automatically on every push (any branch) and pull request (targeting master). Executes all five smoke tests: build, ansible CLI, Python imports, shell passthrough, and playbook execution. Can also be triggered manually via `workflow_dispatch`. +- **build.yaml** — Manually triggered workflow that builds the Docker image using `docker/build-push-action` (no push). + +## Contribution Guidelines + +- Follow conventional commits for PR titles +- Test changes locally with the full smoke test suite +- Ensure all five smoke tests pass +- Update `README.md` for any interface or input changes diff --git a/.context/docs/examples.md b/.context/docs/examples.md new file mode 100644 index 0000000..5173a27 --- /dev/null +++ b/.context/docs/examples.md @@ -0,0 +1,199 @@ +## Examples + +### Example 1: Windows connection via WinRM + +This example runs an Ansible playbook against a Windows target over WinRM. The `pywinrm[credssp]` package is pre-installed in the action's Docker image, so no additional setup is required. + +**Inventory (`inventory.ini`):** + +```ini +[windows] +windows-server-01.example.com + +[windows:vars] +ansible_connection=winrm +ansible_winrm_transport=ntlm +ansible_user=Administrator +ansible_password={{ windows_admin_password }} +ansible_winrm_server_cert_validation=ignore +``` + +**Playbook (`win-ping.yml`):** + +```yaml +--- +- name: Verify Windows host connectivity + hosts: windows + tasks: + - name: Test WinRM connection + ansible.windows.win_ping: +``` + +**Workflow (`.github/workflows/windows-check.yml`):** + +```yaml +name: Windows Connectivity Check + +on: + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +jobs: + winrm-ping: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run WinRM ping playbook + uses: eftechcombr/ansible-cli-github-action@master + with: + command: > + ansible-playbook win-ping.yml + -i inventory.ini + -e windows_admin_password=${{ secrets.WINDOWS_ADMIN_PASSWORD }} +``` + +The `ansible-playbook` command passes the inventory file and injects the Windows administrator password via the `-e` flag, keeping the secret out of the repository. Because `pywinrm[credssp]` is bundled in the image, WinRM connections work without any additional pip install steps. + +--- + +### Example 2: Linux connection via SSH + +This example covers both playbook and ad-hoc command execution against Linux targets over SSH. + +**Inventory (`inventory.yml`):** + +```yaml +all: + hosts: + web-01.example.com: + web-02.example.com: + vars: + ansible_connection: ssh + ansible_user: ubuntu + ansible_ssh_private_key_file: /tmp/ssh_key +``` + +**Playbook (`ping.yml`):** + +```yaml +--- +- name: Verify Linux host connectivity + hosts: all + tasks: + - name: Test SSH connection + ansible.builtin.ping: +``` + +**Workflow (`.github/workflows/linux-check.yml`):** + +```yaml +name: Linux Connectivity Check + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + ssh-ping: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install SSH key + run: | + mkdir -p /tmp + echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/ssh_key + chmod 600 /tmp/ssh_key + + - name: Run playbook + uses: eftechcombr/ansible-cli-github-action@master + with: + command: > + ansible-playbook ping.yml + -i inventory.yml + + - name: Ad-hoc ping test + uses: eftechcombr/ansible-cli-github-action@master + with: + command: ansible all -i inventory.yml -m ping +``` + +The first step writes the SSH private key to disk (sourced from a GitHub secret), then sets restrictive permissions so that Ansible's SSH client accepts it. The playbook step executes `ansible-playbook` against all hosts, and the ad-hoc step demonstrates how to run a single module directly with `ansible ... -m ping`. + +--- + +### Example 3: Connection via GitHub Actions self-hosted runner + +A self-hosted runner is a machine you deploy and manage yourself to run GitHub Actions workflows. It connects to GitHub and picks up jobs intended for its runner group. Self-hosted runners are useful when you need access to internal networks, on-premises infrastructure, or resources not exposed to the public internet. For more details, see the [GitHub documentation on self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners). + +When a job targets a self-hosted runner, the `eftechcombr/ansible-cli-github-action` container runs directly on that machine. Because this action is Docker-based, the runner only needs Docker installed -- it does not need Ansible or Python installed on the host itself. The action's image provides everything required. + +Use a self-hosted runner label in the `runs-on` field to target your runner fleet: + +```yaml +runs-on: [self-hosted, linux, production] +``` + +**Inventory (`inventory.yml`):** + +```yaml +all: + hosts: + db-primary.internal.example.com: + db-replica.internal.example.com: + vars: + ansible_connection: ssh + ansible_user: admin + ansible_ssh_private_key_file: /etc/runner/keys/ansible_rsa +``` + +**Playbook (`health-check.yml`):** + +```yaml +--- +- name: Database infrastructure health check + hosts: all + tasks: + - name: Ensure PostgreSQL is accepting connections + ansible.builtin.wait_for: + port: 5432 + host: "{{ inventory_hostname }}" + timeout: 10 + + - name: Collect uptime + ansible.builtin.shell: uptime + register: result + + - name: Report uptime + ansible.builtin.debug: + msg: "{{ inventory_hostname }} uptime: {{ result.stdout }}" +``` + +**Workflow (`.github/workflows/internal-health.yml`):** + +```yaml +name: Internal Infrastructure Health Check + +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +jobs: + health-check: + runs-on: [self-hosted, linux, production] + steps: + - uses: actions/checkout@v4 + + - name: Run health check playbook against internal hosts + uses: eftechcombr/ansible-cli-github-action@master + with: + command: > + ansible-playbook health-check.yml + -i inventory.yml +``` + +Because the workflow runs on a self-hosted runner inside the internal network, it can resolve `db-primary.internal.example.com` and `db-replica.internal.example.com` -- hosts that would not be reachable from GitHub-hosted runners. The action's Docker image supplies Ansible and all dependencies, so no additional setup is needed on the runner host. diff --git a/.context/docs/testing-strategy.md b/.context/docs/testing-strategy.md new file mode 100644 index 0000000..3e05b21 --- /dev/null +++ b/.context/docs/testing-strategy.md @@ -0,0 +1,78 @@ +--- +type: doc +name: testing-strategy +description: Test frameworks, patterns, coverage requirements, and quality gates +category: testing +generated: 2026-07-29 +status: filled +--- + +# Testing Strategy + +## Approach + +This project is a thin Docker-based GitHub Action. Testing focuses on three areas: + +1. **Container build** — ensure the Docker image builds successfully +2. **Command execution** — verify that Ansible is installed and runnable inside the container +3. **Runtime behavior** — verify shell passthrough, exit code propagation, and playbook execution + +Smoke tests are implemented as standalone shell scripts in `tests/` that can be run locally or in CI. + +## Tests + +### Local Testing + +Build the image first, then run the full smoke test suite: + +```bash +tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh +``` + +Individual smoke tests can also be run independently: + +```bash +tests/smoke_build.sh # must run first — sets image tag +tests/smoke_ansible.sh # checks ansible, ansible-playbook, ansible-inventory +tests/smoke_python.sh # checks Python dependency imports (winrm, ansible) +tests/smoke_shell.sh # checks eval passthrough and exit code propagation +tests/smoke_playbook.sh # checks playbook syntax checking and localhost ping +``` + +Alternatively, build and test with Docker Compose: + +```bash +docker compose build +docker run --rm eftechcombr/ansible-cli-github-action "ansible --version" +``` + +### CI Testing (GitHub Actions) + +The workflow in `.github/workflows/test.yaml` runs all five smoke tests automatically on every push (any branch) and pull request (targeting master). It can also be triggered manually via `workflow_dispatch`. + +The CI pipeline executes the same checks as the local smoke scripts: + +- **smoke_build** — verifies the Docker image is built +- **smoke_ansible** — runs `ansible --version`, `ansible-playbook --version`, `ansible-inventory --help` +- **smoke_python** — imports `winrm` and `ansible` Python packages +- **smoke_shell** — verifies command output passthrough and exit code propagation +- **smoke_playbook** — runs `ansible-playbook --syntax-check` and a ping playbook against localhost + +## Quality Gates + +- Docker image must build without errors +- `ansible --version` must exit with code 0 +- `ansible-playbook --version` must exit with code 0 +- `ansible-inventory --help` must exit with code 0 +- Python `winrm` and `ansible` module imports must succeed +- Shell command output must pass through correctly +- Shell exit codes must propagate correctly +- Playbook syntax checking must pass +- Playbook must run successfully against localhost with the `local` connection plugin + +## What We Don't Test + +- Ansible playbook correctness (the action is a runner, not a linter) +- Specific Ansible modules or features (upstream Ansible tests those) +- Remote host connectivity or SSH configuration +- Multi-node playbook orchestration diff --git a/.context/docs/tooling.md b/.context/docs/tooling.md new file mode 100644 index 0000000..a47f419 --- /dev/null +++ b/.context/docs/tooling.md @@ -0,0 +1,75 @@ +--- +type: doc +name: tooling +description: Scripts, IDE settings, automation, and developer productivity tips +category: tooling +generated: 2026-07-29 +status: filled +--- + +# Tooling + +## Docker Compose + +`docker-compose.yaml` provides a quick way to build and tag the image: + +```bash +docker compose build +``` + +## VS Code Settings + +`.vscode/settings.json` contains: + +```json +{ + "kiroAgent.configureMCP": "Enabled" +} +``` + +## GitHub Actions + +The CI workflow runs automatically: + +- **Test** (`.github/workflows/test.yaml`) — triggered on every push (any branch) and pull request (targeting master). Runs all five smoke tests: build, ansible CLI, Python imports, shell passthrough, and playbook execution. Also supports manual trigger via `workflow_dispatch`. +- **Build** (`.github/workflows/build.yaml`) — manually triggered workflow that builds the Docker image using `docker/build-push-action` (no push to registry). + +## Smoke Tests + +Five standalone shell scripts live in `tests/`: + +| Script | What It Tests | +|---|---| +| `smoke_build.sh` | Docker image builds successfully | +| `smoke_ansible.sh` | Ansible CLI tools are present and runnable | +| `smoke_python.sh` | Python dependencies (`winrm`, `ansible`) are importable | +| `smoke_shell.sh` | Command passthrough and exit code propagation | +| `smoke_playbook.sh` | Playbook syntax checking and localhost execution | + +Run the full suite: + +```bash +tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh +``` + +## Quick Reference + +```bash +# Build locally +docker compose build +# or +docker build -t ansible-cli-smoke:test . + +# Run all smoke tests +tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh + +# Run a command +docker run --rm eftechcombr/ansible-cli-github-action "ansible --version" + +# Interactive shell (for debugging) +docker run --rm -it --entrypoint /bin/bash eftechcombr/ansible-cli-github-action +``` + +## No Package Manager + +This project has no `package.json` — all dependencies are Python-based and managed via `requirements.txt`. diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8d00f7c..8b9af20 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,18 +1,54 @@ -name: Run +name: Smoke tests on: - workflow_dispatch: - inputs: - command: - description: "Command to execute" - required: true - default: "ansible-playbook" - -jobs: - run: + push: + branches: ["**"] + pull_request: + branches: ["master"] + workflow_dispatch: + +jobs: + smoke: runs-on: ubuntu-latest - steps: - - uses: eftechcombr/ansible-cli-github-action@master - with: - command: "ansible --version" - + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t ansible-cli-smoke:test . + + - name: smoke_build — image built + run: docker image inspect ansible-cli-smoke:test > /dev/null + + - name: smoke_ansible — CLI version checks + run: | + docker run --rm ansible-cli-smoke:test "ansible --version" + docker run --rm ansible-cli-smoke:test "ansible-playbook --version" + docker run --rm ansible-cli-smoke:test "ansible-inventory --help" + + - name: smoke_python — dependency imports + run: | + docker run --rm ansible-cli-smoke:test "python -c 'import winrm; print(\"winrm ok\")'" + docker run --rm ansible-cli-smoke:test "python -c 'import ansible; print(\"ansible ok\")'" + + - name: smoke_shell — eval passthrough + run: | + OUTPUT=$(docker run --rm ansible-cli-smoke:test "echo hello from ansible") + [ "$OUTPUT" = "hello from ansible" ] || { echo "FAIL: $OUTPUT"; exit 1; } + + - name: smoke_shell — exit code propagation + run: | + set +e + docker run --rm ansible-cli-smoke:test "exit 42" + RC=$? + set -e + [ "$RC" -eq 42 ] || { echo "FAIL: expected 42, got $RC"; exit 1; } + + - name: smoke_playbook — syntax check + run: | + docker run --rm -v ${{ github.workspace }}/tests:/tests:ro ansible-cli-smoke:test \ + "ansible-playbook --syntax-check /tests/test.yml" + + - name: smoke_playbook — ping localhost + run: | + docker run --rm -v ${{ github.workspace }}/tests:/tests:ro ansible-cli-smoke:test \ + "ansible-playbook -i 'localhost,' -c local /tests/test.yml" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..87f18a4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md + +## Dev environment tips +- Build the Docker image with `docker build -t ansible-cli-smoke:test .` before running tests. +- Use `docker compose build` for the Compose-based build. + +## Testing instructions +- Run all smoke tests: `tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh` +- Or run individual tests: `tests/smoke_build.sh` (must run first — sets image tag), then any of `tests/smoke_ansible.sh`, `tests/smoke_python.sh`, `tests/smoke_shell.sh`, `tests/smoke_playbook.sh` +- CI runs all smoke tests via `.github/workflows/test.yaml` on every push and PR. + +## PR instructions +- Follow Conventional Commits (for example, `feat(scaffolding): add doc links`). +- Cross-link new scaffolds in `docs/README.md` and `agents/README.md` so future agents can find them. +- Attach sample CLI output or generated markdown when behaviour shifts. +- Confirm the built artefacts in `dist/` match the new source changes. + +## Repository map +- `action.yml` — GitHub Action metadata (inputs, branding, Docker runtime config) +- `docker-compose.yaml` — local dev convenience for building and tagging the image +- `Dockerfile` — builds the container from `python:3.11-slim` +- `entrypoint.sh` — container entrypoint, executes `eval $1` +- `README.md` — user-facing documentation +- `requirements.txt` — Python dependencies (`ansible`, `pywinrm[credssp]`) +- `tests/` — smoke test scripts and test fixtures + +## AI Context References +- Documentation index: `.context/docs/README.md` +- Agent playbooks: `.context/agents/README.md` +- Contributor guide: `CONTRIBUTING.md` diff --git a/README.md b/README.md index 9d65861..86840f5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Ansible CLI Github Action -This action is based on `ubuntu:bionic` image. -You can execute all `Ansible` related actions i.e, `ansible` or `ansible-playbook`. -You can also execute standard `Linux` commands as base `Docker` image is `ubuntu`. +This action is based on the `python:3.11-slim` image. +You can execute all Ansible-related commands: `ansible`, `ansible-playbook`, `ansible-galaxy`, and others. +You can also execute standard Linux commands as the base image includes a shell environment. ## Inputs @@ -12,8 +12,26 @@ You can also execute standard `Linux` commands as base `Docker` image is `ubuntu ## Example usage -``` -uses: actions/ansible-cli-action@latest +```yaml +uses: eftechcombr/ansible-cli-github-action@latest with: command: "ansible-playbook main.yml" ``` + +## Testing + +Run the smoke test suite locally to verify the Docker image builds and all Ansible tools work correctly: + +```bash +tests/smoke_build.sh && tests/smoke_ansible.sh && tests/smoke_python.sh && tests/smoke_shell.sh && tests/smoke_playbook.sh +``` + +The smoke tests cover: + +- Docker image build +- Ansible CLI version checks (`ansible`, `ansible-playbook`, `ansible-inventory`) +- Python dependency imports (`winrm`, `ansible`) +- Shell command passthrough and exit code propagation +- Playbook syntax checking and localhost execution + +CI runs these tests automatically on every push and pull request via GitHub Actions. diff --git a/tests/smoke_ansible.sh b/tests/smoke_ansible.sh new file mode 100755 index 0000000..a7d4ea3 --- /dev/null +++ b/tests/smoke_ansible.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e + +. /tmp/ansible_smoke_image 2>/dev/null || IMG_LABEL="ansible-cli-smoke:test" + +echo "=== smoke_ansible: Ansible CLI tools ===" + +echo " -> ansible --version" +docker run --rm "$IMG_LABEL" "ansible --version" + +echo " -> ansible-playbook --version" +docker run --rm "$IMG_LABEL" "ansible-playbook --version" + +echo " -> ansible-inventory --help" +docker run --rm "$IMG_LABEL" "ansible-inventory --help" + +echo "PASS: smoke_ansible" diff --git a/tests/smoke_build.sh b/tests/smoke_build.sh new file mode 100755 index 0000000..abff463 --- /dev/null +++ b/tests/smoke_build.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +echo "=== smoke_build: Docker image build ===" + +IMG_LABEL="ansible-cli-smoke:test" + +docker build -t "$IMG_LABEL" . + +echo "PASS: smoke_build" +echo "IMG_LABEL=$IMG_LABEL" > /tmp/ansible_smoke_image diff --git a/tests/smoke_playbook.sh b/tests/smoke_playbook.sh new file mode 100755 index 0000000..9ba6703 --- /dev/null +++ b/tests/smoke_playbook.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e + +. /tmp/ansible_smoke_image 2>/dev/null || IMG_LABEL="ansible-cli-smoke:test" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "=== smoke_playbook: Ansible playbook parsing and local run ===" + +echo " -> syntax check" +docker run --rm -v "$SCRIPT_DIR:/tests:ro" "$IMG_LABEL" \ + "ansible-playbook --syntax-check /tests/test.yml" + +echo " -> ping localhost" +docker run --rm -v "$SCRIPT_DIR:/tests:ro" "$IMG_LABEL" \ + "ansible-playbook -i 'localhost,' -c local /tests/test.yml" + +echo "PASS: smoke_playbook" diff --git a/tests/smoke_python.sh b/tests/smoke_python.sh new file mode 100755 index 0000000..63afb66 --- /dev/null +++ b/tests/smoke_python.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e + +. /tmp/ansible_smoke_image 2>/dev/null || IMG_LABEL="ansible-cli-smoke:test" + +echo "=== smoke_python: Python dependency imports ===" + +echo " -> import winrm" +docker run --rm "$IMG_LABEL" "python -c \"import winrm; print('winrm version:', winrm.__version__)\"" + +echo " -> import ansible" +docker run --rm "$IMG_LABEL" "python -c \"import ansible; print('ansible loaded ok')\"" + +echo "PASS: smoke_python" diff --git a/tests/smoke_shell.sh b/tests/smoke_shell.sh new file mode 100755 index 0000000..a6ef121 --- /dev/null +++ b/tests/smoke_shell.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +. /tmp/ansible_smoke_image 2>/dev/null || IMG_LABEL="ansible-cli-smoke:test" + +echo "=== smoke_shell: eval passthrough + exit codes ===" + +echo " -> echo passthrough" +OUTPUT=$(docker run --rm "$IMG_LABEL" "echo hello from ansible") +echo " output: $OUTPUT" +[ "$OUTPUT" = "hello from ansible" ] || { echo "FAIL: unexpected output"; exit 1; } + +echo " -> exit code propagation (expect exit 42)" +set +e +docker run --rm "$IMG_LABEL" "exit 42" +RC=$? +set -e +echo " exit code: $RC" +[ "$RC" -eq 42 ] || { echo "FAIL: expected 42, got $RC"; exit 1; } + +echo "PASS: smoke_shell" diff --git a/tests/test.yml b/tests/test.yml new file mode 100644 index 0000000..232ac4f --- /dev/null +++ b/tests/test.yml @@ -0,0 +1,7 @@ +--- +- name: Smoke test connectivity + hosts: localhost + gather_facts: false + tasks: + - name: Verify reachability + ping: