-
-
Notifications
You must be signed in to change notification settings - Fork 38
feat: add build-artifacts input to pass along interim build artifacts between runs
#532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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:-.}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rm --force "${{ steps.identifier.outputs.name }}.zip" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - if: ${{ inputs.plan-encrypt != '' && steps.download.outcome == 'success' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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
AI
Jan 5, 2026
There was a problem hiding this comment.
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
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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
AI
Jan 5, 2026
There was a problem hiding this comment.
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
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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/ |
| 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" | ||
| } |
Check warning
Code scanning / CodeQL
Code injection Medium
Copilot Autofix
AI 4 months ago
To fix the problem, we should stop using
${{ steps.identifier.outputs.name }}directly within the bashrun: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
downloadstep (lines 195–209), we will:PLAN_NAME, set to${{ steps.identifier.outputs.name }}within theenv:mapping.${{ steps.identifier.outputs.name }}inside therun: |script with"$PLAN_NAME"(or$PLAN_NAMEinside a larger double-quoted string), keeping them properly quoted.All changes are confined to the shown section of
action.yml; no new external dependencies are required.