diff --git a/.github/workflows/opentofu-module-ci.yml b/.github/workflows/opentofu-module-ci.yml new file mode 100644 index 0000000..13ea307 --- /dev/null +++ b/.github/workflows/opentofu-module-ci.yml @@ -0,0 +1,158 @@ +--- +name: OpenTofu module CI (reusable) + +on: # yamllint disable-line rule:truthy + workflow_call: + inputs: + tofu_version_file: + description: Path to OpenTofu version file in the caller repository + type: string + default: .opentofu-version + terraform_docs_version: + description: terraform-docs GitHub release tag to install + type: string + default: v0.21.0 + +permissions: + contents: read + +jobs: + pre-commit: + name: Pre-commit + runs-on: ubuntu-latest + env: + TOFU_VERSION_FILE: ${{ inputs.tofu_version_file }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Setup Python + uses: actions/setup-python@v6 + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v6 + - name: Setup Terraform-Docs + uses: jaxxstorm/action-install-gh-release@v2.1.0 + with: + repo: terraform-docs/terraform-docs + tag: ${{ inputs.terraform_docs_version }} + - name: Cache pre-commit + uses: actions/cache@v5 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}- + - name: Install pre-commit + run: pip install pre-commit + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v2 + with: + tofu_version_file: ${{ inputs.tofu_version_file }} + tofu_wrapper: false + - name: Run pre-commit + id: pre_commit + shell: bash + run: | + set -euo pipefail + mkdir -p "${RUNNER_TEMP}/ci-summary" + set -o pipefail + set +e + pre-commit run --all-files 2>&1 | tee "${RUNNER_TEMP}/ci-summary/pre-commit.log" + ec=${PIPESTATUS[0]} + set -e + echo "${ec}" > "${RUNNER_TEMP}/ci-summary/pre-commit.exit" + exit "${ec}" + - name: Job summary + if: always() + shell: bash + env: + TOFU_VERSION_FILE: ${{ inputs.tofu_version_file }} + run: | + set -euo pipefail + SUMMARY_DIR="${RUNNER_TEMP}/ci-summary" + EXIT_FILE="${SUMMARY_DIR}/pre-commit.exit" + LOG_FILE="${SUMMARY_DIR}/pre-commit.log" + if [[ -f "${EXIT_FILE}" ]]; then + PRE_COMMIT_EXIT="$(tr -d ' \n\r' < "${EXIT_FILE}")" + else + PRE_COMMIT_EXIT="" + fi + + { + RUN_URL="${{ github.server_url }}/${{ github.repository }}" + RUN_URL="${RUN_URL}/actions/runs/${{ github.run_id }}" + echo "## πŸ”§ OpenTofu CI: Pre-commit" + echo + echo "### πŸ“‹ Run context" + echo + echo "| | |" + echo "|--|--|" + echo "| Event | \`${{ github.event_name }}\` |" + echo "| Ref | \`${{ github.ref }}\` |" + echo "| SHA | \`${{ github.sha }}\` |" + echo "| Workflow run | [${{ github.run_id }}](${RUN_URL}) |" + echo + echo "### πŸ› οΈ Toolchain (this runner)" + echo + echo "| Tool | Version / source |" + echo "|------|------------------|" + if command -v tofu >/dev/null 2>&1; then + printf "| OpenTofu | \`%s\` |\n" "$(tofu version 2>/dev/null | head -n1 | tr -d '\r')" + fi + if command -v python3 >/dev/null 2>&1; then + printf "| Python | \`%s\` |\n" "$(python3 --version 2>&1 | tr -d '\r')" + fi + if command -v pre-commit >/dev/null 2>&1; then + printf "| pre-commit | \`%s\` |\n" "$(pre-commit --version 2>&1 | tr -d '\r')" + fi + if command -v terraform-docs >/dev/null 2>&1; then + printf "| terraform-docs | \`%s\` |\n" "$(terraform-docs version 2>&1 | tr -d '\r' | head -n1)" + fi + if command -v tflint >/dev/null 2>&1; then + printf "| tflint | \`%s\` |\n" "$(tflint --version 2>&1 | tr -d '\r' | head -n1)" + fi + if [[ -f "${TOFU_VERSION_FILE}" ]]; then + printf "| Pinned OpenTofu (file \`%s\`) | \`%s\` |\n" "${TOFU_VERSION_FILE}" "$(tr -d '\n\r' < "${TOFU_VERSION_FILE}")" + fi + echo + echo "### πŸ§ͺ Pre-commit result" + echo + if [[ "${PRE_COMMIT_EXIT}" == "0" ]]; then + echo "Status: **βœ… passed** (exit 0)." + elif [[ -n "${PRE_COMMIT_EXIT}" ]]; then + echo "Status: **❌ failed** (exit ${PRE_COMMIT_EXIT})." + else + echo "Status: **❓ unknown** (no exit file β€” see Run pre-commit step log)." + fi + echo + echo "Command: \`pre-commit run --all-files\` β€” hooks from " + echo "[\`.pre-commit-config.yaml\`](.pre-commit-config.yaml) (\`commit-msg\` not run in CI)." + echo + if [[ -f "${LOG_FILE}" ]]; then + echo "
" + echo "πŸ“œ Full pre-commit output" + echo + echo "\`\`\`text" + # Cap size so the summary stays well under GitHub limits (~1 MiB) + if [[ "$(wc -c < "${LOG_FILE}")" -gt 61440 ]]; then + head -n 400 "${LOG_FILE}" + echo "" + echo "… truncated (first 400 lines; full output is in the Run pre-commit step log)" + else + cat "${LOG_FILE}" + fi + echo "\`\`\`" + echo + echo "
" + else + echo "_No pre-commit log file was written (see the Run pre-commit step)._" + fi + echo + echo "### πŸ’» If checks failed locally" + echo + echo "\`\`\`bash" + echo "pip install pre-commit" + echo "pre-commit install" + echo "pre-commit install --hook-type commit-msg" + echo "pre-commit run --all-files" + echo "\`\`\`" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/release-please-terraform-module.yml b/.github/workflows/release-please-terraform-module.yml new file mode 100644 index 0000000..b193901 --- /dev/null +++ b/.github/workflows/release-please-terraform-module.yml @@ -0,0 +1,95 @@ +--- +name: Release Please β€” Terraform module (reusable) + +on: # yamllint disable-line rule:truthy + workflow_call: + inputs: + config_file: + description: Release Please config JSON path in the caller repository + type: string + default: release-please-config.json + manifest_file: + description: Release Please manifest JSON path in the caller repository + type: string + default: .release-please-manifest.json + secrets: + RELEASE_PLEASE_TOKEN: + description: PAT with repo contents/PR scope (see module TEMPLATE.md) + required: true + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Release Please + id: release_please + uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + config-file: ${{ inputs.config_file }} + manifest-file: ${{ inputs.manifest_file }} + + - name: Job summary + if: always() + env: + RELEASE_BODY: ${{ steps.release_please.outputs.body }} + run: | + { + RUN_URL="${{ github.server_url }}/${{ github.repository }}" + RUN_URL="${RUN_URL}/actions/runs/${{ github.run_id }}" + echo "## πŸš€ Release Please" + echo + echo "### πŸ“‹ Run context" + echo + echo "| | |" + echo "|--|--|" + echo "| Ref | \`${{ github.ref }}\` |" + echo "| SHA | \`${{ github.sha }}\` |" + echo "| Workflow run | [${{ github.run_id }}](${RUN_URL}) |" + echo "| Config | [${{ inputs.config_file }}](${{ inputs.config_file }}) |" + echo "| Manifest | [${{ inputs.manifest_file }}](${{ inputs.manifest_file }}) |" + echo + echo "### πŸ“€ Action outputs (root component)" + echo + echo "| Output | Value |" + echo "|--------|-------|" + echo "| \`releases_created\` | \`${{ steps.release_please.outputs.releases_created }}\` |" + echo "| \`prs_created\` | \`${{ steps.release_please.outputs.prs_created }}\` |" + echo "| \`release_created\` | \`${{ steps.release_please.outputs.release_created }}\` |" + echo "| \`paths_released\` | \`${{ steps.release_please.outputs.paths_released }}\` |" + echo "| \`version\` | \`${{ steps.release_please.outputs.version }}\` |" + echo "| \`tag_name\` | \`${{ steps.release_please.outputs.tag_name }}\` |" + echo "| \`sha\` | \`${{ steps.release_please.outputs.sha }}\` |" + echo "| \`html_url\` | \`${{ steps.release_please.outputs.html_url }}\` |" + maj="${{ steps.release_please.outputs.major }}" + min="${{ steps.release_please.outputs.minor }}" + pat="${{ steps.release_please.outputs.patch }}" + echo "| \`major\` / \`minor\` / \`patch\` | \`${maj}\` / \`${min}\` / \`${pat}\` |" + echo + echo "### πŸ’‘ Notes" + echo + echo "- Set \`RELEASE_PLEASE_TOKEN\` (PAT). See the consuming repository’s docs (for example \`TEMPLATE.md\` or README)." + echo "- When \`prs_created\` is \`true\`, open or refresh the **release PR** and merge to publish." + echo "- \`release_created\` **true**: GitHub Release published β€” see \`html_url\`, \`tag_name\`." + echo + } >> "$GITHUB_STEP_SUMMARY" + + if [[ -n "${RELEASE_BODY:-}" ]]; then + { + echo "### πŸ“ Release notes preview (\`body\` output)" + echo + echo '```markdown' + printf '%s\n' "${RELEASE_BODY}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..35e8907 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,22 @@ +--- +name: Release Please + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: # yamllint disable-line rule:truthy + push: + branches: + - main + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + release-please: + uses: ./.github/workflows/release-please-terraform-module.yml + secrets: + RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} diff --git a/.github/workflows/semantic-pr-title.yml b/.github/workflows/semantic-pr-title.yml new file mode 100644 index 0000000..2fed80f --- /dev/null +++ b/.github/workflows/semantic-pr-title.yml @@ -0,0 +1,66 @@ +--- +name: Semantic PR title (reusable) + +on: # yamllint disable-line rule:truthy + workflow_call: + +permissions: + pull-requests: read + statuses: write + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - name: Validate Conventional Commit title + id: semantic + uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + ignoreLabels: | + autorelease: pending + autorelease: tagged + + - name: Job summary + if: always() + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_HEAD: ${{ github.event.pull_request.head.ref }} + PR_BASE: ${{ github.event.pull_request.base.ref }} + run: | + { + echo "## 🏷️ Semantic PR title" + echo + echo "### πŸ”€ Pull request" + echo + echo "- **πŸ”’ Number:** #${PR_NUMBER}" + echo "- **πŸ”— URL:** ${PR_URL}" + echo "- **🌿 Branches:** \`${PR_HEAD}\` β†’ \`${PR_BASE}\`" + echo + echo "**✏️ Title:**" + echo + echo '```text' + printf '%s\n' "${PR_TITLE}" + echo '```' + echo + echo "### βš–οΈ Validation result" + echo + if [[ "${{ steps.semantic.outcome }}" == "success" ]]; then + echo "Status: **πŸŽ‰ passed** β€” title matches" + echo "[Conventional Commits](https://www.conventionalcommits.org/) (or ignored label)." + elif [[ "${{ steps.semantic.outcome }}" == "failure" ]]; then + echo "Status: **🚫 failed** β€” use a conventional title" + echo "(e.g. \`feat:\`, \`fix:\`, \`docs:\`, \`chore:\`)." + else + echo "Status: **❓ ${{ steps.semantic.outcome }}** β€” see **Validate Conventional Commit title** log." + fi + echo + echo "### 🏷️ Ignored labels" + echo + echo "Labels \`autorelease: pending\` and \`autorelease: tagged\` skip this check" + echo "(Release Please release PRs)." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..d5d601a --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,23 @@ +--- +name: Semantic PR title + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: # yamllint disable-line rule:truthy + pull_request: + types: + - opened + - edited + - synchronize + branches: + - main + +permissions: + pull-requests: read + statuses: write + +jobs: + main: + uses: ./.github/workflows/semantic-pr-title.yml diff --git a/.github/workflows/validate-workflows.yml b/.github/workflows/validate-workflows.yml new file mode 100644 index 0000000..73ec670 --- /dev/null +++ b/.github/workflows/validate-workflows.yml @@ -0,0 +1,38 @@ +--- +name: Validate workflows + +on: # yamllint disable-line rule:truthy + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v6 + - name: actionlint + id: actionlint + uses: raven-actions/actionlint@v2.1.2 + - name: actionlint Summary + id: actionlint-summary + if: ${{ steps.actionlint.outputs.exit-code != 0 }} # example usage, do echo only when actionlint action failed + run: | + echo "Used actionlint version ${{ steps.actionlint.outputs.version-semver }}" + echo "Used actionlint release ${{ steps.actionlint.outputs.version-tag }}" + echo "actionlint ended with ${{ steps.actionlint.outputs.exit-code }} exit code" + echo "actionlint ended because '${{ steps.actionlint.outputs.exit-message }}'" + echo "actionlint found ${{ steps.actionlint.outputs.total-errors }} errors" + echo "actionlint checked ${{ steps.actionlint.outputs.total-files }} files" + echo "actionlint cache used: ${{ steps.actionlint.outputs.cache-hit }}" + # Propagate failure without a dynamic exit value (shellcheck SC2242). + exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..feb99fc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/actionlint diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..37fcefa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c793645 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Changelog + +Release notes for this repository are maintained by [Release Please](https://github.com/googleapis/release-please) when you merge release pull requests on `main`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1ecab8 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# gha-workflows + +Reusable [GitHub Actions workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) for OpenTofu/Terraform modules (CI, Release Please, semantic PR titles). + +## Consumers + +Module repositories call these workflows with `uses:` and a **pinned git ref** (tag or SHA), for example: + +```yaml +jobs: + pre-commit: + uses: hlvtechnologies/gha-workflows/.github/workflows/opentofu-module-ci.yml@v1.0.0 +``` + +Replace `hlvtechnologies` with your GitHub org or user if you fork this repo. + +### Available workflows + +| File | Purpose | +|------|---------| +| [opentofu-module-ci.yml](.github/workflows/opentofu-module-ci.yml) | Python, TFLint, terraform-docs, OpenTofu from `.opentofu-version`, `pre-commit run --all-files` | +| [release-please-terraform-module.yml](.github/workflows/release-please-terraform-module.yml) | [Release Please](https://github.com/googleapis/release-please); semver tags β€” set `release-type` in your [`release-please-config.json`](release-please-config.json) (e.g. `terraform-module` for modules, `simple` for this repo) | +| [semantic-pr-title.yml](.github/workflows/semantic-pr-title.yml) | Conventional Commit PR titles (skips Release Please labels) | + +Optional `workflow_call` inputs are documented in each file (e.g. `tofu_version_file`, `terraform_docs_version`). + +### Secrets + +- **Release Please:** callers must pass `secrets: RELEASE_PLEASE_TOKEN` (see [inherit](https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow) or map explicitly). Define `RELEASE_PLEASE_TOKEN` in the **calling** repository (module repos and this repo). + +## Versioning this repository + +This repository uses **[Release Please](.github/workflows/release-please.yml)** on `main` with [`release-type: simple`](release-please-config.json) so tags and [CHANGELOG.md](CHANGELOG.md) follow [Semantic Versioning](https://semver.org/) from [Conventional Commits](https://www.conventionalcommits.org/) on `main`. + +Pull requests targeting `main` run **[semantic-pr.yml](.github/workflows/semantic-pr.yml)**, which calls the reusable [semantic-pr-title.yml](.github/workflows/semantic-pr-title.yml) so PR titles match [Conventional Commits](https://www.conventionalcommits.org/) (aligned with Release Please). Release Please release PRs can use labels `autorelease: pending` or `autorelease: tagged` to skip the title check. + +1. Add the **`RELEASE_PLEASE_TOKEN`** repository secret (fine-grained or classic PAT with **Contents** and **Pull requests** read/write for this repo; include **Workflow** if other workflows must run on release events). +2. Merge conventional commits (`feat:`, `fix:`, etc.); Release Please opens a release PR; merge it to publish a GitHub Release and tag `vX.Y.Z`. + +Downstream module repos should still pin `uses: hlvtechnologies/gha-workflows/.github/workflows/....yml@vX.Y.Z` to a **tag** (or SHA), not `main`, so workflow changes roll out only when you bump the ref after a Release Please release. + +If you add or change reusable workflows without going through Release Please (not recommended), you can still tag manually: + +1. Merge to `main`. +2. `git tag v1.x.y && git push origin v1.x.y` +3. Update `uses: ...@v1.x.y` in downstream repos as needed. + +## License + +See [LICENSE](LICENSE). diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..ddb1a23 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "changelog-path": "CHANGELOG.md", + "release-type": "simple", + "include-component-in-tag": false + } + } +}