Skip to content
Open
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ All supported CLI argument inputs are [listed below](#arguments) with accompanyi
| Security | `preserve-plan` | Preserve plan file "tfplan" in the given working directory after workflow execution.</br>Default: `false` |
| Security | `upload-plan` | Upload plan file as GitHub workflow artifact.</br>Default: `true` |
| Security | `retention-days` | Duration after which plan file artifact will expire in days.</br>Example: `90` |
| Artifact | `build-artifacts` | Paths to files/directories to upload with plan and restore during apply.<sup>7</sup></br>Example: `build/bundle.zip` |
| Security | `token` | Specify a GitHub token.</br>Default: `${{ github.token }}` |
| UI | `expand-diff` | Expand the collapsible diff section.</br>Default: `false` |
| UI | `expand-summary` | Expand the collapsible summary section.</br>Default: `false` |
Expand All @@ -199,7 +200,14 @@ All supported CLI argument inputs are [listed below](#arguments) with accompanyi
1. The `on-diff` option is true when the exit code of the last TF command is non-zero (ensure `terraform_wrapper`/`tofu_wrapper` is set to `false`).</br></br>
1. The default behavior of `comment-method` is to update the existing PR comment with the latest plan/apply output, making it easy to track changes over time through the comment's revision history.</br></br>
[![PR comment revision history comparing plan and apply outputs.](/.github/assets/revisions.png)](https://raw.githubusercontent.com/op5dev/tf-via-pr/refs/heads/main/.github/assets/revisions.png "View full-size image.")</br></br>
1. It can be desirable to hide certain arguments from the last run command input to prevent exposure in the PR comment (e.g., sensitive `arg-var` values). Conversely, it can be desirable to show other arguments even if they are not in last run command input (e.g., `arg-workspace` or `arg-backend-config` selection).
1. It can be desirable to hide certain arguments from the last run command input to prevent exposure in the PR comment (e.g., sensitive `arg-var` values). Conversely, it can be desirable to show other arguments even if they are not in last run command input (e.g., `arg-workspace` or `arg-backend-config` selection).</br></br>
1. The `build-artifacts` input accepts newline or comma-separated paths to files or directories that should be uploaded alongside the plan file and restored during apply. This is useful when using `data.archive_file` or other resources that generate interim build files needed during apply (e.g., zip archives for Lambda deployments).</br>
Example usage:
```yaml
build-artifacts: |
build/bundle.zip
dist/
```

</br>

Expand Down Expand Up @@ -322,7 +330,6 @@ View [all notable changes](https://github.com/op5dev/tf-via-pr/releases "Release

- Handling of inputs which contain space(s) (e.g., `working-directory: path to/directory`).
- Handling of comma-separated inputs which contain comma(s) (e.g., `arg-var: token=1,2,3`); workaround with `TF_CLI_ARGS` [environment variable](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_cli_args-and-tf_cli_args_name).
- Handling of interim build artifact(s) between `plan` and `apply` commands (e.g., zip archive); workaround with `arg-auto-approve: true` so that `apply` rebuilds artifact(s) for provisioning ([join discussion](https://github.com/OP5dev/TF-via-PR/issues/517)).

</br>

Expand Down
143 changes: 139 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@
if [[ -z "$artifact_id" ]]; then echo "Unable to locate plan file: ${{ steps.identifier.outputs.name }}." && exit 1; fi
gh api /repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip --header "$GH_API" --method GET > "${{ steps.identifier.outputs.name }}.zip"

# Unzip the plan file to the working directory, then clean up the zip file.
unzip "${{ steps.identifier.outputs.name }}.zip" -d "$INPUTS_ARG_CHDIR"
# Unzip the plan file to the working directory (or current directory if not specified).
unzip "${{ steps.identifier.outputs.name }}.zip" -d "${INPUTS_ARG_CHDIR:-.}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.identifier.outputs.name }
, which may be controlled by an external user.

Copilot Autofix

AI 4 months ago

To fix the problem, we should stop using ${{ steps.identifier.outputs.name }} directly within the bash run: script and instead pass it through an environment variable, then reference that variable using shell syntax. This avoids GitHub expression interpolation at command-construction time and ensures the value is subject to normal shell quoting rules. We should also make sure all usages are double-quoted to prevent word splitting and globbing, which further mitigates injection and path manipulation.

Concretely, in the download step (lines 195–209), we will:

  • Add an environment variable, e.g. PLAN_NAME, set to ${{ steps.identifier.outputs.name }} within the env: mapping.
  • Replace all occurrences of ${{ steps.identifier.outputs.name }} inside the run: | script with "$PLAN_NAME" (or $PLAN_NAME inside a larger double-quoted string), keeping them properly quoted.
  • Keep the rest of the logic (API calls, unzip, rm) exactly the same so functionality does not change, only the way the dynamic value is passed.

All changes are confined to the shown section of action.yml; no new external dependencies are required.

Suggested changeset 1
action.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/action.yml b/action.yml
--- a/action.yml
+++ b/action.yml
@@ -196,17 +196,18 @@
       id: download
       env:
         INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
+        PLAN_NAME: ${{ steps.identifier.outputs.name }}
       shell: bash
       run: |
         # Download plan file.
         # Get the artifact ID of the latest matching plan files for download.
-        artifact_id=$(gh api /repos/${{ github.repository }}/actions/artifacts --header "$GH_API" --method GET --field "name=${{ steps.identifier.outputs.name }}" --jq '.artifacts[0].id' 2>/dev/null)
-        if [[ -z "$artifact_id" ]]; then echo "Unable to locate plan file: ${{ steps.identifier.outputs.name }}." && exit 1; fi
-        gh api /repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip --header "$GH_API" --method GET > "${{ steps.identifier.outputs.name }}.zip"
+        artifact_id=$(gh api /repos/${{ github.repository }}/actions/artifacts --header "$GH_API" --method GET --field "name=$PLAN_NAME" --jq '.artifacts[0].id' 2>/dev/null)
+        if [[ -z "$artifact_id" ]]; then echo "Unable to locate plan file: $PLAN_NAME." && exit 1; fi
+        gh api /repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip --header "$GH_API" --method GET > "$PLAN_NAME.zip"
 
         # Unzip the plan file to the working directory (or current directory if not specified).
-        unzip "${{ steps.identifier.outputs.name }}.zip" -d "${INPUTS_ARG_CHDIR:-.}"
-        rm --force "${{ steps.identifier.outputs.name }}.zip"
+        unzip "$PLAN_NAME.zip" -d "${INPUTS_ARG_CHDIR:-.}"
+        rm --force "$PLAN_NAME.zip"
 
     - if: ${{ inputs.plan-encrypt != '' && steps.download.outcome == 'success' }}
       env:
EOF
@@ -196,17 +196,18 @@
id: download
env:
INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
PLAN_NAME: ${{ steps.identifier.outputs.name }}
shell: bash
run: |
# Download plan file.
# Get the artifact ID of the latest matching plan files for download.
artifact_id=$(gh api /repos/${{ github.repository }}/actions/artifacts --header "$GH_API" --method GET --field "name=${{ steps.identifier.outputs.name }}" --jq '.artifacts[0].id' 2>/dev/null)
if [[ -z "$artifact_id" ]]; then echo "Unable to locate plan file: ${{ steps.identifier.outputs.name }}." && exit 1; fi
gh api /repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip --header "$GH_API" --method GET > "${{ steps.identifier.outputs.name }}.zip"
artifact_id=$(gh api /repos/${{ github.repository }}/actions/artifacts --header "$GH_API" --method GET --field "name=$PLAN_NAME" --jq '.artifacts[0].id' 2>/dev/null)
if [[ -z "$artifact_id" ]]; then echo "Unable to locate plan file: $PLAN_NAME." && exit 1; fi
gh api /repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip --header "$GH_API" --method GET > "$PLAN_NAME.zip"

# Unzip the plan file to the working directory (or current directory if not specified).
unzip "${{ steps.identifier.outputs.name }}.zip" -d "${INPUTS_ARG_CHDIR:-.}"
rm --force "${{ steps.identifier.outputs.name }}.zip"
unzip "$PLAN_NAME.zip" -d "${INPUTS_ARG_CHDIR:-.}"
rm --force "$PLAN_NAME.zip"

- if: ${{ inputs.plan-encrypt != '' && steps.download.outcome == 'success' }}
env:
Copilot is powered by AI and may make mistakes. Always verify output.
rm --force "${{ steps.identifier.outputs.name }}.zip"

- if: ${{ inputs.plan-encrypt != '' && steps.download.outcome == 'success' }}
Expand All @@ -220,6 +220,64 @@
openssl enc -aes-256-ctr -pbkdf2 -salt -in "$path.encrypted" -out "$path.decrypted" -pass file:"$temp_file" -d
mv --force --verbose "$path.decrypted" "$path"

- if: ${{ steps.download.outcome == 'success' }}
id: restore-build-artifacts
env:
INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
shell: bash
run: |
# Restore build artifacts from staging directory.
workdir="${INPUTS_ARG_CHDIR:-.}"
staging_dir="$workdir/.tfviapr-artifacts"
manifest_file="$staging_dir/manifest.json"

# Skip if no build artifacts were staged.
if [[ ! -f "$manifest_file" ]]; then
echo "No build artifacts manifest found, skipping restore."
exit 0
fi

echo "Restoring build artifacts from manifest: $manifest_file"

# Parse manifest and restore each artifact.
artifacts=$(jq -c '.artifacts[]' "$manifest_file" 2>/dev/null || echo "")
if [[ -z "$artifacts" ]]; then
echo "No artifacts in manifest."
rm -rf "$staging_dir"
exit 0
fi

while IFS= read -r artifact; do
rel_path=$(echo "$artifact" | jq -r '.path')
staging_name=$(echo "$artifact" | jq -r '.staging')
artifact_type=$(echo "$artifact" | jq -r '.type')

# Determine destination path.
if [[ "$rel_path" = /* ]]; then
# Absolute path.
dest_path="$rel_path"
else
# Relative path - restore relative to working directory.
dest_path="$workdir/$rel_path"
fi

Comment on lines +250 to +263
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path resolution allows absolute paths to be used as-is without validation. This could be a security concern if the build-artifacts input is not properly controlled, as it allows writing to any location on the filesystem during restore. Consider validating that absolute paths, if allowed, are within expected boundaries, or document this as a security consideration.

Suggested change
while IFS= read -r artifact; do
rel_path=$(echo "$artifact" | jq -r '.path')
staging_name=$(echo "$artifact" | jq -r '.staging')
artifact_type=$(echo "$artifact" | jq -r '.type')
# Determine destination path.
if [[ "$rel_path" = /* ]]; then
# Absolute path.
dest_path="$rel_path"
else
# Relative path - restore relative to working directory.
dest_path="$workdir/$rel_path"
fi
# Canonical base directory for restored artifacts.
base_dir=$(cd "$workdir" && pwd)
while IFS= read -r artifact; do
rel_path=$(echo "$artifact" | jq -r '.path')
staging_name=$(echo "$artifact" | jq -r '.staging')
artifact_type=$(echo "$artifact" | jq -r '.type')
# Determine and validate destination path.
if [[ "$rel_path" = /* ]]; then
# Absolute path from manifest - canonicalize.
dest_path=$(realpath "$rel_path" 2>/dev/null || echo "")
else
# Relative path - canonicalize relative to working directory.
dest_path=$(cd "$workdir" && realpath "$rel_path" 2>/dev/null || echo "")
fi
if [[ -z "$dest_path" ]]; then
echo "Invalid artifact path in manifest: $rel_path"
exit 1
fi
# Ensure destination is within the working directory.
case "$dest_path" in
"$base_dir" | "$base_dir"/*)
;;
*)
echo "Refusing to restore artifact outside working directory:"
echo " manifest path: $rel_path"
echo " resolved path: $dest_path"
exit 1
;;
esac

Copilot uses AI. Check for mistakes.
# Create parent directory if needed.
dest_dir=$(dirname "$dest_path")
mkdir -p "$dest_dir"

# Restore file or directory.
if [[ "$artifact_type" == "directory" ]]; then
cp -r "$staging_dir/$staging_name" "$dest_path"
else
cp "$staging_dir/$staging_name" "$dest_path"
Comment on lines +270 to +272
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the cp command fails during restoration (e.g., due to permission issues or disk space), the script continues without detecting the error. Consider adding error checking after the cp commands to ensure artifacts are successfully restored before proceeding.

Suggested change
cp -r "$staging_dir/$staging_name" "$dest_path"
else
cp "$staging_dir/$staging_name" "$dest_path"
if ! cp -r "$staging_dir/$staging_name" "$dest_path"; then
echo "Error: Failed to restore directory artifact: $staging_dir/$staging_name -> $dest_path" >&2
exit 1
fi
else
if ! cp "$staging_dir/$staging_name" "$dest_path"; then
echo "Error: Failed to restore file artifact: $staging_dir/$staging_name -> $dest_path" >&2
exit 1
fi

Copilot uses AI. Check for mistakes.
fi
echo "Restored build artifact: $staging_dir/$staging_name -> $dest_path"
done <<< "$artifacts"

# Clean up staging directory.
rm -rf "$staging_dir"
echo "Build artifacts restored successfully."

- if: ${{ steps.plan.outcome == 'success' || steps.download.outcome == 'success' }}
env:
INPUTS_TOOL: ${{ inputs.tool }}
Expand Down Expand Up @@ -254,12 +312,72 @@
rm --force "$path"
fi

- if: ${{ inputs.build-artifacts != '' && steps.plan.outcome == 'success' }}
id: stage-build-artifacts
env:
INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
INPUTS_BUILD_ARTIFACTS: ${{ inputs.build-artifacts }}
shell: bash
run: |
# Stage build artifacts for upload.
# Create staging directory and manifest file.
workdir="${INPUTS_ARG_CHDIR:-.}"
staging_dir="$workdir/.tfviapr-artifacts"
manifest_file="$staging_dir/manifest.json"
mkdir -p "$staging_dir"
echo '{"artifacts":[' > "$manifest_file"

# Parse build-artifacts input (newline or comma-separated).
artifacts=$(echo "$INPUTS_BUILD_ARTIFACTS" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$')
first=true

while IFS= read -r artifact_path; do
# Resolve path relative to working directory.
if [[ "$artifact_path" = /* ]]; then
# Absolute path - use as-is.
src_path="$artifact_path"
rel_path="$artifact_path"
else
# Relative path - resolve from working directory.
src_path="$workdir/$artifact_path"
rel_path="$artifact_path"
fi
Comment on lines +334 to +344
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path handling does not sanitize or validate relative paths that contain parent directory references (..). This could allow paths like "../../../etc/passwd" to be processed, potentially causing artifacts to be staged from or restored to unintended locations. Consider validating that paths do not traverse outside the working directory.

Copilot uses AI. Check for mistakes.

if [[ ! -e "$src_path" ]]; then
echo "Warning: Build artifact not found: $src_path"
continue
fi

# Generate a unique staging name to avoid conflicts.
staging_name=$(echo "$rel_path" | md5sum | cut -d' ' -f1)

# Copy file or directory to staging area.
if [[ -d "$src_path" ]]; then
cp -r "$src_path" "$staging_dir/$staging_name"
artifact_type="directory"
else
cp "$src_path" "$staging_dir/$staging_name"
artifact_type="file"
Comment on lines +356 to +360
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the cp command fails during staging (e.g., due to permission issues or disk space), the script continues without detecting the error. This could result in an incomplete manifest that references artifacts that were not actually staged. Consider adding error checking after the cp commands to ensure artifacts are successfully copied before adding them to the manifest.

Suggested change
cp -r "$src_path" "$staging_dir/$staging_name"
artifact_type="directory"
else
cp "$src_path" "$staging_dir/$staging_name"
artifact_type="file"
if cp -r "$src_path" "$staging_dir/$staging_name"; then
artifact_type="directory"
else
echo "Error: Failed to copy directory artifact: $src_path -> $staging_dir/$staging_name"
continue
fi
else
if cp "$src_path" "$staging_dir/$staging_name"; then
artifact_type="file"
else
echo "Error: Failed to copy file artifact: $src_path -> $staging_dir/$staging_name"
continue
fi

Copilot uses AI. Check for mistakes.
fi

# Add to manifest.
if [[ "$first" != "true" ]]; then echo ',' >> "$manifest_file"; fi
first=false
echo "{\"path\":\"$rel_path\",\"staging\":\"$staging_name\",\"type\":\"$artifact_type\"}" >> "$manifest_file"
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON generation does not properly escape special characters in the path values. If a path contains double quotes, backslashes, or newlines, it could corrupt the JSON manifest or cause parsing errors during restoration. Consider using jq to properly generate the JSON entries instead of using echo with manual JSON string concatenation.

Copilot uses AI. Check for mistakes.
echo "Staged build artifact: $src_path -> $staging_dir/$staging_name"
done <<< "$artifacts"

echo ']}' >> "$manifest_file"
echo "Build artifacts manifest created: $manifest_file"

- if: ${{ inputs.command == 'plan' && inputs.upload-plan == 'true' && (github.server_url == 'https://github.com' || contains(github.server_url, '.ghe.com')) }}
id: upload
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ steps.identifier.outputs.name }}
path: ${{ format('{0}{1}tfplan{2}', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '', inputs.plan-encrypt != '' && '.encrypted' || '') }}
path: |
${{ format('{0}{1}tfplan{2}', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '', inputs.plan-encrypt != '' && '.encrypted' || '') }}
${{ inputs.build-artifacts != '' && format('{0}{1}.tfviapr-artifacts', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '') || '' }}
retention-days: ${{ inputs.retention-days }}
overwrite: true

Expand All @@ -268,9 +386,22 @@
uses: actions/upload-artifact@c24449f33cd45d4826c6702db7e49f7cdb9b551d # v3.2.1-node20
with:
name: ${{ steps.identifier.outputs.name }}
path: ${{ format('{0}{1}tfplan{2}', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '', inputs.plan-encrypt != '' && '.encrypted' || '') }}
path: |
${{ format('{0}{1}tfplan{2}', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '', inputs.plan-encrypt != '' && '.encrypted' || '') }}
${{ inputs.build-artifacts != '' && format('{0}{1}.tfviapr-artifacts', inputs.arg-chdir || inputs.working-directory, (inputs.arg-chdir || inputs.working-directory) && '/' || '') || '' }}
retention-days: ${{ inputs.retention-days }}

- if: ${{ inputs.build-artifacts != '' && (steps.upload.outcome == 'success' || steps.upload-v3.outcome == 'success') }}
env:
INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
shell: bash
run: |
# Clean up build artifacts staging directory after upload.
workdir="${INPUTS_ARG_CHDIR:-.}"
staging_dir="$workdir/.tfviapr-artifacts"
rm -rf "$staging_dir"
echo "Cleaned up build artifacts staging directory."
Comment on lines +394 to +403
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the staging step fails partway through (e.g., after creating the staging directory but before successful completion), the cleanup step will not run because it's conditional on upload success. This could leave the .tfviapr-artifacts directory behind. Consider adding error handling or a cleanup on failure.

Copilot uses AI. Check for mistakes.

- if: ${{ inputs.plan-parity == 'true' && (steps.download.outcome == 'success' || inputs.plan-file != '') }}
env:
INPUTS_ARG_CHDIR: ${{ inputs.arg-chdir || inputs.working-directory }}
Expand Down Expand Up @@ -636,6 +767,10 @@
default: "true"
description: "Upload plan file as GitHub workflow artifact (e.g., `true`)."
required: false
build-artifacts:
default: ""
description: "Newline or comma-separated paths to files/directories to upload with plan and restore during apply (e.g., `build/bundle.zip`)."
required: false
validate:
default: "false"
description: "Check validation of TF code (e.g., `false`)."
Expand Down
8 changes: 8 additions & 0 deletions tests/fail_build_artifact_missing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Terraform
.terraform/
.terraform.lock.hcl
*.tfplan
tfplan

# Build artifacts (generated during plan)
build/
92 changes: 92 additions & 0 deletions tests/fail_build_artifact_missing/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Test case demonstrating build artifact issue (#517)
# This replicates the scenario where data.archive_file generates a zip
# during plan, but the zip is missing during apply because only the
# tfplan file is uploaded/downloaded between workflow runs.
#
# To test this scenario (simulating separate plan/apply jobs):
# 1. Run: terraform init
# 2. Run: terraform plan -out=tfplan
# (This creates build/bundle.zip via data.archive_file)
# 3. Simulate artifact transfer: rm -rf build/
# 4. Run: terraform apply tfplan
# (This FAILS because bundle.zip no longer exists)
#
# The new `build-artifacts` input solves this by allowing users to
# specify files/directories to upload alongside the tfplan file.
#
# Example workflow usage with build-artifacts:
# - uses: op5dev/tf-via-pr@v13
# with:
# command: plan
# build-artifacts: build/bundle.zip
# # Or for multiple files/directories:
# # build-artifacts: |
# # build/bundle.zip
# # dist/

terraform {
required_providers {
archive = {
source = "hashicorp/archive"
version = "~> 2.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}

# Archive inline content - creates zip during plan phase
data "archive_file" "bundle" {
type = "zip"
output_path = "${path.module}/build/bundle.zip"

source {
content = "exports.handler = async (event) => { return { statusCode: 200 }; };"
filename = "index.js"
}

source {
content = jsonencode({ name = "lambda-function", version = "1.0.0" })
filename = "package.json"
}
}

# Simulates deploying the archive (e.g., to S3, Lambda, etc.)
# This will fail during apply if the zip file doesn't exist
resource "null_resource" "deploy" {
triggers = {
archive_hash = data.archive_file.bundle.output_base64sha256
}

provisioner "local-exec" {
command = <<-EOT
if [ -f "${data.archive_file.bundle.output_path}" ]; then
echo "SUCCESS: Archive found at ${data.archive_file.bundle.output_path}"
echo "Archive size: $(wc -c < "${data.archive_file.bundle.output_path}") bytes"
echo "Archive hash: ${data.archive_file.bundle.output_base64sha256}"
else
echo "ERROR: Archive missing at ${data.archive_file.bundle.output_path}"
echo ""
echo "This failure occurs when:"
echo " 1. 'terraform plan' runs in one workflow job (creates the zip)"
echo " 2. Only the tfplan file is transferred to another job"
echo " 3. 'terraform apply tfplan' runs but the zip is missing"
echo ""
echo "Solution: Use 'build-artifacts' input to transfer the zip file"
exit 1
fi
EOT
}
}

output "archive_path" {
value = data.archive_file.bundle.output_path
description = "Path to the generated archive file"
}

output "archive_size" {
value = data.archive_file.bundle.output_size
description = "Size of the archive in bytes"
}
12 changes: 12 additions & 0 deletions tests/tf.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,15 @@ terraform -chdir=tests/pass_one init -no-color 2> >(tee pass_one.txt) > >(tee pa
terraform -chdir=tests/fail_format_diff fmt -check=true -diff=true -no-color 2> >(tee fail_format_diff.txt) > >(tee fail_format_diff.txt)
terraform -chdir=tests/fail_data_source_error init -no-color 2> >(tee fail_data_source_error.txt) > >(tee fail_data_source_error.txt)
terraform -chdir=tests/fail_invalid_resource_type init -no-color 2> >(tee fail_invalid_resource_type.txt) > >(tee fail_invalid_resource_type.txt)

# Test case for build artifact issue (#517)
# This demonstrates the scenario where interim build artifacts are missing during apply
echo "=== Testing build artifact scenario (issue #517) ==="
terraform -chdir=tests/fail_build_artifact_missing init -no-color
terraform -chdir=tests/fail_build_artifact_missing plan -out=tfplan -no-color
# Simulate artifact transfer (only tfplan, not the build directory)
rm -rf tests/fail_build_artifact_missing/build/
# This apply should fail because bundle.zip is missing
terraform -chdir=tests/fail_build_artifact_missing apply tfplan -no-color 2> >(tee fail_build_artifact_missing.txt) > >(tee fail_build_artifact_missing.txt) || echo "Expected failure: build artifact missing"
# Cleanup
rm -f tests/fail_build_artifact_missing/tfplan
Loading