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
118 changes: 116 additions & 2 deletions .github/workflows/test-apply.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1467,18 +1467,22 @@ jobs:
with:
persist-credentials: false

- name: Plan using default ephemeral value
- name: Plan with required ephemeral variable
uses: ./terraform-plan
with:
label: test-apply ephemeral
path: tests/workflows/test-apply/ephemeral
variables: |
secret = "super-secret"

- name: Apply using default ephemeral value
- name: Apply with required ephemeral variable
uses: ./terraform-apply
id: apply
with:
label: test-apply ephemeral
path: tests/workflows/test-apply/ephemeral
variables: |
secret = "super-secret"

- name: Verify outputs
env:
Expand All @@ -1499,6 +1503,7 @@ jobs:
variables: |
region = "eu-west-1"
mv = "hello"
secret = "super-secret"

- name: Apply using explicit ephemeral value
uses: ./terraform-apply
Expand All @@ -1509,6 +1514,7 @@ jobs:
variables: |
region = "eu-west-1"
mv = "hello"
secret = "super-secret"

- name: Verify outputs
env:
Expand All @@ -1529,6 +1535,7 @@ jobs:
variables: |
region = "eu-west-2"
mv = "goodbye"
secret = "super-secret"

- name: Apply using mismatched explicit non-ephemeral value
uses: ./terraform-apply
Expand All @@ -1540,6 +1547,113 @@ jobs:
variables: |
region = "eu-west-2"
mv = "mismatch"
secret = "super-secret"

- name: Check failed to apply
env:
OUTCOME: ${{ steps.apply3.outcome }}
run: |
if [[ "$OUTCOME" != "failure" ]]; then
echo "Apply did not fail correctly"
exit 1
fi

ephemeral_opentofu:
runs-on: ubuntu-24.04
name: Apply a plan with ephemeral variables using OpenTofu
permissions:
contents: read
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENTOFU_VERSION: "1.11.12"
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Plan with required ephemeral variable
uses: ./tofu-plan
with:
label: test-apply ephemeral tofu
path: tests/workflows/test-apply/ephemeral
variables: |
secret = "super-secret"

- name: Apply with required ephemeral variable
uses: ./tofu-apply
id: apply
with:
label: test-apply ephemeral tofu
path: tests/workflows/test-apply/ephemeral
variables: |
secret = "super-secret"

- name: Verify outputs
env:
OUTPUT_STRING: ${{ steps.apply.outputs.v }}
run: |
if [[ "$OUTPUT_STRING" != "non-ephemeral" ]]; then
echo "::error:: output v not set correctly"
exit 1
fi

##

- name: Plan using explicit ephemeral value
uses: ./tofu-plan
with:
label: test-apply ephemeral tofu 2
path: tests/workflows/test-apply/ephemeral
variables: |
region = "eu-west-1"
mv = "hello"
secret = "super-secret"

- name: Apply using explicit ephemeral value
uses: ./tofu-apply
id: apply2
with:
label: test-apply ephemeral tofu 2
path: tests/workflows/test-apply/ephemeral
variables: |
region = "eu-west-1"
mv = "hello"
secret = "super-secret"

- name: Verify outputs
env:
OUTPUT_STRING: ${{ steps.apply2.outputs.v }}
run: |
if [[ "$OUTPUT_STRING" != "hello" ]]; then
echo "::error:: output v not set correctly"
exit 1
fi

##

- name: Plan using explicit non-ephemeral value
uses: ./tofu-plan
with:
label: test-apply ephemeral tofu 3
path: tests/workflows/test-apply/ephemeral
variables: |
region = "eu-west-2"
mv = "goodbye"
secret = "super-secret"

- name: Apply using mismatched explicit non-ephemeral value
uses: ./tofu-apply
continue-on-error: true
id: apply3
with:
label: test-apply ephemeral tofu 3
path: tests/workflows/test-apply/ephemeral
variables: |
region = "eu-west-2"
mv = "mismatch"
secret = "super-secret"

- name: Check failed to apply
env:
Expand Down
1 change: 1 addition & 0 deletions docs-gen/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def assert_ordering(self):
"text_plan_path",
"junit_xml_path",
"to_add",
"to_invoke",
"failure_reason",
"lock_info",
"run_id",
Expand Down
2 changes: 2 additions & 0 deletions docs-gen/actions/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from outputs.run_id import run_id
from outputs.text_plan_path import text_plan_path
from outputs.to_add import to_add
from outputs.to_invoke import to_invoke

