From e3faba17390aab29bc9a75d7d477f4e3f9d1af5d Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Thu, 31 Jul 2025 15:34:56 +0100 Subject: [PATCH 1/3] Add json_output_path --- .github/workflows/test-output.yaml | 54 ++++++++++++++++++++++++++++ docs-gen/action.py | 1 + docs-gen/actions/apply.py | 2 ++ docs-gen/actions/output.py | 2 ++ docs-gen/actions/remote_state.py | 2 ++ docs-gen/outputs/json_output_path.py | 28 +++++++++++++++ image/actions.sh | 6 +++- image/tools/convert_output.py | 12 +++++++ terraform-apply/README.md | 25 +++++++++++++ terraform-apply/action.yaml | 22 ++++++++++++ terraform-output/README.md | 25 +++++++++++++ terraform-output/action.yaml | 24 +++++++++++++ terraform-remote-state/README.md | 25 +++++++++++++ terraform-remote-state/action.yaml | 24 +++++++++++++ tofu-apply/README.md | 25 +++++++++++++ tofu-apply/action.yaml | 22 ++++++++++++ tofu-output/README.md | 25 +++++++++++++ tofu-output/action.yaml | 24 +++++++++++++ tofu-remote-state/README.md | 25 +++++++++++++ tofu-remote-state/action.yaml | 24 +++++++++++++ 20 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 docs-gen/outputs/json_output_path.py diff --git a/.github/workflows/test-output.yaml b/.github/workflows/test-output.yaml index 2b1074e8..05fc79f8 100644 --- a/.github/workflows/test-output.yaml +++ b/.github/workflows/test-output.yaml @@ -41,6 +41,7 @@ jobs: MY_OBJECT_FIRST: ${{ fromJson(steps.terraform-output.outputs.my_object).first }} MY_TUPLE: ${{ join(fromJson(steps.terraform-output.outputs.my_tuple)) }} MY_SET: ${{ contains(fromJson(steps.terraform-output.outputs.my_set), 'one') }} + JSON_OUTPUT_PATH: ${{ steps.terraform-output.outputs.json_output_path }} run: | if [[ "$MY_NUMBER" != "5" ]]; then echo "::error:: output my_number not set correctly" @@ -114,3 +115,56 @@ jobs: echo "::error:: steps.terraform-output.outputs.my_multiline_string not set correctly" exit 1 fi + + ## Check if the JSON output file exists and validate its contents + + cat "$JSON_OUTPUT_PATH" + + if [[ ! -f "$JSON_OUTPUT_PATH" ]]; then + echo "::error:: JSON output file not found at $JSON_OUTPUT_PATH" + exit 1 + fi + + # Parse JSON and validate primitive types + JSON_MY_NUMBER=$(jq -r '.my_number' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_NUMBER" != "5" ]]; then + echo "::error:: JSON my_number should be 5, got: $JSON_MY_NUMBER" + exit 1 + fi + + JSON_MY_STRING=$(jq -r '.my_string' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_STRING" != "hello" ]]; then + echo "::error:: JSON my_string should be 'hello', got: $JSON_MY_STRING" + exit 1 + fi + + JSON_MY_BOOL=$(jq -r '.my_bool' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_BOOL" != "true" ]]; then + echo "::error:: JSON my_bool should be true, got: $JSON_MY_BOOL" + exit 1 + fi + + # Validate sensitive values are included in JSON + JSON_MY_SENSITIVE_NUMBER=$(jq -r '.my_sensitive_number' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_SENSITIVE_NUMBER" != "6" ]]; then + echo "::error:: JSON my_sensitive_number should be 6, got: $JSON_MY_SENSITIVE_NUMBER" + exit 1 + fi + + # List from tolist(toset()) may have different order, so check elements exist + JSON_MY_LIST_HAS_ONE=$(jq -r '.my_list | contains(["one"])' "$JSON_OUTPUT_PATH") + JSON_MY_LIST_HAS_TWO=$(jq -r '.my_list | contains(["two"])' "$JSON_OUTPUT_PATH") + JSON_MY_LIST_LENGTH=$(jq -r '.my_list | length' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_LIST_HAS_ONE" != "true" || "$JSON_MY_LIST_HAS_TWO" != "true" || "$JSON_MY_LIST_LENGTH" != "2" ]]; then + echo "::error:: JSON my_list should contain 'one' and 'two' with length 2" + exit 1 + fi + + # Validate map/object becomes JSON object + JSON_MY_MAP_FIRST=$(jq -r '.my_map.first' "$JSON_OUTPUT_PATH") + JSON_MY_MAP_SECOND=$(jq -r '.my_map.second' "$JSON_OUTPUT_PATH") + JSON_MY_MAP_THIRD=$(jq -r '.my_map.third' "$JSON_OUTPUT_PATH") + if [[ "$JSON_MY_MAP_FIRST" != "one" || "$JSON_MY_MAP_SECOND" != "two" || "$JSON_MY_MAP_THIRD" != "3" ]]; then + echo "::error:: JSON my_map should have correct key-value pairs" + exit 1 + fi diff --git a/docs-gen/action.py b/docs-gen/action.py index 5191ddcc..40c14706 100644 --- a/docs-gen/action.py +++ b/docs-gen/action.py @@ -195,6 +195,7 @@ def assert_ordering(self): "terraform", "tofu", "Provider Versions", + "json_output_path", "$ProductName Outputs", ], self.outputs) diff --git a/docs-gen/actions/apply.py b/docs-gen/actions/apply.py index 5f522911..d6739157 100644 --- a/docs-gen/actions/apply.py +++ b/docs-gen/actions/apply.py @@ -25,6 +25,7 @@ from inputs.var import var from inputs.workspace import workspace from outputs.failure_reason import failure_reason +from outputs.json_output_path import json_output_path from outputs.json_plan_path import json_plan_path from outputs.lock_info import lock_info from outputs.run_id import run_id @@ -112,6 +113,7 @@ ), lock_info, run_id, + json_output_path, terraform_outputs ], environment_variables=[ diff --git a/docs-gen/actions/output.py b/docs-gen/actions/output.py index 14c39051..34c9da29 100644 --- a/docs-gen/actions/output.py +++ b/docs-gen/actions/output.py @@ -12,6 +12,7 @@ from inputs.var_file import var_file from inputs.variables import variables from inputs.workspace import workspace +from outputs.json_output_path import json_output_path from outputs.terraform_outputs import terraform_outputs output = Action( @@ -39,6 +40,7 @@ TERRAFORM_PRE_RUN ], outputs=[ + json_output_path, terraform_outputs ], extra=''' diff --git a/docs-gen/actions/remote_state.py b/docs-gen/actions/remote_state.py index afa0df4f..7b3a65c8 100644 --- a/docs-gen/actions/remote_state.py +++ b/docs-gen/actions/remote_state.py @@ -7,6 +7,7 @@ from inputs.backend_config_file import backend_config_file from inputs.backend_type import backend_type from inputs.workspace import workspace +from outputs.json_output_path import json_output_path from outputs.terraform_outputs import terraform_outputs remote_state = Action( @@ -21,6 +22,7 @@ backend_config_file, ], outputs=[ + json_output_path, terraform_outputs ], environment_variables=[ diff --git a/docs-gen/outputs/json_output_path.py b/docs-gen/outputs/json_output_path.py new file mode 100644 index 00000000..a2dad73f --- /dev/null +++ b/docs-gen/outputs/json_output_path.py @@ -0,0 +1,28 @@ +from action import Output + +json_output_path = Output( + name='json_output_path', + type='string', + description=''' +This is the path to all the root module outputs in a JSON file. +The path is relative to the Actions workspace. + +For example, with the $ProductName config: + +```hcl +output "service_hostname" { + value = "example.com" +} +``` + +The file pointed to by this output will contain: + +```json +{ + "service_hostname": "example.com" +} +``` + +$ProductName list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. +''' +) diff --git a/image/actions.sh b/image/actions.sh index 046d987c..dc9bbd9a 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -443,7 +443,11 @@ function output() { # shellcheck disable=SC2086 debug_log "$TOOL_COMMAND_NAME" output -json # shellcheck disable=SC2086 - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME output -json | tee "$STEP_TMP_DIR/terraform_output.json" | convert_output) + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME output -json | tee "$STEP_TMP_DIR/terraform_output.json" | convert_output "$STEP_TMP_DIR/outputs.json") + + mkdir -p "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR" + cp "$STEP_TMP_DIR/outputs.json" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/outputs.json" + set_output json_output_path "$WORKSPACE_TMP_DIR/outputs.json" } function random_string() { diff --git a/image/tools/convert_output.py b/image/tools/convert_output.py index bcfa67d3..5430d29d 100755 --- a/image/tools/convert_output.py +++ b/image/tools/convert_output.py @@ -3,6 +3,7 @@ import json import sys from dataclasses import dataclass +from pathlib import Path from typing import Dict, Iterable, Union from github_actions.commands import output, mask @@ -55,6 +56,15 @@ def read_input(s: str) -> dict: jstr = '\n'.join(lines) return json.loads(jstr) +def write_json_outputs(json_output_path: Path, outputs: Dict): + """ + Flatten the terraform json output format to a simple dictionary, and write it to a file. + """ + + json_outputs = {name: o['value'] for name, o in outputs.items()} + + with open(json_output_path, 'w') as f: + json.dump(json_outputs, f, indent=2, sort_keys=True) if __name__ == '__main__': @@ -67,6 +77,8 @@ def read_input(s: str) -> dict: sys.stderr.write(input_string) raise + write_json_outputs(Path(sys.argv[1]), outputs) + for command in convert_to_github(outputs): if isinstance(command, Output): output(command.name, command.value) diff --git a/terraform-apply/README.md b/terraform-apply/README.md index 72ed44b8..53f2d948 100644 --- a/terraform-apply/README.md +++ b/terraform-apply/README.md @@ -269,6 +269,31 @@ These input values must be the same as any [`dflook/terraform-plan`](https://git - Type: string +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * Terraform Outputs An action output will be created for each output of the Terraform configuration. diff --git a/terraform-apply/action.yaml b/terraform-apply/action.yaml index 81b73c9e..5c71fb1f 100644 --- a/terraform-apply/action.yaml +++ b/terraform-apply/action.yaml @@ -140,6 +140,28 @@ outputs: ``` run_id: description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. runs: using: docker diff --git a/terraform-output/README.md b/terraform-output/README.md index 33b1ae0f..f2856a3e 100644 --- a/terraform-output/README.md +++ b/terraform-output/README.md @@ -49,6 +49,31 @@ Retrieve the root-level outputs from a Terraform configuration. ## Outputs +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * Terraform Outputs An action output will be created for each output of the Terraform configuration. diff --git a/terraform-output/action.yaml b/terraform-output/action.yaml index bf5e3c1b..d2812a6f 100644 --- a/terraform-output/action.yaml +++ b/terraform-output/action.yaml @@ -22,6 +22,30 @@ inputs: required: false default: "" +outputs: + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + runs: using: docker image: ../image/Dockerfile diff --git a/terraform-remote-state/README.md b/terraform-remote-state/README.md index 9e2aea8a..949fdbcf 100644 --- a/terraform-remote-state/README.md +++ b/terraform-remote-state/README.md @@ -48,6 +48,31 @@ Retrieves the root-level outputs from a Terraform remote state. ## Outputs +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * Terraform Outputs An action output will be created for each output of the Terraform configuration. diff --git a/terraform-remote-state/action.yaml b/terraform-remote-state/action.yaml index f3204a46..7332aede 100644 --- a/terraform-remote-state/action.yaml +++ b/terraform-remote-state/action.yaml @@ -21,6 +21,30 @@ inputs: required: false default: "" +outputs: + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the Terraform config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + Terraform list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + runs: using: docker image: ../image/Dockerfile diff --git a/tofu-apply/README.md b/tofu-apply/README.md index d6a601c8..6b8b4645 100644 --- a/tofu-apply/README.md +++ b/tofu-apply/README.md @@ -286,6 +286,31 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c - Type: string +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * OpenTofu Outputs An action output will be created for each output of the OpenTofu configuration. diff --git a/tofu-apply/action.yaml b/tofu-apply/action.yaml index e1fd3e51..77468d06 100644 --- a/tofu-apply/action.yaml +++ b/tofu-apply/action.yaml @@ -148,6 +148,28 @@ outputs: ``` run_id: description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. runs: env: diff --git a/tofu-output/README.md b/tofu-output/README.md index be782764..ee7c7a89 100644 --- a/tofu-output/README.md +++ b/tofu-output/README.md @@ -83,6 +83,31 @@ Retrieve the root-level outputs from an OpenTofu configuration. ## Outputs +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * OpenTofu Outputs An action output will be created for each output of the OpenTofu configuration. diff --git a/tofu-output/action.yaml b/tofu-output/action.yaml index 05f87420..c95a8125 100644 --- a/tofu-output/action.yaml +++ b/tofu-output/action.yaml @@ -33,6 +33,30 @@ inputs: required: false default: "" +outputs: + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + runs: env: OPENTOFU: true diff --git a/tofu-remote-state/README.md b/tofu-remote-state/README.md index cd890d1a..c96ac454 100644 --- a/tofu-remote-state/README.md +++ b/tofu-remote-state/README.md @@ -48,6 +48,31 @@ Retrieves the root-level outputs from an OpenTofu remote state. ## Outputs +* `json_output_path` + + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + + - Type: string + * OpenTofu Outputs An action output will be created for each output of the OpenTofu configuration. diff --git a/tofu-remote-state/action.yaml b/tofu-remote-state/action.yaml index d334f301..1d9a969f 100644 --- a/tofu-remote-state/action.yaml +++ b/tofu-remote-state/action.yaml @@ -21,6 +21,30 @@ inputs: required: false default: "" +outputs: + json_output_path: + description: | + This is the path to all the root module outputs in a JSON file. + The path is relative to the Actions workspace. + + For example, with the OpenTofu config: + + ```hcl + output "service_hostname" { + value = "example.com" + } + ``` + + The file pointed to by this output will contain: + + ```json + { + "service_hostname": "example.com" + } + ``` + + OpenTofu list, set and tuple types are cast to a JSON array, map and object types are cast to a JSON object. + runs: env: OPENTOFU: true From aecb432445b509334ab57775a7b8940d1d7c60f9 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Thu, 31 Jul 2025 20:33:36 +0100 Subject: [PATCH 2/3] :bookmark: v2.2.0 --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d09180..fa58bb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,23 @@ The actions are versioned as a suite. Some actions may have no change in behavio When using an action you can specify the version as: -- `@v2.1.0` to use an exact release -- `@v2.1` to use the latest patch release for the specific minor version +- `@v2.2.0` to use an exact release +- `@v2.2` to use the latest patch release for the specific minor version - `@v2` to use the latest patch release for the specific major version +## [2.2.0] - 2025-07-31 + +### Added +- New `json_output_path` output for [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply), + [dflook/terraform-output](https://github.com/dflook/terraform-github-actions/tree/main/terraform-output), + [dflook/terraform-remote-state](https://github.com/dflook/terraform-github-actions/tree/main/terraform-remote-state), + [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/main/tofu-apply), + [dflook/tofu-output](https://github.com/dflook/terraform-github-actions/tree/main/tofu-output), and + [dflook/tofu-remote-state](https://github.com/dflook/terraform-github-actions/tree/main/tofu-remote-state) actions. + + The `json_output_path` output points to a JSON file containing all root module outputs in a simple key-value format. + This addresses GitHub Actions' output size limitations and enables easier access to all outputs as a single object. + ## [2.1.0] - 2025-06-16 ### Added @@ -772,6 +785,7 @@ First release of the GitHub Actions: - [dflook/terraform-new-workspace](terraform-new-workspace) - [dflook/terraform-destroy-workspace](terraform-destroy-workspace) +[2.2.0]: https://github.com/dflook/terraform-github-actions/compare/v2.1.0...v2.2.0 [2.1.0]: https://github.com/dflook/terraform-github-actions/compare/v2.0.1...v2.1.0 [2.0.1]: https://github.com/dflook/terraform-github-actions/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/dflook/terraform-github-actions/compare/v1.49.0...v2.0.0 From 4d97209b764579dfb9a843850f49c7c0f44c2fe4 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 1 Aug 2025 11:47:02 +0100 Subject: [PATCH 3/3] :bookmark: v2.2.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa58bb7a..d104c6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ When using an action you can specify the version as: - `@v2.2` to use the latest patch release for the specific minor version - `@v2` to use the latest patch release for the specific major version -## [2.2.0] - 2025-07-31 +## [2.2.0] - 2025-08-01 ### Added - New `json_output_path` output for [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply),