diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f4b6203 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,109 @@ +name: Terramate Action Tests + +on: + push: + branches: + - main + pull_request: + +defaults: + run: + shell: bash + +jobs: + simple: + name: Terramate latest + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install latest Terramate + uses: ./ + + - name: Validate execution + run: terramate version + + asdf: + name: Terramate asdf + runs-on: ubuntu-latest + strategy: + matrix: + version: [0.4.2, 0.4.3, skip] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare asdf config + if: ${{ matrix.version != 'skip' }} + run: echo "terramate ${{ matrix.version}}" >.tool-versions + + - name: Prepare empty asdf config + if: ${{ matrix.version == 'skip' }} + run: echo >.tool-versions + + - name: Install asdf Terramate + uses: ./ + + - name: Validate execution + run: terramate version + + - name: Validate version - ${{ matrix.version }} + id: version + if: ${{ matrix.version != 'skip' }} + run: terramate version | grep ${{ matrix.version }} + + - name: Validate outputs - ${{ matrix.version }} + if: ${{ matrix.version != 'skip' }} + run: echo "${{ steps.version.outputs.stdout }}" | grep ${{ matrix.version }} + + + wrapper: + name: Terramate with wrapper + runs-on: ubuntu-latest + strategy: + matrix: + version: [0.4.3, latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Terramate Wrapper - ${{ matrix.version }} + uses: ./ + with: + version: ${{ matrix.version }} + + - name: Validate execution - ${{ matrix.version }} + run: terramate version + + - name: Validate version - ${{ matrix.version }} + id: version + if: ${{ matrix.version != 'latest' }} + run: terramate version | grep ${{ matrix.version }} + + - name: Validate outputs - ${{ matrix.version }} + if: ${{ matrix.version != 'latest' }} + run: echo "${{ steps.version.outputs.stdout }}" | grep ${{ matrix.version }} + + no-wrapper: + name: Terramate without wrapper + runs-on: ubuntu-latest + strategy: + matrix: + version: [0.4.3, latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Terramate - ${{ matrix.version }} + uses: ./ + with: + version: ${{ matrix.version }} + use_wrapper: false + + - name: Validate execution - ${{ matrix.version }} + run: terramate version + + - name: Validate - ${{ matrix.version }} + if: ${{ matrix.version != 'latest' }} + run: terramate version | grep ${{ matrix.version }} diff --git a/README.md b/README.md index 268b986..2a453ac 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,88 @@ # Terramate Github Action -This is a very simple Github Action that installs Terramate and (by default) a wrapper that allows you to use the stdout, stderr and exit code in subsequent workflow steps. It is only compatible with Ubuntu runners. +The [`terramate-io/terramate-action`] is a GitHub composite action that sets up Terramate CLI in your GitHub Actions workflows. + +- It downloads a specific version or falls back to an [asdf] configured version or the latest available release of [Terramate CLI]. +- It installs [Terramate CLI] into a user specified path or by default to `/usr/local/bin` +- It installs a wrapper script by default so that calls to `terramate` binary will expose GitHub Action outputs to access the `stderr`, `stderr`, and the `exitcode` of the `terramate` execution. +- It allows you to configure a default [Terramate Cloud] organization to use Terramate Cloud Features like Drift Detection and Stack Health Information. + +## Compatbility + +The action currently only supports `ubuntu` runners. +Please open an issue, if more runner support is required. ## Usage -There are three optional inputs -* `version` is the version of Terramate to use. If not defined, the [asdf](https://asdf-vm.com/) `.tool-versions` file is checked, and if that doesn't contain a Terramate entry then the latest Terramate version is used by default. -* `use_wrapper` if explicitly set to `false` Terramate CLI will not be called via a wrapper script. This means that the GitHub Action Outputs cannot be used in subsequent steps. -* `cloud_organization` sets the Terramate Cloud organization to use for all steps in this job +The default action installs Terramate CLI in it's latest version unless a specific version is configured by [asdf] config file `.tool-versions`. + +```yaml +steps: + - uses: terramate-io/terramate-action@v1 +``` + +You can disable [asdf] integration by explicitly specifying `"latest"` as the desired version. + +```yaml +steps: + - uses: terramate-io/terramate-action@v1 + with: + version: "latest" +``` + +To install a specific version the version can be specified using the `version` argument: + +```yaml +steps: + - uses: terramate-io/terramate-action@v1 + with: + version: "0.4.2" +``` + +The binary will be installed to `/usr/local/bin` by default. This location can be changed using the `bindir` argument: + +```yaml +steps: + - uses: terramate-io/terramate-action@v1 + with: + bindir: /usr/local/bin +``` + +To configure the default [Terramate Cloud] Organization set `cloud_organization` argument to your organization short name: + +```yaml +steps: + - uses: terramate-io/terramate-action@v1 + with: + cloud_organization: myorganization +``` + +To disable using the optional wrapper script by default the `use_wrapper` argument can be set to `"false"`: -Outputs are `stdout`, `stderr` and `exitcode` which can be used in subsequent commands, e.g. +```yaml +steps: + - uses: terramate-io/terramate-action@v1 + with: + use_wrapper: "false" +``` + +Subsequent steps can access outputs when the wrapper script is installed: ```yaml -- name: Install terramate - uses: terramate-io/terramate-action@main - -- name: Terramate run plan - id: plan - run: terramate run --changed --disable-check-gen-code -- terraform plan -lock-timeout=5m -out out.tfplan - -- name: Publish Plans for Changed Stacks - uses: marocchino/sticky-pull-request-comment@v2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - message: | - ${{ steps.plan.outputs.stdout }} +steps: + - uses: terramate-io/terramate-action@v1 + + - id: list + run: terramate list --changed + + - run: echo ${{ steps.list.outputs.stdout }} + - run: echo ${{ steps.list.outputs.stderr }} + - run: echo ${{ steps.list.outputs.exitcode }} ``` -There is a full example [here](./examples/basic.yml) + +[asdf]: https://asdf-vm.com/ +[`terramate-io/terramate-action`]: https://github.com/terramate-io/terramate-action +[Terramate CLI]: https://terramate.io/cli/docs +[Terramate Cloud]: https://terramate.io diff --git a/action.yml b/action.yml index d729299..daf916f 100644 --- a/action.yml +++ b/action.yml @@ -2,28 +2,38 @@ name: 'Terramate' description: 'Terramate' inputs: version: - description: 'Terramate version' + description: | + The Terramate Version to install. + If not set and an asdf config is available in .tool-versions at the root of the repository is available, the configured version is used by default. + If not set and no asdf config is found, the latest version will be installed. required: false + bindir: + description: The destination directory of the installed terramate executable. + required: false + default: /usr/local/bin use_wrapper: - description: 'Use wrapper' + description: | + The default wrapper script installation can be skipped by setting the use_wrapper variable to 'false'. required: false default: "true" cloud_organization: - description: 'Terramate Cloud organization to use' + description: Terramate Cloud organization to use by default when using Terramate Cloud features. required: false + runs: using: "composite" + steps: - - name: Adding action_path to GITHUB_PATH - run: echo "${{ github.action_path }}" >> $GITHUB_PATH - shell: bash + - name: Installing Terramate - run: install.sh shell: bash + run: ${{ github.action_path }}/install.sh env: - TM_VERSION: ${{inputs.version}} - USE_WRAPPER: ${{inputs.use_wrapper}} + TMA_INPUT_VERSION: ${{inputs.version}} + TMA_INPUT_USE_WRAPPER: ${{inputs.use_wrapper}} + TMA_INPUT_BINDIR: ${{inputs.bindir}} + - name: Configuring Terramate execution environment - run: echo "TM_CLOUD_ORGANIZATION=${{inputs.cloud_organization}}" >> $GITHUB_ENV if: ${{ inputs.cloud_organization != '' }} shell: bash + run: echo "TM_CLOUD_ORGANIZATION=${{inputs.cloud_organization}}" >> $GITHUB_ENV diff --git a/examples/basic.yml b/examples/basic.yml deleted file mode 100644 index fed2ec5..0000000 --- a/examples/basic.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Basic usage that publishes the output to a PR -name: 'Terramate GitHub Actions' -on: - pull_request: -jobs: - PlanAndPreview: - name: 'Terramate Plan and Preview' - runs-on: ubuntu-latest - permissions: - id-token: write # can't be 'read' for some reason - contents: write - pull-requests: write - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 # (essential for change detection to work correctly) - - uses: hashicorp/setup-terraform@v2 - with: - terraform_wrapper: false - - name: Install terramate - uses: terramate-io/terramate-action@main - - id: init - run: terramate run -- terraform init -no-color - - id: plan - uses: terramate-io/terramate-action@main - run: terramate run -- terraform plan -no-color - - name: Publish Plans for Changed Stacks - uses: marocchino/sticky-pull-request-comment@v2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - message: | - # STDOUT - ``` - ${{ steps.plan.outputs.stdout }} - ``` - # STDERR - ``` - ${{ steps.plan.outputs.stderr }} - ``` diff --git a/install.sh b/install.sh index 48e0943..96a143b 100755 --- a/install.sh +++ b/install.sh @@ -2,41 +2,134 @@ set -eu -fatal(){ - echo "$*" - exit 1 +## GHA inputs + +input_use_wrapper=${TMA_INPUT_USE_WRAPPER-true} +input_tm_version=${TMA_INPUT_VERSION-} +input_bindir=${TMA_INPUT_BINDIR-/usr/local/bin} + +## GHA context + +context_github_action_path=${GITHUB_ACTION_PATH-} + +## functions + +get_latest_version() { + echo >&2 "latest version: Getting latest Terramate release information from GitHub Releases" + + latest_url="https://api.github.com/repos/terramate-io/terramate/releases/latest" + latest_json=$(curl -s "${latest_url}") + tag_version=$(jq -r .tag_name <<<"${latest_json}") + + if [ -z "${tag_version}" ] || [ "${tag_version}" == "null" ] ; then + echo >&2 "latest version: Error extracting version from latest Terramate release on GitHub!" + exit 1 + fi + + echo >&2 "latest version: Using latest version ${tag_version#v}!" + + echo "${tag_version#v}" } -install(){ - if [[ -z $TM_VERSION ]]; then - asdf_version=$(awk '$1 == "terramate" {print $2}' .tool-versions 2>/dev/null||true) - if [[ -z $asdf_version ]]; then - curl -s https://api.github.com/repos/terramate-io/terramate/releases/latest > /tmp/gh_output || fatal "couldn't get Terramate release data from github" - TM_VERSION=$(jq -r .name /tmp/gh_output|sed 's/^v//') - else - TM_VERSION=$asdf_version - fi +get_asdf_version() { + if [ "${input_tm_version}" == "latest" ] ; then + return + fi + + asdf_config_file=".tool-versions" + if [ ! -r "${asdf_config_file}" ] ; then + echo >&2 "asdf integration: Skipping. No config file found at ${asdf_config_file}!" + return + fi + + echo >&2 "asdf integration: Getting desired Terramate Version from asdf config at ${asdf_config_file}!" + + asdf_version=$(awk '$1 == "terramate" {print $2}' "${asdf_config_file}") + + if [ -z "${asdf_version}" ] ; then + echo >&2 "asdf integration: Skipping. No Terramate config found in asdf file ${asdf_config_file}!" + else + echo >&2 "asdf integration: Using asdf version ${asdf_version}!" + echo "${asdf_version}" + fi +} + +get_version() { + if [ "${input_tm_version}" == "latest" ] ; then + get_latest_version + return fi - echo "# Installing Terramate v$TM_VERSION" - dl_url="https://github.com/terramate-io/terramate/releases/download/v${TM_VERSION}/terramate_${TM_VERSION}_linux_x86_64.tar.gz" - status_code=$(curl -LsI -o /dev/null -w "%{http_code}" "$dl_url") - [[ $status_code != 200 ]] && fatal "Download URL ($dl_url) returns $status_code" - curl -sLO "$dl_url" - tar xzf "terramate_${TM_VERSION}_linux_x86_64.tar.gz" -C /tmp || fatal "Couldn't unpack tarball" - mv /tmp/terramate /usr/local/bin/terramate-bin - rm "terramate_${TM_VERSION}_linux_x86_64.tar.gz" + if [ -n "${input_tm_version}" ] ; then + echo >&2 "input: Using user provided version ${input_tm_version}!" + echo "${input_tm_version}" + return + fi + + asdf_version=$(get_asdf_version) + if [ -n "${asdf_version}" ] ; then + echo "${asdf_version}" + return + fi + + get_latest_version +} + +## main install function + +install() { + destdir="${input_bindir}" + + version=$(get_version) + echo >&2 "install: Downloading Terramate v${version}" + + tmpdir=$(mktemp -d) + echo >&2 "install: Created tmp directory at ${tmpdir}" + + url="https://github.com/terramate-io/terramate/releases/download/v${version}/terramate_${version}_linux_x86_64.tar.gz" + + status=$(curl -w "%{http_code}" -o "${tmpdir}/terramate.tar.gz" -L "${url}") + if [ "${status}" != "200" ] ; then + echo >&2 "install: Error downloading release. Expected HTTP status 200 and got ${status}." + exit 1 + fi + + echo >&2 "install: extracting terramate binary into ${tmpdir}" + tar xzf "${tmpdir}/terramate.tar.gz" -C "${tmpdir}" terramate + + echo >&2 "install: installing terramate into ${destdir}" + cp "${tmpdir}/terramate" "${destdir}/terramate-bin" + + if [ "${input_use_wrapper}" != "false" ] && [ -n "${context_github_action_path}" ] ; then + echo >&2 "install: installing terramate-wrapper into ${destdir}" + cp "${context_github_action_path}/terramate-wrapper.sh" "${destdir}/" + ln -sf terramate-wrapper.sh "${destdir}/terramate" + else + ln -sf terramate-bin "${destdir}/terramate" + fi - cp "$GITHUB_ACTION_PATH/terramate-wrapper.sh" /usr/local/bin/terramate-wrapper.sh - if [[ $USE_WRAPPER == "false" ]]; then - ln /usr/local/bin/terramate-bin /usr/local/bin/terramate + terramate_version=$("${destdir}/terramate" --version) + if [ "${terramate_version}" != "${version}" ] ; then + echo >&2 "install: ERROR: terramate at ${destdir}/terramate is reporting a wrong version ${terramate_version}. Expected version is ${version}!" + exit 1 else - ln /usr/local/bin/terramate-wrapper.sh /usr/local/bin/terramate + echo >&2 "install: Version of '${destdir}/terramate' is '${version}'." fi - [[ $TM_VERSION != $(terramate --version) ]] && fatal "Installed Terramate version is not v$TM_VERSION" - echo $? + default_terrmate_location=$(type -f -p terramate) + if [ "${default_terrmate_location}" != "${destdir}/terramate" ] ; then + echo >&2 "install: WARNING: Installation succeeded in ${destdir}/terramate but we are using ${default_terrmate_location} by default!" + + default_terramate_version=$(terramate --version) + if [ "${default_terramate_version}" != "${version}" ] ; then + echo >&2 "install: WARNING: terramate at ${default_terrmate_location} is using a different version ${default_terramate_version}. Expected version is '${version}'!" + else + echo >&2 "install: Version of (first in path) 'terramate' is '${version}'." + fi + exit 0 + fi } +## main script + install -exit 0 diff --git a/terramate-wrapper.sh b/terramate-wrapper.sh index 7fa27a5..39a32d0 100755 --- a/terramate-wrapper.sh +++ b/terramate-wrapper.sh @@ -1,12 +1,22 @@ #!/bin/bash -terramate-bin "$@" > >(tee /tmp/stdout) 2> >(tee /tmp/stderr) + +tmpdir=$(mktemp -d) + +stdout="${tmpdir}/stdout.txt" +stderr="${tmpdir}/stderr.txt" + +terramate-bin "$@" > >(tee "${stdout}") 2> >(tee "${stderr}") + exitcode=$? -echo "exitcode=$exitcode" >> "$GITHUB_OUTPUT" + +echo "exitcode=${exitcode}" >> "$GITHUB_OUTPUT" + echo 'stdout<> "$GITHUB_OUTPUT" -cat /tmp/stdout >> "$GITHUB_OUTPUT" +cat "${stdout}" >> "$GITHUB_OUTPUT" echo EOF >> "$GITHUB_OUTPUT" + echo 'stderr<> "$GITHUB_OUTPUT" -cat /tmp/stderr >> "$GITHUB_OUTPUT" +cat "${stderr}" >> "$GITHUB_OUTPUT" echo EOF >> "$GITHUB_OUTPUT" -exit $exitcode +exit ${exitcode}