Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @stacklet/engineering
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions: {}

jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set tool versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
postfix: _TOOL_VERSION

- uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
with:
# Without a pin this installs whatever prek released most recently,
# so a hostile release reaches CI with no change here. The version
# comes from .tool-versions, so CI and a local run agree on it.
prek-version: ${{ env.PREK_TOOL_VERSION }}
env:
# zizmor's five online audits skip silently without a token, and its
# summary line reads the same either way, so CI would pass while the
# supply-chain audits never ran.
GH_TOKEN: ${{ github.token }}

test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Run the tests
run: python3 -m unittest discover -s validate-min-terraform -p 'test_*.py'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
repos:
- repo: https://github.com/rhysd/actionlint
rev: 393031adb9afb225ee52ae2ccd7a5af5525e03e8 # frozen: v1.7.11
hooks:
- id: actionlint

- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: ea2eb407b4cbce87cf0d502f36578950494f5ac9 # frozen: v1.23.1
hooks:
- id: zizmor

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: f12be1ebaa5351c1fc76472de98db2c3446c8253 # frozen: v0.16.10
hooks:
- id: ruff-check
- id: ruff-format

- repo: https://github.com/codespell-project/codespell
rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2
hooks:
- id: codespell
3 changes: 3 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
just 1.58.0
prek 0.5.5
uv 0.12.23
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
# github-tooling
Reusable GitHub Actions and workflows shared across Stacklet repositories

Reusable GitHub Actions and workflows shared across Stacklet repositories.

Each tool lives in its own top-level directory with an `action.yml` and a `README.md`. Reference one by path:

```yaml
- uses: stacklet/github-tooling/<tool>@<commit-sha>
```

Pin by commit SHA. This repository publishes no release tags, so a SHA is the only reference that cannot move.

## Tools

| Tool | Purpose |
| --- | --- |
| [validate-min-terraform](validate-min-terraform/) | Runs `terraform validate` at the floor a module's `required_version` declares, and fails when that floor is below a configured minimum. |

## Adding a tool

1. Create a directory named for the tool.
2. Add an `action.yml` and a `README.md` covering usage, inputs, and outputs.
3. Keep any non-trivial logic in its own script beside the `action.yml`, so that a linter reads it and a test can run it without a runner. Prefer Python over shell for anything that parses or compares.
4. Add a row to the table above.
7 changes: 7 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# lint files
lint:
prek run --all-files

# run the tests
test:
python3 -m unittest discover -s validate-min-terraform -p 'test_*.py'
69 changes: 69 additions & 0 deletions validate-min-terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# validate-min-terraform

Runs `terraform validate` against a Terraform module at the oldest release its
`required_version` allows, so the bottom of the declared range gets tested
rather than assumed.

## Usage

```yaml
jobs:
validate-min-terraform:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: stacklet/github-tooling/validate-min-terraform@<commit-sha>
```

> [!WARNING]
> `terraform init` installs the providers the module names, and `validate` runs
> each provider binary to read its schema. On a pull request that configuration
> is untrusted, so give the job `contents: read`, and no secrets.

## Inputs

| Name | Default | Description |
| --- | --- | --- |
| `working-directory` | `.` | Directory holding the module. One directory per call, with no descent into subdirectories. |
| `minimum-floor` | `1.14.0` | Refuse to run below this release. The default is the oldest release HashiCorp still ships fixes for. |
| `terraform-docs-image` | a digest-pinned terraform-docs 0.20.0 | Image used to parse the configuration. |

## Outputs

| Name | Description |
| --- | --- |
| `version` | The Terraform release the module declares as its floor. |

## Requirements

The runner needs Docker and `python3`. `ubuntu-latest` has both, and the macOS
runners have no Docker.

## Version constraints

`required_version` must state a lower bound with `>=`, `~>` or `=`, or name an
exact version such as `1.14.0`. Upper bounds are allowed and ignored, as is
`!=` unless it excludes the floor itself. The action rejects any constraint it
cannot resolve to a definite release, and fails when the floor it resolves
falls below `minimum-floor`.

Every `required_version` in the directory counts, including one in a `.tf.json`
or split across lines. The floor is the highest lower bound among them, which
is what Terraform itself applies.

## Known limitation

> [!NOTE]
> `terraform validate` treats every root variable as unknown, so it never
> evaluates `validation` blocks. A floor that is wrong only inside a variable
> validation still passes here.

## Tests

`just test` runs `test_read_floor.py`, and CI runs the same command.
67 changes: 67 additions & 0 deletions validate-min-terraform/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Validate the declared Terraform floor
description: >-
Reads the Terraform floor out of a module's required_version and runs
terraform validate at that release.

inputs:
working-directory:
description: Directory holding the Terraform module to validate.
required: false
default: .
minimum-floor:
description: >-
Refuse to run below this release. The floor comes out of the
configuration under test, so on a pull request the branch decides which
Terraform CI installs.
required: false
# HashiCorp routes a version to a support regime by release date, and 1.14
# falls in the band still receiving fixes and security patches, so this is
# the oldest release with a published claim to them. It wants revisiting
# when a new minor lands. See the README.
default: 1.14.0
terraform-docs-image:
description: >-
Image used to parse the configuration. Pin by digest, not by tag.
required: false
# terraform-docs publishes no immutable releases, which leaves both a tag
# and a release checksum able to change underneath us. A digest is content
# addressed, so it cannot. This one is terraform-docs 0.20.0, the last
# release published as a multi-arch index. Later tags are per-architecture,
# so a digest pin would have to choose one.
default: quay.io/terraform-docs/terraform-docs@sha256:37329e2dc2518e7f719a986a3954b10771c3fe000f50f83fd4d98d489df2eae2

outputs:
version:
description: The Terraform release the module declares as its floor.
value: ${{ steps.floor.outputs.version }}

runs:
using: composite
steps:
# The floor is read out of required_version rather than passed in, so that
# editing the constraint moves what CI tests in the same commit.
# terraform-docs parses the configuration instead of matching a regex
# against one file, so it reports every required_version in the directory,
# including one in a .tf.json or split across lines.
- name: Read the declared Terraform floor
id: floor
shell: bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnsca Hmm, why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hold-over from pre-refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A composite step has to declare a shell, so shell: bash itself cannot go. The hold-over was the logic on that line.

read_floor.py now writes version= to $GITHUB_OUTPUT itself, so the step is a bare invocation of the script. A test covers the file contents.

Filed by Claude on Cory's behalf.

working-directory: ${{ inputs.working-directory }}
env:
TERRAFORM_DOCS_IMAGE: ${{ inputs.terraform-docs-image }}
MINIMUM_FLOOR: ${{ inputs.minimum-floor }}
# The script writes the step output itself, so this step carries no shell
# logic. A composite step has to name a shell even so.
run: '"$GITHUB_ACTION_PATH/read_floor.py"'

- name: Setup Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: ${{ steps.floor.outputs.version }}

- name: Validate at the declared floor
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
terraform init -backend=false -input=false
terraform validate
Loading
Loading