Skip to content

Commit

Permalink
feat: check plan diff for staleness before applying in merge queue (#290
Browse files Browse the repository at this point in the history
)

* introduce new input parameter for toggle control

Signed-off-by: Rishav Dhar <19497993+RDhar@users.noreply.github.com>

* Update action.yml (#291)

* remove `outline_enable` option in order to enable by default

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* update readme with new input parameter

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* add logic

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* remove artifact

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* update `merge_matrix` example

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* test `plan_parity`

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* test `tf_tests` apply with `plan_parity` enabled

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* pass `plan_parity` as env var

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* ready to merge

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

* doc wording

Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>

---------

Signed-off-by: Rishav Dhar <19497993+RDhar@users.noreply.github.com>
Signed-off-by: Rishav Dhar <19497993+rdhar@users.noreply.github.com>
Co-authored-by: Kamil Sitnik <72462886+ksitnik-tc@users.noreply.github.com>
  • Loading branch information
rdhar and ksitnik-tc authored Sep 8, 2024
1 parent be9c70e commit 31f7cab
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 30 deletions.
1 change: 1 addition & 0 deletions .github/examples/pr_merge_matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ jobs:
arg_lock: ${{ github.event_name == 'merge_group' && 'true' || 'false' }}
arg_var_file: env/${{ matrix.deployment }}.tfvars
arg_workspace: ${{ matrix.deployment }}
plan_parity: true
1 change: 1 addition & 0 deletions .github/workflows/tf_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
continue-on-error: true
uses: ./
with:
plan_parity: true
arg_chdir: tests/${{ matrix.path }}
arg_command: ${{ github.event.pull_request.merged && 'apply' || 'plan' }}
arg_lock: ${{ github.event.pull_request.merged && 'true' || 'false' }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ In order to locally decrypt the TF plan file, use the following command (noting
| `encrypt_passphrase`</br>Example: `${{ secrets.KEY }}` | String passphrase to encrypt the TF plan file. |
| `fmt_enable`</br>Default: `true` | Boolean flag to enable TF fmt command and display diff of changes. |
| `label_pr`</br>Default: `true` | Boolean flag to add PR label of TF command to run. |
| `outline_enable`</br>Default: `true` | Boolean flag to add an outline diff of TF plan file. |
| `plan_parity`</br>Default: `false` | Boolean flag to compare the TF plan file with a newly-generated one to prevent stale apply. |
| `tf_tool`</br>Default: `terraform` | String name of the TF tool to use and override default assumption from wrapper environment variable. |
| `tf_version`</br>Example: `~>` 1.8.0 | String version constraint of the TF tool to install and use. |
| `update_comment`</br>Default: `false` | Boolean flag to update existing PR comment instead of creating a new comment and deleting the old one. |
Expand Down
124 changes: 100 additions & 24 deletions action.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,30 +247,61 @@ module.exports = async ({ context, core, exec, github }) => {
3
);

if (/^true$/i.test(process.env.outline_enable)) {
result_outline = cli_result
.split("\n")
.filter((line) => line.startsWith(" # "))
.map((line) => {
const diff_line = line.slice(4);
if (diff_line.includes(" created")) return "+ " + diff_line;
if (diff_line.includes(" destroyed")) return "- " + diff_line;
if (diff_line.includes(" updated") || diff_line.includes(" replaced"))
return "! " + diff_line;
return "# " + diff_line;
})
.join("\n");
if (result_outline?.length >= result_outline_limit) {
result_outline = result_outline.substring(0, result_outline_limit) + "…";
}
core.setOutput("outline", result_outline);
result_outline = cli_result
.split("\n")
.filter((line) => line.startsWith(" # "))
.map((line) => {
const diff_line = line.slice(4);
if (diff_line.includes(" created")) return "+ " + diff_line;
if (diff_line.includes(" destroyed")) return "- " + diff_line;
if (diff_line.includes(" updated") || diff_line.includes(" replaced"))
return "! " + diff_line;
return "# " + diff_line;
})
.join("\n");
if (result_outline?.length >= result_outline_limit) {
result_outline = result_outline.substring(0, result_outline_limit) + "…";
}
core.setOutput("outline", result_outline);
}

// TF apply.
if (process.env.arg_command === "apply") {
// Download the TF plan file if not auto-approved.
if (!/^true$/i.test(process.env.auto_approve)) {
// TF plan anew for later comparison if plan_parity is enabled.
if (/^true$/i.test(process.env.plan_parity)) {
await exec_tf(
[
process.env.arg_chdir,
"plan",
process.env.arg_out + ".new",
process.env.arg_var_file,
process.env.arg_destroy,
process.env.arg_compact_warnings,
process.env.arg_concise,
process.env.arg_detailed_exitcode,
process.env.arg_generate_config_out,
process.env.arg_json,
process.env.arg_lock_timeout,
process.env.arg_lock,
process.env.arg_parallelism,
process.env.arg_refresh_only,
process.env.arg_refresh,
process.env.arg_replace,
process.env.arg_target,
process.env.arg_var,
],
[
"plan",
process.env.arg_chdir,
process.env.arg_workspace_alt,
process.env.arg_backend_config,
],
3
);
}

process.env.arg_auto_approve = process.env.arg_out.replace(/^-out=/, "");
process.env.arg_var_file = process.env.arg_var = "";

Expand Down Expand Up @@ -327,9 +358,8 @@ module.exports = async ({ context, core, exec, github }) => {
`mv ${working_directory}.decrypted ${working_directory}`,
]);
}
}

if (/^true$/i.test(process.env.outline_enable)) {
// Generate an outline of the TF plan.
await exec_tf(
[process.env.arg_chdir, "show", process.env.arg_out.replace(/^-out=/, "")],
[
Expand All @@ -354,6 +384,53 @@ module.exports = async ({ context, core, exec, github }) => {
return "# " + diff_line;
})
.join("\n");

// Compare normalized output of the old TF plan with the new one.
// If they match, then replace the old TF plan with the new one to avoid stale apply.
// Otherwise, proceed with the stale apply.
if (/^true$/i.test(process.env.plan_parity)) {
await exec_tf(
[process.env.arg_chdir, "show", process.env.arg_out.replace(/^-out=/, "") + ".new"],
[
"show",
process.env.arg_chdir,
process.env.arg_workspace_alt,
process.env.arg_backend_config,
process.env.arg_var_file,
process.env.arg_destroy,
],
2
);

const result_outline_old = result_outline.split("\n").sort().join("\n");
const result_outline_new = cli_result
.split("\n")
.filter((line) => line.startsWith(" # "))
.map((line) => {
const diff_line = line.slice(4);
if (diff_line.includes(" created")) return "+ " + diff_line;
if (diff_line.includes(" destroyed")) return "- " + diff_line;
if (diff_line.includes(" updated") || diff_line.includes(" replaced"))
return "! " + diff_line;
return "# " + diff_line;
})
.sort()
.join("\n");

if (result_outline_old === result_outline_new) {
await exec.exec("/bin/bash", [
"-c",
`mv ${process.env.arg_chdir.replace(/^-chdir=/, "")}/${process.env.arg_out.replace(
/^-out=/,
""
)}.new ${process.env.arg_chdir.replace(/^-chdir=/, "")}/${process.env.arg_out.replace(
/^-out=/,
""
)}`,
]);
}
}

if (result_outline?.length >= result_outline_limit) {
result_outline = result_outline.substring(0, result_outline_limit) + "…";
}
Expand Down Expand Up @@ -423,8 +500,8 @@ module.exports = async ({ context, core, exec, github }) => {
// Render the TF fmt command output.
const output_fmt =
process.env.arg_command === "plan" &&
/^true$/i.test(process.env.fmt_enable) &&
fmt_result?.length
/^true$/i.test(process.env.fmt_enable) &&
fmt_result?.length
? `<details><summary>Format diff check.</summary>
\`\`\`diff
Expand Down Expand Up @@ -456,11 +533,10 @@ ${output_outline}
<!-- main -->
<details><summary>${result_summary}</br>
###### ${context.workflow} by @${context.actor} via [${context.eventName}](${check_url}) at ${
context.payload.pull_request?.updated_at ||
###### ${context.workflow} by @${context.actor} via [${context.eventName}](${check_url}) at ${context.payload.pull_request?.updated_at ||
context.payload.head_commit?.timestamp ||
context.payload.merge_group?.head_commit.timestamp
}.
}.
</summary>
\`\`\`hcl
Expand Down
10 changes: 5 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ inputs:
description: Boolean flag to add PR comment of TF command output.
required: false
default: "true"
plan_parity:
description: Boolean flag to compare the TF plan file with a newly-generated one to prevent stale apply.
required: false
default: "false"
encrypt_passphrase:
description: String passphrase to encrypt the TF plan file.
required: false
Expand All @@ -28,10 +32,6 @@ inputs:
description: Boolean flag to add PR label of TF command to run.
required: false
default: "true"
outline_enable:
description: Boolean flag to add an outline diff of TF plan file.
required: false
default: "true"
tf_tool:
description: String name of the TF tool to use and override default assumption from wrapper environment variable.
required: false
Expand Down Expand Up @@ -282,10 +282,10 @@ runs:
# Input parameters.
cache_hit: ${{ steps.cache_plugins.outputs.cache-hit }}
comment_pr: ${{ inputs.comment_pr }}
plan_parity: ${{ inputs.plan_parity }}
encrypt_passphrase: ${{ inputs.encrypt_passphrase }}
fmt_enable: ${{ inputs.fmt_enable }}
label_pr: ${{ inputs.label_pr }}
outline_enable: ${{ inputs.outline_enable }}
tf_tool: ${{ inputs.tf_tool }}
update_comment: ${{ inputs.update_comment }}
validate_enable: ${{ inputs.validate_enable }}
Expand Down

0 comments on commit 31f7cab

Please sign in to comment.