diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1286f37 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +.github/* @n3tuk/admin diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..ce0bd00 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,206 @@ +# Contributing to n3tuk Repositories + +[`n3tuk`][n3tuk] is a personal GitHub Organisation which contains a collection +of configurations and services for personal development, alongside the operation +of personal systems. As such, much of the code and configuration therein is +highly opinionated based on my own needs and ideas, or as part of testing and +development of resources. + +[n3tuk]: https://github.com/n3tuk + +This guide provides help on how to work with and develop for this repository, +including the tooling needed and expected practices around naming, files, +variables, etc.. + +## Tooling + +There are four main tools used to manage code quality as well as operate +automations and deployments: + +1. [`pre-commit`](#pre-commit) +1. [`task`](#task) +1. [GitHub Workflows](#github-workflows) +1. [Dependabot](#dependabot) + +### `pre-commit` + +This repository uses the [`pre-commit`][pre-commit] tool to provide a set of +common and specific steps with an expectation to run these before committing any +code to this repository: + +[pre-commit]: https://pre-commit.com + +```sh +$ sudo pacman -S python-pre-commit +$ pre-commit --install +pre-commit installed at .git/hooks/pre-commit +``` + +This includes: + +- Checking that the file names are compatible with all operating systems; +- Checking that large files are not staged and committed; +- Checking that files do not contain trailing spaces and have new lines at the + end of the file; +- Documentation doesn't have any bad links; +- That YAML and Markdown files are valid; and +- Any local application-specific code or configurations are correct and valid. + +I **strongly** recommend using it as it provides useful fast feedback on any +changes before committing and pushing them up to the repository branch. As there +is no way to automatically install `pre-commit` upon cloning this repository +(`init-templatedir` aside), the `task` tooling works to check this installation +of `pre-commit` whenever used. + +### `task` + +This repository also uses the [`task`][taskfile] tooling from +[Taskfile][taskfile] to provide the automation of standard tasks and checks: + +[taskfile]: https://taskfile.dev/ + +To use [Taskfile][taskfile], you can run `task` from the command-line: + +```sh +$ task --list +task: Available tasks for this project: +(...) +``` + +`task` becomes even more useful when paired with the `--watch` command-line +flags, allowing it to run the requested tasks, and then check files for changes, +triggering each task as needed. For example, this allows for automated updates +of the documentation for Terraform as you write the Terraform configuration, or +to run tests and linters for Go source code as you write an application. + +```sh +$ task --watch +task: Started watching for tasks: default +(...) +``` + +### Dependabot + +[Dependabot][dependabot] is a code security analysis tool provided by GitHub to +automate the scanning of versions in supported codebases, and provide automated +Pull Requests to increase the version one a dependency either based on a new +release, or an identified security issue. + +[dependabot]: https://docs.github.com/en/code-security/dependabot + +[`.github/dependabot.yaml`](dependabot.yaml) holds the configuration for +Dependabot in this repository, and defines what types of checks are run, and +when. + +### GitHub Workflows + +[GitHub Workflows][github-workflows] are the primary CI/CD mechanism for all +repositories inside `n3tuk`. All workflows for this repository are available in +the [`.github/workflows/`](workflows) directory. + +[github-workflows]: https://docs.github.com/en/actions/using-workflows + +#### `force-ci-run` Label + +GitHub Workflows have an issue when it comes to checking and committing changes +which it itself has authored: [These changes cannot trigger GitHub Workflows +themselves][token-in-workflow], as so to prevent infinite loops. Although a full +Personal Access Token can support this, creating PATs for each repository and +managing their scopes is difficult and it presents a wider security risk. + +[token-in-workflow]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow + +As such, this repository has a setting to allow forcing a CI run by using a +label: `force-ci-run`. So, for example, if [`dependabot`](#dependabot) makes an +update to a module or package, there could be a change to the documentation +which is automatically generated and committed within a GitHub Workflow. + +Once committed and pushed up to the Pull Request, GitHub will not trigger +further runs of any Workflows, preventing approval of the Pull Request. + +By adding the label `force-ci-run` (which in turn the GitHub Workflows will +remove one triggered), you can forcefully run all the GitHub Workflow listing +for that label and get the results without having to explicitly commit and push +any code changes yourself. + +## Committing Changes + +This repository operates mainly on the [GitHub Flow][github-flow] model, with +the expectation to make a change a feature branch (in _draft_ mode, or with the +`work-in-progress` label, if under active development). Upon each commit, the +configured GitHub Workflows, plus any connected third-party status checks, will +run, checking code changes. + +If all these pass (and an approved review, if required), merging the code to the +default branch (`main`) using a [_Rebase Merge_][rebase-merge] will be +available. All `n3tuk` repositories have _Merge_ and _Squash Merge_ options +disabled to keep the history linear, and enforce signed commits. + +[github-flow]: https://docs.github.com/en/get-started/quickstart/github-flow +[rebase-merge]: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github#rebasing-and-merging-your-commits + +### Committing Standards + +A commit message should be a statement of what the **commit** will do once +applied, not what **you** have done in writing it, and as such it must be +imperative. Use the present tense (_change_, not _changes_ or _changed_) for the +message, and cover the what/why, but not the how. + +#### Rules + +- Commit messages must have a subject line as the first line. It may have body + copy where the subject line does not convey the reasoning enough. A blink line + must separate these. +- The subject line must not exceed 50 characters and must not end in a period. +- Capitalize the first word of the subject line. +- Write the subject line in an imperative mood (_Fix_, not _Fixed_ or _Fixes_). +- Wrap the body copy at 72 columns. +- The body copy should extend to the what and the why of the commit, never the + how. The latter belongs in documentation and implementation. + +> **Warning** +> By limited the use of the Pull Request description to link to issues and other +> Pull Requests, it eliminates the duplication of the message when rebasing or +> refactoring code. + +### Pull Request Standards + +A Pull Request will be a set of one or more commits which handle a focused, +concise change to the codebase. As this repository operates on a GitHub Flow +basis, there is no need for branch name prefixes, such as `fix/` or `feature/`. + +The name of the branch should be a concise name of the change in the pull +request in [snake case][snake-case], such as `refactor-the-example-type` or +`fix-hostname-in-ec2-instance`. + +The title and description of the Pull Request should be concise and follow the +same guidelines as the [Committing Standards](#committing-standards) above, with +links to other Pull Requests or Issues in this or other repositories linked in +the Pull Request, rather than the commit. For example: + +```markdown +Resolves: #123 +See also: #456, #789 +``` + +> **Note** +> By limited the use of the Pull Request description to link to Issues and other +> Pull Requests, it eliminates the duplication of the message when rebasing or +> refactoring code before merging. + +## Naming Conventions + +The requirements for the naming of resources is as follows. In all cases the +naming should be in lower-case. + +| Resource Identifier Name | Use Case Type | Notes | +| :--------------------------------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `{terraform-filename}` | [`kebab-case`][kebab-case] | (None) | +| `{terraform-resource}` | [`snake-case`][snake-case] | (None) | +| `{terraform-variables}` | [`snake-case`][snake-case] | (None) | +| `{terraform-outputs}` | [`snake-case`][snake-case] | (None) | +| `{aws-resource}` | [`kebab-case`][kebab-case] | Although services support ranges of characters and cases, bar some small edge-cases, the most common case which works across all services is `kebab-case`. | +| `{tfc-resource}` | [`kebab-case`][kebab-case] | (None) | + +[kebab-case]: https://en.wikipedia.org/wiki/Letter_case#Kebab_case +[snake-case]: https://en.wikipedia.org/wiki/Snake_case diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2216d21 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Checklist + +Please confirm the following checks: + +- [ ] My pull request follows the guidelines set out in `CONTRIBUTING.md`. +- [ ] I have performed a self-review of my code and run any tests locally to check. +- [ ] I have added tests that prove my changes are effective and work correctly. +- [ ] I have made corresponding changes to the documentation as needed. +- [ ] I have checked my code and corrected any misspellings. +- [ ] Each commit in this pull request has a meaningful subject & body for context. +- [ ] I have squashed all "fix(up)" commits to provide a clean code history. +- [ ] My pull request has an appropriate title and description for context. +- [ ] I have linked this pull request to other issues or pull requests as needed. +- [ ] I have added `type/...`, `changes/...`, and 'release/...' labels as needed. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..44ddb05 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,22 @@ +--- +version: 2 + +updates: + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: thursday + time: "18:00" + timezone: "Europe/London" + commit-message: + prefix: actions + include: scope + reviewers: + - n3tuk/admin + labels: + - type/dependencies + - update/github-workflows + - release/chore + - priority/normal diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 0000000..1589d1e --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,61 @@ +--- +# When working with Dependabot and GitHub Actions, where those Actions can make +# changes to documentation or code based on updates to the repository's +# dependencies, it is always necessary for the pull requests to go though +# multiple stagings using the force-ci-run label. These rules take this into +# account and allow those stages to be managed and triggered as necessary. + +pull_request_rules: + + # If the pull request was raised by Dependabot, and only Dependabot or GitHub + # Actions have make changes to the branch, then automatically approve if it's + # not in conflict with the base branch, and not closed. + - name: Automatic approval for Dependabot pull requests + conditions: + - "author=dependabot[bot]" + - "base=main" + - "#commits-behind=0" + - "-conflict" + - "-closed" + - or: + - "commits[*].author==dependabot[bot]" + - "commits[*].author==github-actions[bot]" + actions: + review: + type: APPROVE + + # If the pull request was raised by Dependabot, it only has a linear history, + # it has been approved for merging into the main or master branches, and is + # neither in conflict with the main branch, fallen behind, nor the + # force-ci-run label is still present, then automatically merge this request. + - name: Automatic merge for Dependabot pull requests + conditions: + - "author=dependabot[bot]" + - "base=main" + - "linear-history" + - "#approved-reviews-by>=1" + - "-label=force-ci-run" + - "-conflict" + - "-closed" + actions: + merge: + method: rebase + + # If there is a conflict between this pull request and the default branch, or + # this pull request has failed behind the default branch due to other pull + # requests being merged before it, then add a message for Dependabot to + # recreate the pull request. This should be recreate rather than rebase as + # there may be non-dependabot changes added as commits to the branch for + # documentation changes. Although these non-dependabot changes will be lost, + # they should be re-created nonetheless. + - name: Trigger Dependabot to recreate on merge conflict + conditions: + - "author=dependabot[bot]" + - "base=main" + - "-closed" + - or: + - "conflict" + - "#commits-behind>0" + actions: + comment: + message: "@dependabot recreate" diff --git a/.github/release-drafter.yaml b/.github/release-drafter.yaml new file mode 100644 index 0000000..9ee81e7 --- /dev/null +++ b/.github/release-drafter.yaml @@ -0,0 +1,47 @@ +--- +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +categories: + - title: "Breaking Changes" + labels: + - "release/breaking" + - title: "Features & Updates" + labels: + - "release/feature" + - "release/update" + - title: "Bug Fixes" + labels: + - "release/fix" + - title: "Maintenance" + collapse-after: 3 + labels: + - "release/chore" +exclude-labels: + - "release/skip" +exclude-contributors: + - "dependabot" +change-template: "- $TITLE ([#$NUMBER]($URL), @$AUTHOR)" +no-changes-template: "- (No changes)" +change-title-escapes: '\<*_&@' +version-resolver: + major: + labels: + - "release/breaking" + minor: + labels: + - "release/feature" + patch: + labels: + - "release/update" + - "release/chore" + - "release/fix" + default: patch +# yamllint disable rule:line-length +template: | + # `$REPOSITORY` v$RESOLVED_VERSION + + `$REPOSITORY` is a GitHub Action for running [`terraform-docs`](https://github.com/terraform-docs/terraform-docs) and synchronising changes with the repository, if requested. + + The following is the list of the fixes, updates, and new features, against `$REPOSITORY` since [$PREVIOUS_TAG](https://github.com/$OWNER/$REPOSITORY/releases/tag/$PREVIOUS_TAG) (see [v$RESOLVED_VERSION changes after $PREVIOUS_TAG](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) for the detailed changelog). + + $CHANGES diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml new file mode 100644 index 0000000..f64bcfc --- /dev/null +++ b/.github/workflows/draft-release.yaml @@ -0,0 +1,28 @@ +--- +name: Draft Release + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: read + +jobs: + release-draft: + name: Draft the Release + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Draft the release + id: drafter + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yaml + disable-autolabeler: true diff --git a/.github/workflows/pull-requester.yaml b/.github/workflows/pull-requester.yaml new file mode 100644 index 0000000..5199982 --- /dev/null +++ b/.github/workflows/pull-requester.yaml @@ -0,0 +1,39 @@ +--- +name: Pull Requester + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + - labeled + - unlabeled + branches: + - main + +permissions: + contents: read + packages: read + issues: write + pull-requests: write + +jobs: + pull-requester: + runs-on: ubuntu-latest + name: Check the Pull Request + + concurrency: + # Ensure that only a single concurrent job is run for any Pull Requester + # event on any one pull request (or github.event.number here), and bias + # that to the most recently started job, which will likely have the most + # complete information to process the metadata with. + group: pull-requester-${{ github.event.number }} + cancel-in-progress: true + + steps: + - name: Pull Requester + # Target main here as it's easier than trying to deal with future + # versions that this pull request will become + uses: n3tuk/action-pull-requester@v1 diff --git a/.github/workflows/script-integrations.yaml b/.github/workflows/script-integrations.yaml new file mode 100644 index 0000000..169fba0 --- /dev/null +++ b/.github/workflows/script-integrations.yaml @@ -0,0 +1,112 @@ +--- +name: Script Integrations + +on: + pull_request: + branches: + - main + +permissions: + contents: read + issues: write + checks: write + pull-requests: write + +defaults: + run: + # Error handling and pipefile must be explicitly set via the default shell + # https://github.com/actions/runner/issues/353#issuecomment-1067227665 + shell: bash --noprofile --norc -eo pipefail {0} + +jobs: + continuous-integration: + name: Continuous Integration + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Set up BATS v1.9.0 + uses: mig4/setup-bats@v1 + with: + bats-version: 1.9.0 + + - name: Set up BATS libraries + uses: brokenpip3/setup-bats-libs@0.1.0 + with: + support-install: true + assert-install: true + file-install: true + detik-install: false + + - name: Set up the BATS reporting location + id: logs + run: | + mkdir -p logs + LOG_DIR=$(mktemp --directory --tmpdir=logs XXX-bats-results) + echo "dir=${LOG_DIR}" >> $GITHUB_OUTPUT + + - name: Test the shell scripts + env: + BATS_LIB_PATH: lib:/usr/lib + run: |- + bats --tap tests/ + + - name: Create the shell scripts test reports + env: + BATS_LIB_PATH: lib:/usr/lib + run: |- + bats --verbose-run --formatter junit tests/ \ + > ${{ steps.logs.outputs.dir }}/bats.xml + + - name: Publish the BATS summary + id: junit-summary + uses: phoenix-actions/test-reporting@v12 + if: always() + with: + name: BATS Summary + output-to: step-summary + working-directory: ${{ steps.logs.outputs.dir }} + path: bats.xml + reporter: java-junit + + - name: Publish the BATS results + id: junit-report + uses: phoenix-actions/test-reporting@v12 + if: always() + with: + name: 'Scripts Integrations / BATS Results' + output-to: checks + working-directory: ${{ steps.logs.outputs.dir }} + path: bats.xml + reporter: java-junit + + - name: Add link for BATS results + if: always() + run: | + echo "::notice::BATS Report is available at ${{ steps.junit-report.outputs.runHtmlUrl }}" + echo "See the full BATS Results Report at [GitHub Actions / Scripts Integrations / BATS Results](${{ steps.junit-report.outputs.runHtmlUrl }})" >> $GITHUB_STEP_SUMMARY + + - name: Save the BATS logs and reports as an artifact + uses: actions/upload-artifact@v3 + if: always() + with: + name: BATS Logs and Reports + path: ${{ steps.logs.outputs.dir }} + + linting: + name: Linting + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Lint the shell scripts + uses: luizm/action-sh-checker@v0.7.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHFMT_OPTS: --indent 2 --binary-next-line --case-indent --simplify + with: + sh_checker_comment: true diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml new file mode 100644 index 0000000..ba777ea --- /dev/null +++ b/.github/workflows/tag-release.yaml @@ -0,0 +1,46 @@ +--- +name: Tag Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: read + issues: write + +defaults: + run: + # Error handling and pipefile must be explicitly set via the default shell + # https://github.com/actions/runner/issues/353#issuecomment-1067227665 + shell: bash --noprofile --norc -eo pipefail {0} + +jobs: + + update-tags: + name: Update Major/Minor Tags + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Fetch the tags + run: git fetch --force --tags + + - name: Force update the major tag + run: | + TAG=${GITHUB_REF/refs\/tags\//} + VERSION=${TAG#v} + MAJOR=${VERSION%%.*} + git tag v${MAJOR} ${TAG} -f + git push origin refs/tags/v${MAJOR} -f + + - name: Force update the major/minor tag + run: | + TAG=${GITHUB_REF/refs\/tags\//} + VERSION=${TAG#v} + MAJOR_MINOR=${VERSION%.*} + git tag v${MAJOR_MINOR} ${TAG} -f + git push origin refs/tags/v${MAJOR_MINOR} -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e34af4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore taskfile processing directory +.task diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..a160951 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,37 @@ +--- +# Enable the default rules +default: true + +# no-hard-tabs +MD010: + # Include code blocks + code_blocks: true + # Fenced code languages to ignore + ignore_code_languages: [] + # Number of spaces for each hard tab + spaces_per_tab: 2 + +# line-length +MD013: + # Number of characters + line_length: 80 + # Number of characters for headings + heading_line_length: 50 + # Include code blocks + code_blocks: false + # Include tables + tables: false + # Strict length checking + strict: false + # Stern length checking + stern: false + +# no-inline-html +MD033: + allowed_elements: + - "a" + - "pre" + - "br" + +# no-bare-urls +MD034: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6fb9999 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.4.0 + hooks: + - id: no-commit-to-branch + name: Check this commit is not to the main/master branch + - id: check-merge-conflict + name: Check for merge conflicts before committing + - id: check-case-conflict + name: Check for case conflicts for case-sensitive filesystems + - id: check-symlinks + name: Check for broken syslinks in the repository + - id: destroyed-symlinks + name: Check for destroyed symlinks in the repository + - id: check-added-large-files + name: Check no large files have been added to the commit + - id: trailing-whitespace + name: Check all trailing whitespace is removed + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + name: Check all files end in a new-line only + + - repo: https://github.com/python-jsonschema/check-jsonschema.git + rev: 0.23.2 + hooks: + - name: Check the GitHub Workflows for correctness + id: check-github-workflows + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.32.0 + hooks: + - id: yamllint + name: Lint YAML files for correctness and formatting + args: [--config-file, .yamllint.yaml] + + - repo: https://github.com/jumanjihouse/pre-commit-hooks.git + rev: 3.0.0 + hooks: + - id: script-must-have-extension + name: Check non-executable shell scripts end with .sh extension + exclude: '^tests/' + - id: script-must-not-have-extension + name: Check executable shell scripts to not have extension + exclude: '^tests/' + - id: shellcheck + name: Check shell scripts with shellcheck + exclude: '^tests/' + - id: shfmt + name: Check shell scripts formtting with shfmt + args: ["-i", "2", "-bn", "-ci", "-s"] + + - repo: https://github.com/igorshubovych/markdownlint-cli.git + rev: v0.35.0 + hooks: + - id: markdownlint + name: Check Markdown correctness and formatting + + - repo: https://github.com/zricethezav/gitleaks.git + rev: v8.17.0 + hooks: + - id: gitleaks + name: Check for hard-coded secrets, keys, and credentials diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..946bedf --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,47 @@ +--- +extends: default + +rules: + document-start: + present: true + document-end: + present: false + + line-length: + max: 200 + allow-non-breakable-words: true + trailing-spaces: {} # Enabled + empty-lines: + max: 1 + max-start: 1 + max-end: 0 + + indentation: + spaces: 2 + # Allow 0 indentations in files provided the file is consistent + indent-sequences: consistent + + truthy: + check-keys: false + + colons: + max-spaces-before: 0 + max-spaces-after: 1 + + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + + # quoted-strings: + # quote-type: single + # required: only-when-needed + + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + + comments: + require-starting-space: true + ignore-shebangs: true + min-spaces-from-content: 1 diff --git a/README.md b/README.md index 151cbb0..b2f3ecf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,159 @@ -# action-terraform-docs -A GitHub Action for running terraform-docs against a repository to automatically update the documentation +# n3tuk Terraform Documentation Action + +A GitHub Action for running [`terraform-docs`][terraform-docs] against a +repository to automatically update the `README.md` file in a configuration with +standard documentation covering items such as variables, outputs, and resources. + +[terraform-docs]: https://github.com/terraform-docs/terraform-docs + +```yaml +--- +name: terraform-docs + +on: + pull_requests: + branches: + - main + - master + +permissions: + contents: write + packages: read + issues: read + pull-requests: write + +jobs: + terraform-docs: + name: Run terraform-docs + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Render and synchronise the template repository + uses: n3tuk/action-terraform-docs@v1 + with: + working-directory: |- + terraform + modules + config-file: .terraform-docs.yaml + recursive: "true" + git-push: "true" +``` + +## Using `GITHUB_TOKEN` + +Note that this GitHub Action does not use `GITHUB_TOKEN` directly. It will not +create a pull request on changes, or make direct requests against GitHub. If +there are changes it will either fail, or stage and commit directly via `git` +and push back to the `origin`. + +As such `git` will use the credentials provided when checking out the repository +through the `when.token` argument, shown above. + +By default this action uses the automatically generated `GITHUB_TOKEN` which +allows it to push changes (assuming `permissions.contents` is set to `write`) +back to the pull request, and uses the account of GitHub Actions to provide the +`git-name` and `git-email` values for the configuration of `git`. + +If you change `GITHUB_TOKEN` to be a specific bot or user account, it's +recommended to update `git-name` and `git-email` to reflect the details of that +account. + +## Configuration Files + +The configuration files for `terraform-docs`, when committed to the repository +and configured in the GitHub Action, can be set one of three levels: + +1. The directory of a Terraform configuration, which will be used for that + configuration only; +1. The `working-directory` being processed, which will be used as the default + configuration used for all Terraform configurations found under that + `working-directory`; and +1. The `root` of the repository, which will be in effect the default + configuration file used for all Terraform configurations if no other file is + found. + +Assuming the structure of the Terraform repository is as follows: + +```sh +terraform-configuration/ +├── terraform/ +│   ├── main.tf +│   ├── terraform.tf +├── modules/ +│   ├── module-one/ +│   │   ├── main.tf +│   │   ├── terraform.tf +│   ├── module-two/ +│   │   ├── module-three/ +│   │   │   ├── main.tf +│   │   │   └── terraform.tf +│   │   ├── main.tf +│   │   ├── terraform.tf +│   │   └── .terraform-docs.yaml +│   └── .terraform-docs.yaml +├── README.md +└── .terraform-docs.yaml +``` + +And the GitHub Action is configured to recursively scan the `terraform/` and +`modules/` directories, looking for any with `.tf` files within, and the +`config-file` set to look for the file `.terraform-docs.yaml`, as follows: + +```yaml +- name: Render and synchronise the template repository + uses: n3tuk/action-terraform-docs@v1 + with: + working-directory: |- + terraform + modules + config-file: .terraform-docs.yaml + recursive: true +``` + +The following four Terraform configurations will be found, and will use the +noted configuration file: + +1. `modules/module-two/module-three` will use `modules/.terraform-docs.yaml` + (The file `modules/module-two/.terraform-docs.yaml` in the parent directory + will not be used for this module as that is specific to `module-two`, so the + fallback is to `modules/.terraform-docs.yaml`); +1. `modules/module-two` will use `modules/module-two/.terraform-docs.yaml` + (specific override for this module); +1. `modules/module-one` will use `modules/.terraform-docs.yaml` (use the + default for the `working-directory`); and +1. `terraform` will use `.terraform-docs.yaml` in the `root` of the repository + (no local override, or `working-directory` override - they are the same + directory in effect - so use the repository default). + +## Output Configurations + +TODO + + + +## Action Inputs + +| Input | Description | Required | Default | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------: | :--------------------------------------------------------------------- | +| `working-directory` | A list of one or more working directories that `terraform-docs` should be run within, with each directory on a new line when required. Use `.` for the root directory of the repository. | `true` | | +| `config` | The configuration file for `terraform-docs` which will be given when it is run within each of the working directories. If the file resides in the directory being run, that has priority, followed by the working directory, and then the `root` of the repository. | `false` | | +| `recursive` | Recursively scan each of the `working-directory` entries for directories with `.tf` files for processing by `terraform-docs`. | `false` | `false` | +| `output-format` | The output type to generate the content from `terraform-docs` (e.g. `markdown table`). This is ignored if `config-file` is set. | `false` | `markdown table` | +| `output-mode` | One of the three `replace`, `inject`, or `print` modes for handling the generated content. This is ignored if `config-file` is set. | `false` | `inject` | +| `indent` | When using one of the `markdown` formats, set how deep the header indentation is for the generated content. This is ingored if `config-file` is set. | `false` | `2` | +| `output-template` | The template to use when generating content before being processed by the selected `output-mode` into the `output-file`. Can be useful for adding comments when using `inject`. This is ignored if `config-file` is set. | `false` | `
{{ .Content }}
` | +| `output-file` | This is the name of the file to work with based on the `output-mode` configured, and to which the generated content will be handled. This is ignored if `config-file` is set. | `false` | `README.md` | +| `lockfile` | Read the `.terraform.lock.hcl` file in the configuration, if present, for the versions of the Terraform providers used in this configuration. This is ignored if `config-file` is set. | `false` | `true` | +| `fail-on-diff` | Set whether or not to fail the GitHub Action is any changes are detected in the repository after `terraform-docs` has been run. | `false` | `false` | +| `show-on-diff` | Set whether or not to show any changes detected in the repository after `terraform-docs` has been run. | `false` | `true` | +| `git-push` | Set whether or not to stage, commit, and push any changes made in the repository after running `terraform-docs` back to the branch. | `false` | `false` | +| `git-name` | Set the name of the comitter for git if changes are staged and comitted. This is ignored unless `git-push` is set. | `false` | `github-actions[bot]` | +| `git-email` | Set the email of the comitter for git if changes are staged and comitted. This is ignored unless `git-push` is set. | `false` | `41898282+github-actions[bot]@users.noreply.github.com` | +| `git-title` | Set the title of the commit message (i.e. the first line) if any changes are staged and commited back to the repository after `terraform-docs` has been run. This is ignored unless `git-push` is set. | `false` | ``Syncing changes made by `terraform-docs``` | +| `git-body` | Set the body of the commit message (if required) if any changes are staged and commited back to the repository after `terraform-docs` has been run. This is ignored unless `git-push` is set. | `false` | | + + diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..7de03f5 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,122 @@ +--- +version: 3 +interval: 1000ms +output: group + +vars: + bin_name: pull-requester + +tasks: + default: + cmds: + - task: clean + - task: test + + test: + desc: Run the unit tests for the scripts + aliases: + - t + sources: + - "bin/*" + - "lib/*.sh" + - "tests/*.bats" + - "tests/helpers/*.bash" + env: + BATS_LIB_PATH: lib:/usr/lib + cmds: + # Enter key to the same pane as it is running in so that it exits + # scrolling mode and starts refreshing the screen as new tests are run + - cmd: |- + test -n "$TMUX_PANE" && tmux send-keys -t $TMUX_PANE Enter + silent: true + - cmd: |- + bats --pretty tests/ + + fmt: + desc: Properly format all the bash and bats files + aliases: + - f + sources: + - "bin/*" + - "lib/*.sh" + - "tests/*.bats" + cmds: + - cmd: |- + shfmt \ + --indent 2 --binary-next-line --case-indent \ + --write --language-dialect bash --simplify \ + bin/* lib/*.sh tests/helpers/*.bash + - cmd: |- + shfmt \ + --indent 2 --binary-next-line --case-indent \ + --write --language-dialect bats --simplify \ + tests/*.bats + + docs: + desc: Generate the documentation from action.yml to README.md + aliases: + - d + sources: + - "README.md" + - "action.yml" + cmds: + - cmd: |- + INPUTS=$(yq -r \ + 'if has("inputs") then .inputs else [] end + | to_entries[] + | ( "| " + + ("`" + .key + "`") + + " | " + + (.value.description | gsub("[\\n]"; " ") | gsub("\\[optional\\] "; "")) + + " | " + + (.value | if has("required") then ("`" + (.required | tostring) + "`") else "`false`" end) + + " | " + + (.value | if (has("default") and .default != "") then ("`" + (.default | tostring | gsub("[\\n]"; "
")) + "`") else "" end) + + " |")' \ + action.yml) + + OUTPUTS=$(yq -r \ + 'if has("outputs") then .outputs else [] end + | to_entries[] + | ( "| " + + ("`" + .key + "`") + + " | " + + (.value.description | gsub("[\\n]"; "")) + + " |")' \ + action.yml) + + sed -i "/^/{:b;$!N;/$/!bb;s///}" README.md + + ( echo "" + echo + + if [[ -n "${INPUTS}" ]] + then + echo "## Action Inputs" + echo "| Input | Description | Required | Default |" + echo "| :--- | :--- | :---: | :--- |" + echo "${INPUTS}" + echo + fi + + if [[ -n "${OUTPUTS}" ]] + then + echo "## Action Outputs" + echo "| Output | Description |" + echo "| :--- | :--- |" + echo "${OUTPUTS}" + echo + fi + + echo "" + ) >> README.md + silent: true + - cmd: prettier -w README.md + + clean: + desc: Clean up temporary files and locations + aliases: + - c + run: once + cmds: + - cmd: rm -rf .task diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..569a475 --- /dev/null +++ b/action.yml @@ -0,0 +1,130 @@ +--- +name: terraform-docs +author: Jonathan Wright +description: |- + A GitHub Action for running `terraform-docs` against one or more Terraform + configurations inside a repository. + +inputs: + working-directory: + description: |- + A list of one or more working directories that `terraform-docs` should be + run within, with each directory on a new line when required. Use `.` for + the root directory of the repository. + required: true + config: + description: |- + [optional] The configuration file for `terraform-docs` which will be given + when it is run within each of the working directories. If the file resides + in the directory being run, that has priority, followed by the working + directory, and then the `root` of the repository. + default: '' + recursive: + description: |- + [optional] Recursively scan each of the `working-directory` entries for + directories with `.tf` files for processing by `terraform-docs`. + default: 'false' + output-format: + description: |- + [optional] The output type to generate the content from `terraform-docs` + (e.g. `markdown table`). This is ignored if `config-file` is set. + default: 'markdown table' + output-mode: + description: |- + [optional] One of the three `replace`, `inject`, or `print` modes for + handling the generated content. This is ignored if `config-file` is set. + default: 'inject' + indent: + description: |- + [optional] When using one of the `markdown` formats, set how deep the + header indentation is for the generated content. This is ingored if + `config-file` is set. + default: '2' + output-template: + description: |- + [optional] The template to use when generating content before being + processed by the selected `output-mode` into the `output-file`. Can be + useful for adding comments when using `inject`. This is ignored if + `config-file` is set. + default: |- + + {{ .Content }} + + output-file: + description: |- + [optional] This is the name of the file to work with based on the + `output-mode` configured, and to which the generated content will be + handled. This is ignored if `config-file` is set. + default: 'README.md' + lockfile: + description: |- + [optional] Read the `.terraform.lock.hcl` file in the configuration, if + present, for the versions of the Terraform providers used in this + configuration. This is ignored if `config-file` is set. + default: 'true' + fail-on-diff: + description: |- + [optional] Set whether or not to fail the GitHub Action is any changes are + detected in the repository after `terraform-docs` has been run. + default: 'false' + show-on-diff: + description: |- + [optional] Set whether or not to show any changes detected in the + repository after `terraform-docs` has been run. + default: 'true' + git-push: + description: |- + [optional] Set whether or not to stage, commit, and push any changes made + in the repository after running `terraform-docs` back to the branch. + default: 'false' + git-name: + description: |- + [optional] Set the name of the comitter for git if changes are staged + and comitted. This is ignored unless `git-push` is set. + default: 'github-actions[bot]' + git-email: + description: |- + [optional] Set the email of the comitter for git if changes are staged + and comitted. This is ignored unless `git-push` is set. + default: '41898282+github-actions[bot]@users.noreply.github.com' + git-title: + description: |- + [optional] Set the title of the commit message (i.e. the first line) if + any changes are staged and commited back to the repository after + `terraform-docs` has been run. This is ignored unless `git-push` is set. + default: 'Syncing changes made by terraform-docs' + git-body: + description: |- + [optional] Set the body of the commit message (if required) if any changes + are staged and commited back to the repository after `terraform-docs` has + been run. This is ignored unless `git-push` is set. + default: '' + +runs: + using: 'composite' + steps: + - name: Run terraform-docs in the repository + id: run + shell: bash + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + CONFIG: ${{ inputs.config }} + RECURSIVE: ${{ inputs.recursive }} + OUTPUT_FORMAT: ${{ inputs.output-format }} + OUTPUT_MODE: ${{ inputs.output-mode }} + INDENT: ${{ inputs.indent }} + OUTPUT_TEMPALTE: ${{ inputs.output-template }} + OUTPUT_FILE: ${{ inputs.output-file }} + FAIL_ON_DIFF: ${{ inputs.fail-on-diff }} + SHOW_ON_DIFF: ${{ inputs.show-on-diff }} + GIT_PUSH: ${{ inputs.git-push }} + GIT_NAME: ${{ inputs.git-name }} + GIT_EMAIL: ${{ inputs.git-email }} + GIT_TITLE: ${{ inputs.git-title }} + GIT_BODY: ${{ inputs.git-body }} + run: |- + ${{ github.action_path }}/bin/run + +branding: + icon: file-text + color: gray-dark diff --git a/bin/run b/bin/run new file mode 100755 index 0000000..4479d70 --- /dev/null +++ b/bin/run @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1091 # Using dynamic lookups for sources files + +source "$(dirname "${0}")/../lib/common.sh" +source "$(dirname "${0}")/../lib/search.sh" +source "$(dirname "${0}")/../lib/arguments.sh" +source "$(dirname "${0}")/../lib/run.sh" + +check_variables \ + GITHUB_WORKSPACE WORKING_DIRECTORY CONFIGURATIONS \ + CONFIG RECURSIVE \ + OUTPUT_FORMAT OUTPUT_MODE +check_commands find terraform-docs + +echo "${WORKING_DIRECTORY}" | while read -r base; do + show_debug "searching for configurations in ${base}" + + # Iterate over all the possible working directories provided and then find all + # the unique Terraform configurations within to be passed onto the next steps + find_configurations "${base}" "${RECURSIVE}" | sort | uniq \ + | while read -r configuration; do + + show_debug "processing the configuration in ${configuration}" + arguments=$(build_settings "${base}" "${configuration}" "${CONFIG}") + + show_debug "running terraform-docs ${arguments}" + # shellcheck disable=SC2086 # $arguments does not need quoting + run_terraform_docs "${configuration}" ${arguments} + done +done diff --git a/lib/arguments.sh b/lib/arguments.sh new file mode 100644 index 0000000..2e14a0e --- /dev/null +++ b/lib/arguments.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +function find_config_file { + local working_directory=${1} + local configuration=${2} + local config_name=${3} + + for dir in "${configuration}" "${working_directory}" "."; do + local path="${GITHUB_WORKSPACE}" + if [[ ${dir} != "." ]]; then + path+="/${dir}" + fi + + if [[ -f "${path}/${config_name}" ]]; then + if [[ ${dir} != "." ]]; then + echo "${dir}/${config_name}" + return + else + echo "${config_name}" + return + fi + fi + done +} + +function build_settings { + local working_directory=${1} + local configuration=${2} + local config_name=${3} + + # shellcheck disable=SC2155 # return values are not required + local config_path=$( + find_config_file \ + "${working_directory}" \ + "${configuration}" \ + "${config_name}" + ) + + if [[ -n ${config_path} ]]; then + # Use the absolute path to avoid having to calculate the relative path + echo -n "--config ${GITHUB_WORKSPACE}/${config_path}" + return # quick exit + fi + + if [[ "${OUTPUT_FORMAT}" ]]; then + echo -n "${OUTPUT_FORMAT}" + fi + + if [[ ${OUTPUT_FORMAT} =~ "markdown" && -n ${INDENT} ]]; then + echo -n " --indent ${INDENT}" + fi + + if [[ "${OUTPUT_MODE}" ]]; then + echo -n " --mode ${OUTPUT_MODE}" + fi + + if [[ -n ${LOCKFILE} && ${LOCKFILE} == "true" ]]; then + echo -n " --lockfile" + fi + + if [[ -n ${OUTPUT_TEMPLATE} ]]; then + echo -n " --template \"${OUTPUT_TEMPLATE}\"" + fi + + if [[ ${OUTPUT_MODE} != "print" && -n ${OUTPUT_FILE} ]]; then + echo -n " --output-file ${OUTPUT_FILE}" + fi +} diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..e9a4f20 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,84 @@ +#! /usr/bin/env bash + +set -euo pipefail + +# Set up colours for improving output +YELLOW='\033[1;33m' +WHITE='\033[1;37m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Check to see if the variable name provided both exists and has a value +# associated with it, otherwise error and exit the script +function check_variables { + for variable in "${@}"; do + check_variable "${variable}" + done +} +# Check to see if the variable name provided both exists and has a value +# associated with it, otherwise error and exit the script +function check_variable { + set +u # Don't error out on unbound variables in this function + if [[ -z ${!1} ]]; then + exit_error "Missing the environment variable '${1}'" + fi +} + +# Check to see if all the commands provided exists and are executable, otherwise +# error and exit the script +function check_commands { + for command in "${@}"; do + check_command "${command}" + done +} + +# Check to see if the command provided both exists and is executable, otherwise +# error and exit the script +function check_command { + if [[ ! -x "$(command -v "${1}")" ]]; then + exit_error "Missing the ${1} application. Please install and try again." + fi +} + +# Initiate the starting of a grouped output for GitHub Actions +function start_group { + echo "::group::${1}" + show_stage "${1}" +} + +# End the grouped output section for GitHub Actions +function end_group { + echo "::endgroup::" +} + +# Output a debug message for GitHub Actions +function show_debug { + echo >&2 "::debug::${1}" +} + +# Output the header for a new stage in the application +function show_stage { + echo -e "${YELLOW}==>${NC} ${WHITE}${1}${NC}" +} + +# Output the message for a step in the application +function show_step { + echo -e " ${BLUE}->${NC} ${WHITE}${1}${NC}" +} + +# Define an output in the GitHub Action for GitHub Workflows +function put_output { + echo "${1}=${2}" >>"${GITHUB_OUTPUT}" +} + +# Output an error message for GitHub Actions +function show_error { + echo >&2 "::error::${1}" +} + +# Output an error message for GitHub Actions and then immediately exit the +# script +function exit_error { + show_error "${1}" + exit 1 +} diff --git a/lib/run.sh b/lib/run.sh new file mode 100644 index 0000000..ee8a46d --- /dev/null +++ b/lib/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +function run_terraform_docs { + local configuration="${1}" + shift + + local arguments="${*}" + + ( # shellcheck disable=SC2164 # $path has already been tested + cd "${configuration}" + # shellcheck disable=SC2086 # $arguments does not need quoting + terraform-docs ${arguments} . + ) +} diff --git a/lib/search.sh b/lib/search.sh new file mode 100644 index 0000000..abdd352 --- /dev/null +++ b/lib/search.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +function find_configurations { + local path=${1} + local recursive=${2} + + if [[ ! -d ${path} ]]; then + exit_error "The path '${path}' is not a directory or could not be found" + fi + + ( # shellcheck disable=SC2164 # $path has already been tested + cd "${path}" + if [[ ${recursive} == "true" ]]; then + find . \ + -type f \ + -name '*.tf' -and -not -path '*.terraform*' \ + -printf '%h\n' \ + | sort \ + | uniq \ + | sed -e 's|^\./||g' + else + # Use -quit to print one directory entry and exit, assuming there is a .tf + # file in that directory, as we're only testing the one directory + find . \ + -maxdepth 1 \ + -type f \ + -name '*.tf' \ + -printf '%h\n' \ + -quit \ + | sed -e 's|^\./||g' + fi + ) +} diff --git a/tests/arguments.bats b/tests/arguments.bats new file mode 100644 index 0000000..6299b55 --- /dev/null +++ b/tests/arguments.bats @@ -0,0 +1,173 @@ +#!/usr/bin/env bats + +bats_load_library "bats-support" +bats_load_library "bats-assert" + +load "helpers/common" + +setup() { + set_environment_variables + + test_dir=$(mktemp --directory --suffix=-bats) + export GITHUB_WORKSPACE="${test_dir}" +} + +teardown() { + rm -rf "${test_dir}" +} + +@test "find_config_file() finds configuration config file as preferred" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + {,modules/,modules/mod-one/}.terraform-docs.yaml + + run find_config_file modules modules/mod-one .terraform-docs.yaml + assert_output "modules/mod-one/.terraform-docs.yaml" + refute_output "modules/.terraform-docs.yaml" + refute_output ".terraform-docs.yaml" + assert_success +} + +@test "build_settings() provides --config file as only setting when present" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + {,modules/,modules/mod-one/}.terraform-docs.yaml + + run build_settings modules modules/mod-one .terraform-docs.yaml + assert_output "--config ${test_dir}/modules/mod-one/.terraform-docs.yaml" + assert_success +} + +@test "find_config_file() finds base config file as first fallback" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + {,modules/}.terraform-docs.yaml + + run find_config_file modules modules/mod-one .terraform-docs.yaml + refute_output "modules/mod-one/.terraform-docs.yaml" + assert_output "modules/.terraform-docs.yaml" + refute_output ".terraform-docs.yaml" + assert_success +} + +@test "find_config_file() finds default config file as second fallback" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + .terraform-docs.yaml + + run find_config_file modules modules/mod-one .terraform-docs.yaml + refute_output "modules/mod-one/.terraform-docs.yaml" + refute_output "modules/.terraform-docs.yaml" + assert_output ".terraform-docs.yaml" + assert_success +} + +@test "find_config_file() finds one config file in three references" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + .terraform-docs.yaml + + run find_config_file . . .terraform-docs.yaml + assert_output ".terraform-docs.yaml" + assert_success +} + +@test "find_config_file() finds no default config file" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + {modules/,modules/mod-one/}.terraform-docs.yaml + + run find_config_file terraform terraform .terraform-docs.yaml + refute_output "modules/mod-one/.terraform-docs.yaml" + refute_output "modules/.terraform-docs.yaml" + refute_output "terraform/.terraform-docs.yaml" + refute_output ".terraform-docs.yaml" + assert_success +} + +@test "build_settings() provides no --config file when none present" { + source lib/common.sh + source lib/arguments.sh + + create_test_files \ + "${test_dir}" \ + {modules/,modules/mod-one/}.terraform-docs.yaml + + run build_settings terraform terraform .terraform-docs.yaml + refute_output --partial "--config ${test_dir}/.terraform-docs.yaml" + assert_success +} + +@test "build_settings() sets the format to markdown table by default" { + source lib/common.sh + source lib/arguments.sh + + run build_settings terraform terraform .terraform-docs.yaml + refute_output --partial "--config .terraform-docs.yaml" + refute_output --partial "--config terraform/.terraform-docs.yaml" + assert_output --partial "markdown table" + assert_output --partial "--indent 2" + refute_output --partial "json" + assert_success +} + +@test "build_settings() sets the format to json on override, bypassing --indent" { + source lib/common.sh + source lib/arguments.sh + + # shellcheck disable=SC2030 # override specifically for this test + export OUTPUT_FORMAT="json" + + run build_settings terraform terraform .terraform-docs.yaml + refute_output --partial "--config .terraform-docs.yaml" + refute_output --partial "--config terraform/.terraform-docs.yaml" + refute_output --partial "markdown table" + assert_output --partial "json" + refute_output --partial "--indent 2" + assert_success +} + +@test "build_settings() sets the defaults for --mode, --lockfile, and --output-file" { + source lib/common.sh + source lib/arguments.sh + + run build_settings terraform terraform .terraform-docs.yaml + refute_output --partial "--config terraform/.terraform-docs.yaml" + assert_output --partial "--mode inject" + assert_output --partial "--lockfile" + assert_output --partial "--output-file README.md" + assert_success +} + +@test "build_settings() sets disables --output-file when --output-mode is print" { + source lib/common.sh + source lib/arguments.sh + + # shellcheck disable=SC2030 # override specifically for this test + export OUTPUT_MODE="print" + + run build_settings terraform terraform .terraform-docs.yaml + refute_output --partial "--config terraform/.terraform-docs.yaml" + refute_output --partial "--mode inject" + assert_output --partial "--mode print" + refute_output --partial "--output-file README.md" + assert_success +} diff --git a/tests/common.bats b/tests/common.bats new file mode 100644 index 0000000..6d80c13 --- /dev/null +++ b/tests/common.bats @@ -0,0 +1,110 @@ +#!/usr/bin/env bats + +bats_load_library "bats-support" +bats_load_library "bats-assert" + +load "helpers/common" + +setup() { + set_environment_variables +} + +@test "start_group() outputs correct format for GitHub Workflows" { + source lib/common.sh + run start_group test + assert_output --partial "::group::test" + assert_success +} + +@test "end_group() outputs correct format for GitHub Workflows" { + source lib/common.sh + run end_group + assert_output "::endgroup::" + assert_success +} + +@test "show_debug() outputs correct format for GitHub Workflows" { + source lib/common.sh + run show_debug test + assert_output "::debug::test" + assert_success +} + +@test "show_error() outputs correct format for GitHub Workflows" { + source lib/common.sh + run show_error test + assert_output "::error::test" + assert_success +} + +@test "exit_error() outputs correct format for GitHub Workflows with exit status" { + source lib/common.sh + run exit_error test + assert_output "::error::test" + assert_failure +} + +@test "check_variable() successfully finds WORKING_DIRECTORY variable" { + refute_empty "${WORKING_DIRECTORY}" + source lib/common.sh + run check_variable WORKING_DIRECTORY RECURSIVE + assert_success +} + +@test "check_variables() successfully finds RECURSIVE variable with other variables" { + refute_empty "${WORKING_DIRECTORY}" + refute_empty "${RECURSIVE}" + source lib/common.sh + run check_variables WORKING_DIRECTORY RECURSIVE + assert_success +} + +@test "check_variable() fails on missing WORKING_DIRECTORY variable" { + unset WORKING_DIRECTORY + assert_empty "${WORKING_DIRECTORY}" + source lib/common.sh + run check_variable WORKING_DIRECTORY + assert_output --partial "::error::" + assert_output --partial "WORKING_DIRECTORY" + assert_failure +} + +@test "check_variables() fails on missing FAIL_ON_DIFF variable among others" { + unset FAIL_ON_DIFF + refute_empty "${WORKING_DIRECTORY}" + refute_empty "${RECURSIVE}" + assert_empty "${FAIL_ON_DIFF}" + source lib/common.sh + run check_variables WORKING_DIRECTORY RECURSIVE FAIL_ON_DIFF + assert_output --partial "::error::" + assert_output --partial "FAIL_ON_DIFF" + assert_failure +} + +@test "check_command() successfully finds bash command" { + source lib/common.sh + run check_command bash + assert_success +} + +@test "check_commands() successfully finds bash and sh commands" { + source lib/common.sh + run check_commands bash sh + assert_success +} + +@test "check_command() fails on missing does-not-exist command" { + source lib/common.sh + run check_command does-not-exist + assert_output --partial "::error::" + assert_output --partial "does-not-exist" + assert_failure +} + +@test "check_commands() fails on missing does-not-exist command" { + source lib/common.sh + run check_commands bash sh does-not-exist + assert_output --partial "::error::" + assert_output --partial "does-not-exist" + assert_failure +} diff --git a/tests/helpers/common.bash b/tests/helpers/common.bash new file mode 100644 index 0000000..a109a70 --- /dev/null +++ b/tests/helpers/common.bash @@ -0,0 +1,82 @@ +#!/usr/bin/env bats + +# Set up the standard environment variables which will normally be provided by +# the GitHub Action configuration +function set_environment_variables { + # General directory and configuration settings + WORKING_DIRECTORY="terraform\nmodules" + CONFIG="" + RECURSIVE="false" + export WORKING_DIRECTORY CONFIG RECURSIVE + + # Default settings for output configurations (which will be overridden by + # the CONFIG_FILE variable, if set) for terraform-docs + OUTPUT_FORMAT="markdown table" + OUTPUT_MODE="inject" + INDENT="2" + OUTPUT_TEMPLATE="\n{{ .Content }}\n" + OUTPUT_FILE="README.md" + LOCKFILE="true" + export OUTPUT_FORMAT OUTPUT_MODE INDENT OUTPUT_TEMPLATE OUTPUT_FILE LOCKFILE + + # Default settings for dealing with repository difference detection + FAIL_ON_DIFF="false" + SHOW_ON_DIFF="true" + export FAIL_ON_DIFF SHOW_ON_DIFF + + # Default settings for git configuration and whether to run git staging, + # committing, and pushing back to the repository on changes + GIT_PUSH="false" + GIT_NAME="github-actions[bot]" + GIT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" + GIT_TITLE="Syncing changes made by terraform-docs" + GIT_BODY="" + export GIT_PUSH GIT_NAME GIT_EMAIL GIT_TITLE GIT_BODY +} + +# Create the required files for validating testing of Terraform configuration +# layouts and terraform-docs configuration files +function create_test_files { + local base=${1} + shift + + show_debug "create_test_files() base=${base} files=${*}" + + for file in "${@}"; do + # Make sure the parent directory is created first + mkdir -p "${base}/$(dirname "${file}")" + touch "${base}/${file}" + echo "${file}" >>"${base}/.test-files" + done +} + +# Remove the created files for validating testing of various directory +# structures and configurations +function clean_all_files { + find "${1}" -type f -delete \ + \( -name '*.tf' -or -name '.terraform-docs.yaml' \) +} + +# Remove the created files for validating testing of various directory +# structures and configurations +function clean_test_files { + local base=${1} + + ( + cd "${base}" || exit + test -e .test-files || exit + while read -r file <.test-files; do + rm -f "${file}" + # Attempt to clean any directories created alongside the files + rmdir --ignore-fail-on-non-empty --parents "$(dirname "${file}")" + done + ) +} + +function assert_empty { + assert [ -z "${1}" ] +} + +function refute_empty { + assert [ ! -z "${1}" ] +} diff --git a/tests/helpers/terraform-docs.bash b/tests/helpers/terraform-docs.bash new file mode 100644 index 0000000..4d965f0 --- /dev/null +++ b/tests/helpers/terraform-docs.bash @@ -0,0 +1 @@ +#!/usr/bin/env bats diff --git a/tests/run.bats b/tests/run.bats new file mode 100644 index 0000000..b01fe69 --- /dev/null +++ b/tests/run.bats @@ -0,0 +1,15 @@ +#!/usr/bin/env bats + +bats_load_library "bats-support" +bats_load_library "bats-assert" + +load "helpers/common" + +setup() { + set_environment_variables + test_dir=$(mktemp --directory --suffix=-bats) +} + +teardown() { + rm -rf "${test_dir}" +} diff --git a/tests/search.bats b/tests/search.bats new file mode 100644 index 0000000..988bdc1 --- /dev/null +++ b/tests/search.bats @@ -0,0 +1,85 @@ +#!/usr/bin/env bats + +bats_load_library "bats-support" +bats_load_library "bats-assert" + +load "helpers/common" + +setup() { + set_environment_variables + test_dir=$(mktemp --directory --suffix=-bats) +} + +teardown() { + rm -rf "${test_dir}" +} + +@test "find_configurations() searches single path without recursion" { + source lib/common.sh + source lib/search.sh + + create_test_files \ + "${test_dir}" \ + terraform/{main,terraform}.tf + + run find_configurations "${test_dir}/terraform" false + assert_line "." + assert_success +} + +@test "find_configurations() searches multiple paths without recursion" { + source lib/common.sh + source lib/search.sh + + create_test_files \ + "${test_dir}" \ + terraform/{main,terraform}.tf \ + modules/test-{one,two}/{main,terraform}.tf + + run find_configurations "${test_dir}/terraform" false + assert_line "." + assert_success +} + +@test "find_configurations() searches empty path without recursion" { + source lib/common.sh + source lib/search.sh + + create_test_files \ + "${test_dir}" \ + terraform/{main,terraform}.tf + + run find_configurations "${test_dir}" false + refute_output --partial '::error::' + refute_line "." + refute_line "terraform" + assert_success +} + +@test "find_configurations() searches multiple paths with recursion" { + source lib/common.sh + source lib/search.sh + + create_test_files \ + "${test_dir}" \ + terraform/{main,terraform}.tf \ + modules/test-{one,two}/{main,terraform}.tf + + run find_configurations "${test_dir}" true + refute_line "." + assert_line "terraform" + assert_line "modules/test-one" + assert_line "modules/test-two" + assert_success +} + +@test "find_configurations() searches invalid path" { + source lib/common.sh + source lib/search.sh + + run find_configurations "${test_dir}/terraform" false + assert_output --partial '::error::' + refute_line "." + refute_line "terraform" + assert_failure +}