|
| 1 | +name: Validate the declared Terraform floor |
| 2 | +description: >- |
| 3 | + Reads the Terraform floor out of a module's required_version and runs |
| 4 | + terraform validate at that release. |
| 5 | +
|
| 6 | +inputs: |
| 7 | + working-directory: |
| 8 | + description: Directory holding the Terraform module to validate. |
| 9 | + required: false |
| 10 | + default: . |
| 11 | + terraform-docs-image: |
| 12 | + description: >- |
| 13 | + Image used to parse the configuration. Pin by digest, not by tag. |
| 14 | + required: false |
| 15 | + # terraform-docs publishes no immutable releases, which leaves both a tag |
| 16 | + # and a release checksum able to change underneath us. A digest is content |
| 17 | + # addressed, so it cannot. This one is terraform-docs 0.20.0. |
| 18 | + default: quay.io/terraform-docs/terraform-docs@sha256:37329e2dc2518e7f719a986a3954b10771c3fe000f50f83fd4d98d489df2eae2 |
| 19 | + |
| 20 | +outputs: |
| 21 | + version: |
| 22 | + description: The Terraform release the module declares as its floor. |
| 23 | + value: ${{ steps.floor.outputs.version }} |
| 24 | + |
| 25 | +runs: |
| 26 | + using: composite |
| 27 | + steps: |
| 28 | + # The floor is read out of required_version rather than passed in, so that |
| 29 | + # editing the constraint moves what CI tests in the same commit. |
| 30 | + # terraform-docs parses the configuration instead of matching a regex |
| 31 | + # against one file, so it reports every required_version in the module, |
| 32 | + # including one in a .tf.json or split across lines. |
| 33 | + - name: Read the declared Terraform floor |
| 34 | + id: floor |
| 35 | + shell: bash |
| 36 | + working-directory: ${{ inputs.working-directory }} |
| 37 | + env: |
| 38 | + TERRAFORM_DOCS_IMAGE: ${{ inputs.terraform-docs-image }} |
| 39 | + run: | |
| 40 | + # terraform-docs writes its own diagnostics to stderr, and merging those |
| 41 | + # into the pipe below breaks the JSON parse. They are kept aside rather |
| 42 | + # than discarded so that a failed pull or a parse error still says what |
| 43 | + # went wrong instead of surfacing as empty output. |
| 44 | + stderr=$(mktemp) |
| 45 | + if ! parsed=$(docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/wd" -w /wd \ |
| 46 | + "$TERRAFORM_DOCS_IMAGE" json . --output-file '' 2>"$stderr"); then |
| 47 | + echo "terraform-docs could not read this module:" >&2 |
| 48 | + cat "$stderr" >&2 |
| 49 | + exit 1 |
| 50 | + fi |
| 51 | + constraints=$(printf '%s' "$parsed" \ |
| 52 | + | jq -r '.requirements[] | select(.name == "terraform") | .version') |
| 53 | + if [ -z "$constraints" ]; then |
| 54 | + echo "this module declares no required_version" >&2 |
| 55 | + exit 1 |
| 56 | + fi |
| 57 | +
|
| 58 | + # Terraform applies every required_version in the module at once, so the |
| 59 | + # effective floor is the highest lower bound among them. Only >= and ~> |
| 60 | + # state that bound literally, and an operator such as != or = can rule |
| 61 | + # out the bound another term states. Resolving either needs the release |
| 62 | + # list, so reject what this cannot read rather than guess at it. |
| 63 | + bounds="" |
| 64 | + while IFS= read -r term; do |
| 65 | + term=$(printf '%s' "$term" | tr -d '[:space:]') |
| 66 | + [ -n "$term" ] || continue |
| 67 | + case "$term" in |
| 68 | + '>='*|'~>'*) bound=${term#??} ;; |
| 69 | + # An upper bound cannot move the floor, so it needs no reading. |
| 70 | + '<'*) continue ;; |
| 71 | + *) |
| 72 | + echo "cannot resolve a Terraform floor from: $term" >&2 |
| 73 | + echo "required_version may use >=, ~>, < and <= only" >&2 |
| 74 | + exit 1 |
| 75 | + ;; |
| 76 | + esac |
| 77 | + if ! printf '%s' "$bound" | grep -qE '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then |
| 78 | + echo "not a version this can resolve: $term" >&2 |
| 79 | + exit 1 |
| 80 | + fi |
| 81 | + case "$bound" in *.*.*) ;; *) bound="$bound.0" ;; esac |
| 82 | + bounds=$(printf '%s\n%s' "$bounds" "$bound") |
| 83 | + done <<EOF |
| 84 | + $(printf '%s' "$constraints" | tr ',' '\n') |
| 85 | + EOF |
| 86 | +
|
| 87 | + # grep exits non-zero on no match, which under -e would end the step |
| 88 | + # before the message below gets to say why. |
| 89 | + floor=$(printf '%s' "$bounds" | grep -v '^$' | sort -V | tail -1) || true |
| 90 | + if [ -z "$floor" ]; then |
| 91 | + echo "required_version states no lower bound; found:" >&2 |
| 92 | + printf ' %s\n' "$constraints" >&2 |
| 93 | + exit 1 |
| 94 | + fi |
| 95 | + echo "version=$floor" >> "$GITHUB_OUTPUT" |
| 96 | +
|
| 97 | + - name: Setup Terraform |
| 98 | + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 |
| 99 | + with: |
| 100 | + terraform_version: ${{ steps.floor.outputs.version }} |
| 101 | + |
| 102 | + - name: Validate at the declared floor |
| 103 | + shell: bash |
| 104 | + working-directory: ${{ inputs.working-directory }} |
| 105 | + run: | |
| 106 | + terraform init -backend=false |
| 107 | + terraform validate |
0 commit comments