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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ go/fmt:
@gofmt -s -w .

# Check formatting
.PHONY: fmt-check
fmt-check:
.PHONY: go/fmt-check
go/fmt-check:
@test -z "$$(gofmt -s -l . | tee /dev/stderr)" || (echo "Code is not formatted. Run 'make go/fmt'" && exit 1)

# Release targets
Expand Down Expand Up @@ -120,7 +120,11 @@ logotools:
}

.PHONY: check
check: fmt-check go/lint go/test
check: go/fmt-check go/lint go/test

.PHONY: e2e
e2e: bin
@bash e2e/test.sh

# Help (make usage)
.PHONY: help
Expand All @@ -143,12 +147,13 @@ help:
@echo " go/test Run all tests"
@echo " go/lint Run golangci-lint"
@echo " go/fmt Format go code"
@echo " fmt-check Check go code formatting"
@echo " go/fmt-check Check go code formatting"
@echo " e2e Run the HCP Terraform end-to-end test"
@echo ""
@echo "Release:"
@echo " gen/openapi Update embedded OpenAPI spec"
@echo " prepare-release Prepare next semantic version release,"
@echo " requires VERSION argument"
@echo " cleanup-release Clean up after a release"
@echo " requires DEV_VERSION argument"
@echo ""
@echo ""
10 changes: 10 additions & 0 deletions e2e/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright IBM Corp. 2026
# SPDX-License-Identifier: MPL-2.0

terraform {
required_version = ">= 1.4.0"
}

resource "terraform_data" "e2e" {
input = "tfctl-e2e"
}
206 changes: 206 additions & 0 deletions e2e/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# Copyright IBM Corp. 2026
# SPDX-License-Identifier: MPL-2.0


set -euo pipefail

# End-to-end test for tfctl
#
# Runs dist/tfctl through some basic test cases. Presumes the default profile
# is already configured. 'setup' creates $organization and all cases should
# use it.
#
# System prerequisites are:
# tar
#. curl

root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tfctl_bin="${TFCTL_BIN:-$root_dir/dist/tfctl}"
run_id="${GITHUB_RUN_ID:-$(date +%s)}-${GITHUB_RUN_ATTEMPT:-$RANDOM}"
organization="tfctl-e2e-${run_id}"
organization_created=false

teardown() {
local status=$?

if [ "$organization_created" = true ]; then
"$tfctl_bin" harness exec --allow-delete=organizations -- \
"$tfctl_bin" api "/organizations/$organization" -X DELETE || status=1
fi

exit "$status"
}
trap teardown EXIT

setup() {
if [ ! -x "$tfctl_bin" ]; then
printf 'tfctl binary not found at %s\n' "$tfctl_bin" >&2
exit 1
fi

for command in curl tar; do
if ! command -v "$command" >/dev/null 2>&1; then
printf '%s is required to run the end-to-end test\n' "$command" >&2
exit 1
fi
done

printf 'Creating organization %s\n' "$organization"
"$tfctl_bin" api "/organizations" -X POST -a "name=$organization" -a "email=tfctl-e2e@example.com" --quiet
organization_created=true
}

run_case() {
local name=$1
printf '\n=== %s ===\n' "$name"
"$name"
}

assert_contains() {
local value=$1
local expected=$2

case "$value" in
*"$expected"*) ;;
*)
printf 'expected output to contain %q:\n%s\n' "$expected" "$value" >&2
return 1
;;
esac
}

assert_not_contains() {
local value=$1
local unexpected=$2

case "$value" in
*"$unexpected"*)
printf 'expected output not to contain %q:\n%s\n' "$unexpected" "$value" >&2
return 1
;;
*) ;;
esac
}

create_auto_apply_workspace() {
local workspace="tfctl-e2e-$RANDOM"

"$tfctl_bin" create workspace --organization "$organization" --jq '.data.id' \
-a "name=$workspace" -a auto-apply=true
}

upload_configuration() {
local workspace_id=$1
local archive=$2
local configuration_id configuration_status upload_url

upload_url="$("$tfctl_bin" api "/workspaces/$workspace_id/configuration-versions" --no-redact --jq '.data.attributes["upload-url"]' -i \
'{"data":{"type":"configuration-versions","attributes":{"auto-queue-runs":false}}}')"

printf 'Uploading configuration\n'
curl --fail --silent --show-error --request PUT --upload-file "$archive" "$upload_url"

configuration_id="$("$tfctl_bin" api "/workspaces/$workspace_id" --jq \
'.data.relationships["current-configuration-version"].data.id')"

for _ in $(seq 1 60); do
configuration_status="$("$tfctl_bin" api "/configuration-versions/$configuration_id" --jq '.data.attributes.status')"
if [ "$configuration_status" = "uploaded" ]; then
return
fi
if [ "$configuration_status" = "errored" ]; then
printf 'current configuration version %s failed to upload\n' "$configuration_id" >&2
return 1
fi
sleep 2
done

printf 'current configuration version %s did not finish uploading\n' "$configuration_id" >&2
return 1
}

