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
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
---
# Reusable Smurf SDKR workflow β€” build, scan, and push Docker images.
# Docs: docs/docker-smurf.md
name: 🐳 Smurf-Docker

on:
Expand Down Expand Up @@ -148,7 +146,7 @@ jobs:
uses: actions/checkout@v7

- name: βš™οΈ Setup Smurf
uses: clouddrove/smurf@v1.1.5
uses: clouddrove/smurf@v1.1.6

- name: 🐳 Docker Image Build
if: inputs.docker_buildkit_enable != 'true'
Expand Down Expand Up @@ -193,7 +191,7 @@ jobs:
uses: actions/checkout@v7

- name: βš™οΈ Setup Smurf
uses: clouddrove/smurf@v1.1.5
uses: clouddrove/smurf@v1.1.6

- name: πŸ“₯ Download Docker Image Artifact
uses: actions/download-artifact@v8
Expand Down Expand Up @@ -272,4 +270,4 @@ jobs:
run: |
smurf sdkr push ${{ inputs.docker_registry }} \
${{ inputs.docker_registry_url }}/${{ inputs.docker_image_name }}:${{ inputs.docker_image_tag }} \
--project-id ${{ inputs.gcp_project_id }}
--project-id ${{ inputs.gcp_project_id }}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
---
# Reusable Smurf SELM workflow β€” lint, template, deploy, and rollback Helm charts.
# Docs: docs/helm-smurf.md
name: ⎈ Smurf Helm Shared Workflow

on:
Expand Down Expand Up @@ -182,7 +180,7 @@ jobs:
uses: actions/checkout@v7

- name: βš™οΈ Setup Smurf
uses: clouddrove/smurf@v1.1.5
uses: clouddrove/smurf@v1.1.6
with:
version: ${{ inputs.smurf_version }}

Expand Down Expand Up @@ -211,7 +209,7 @@ jobs:
uses: actions/checkout@v7

- name: βš™οΈ Setup Smurf
uses: clouddrove/smurf@v1.1.5
uses: clouddrove/smurf@v1.1.6
with:
version: ${{ inputs.smurf_version }}

Expand Down Expand Up @@ -317,7 +315,7 @@ jobs:
uses: actions/checkout@v7

- name: βš™οΈ Setup Smurf
uses: clouddrove/smurf@v1.1.5
uses: clouddrove/smurf@v1.1.6
with:
version: ${{ inputs.smurf_version }}

Expand Down Expand Up @@ -389,4 +387,4 @@ jobs:
run: |
smurf selm rollback "${{ inputs.helm_release_name }}" \
"${{ inputs.helm_revision }}" \
-n "${{ inputs.helm_namespace }}"
-n "${{ inputs.helm_namespace }}"
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,4 @@ jobs:
# - Running command to check terraform formatting changes.
- name: 🧹 Check Terraform format changes
run: smurf stf fmt --recursive --timeout 5s
...
...
301 changes: 301 additions & 0 deletions .github/workflows/smurf-tf-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
---
name: 'πŸ“‘ Smurf STF Terraform Drift Detection'

on:
workflow_call:
inputs:
terraform_directory:
required: true
type: string
description: 'Root directory of the Terraform configuration.'
provider:
required: true
type: string
description: 'Cloud provider: aws or gcp'
aws_region:
required: false
type: string
default: us-east-1
description: 'AWS region for Terraform deployment.'
var_file:
required: false
type: string
default: ''
description: 'Terraform var file path relative to terraform_directory. e.g. vars/dev.tfvars'
terraform_version:
type: string
default: '1.5.7'
description: 'Terraform CLI version (used for terraform show on plan file).'
smurf_version:
type: string
default: v1.1.6
description: 'Smurf CLI version'
plan_out:
type: string
default: tfplan
description: 'Plan filename written inside terraform_directory.'
create_issue:
type: boolean
default: true
description: 'Open/update a GitHub issue when drift is detected.'
close_issue_on_clean:
type: boolean
default: true
description: 'Close open drift issues when no drift is detected.'
drift_issue_title:
type: string
default: 'Terraform Configuration Drift Detected'
description: 'Title for the drift GitHub issue.'
token_format:
required: false
type: string
default: access_token
access_token_lifetime:
required: false
type: string
default: 300s
project_id:
required: false
type: string
default: ''
create_credentials_file:
required: false
type: string
default: 'true'
backend_config:
required: false
type: string
default: ''
description: 'Backend config file path relative to terraform_directory. e.g. vars/backend.hcl'
secrets:
AWS_ACCESS_KEY_ID:
required: false
AWS_SECRET_ACCESS_KEY:
required: false
AWS_SESSION_TOKEN:
required: false
BUILD_ROLE:
required: false
GCP_CREDENTIALS:
required: false
WORKLOAD_IDENTITY_PROVIDER:
required: false
SERVICE_ACCOUNT:
required: false
env-vars:
required: false

jobs:
smurf-stf-drift:
name: 'Smurf STF Drift Check'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
outputs:
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}

steps:
- name: πŸ“¦ Checkout Repository
uses: actions/checkout@v4

- name: 🌱 Set environment variables
env:
ENV_VARS: ${{ secrets.env-vars }}
run: |
if [ -n "$ENV_VARS" ]; then
(
cat <<'_EOT'
$ENV_VARS
_EOT
) >> "$GITHUB_ENV"
fi

- name: 🟦 Configure AWS credentials
if: ${{ inputs.provider == 'aws' }}
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
role-to-assume: ${{ secrets.BUILD_ROLE }}
aws-region: ${{ inputs.aws_region }}
role-duration-seconds: 900
role-skip-session-tagging: true