plan = Action(
'plan',
Expand Down Expand Up @@ -70,6 +71,7 @@
json_plan_path,
text_plan_path,
to_add,
to_invoke,
run_id
],
environment_variables=[
Expand Down
2 changes: 1 addition & 1 deletion docs-gen/outputs/to_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
The number of resources that would be affected by each type of operation.
''',
meta_description='The number of resources that would be affected by this operation.',
aliases=['to_change', 'to_destroy', 'to_move', 'to_import']
aliases=['to_change', 'to_destroy', 'to_move', 'to_import', 'to_forget']
)
13 changes: 13 additions & 0 deletions docs-gen/outputs/to_invoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from action import Output, Terraform

to_invoke = Output(
name='to_invoke',
type='number',
description='''
The number of actions that would be invoked by the plan, using `action_trigger` lifecycle events.

Requires Terraform 1.14+.
''',
meta_description='The number of actions that would be invoked by this operation.',
available_in=[Terraform]
)
11 changes: 11 additions & 0 deletions image/src/github_pr_comment/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def create_summary(plan: Plan, changes: bool=True) -> Optional[str]:
summary = None

to_move = 0
to_forget = 0

for line in plan.splitlines():
if line.startswith('No changes') or line.startswith('Error'):
Expand All @@ -208,12 +209,22 @@ def create_summary(plan: Plan, changes: bool=True) -> Optional[str]:
if re.match(r' # \S+ has moved to \S+$', line):
to_move += 1

# Terraform doesn't include forgotten resources in the summary line, so count them
if re.match(r' # \S+ will no longer be managed by Terraform, but will not be destroyed$', line):
to_forget += 1

if re.match(r' # \S+ will be removed from the OpenTofu state but will not be destroyed$', line):
to_forget += 1

if line.startswith('Plan:'):
summary = line

if to_move and 'move' not in summary:
summary = summary.rstrip('.') + f', {to_move} to move.'

if to_forget and 'forget' not in summary:
summary = summary.rstrip('.') + f', {to_forget} to forget.'

if line.startswith('Changes to Outputs'):
if summary:
return summary + ' Changes to Outputs.'
Expand Down
19 changes: 18 additions & 1 deletion image/src/plan_summary/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- to_add
- to_change
- to_destroy
- to_move
- to_import
- to_forget
- to_invoke

Usage:
plan_summary
Expand All @@ -22,15 +26,25 @@ def summary(plan: str) -> dict[str, int]:
'change': 0,
'destroy': 0,
'move': None,
'import': 0
'import': 0,
'forget': None,
'invoke': 0
}

to_move = 0
to_forget = 0

for line in plan.splitlines():
if re.match(r' # \S+ has moved to \S+$', line):
to_move += 1

# Terraform doesn't include forgotten resources in the summary line, so count them
if re.match(r' # \S+ will no longer be managed by Terraform, but will not be destroyed$', line):
to_forget += 1

if re.match(r' # \S+ will be removed from the OpenTofu state but will not be destroyed$', line):
to_forget += 1

if not line.startswith('Plan:'):
continue

Expand All @@ -40,6 +54,9 @@ def summary(plan: str) -> dict[str, int]:
if operations['move'] is None:
operations['move'] = to_move

if operations['forget'] is None:
operations['forget'] = to_forget

return operations


Expand Down
9 changes: 9 additions & 0 deletions terraform-plan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,20 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/
* `to_destroy`
* `to_move`
* `to_import`
* `to_forget`

The number of resources that would be affected by each type of operation.

- Type: number

* `to_invoke`

The number of actions that would be invoked by the plan, using `action_trigger` lifecycle events.

Requires Terraform 1.14+.

- Type: number

* `run_id`

If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id.
Expand Down
4 changes: 4 additions & 0 deletions terraform-plan/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ outputs:
description: The number of resources that would be affected by this operation.
to_import:
description: The number of resources that would be affected by this operation.
to_forget:
description: The number of resources that would be affected by this operation.
to_invoke:
description: The number of actions that would be invoked by this operation.
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.

Expand Down
41 changes: 39 additions & 2 deletions tests/github_pr_comment/test_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,17 +169,54 @@ def test_summary_move_only():
length = 8
# (8 unchanged attributes hidden)
}

# random_string.blah_string has moved to random_string.my_string2
resource "random_string" "my_string2" {
id = "Iyh3jLKc"
length = 8
# (8 unchanged attributes hidden)
}
}

Plan: 0 to add, 0 to change, 0 to destroy.
"""

expected = "Plan: 0 to add, 0 to change, 0 to destroy, 2 to move."

assert create_summary(plan) == expected

def test_summary_forget_only():
# Terraform does not include forgotten resources in the summary line,
# so they are counted from the plan text
plan = """Terraform will perform the following actions:

# terraform_data.x will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
. resource "terraform_data" "x" {
id = "c5464233-23a4-f524-e5c8-d28838eb0687"
# (2 unchanged attributes hidden)
}

Plan: 0 to add, 0 to change, 0 to destroy.
"""

expected = "Plan: 0 to add, 0 to change, 0 to destroy, 1 to forget."

assert create_summary(plan) == expected

def test_summary_forget_opentofu():
# OpenTofu includes forgotten resources in the summary line, which is used as-is
plan = """OpenTofu will perform the following actions:

# terraform_data.x will be removed from the OpenTofu state but will not be destroyed
. resource "terraform_data" "x" {
id = "c5464233-23a4-f524-e5c8-d28838eb0687"
input = "hello"
output = "hello"
}

Plan: 0 to add, 0 to change, 0 to destroy, 1 to forget.
"""

expected = "Plan: 0 to add, 0 to change, 0 to destroy, 1 to forget."

assert create_summary(plan) == expected
Loading
Loading