case_get_formats() {
local workspace workspace_id output
workspace="tfctl-e2e-formats-$RANDOM"
workspace_id="$("$tfctl_bin" create workspace --organization "$organization" --jq '.data.id' -a "name=$workspace")"

output="$("$tfctl_bin" get workspaces --organization "$organization")"
assert_contains "$output" "ID"
output="$("$tfctl_bin" get workspaces --organization "$organization" --json)"
assert_contains "$output" "\"$workspace\""
output="$("$tfctl_bin" get workspaces --organization "$organization" --markdown)"
assert_contains "$output" "$workspace"

output="$("$tfctl_bin" get workspace "$workspace_id")"
assert_contains "$output" "$workspace"
output="$("$tfctl_bin" get workspace "$workspace_id" --json)"
assert_contains "$output" "\"id\": \"$workspace_id\""
output="$("$tfctl_bin" get workspace "$workspace_id" --markdown)"
assert_contains "$output" "$workspace"
}

case_dry_run_is_no_op() {
local workspace output
workspace="tfctl-e2e-dry-run-$RANDOM"

output="$("$tfctl_bin" create workspace --organization "$organization" -a "name=$workspace" --dry-run 2>&1)"
assert_contains "$output" "would send POST request"

output="$("$tfctl_bin" get workspaces --organization "$organization" --json)"
assert_not_contains "$output" "$workspace"
}

case_quiet_minimizes_output() (
local workspace stdout stderr
workspace="tfctl-e2e-quiet-$RANDOM"
stdout="$(mktemp)"
stderr="$(mktemp)"
trap 'rm -f "$stdout" "$stderr"' EXIT

"$tfctl_bin" create workspace --organization "$organization" -a "name=$workspace" --quiet >"$stdout" 2>"$stderr"
[ ! -s "$stdout" ]
[ ! -s "$stderr" ]
)

case_harness_install() (
local temp_dir skill_path
temp_dir="$(mktemp -d)"
skill_path="$temp_dir/.agents/skills/tfctl/SKILL.md"
trap 'rm -rf "$temp_dir"' EXIT

(
cd "$temp_dir"
"$tfctl_bin" harness install opencode
)
[ -s "$skill_path" ]
)

case_profile_display() {
local output
output="$("$tfctl_bin" profile display --json)"
assert_contains "$output" "\"Name\":"
assert_not_contains "$output" "token"
}

case_create_and_apply_workspace() (
local archive workspace_id
archive="$(mktemp)"
trap 'rm -f "$archive"' EXIT

printf 'Creating auto-apply workspace\n'
workspace_id="$(create_auto_apply_workspace)"
tar -C "$root_dir/e2e" -czf "$archive" main.tf
upload_configuration "$workspace_id" "$archive"

printf 'Starting and waiting for the auto-apply run\n'
"$tfctl_bin" run start "$workspace_id" --wait --timeout 20m
)

setup
run_case case_get_formats
run_case case_dry_run_is_no_op
run_case case_quiet_minimizes_output
run_case case_harness_install
run_case case_profile_display
run_case case_create_and_apply_workspace
8 changes: 6 additions & 2 deletions internal/commands/versioncmd/versioncmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ func runDetectOutdatedVersion(_ context.Context, io iostreams.IOStreams) {
fmt.Fprintf(io.ErrUnessential(), "A new version of %s is available: %s\n", version.Name, cs.String(fmt.Sprintf("v%s", versionInfo.Latest)).Color(cs.Purple()).Bold())
fmt.Fprintln(io.ErrUnessential())
} else {
fmt.Fprintln(io.ErrUnessential(), heredoc.New(io).Mustf(`Release notes for this version are available at
{{ template "mdCodeOrBold" "https://github.com/hashicorp/tfctl-cli/blob/%s/CHANGELOG.md" }}`, version.Version))
if version.IsDev() {
fmt.Fprintln(io.ErrUnessential(), heredoc.New(io).Mustf(`This is a development version of %s, not an official release.`, version.Name))
} else {
fmt.Fprintln(io.ErrUnessential(), heredoc.New(io).Mustf(`Release notes for this version are available at
{{ template "mdCodeOrBold" "https://github.com/hashicorp/tfctl-cli/blob/%s/CHANGELOG.md" }}`, version.Version))
}
fmt.Fprintln(io.ErrUnessential())
}

Expand Down