- name: ☁️ Authenticate to Google Cloud
if: ${{ inputs.provider == 'gcp' }}
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
create_credentials_file: ${{ inputs.create_credentials_file }}
token_format: ${{ inputs.token_format }}
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.SERVICE_ACCOUNT }}
access_token_lifetime: ${{ inputs.access_token_lifetime }}
project_id: ${{ inputs.project_id }}

- name: πŸ› οΈ Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.terraform_version }}
terraform_wrapper: false

- name: βš™οΈ Set up Smurf
uses: clouddrove/smurf@v1.1.6
with:
version: ${{ inputs.smurf_version }}

- name: πŸ—οΈ Smurf Terraform Init
run: |
INIT_ARGS=(--dir=${{ inputs.terraform_directory }})
if [ -n "${{ inputs.backend_config }}" ]; then
INIT_ARGS+=(--backend-config=${{ inputs.terraform_directory }}/${{ inputs.backend_config }})
fi
smurf stf init "${INIT_ARGS[@]}"

- name: πŸ”Ž Smurf Terraform Validate
run: smurf stf validate --dir=${{ inputs.terraform_directory }}

- name: πŸ“ Smurf Terraform Plan (drift check)
id: tf-plan
working-directory: ${{ inputs.terraform_directory }}
run: |
set +e
PLAN_ARGS="--out=${{ inputs.plan_out }}"
if [ -n "${{ inputs.var_file }}" ]; then
PLAN_ARGS="$PLAN_ARGS --var-file=${{ inputs.var_file }}"
fi
smurf stf plan $PLAN_ARGS
PLAN_EXIT=$?
set -e

if [ "$PLAN_EXIT" -ne 0 ]; then
echo "exitcode=1" >> "$GITHUB_OUTPUT"
echo "Smurf plan failed with exit code $PLAN_EXIT"
exit 1
fi

# Smurf removes an empty plan file when there are no changes.
if [ -f "${{ inputs.plan_out }}" ] && [ -s "${{ inputs.plan_out }}" ]; then
PLAN_JSON=$(terraform show -json "${{ inputs.plan_out }}")
ONLY_CREATE=$(echo "$PLAN_JSON" | jq -r 'if (.resource_changes | length) == 0 then false else ([.resource_changes[].change.actions[]] | all(. == "create")) end')

if [ "$ONLY_CREATE" = "true" ]; then
echo "exitcode=3" >> "$GITHUB_OUTPUT"
echo "Bootstrap required β€” plan only contains create actions (no remote state or first run)."
else
echo "exitcode=2" >> "$GITHUB_OUTPUT"
echo "Drift detected β€” plan file exists and contains update/replace/destroy changes."
fi
else
echo "exitcode=0" >> "$GITHUB_OUTPUT"
echo "No drift detected."
fi

- name: πŸ’Ύ Upload plan artifact
if: steps.tf-plan.outputs.exitcode == '2' || steps.tf-plan.outputs.exitcode == '3'
uses: actions/upload-artifact@v4
with:
name: smurf-tfplan-${{ github.run_id }}
path: ${{ inputs.terraform_directory }}/${{ inputs.plan_out }}
if-no-files-found: ignore

- name: 🧡 Build plan summary
id: tf-plan-string
if: steps.tf-plan.outputs.exitcode == '2' || steps.tf-plan.outputs.exitcode == '3'
working-directory: ${{ inputs.terraform_directory }}
run: |
TERRAFORM_PLAN=$(terraform show -no-color "${{ inputs.plan_out }}")
delimiter="$(openssl rand -hex 8)"
TITLE="## Terraform Drift Detected (Smurf STF)"
if [ "${{ steps.tf-plan.outputs.exitcode }}" = "3" ]; then
TITLE="## Bootstrap Required (Smurf STF)"
fi
{
echo "summary<<${delimiter}"
echo "$TITLE"
echo ""
echo "### Working Directory: \`${{ inputs.terraform_directory }}\`"
echo "### Provider: \`${{ inputs.provider }}\`"
echo ""
echo "<details><summary>Click to expand plan output</summary>"
echo ""
echo '```terraform'
echo "$TERRAFORM_PLAN"
echo '```'
echo "</details>"
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"

- name: πŸ“ Publish plan to job summary
if: steps.tf-plan.outputs.exitcode == '2' || steps.tf-plan.outputs.exitcode == '3'
env:
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }}
run: echo "$SUMMARY" >> "$GITHUB_STEP_SUMMARY"

- name: 🚨 Create or update drift issue
if: inputs.create_issue && steps.tf-plan.outputs.exitcode == '2'
uses: actions/github-script@v7
env:
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }}
ISSUE_TITLE: ${{ inputs.drift_issue_title }}
with:
github-token: ${{ github.token }}
script: |
const body = `${process.env.SUMMARY}`;
const title = process.env.ISSUE_TITLE;
const creator = 'github-actions[bot]';

const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
creator,
title,
});

if (issues.data.length > 0) {
const issue = issues.data[0];
if (issue.body !== body) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body,
});
}
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
});
}

- name: βœ… Close drift issue when clean
if: inputs.close_issue_on_clean && steps.tf-plan.outputs.exitcode == '0'
uses: actions/github-script@v7
env:
ISSUE_TITLE: ${{ inputs.drift_issue_title }}
with:
github-token: ${{ github.token }}
script: |
const title = process.env.ISSUE_TITLE;
const creator = 'github-actions[bot]';
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
creator,
title,
});
if (issues.data.length > 0) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues.data[0].number,
state: 'closed',
});
}
Loading
Loading