Skip to content
Merged
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
53 changes: 53 additions & 0 deletions .github/actions/pr-comment/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: 'Create or Update PR Comment'
description: 'Creates or updates a PR comment with specified content'

inputs:
body-includes:
description: 'Unique string to identify existing comments'
required: true
comment-body:
description: 'The comment body content'
required: true
issue-number:
description: 'PR number'
required: true
app-id:
description: 'GitHub App ID'
required: false
default: '2161597'
private-key:
description: 'GitHub App private key'
required: true

outputs:
comment-id:
description: 'The ID of the created/updated comment'
value: ${{ steps.create-comment.outputs.comment-id }}

runs:
using: 'composite'
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}

- name: Find existing comment
uses: peter-evans/find-comment@v3
id: find-comment
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ inputs.issue-number }}
comment-author: 'ainsley-dev-bot[bot]'
body-includes: ${{ inputs.body-includes }}

- name: Create or update comment
uses: peter-evans/create-or-update-comment@v5
id: create-comment
with:
token: ${{ steps.app-token.outputs.token }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ inputs.issue-number }}
body: ${{ inputs.comment-body }}
edit-mode: replace
222 changes: 222 additions & 0 deletions .github/actions/slack-notify/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
name: 'Slack Notification'
description: 'Send rich notifications to Slack with Block Kit formatting'

inputs:
slack_bot_token:
description: 'Slack bot token (xoxb-...)'
required: true
channel_id:
description: 'Slack channel ID to post to'
required: true
title:
description: 'Notification title (no emojis - added automatically based on status)'
required: true
message:
description: 'Main message body'
required: true
status:
description: 'Notification status: success, failure, warning, or info'
required: true
commit_sha:
description: 'Commit SHA for linking'
required: false
default: ''
buttons:
description: 'Optional JSON array of custom buttons: [{"text": "Button Text", "url": "https://..."}]'
required: false
default: ''

runs:
using: 'composite'
steps:
- name: Send Slack Notification
shell: bash
env:
SLACK_BOT_TOKEN: ${{ inputs.slack_bot_token }}
CHANNEL_ID: ${{ inputs.channel_id }}
TITLE: ${{ inputs.title }}
MESSAGE: ${{ inputs.message }}
STATUS: ${{ inputs.status }}
COMMIT_SHA: ${{ inputs.commit_sha }}
BUTTONS: ${{ inputs.buttons }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
# Fail on errors, undefined vars, and fail if any part of a pipe fails
set -euo pipefail

# Determine colour based on status
case "$STATUS" in
success)
COLOR="#36a64f"
;;
failure)
COLOR="#ff0000"
;;
warning)
COLOR="#ffaa00"
;;
info)
COLOR="#2196F3"
;;
*)
COLOR="#808080"
;;
esac

# Get current timestamp
TIMESTAMP=$(date +%s)

# Convert \n escape sequences to actual newlines for proper Slack rendering
# This handles both literal \n from YAML and actual newlines from command output
MESSAGE_FORMATTED=$(printf '%b' "$MESSAGE")

# Build Slack message with Block Kit using jq for proper JSON escaping
if [ -n "$COMMIT_SHA" ]; then
SHORT_SHA="${COMMIT_SHA:0:7}"
# Build commit link text with actual newline
COMMIT_LINK=$(printf '*Commit:*\n<%s/commit/%s|%s>' "$REPO_URL" "$COMMIT_SHA" "$SHORT_SHA")
FIELDS_JSON=$(jq -n \
--arg status_text "$(printf '*Status:*\n%s' "${STATUS^}")" \
--arg actor_text "$(printf '*Triggered By:*\n%s' "$ACTOR")" \
--arg commit_text "$COMMIT_LINK" \
'[
{"type": "mrkdwn", "text": $status_text},
{"type": "mrkdwn", "text": $actor_text},
{"type": "mrkdwn", "text": $commit_text}
]')
else
FIELDS_JSON=$(jq -n \
--arg status_text "$(printf '*Status:*\n%s' "${STATUS^}")" \
--arg actor_text "$(printf '*Triggered By:*\n%s' "$ACTOR")" \
'[
{"type": "mrkdwn", "text": $status_text},
{"type": "mrkdwn", "text": $actor_text}
]')
fi

# Build custom buttons array from input
if [ -n "$BUTTONS" ] && [ "$BUTTONS" != "[]" ] && [ "$BUTTONS" != "" ]; then
# Parse custom buttons and convert to Slack button format
CUSTOM_BUTTONS=$(echo "$BUTTONS" | jq -c '[.[] | {
"type": "button",
"text": {
"type": "plain_text",
"text": .text
},
"url": .url
}]')
else
CUSTOM_BUTTONS="[]"
fi

# Build standard buttons (workflow and repository)
STANDARD_BUTTONS=$(jq -n \
--arg workflow_url "$WORKFLOW_URL" \
--arg repo_url "$REPO_URL" \
'[
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow"
},
"url": $workflow_url
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Repository"
},
"url": $repo_url
}
]')

# Combine custom buttons with standard buttons (custom buttons first)
ALL_BUTTONS=$(jq -n \
--argjson custom "$CUSTOM_BUTTONS" \
--argjson standard "$STANDARD_BUTTONS" \
'$custom + $standard')

# Build the full payload with jq to ensure proper JSON escaping
PAYLOAD=$(jq -n \
--arg channel "$CHANNEL_ID" \
--arg color "$COLOR" \
--arg title "$TITLE" \
--argjson fields "$FIELDS_JSON" \
--arg message "$MESSAGE_FORMATTED" \
--argjson buttons "$ALL_BUTTONS" \
--arg workflow "$GITHUB_WORKFLOW" \
--arg timestamp "$TIMESTAMP" \
'{
"channel": $channel,
"text": "",
"attachments": [
{
"color": $color,
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": $title
}
},
{
"type": "section",
"fields": $fields
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": $message
}
},
{
"type": "actions",
"elements": $buttons
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ("Workflow: " + $workflow + " | Time: <!date^" + $timestamp + "^{date_short_pretty} at {time}|" + (now|strftime("%c")) + ">")
}
]
}
]
}
]
}')

# Send to Slack
RESPONSE=$(curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "${PAYLOAD}" \
-w "\n%{http_code}" \
-s)

# Extract HTTP status code and response body
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
RESPONSE_BODY=$(echo "$RESPONSE" | head -n-1)

# Check if the request was successful
if [ "$HTTP_CODE" != "200" ]; then
echo "Error: Failed to send Slack notification (HTTP ${HTTP_CODE})"
echo "Response: ${RESPONSE_BODY}"
exit 1
fi

# Check if Slack API returned ok: true
if ! echo "$RESPONSE_BODY" | grep -q '"ok":true'; then
echo "Error: Slack API returned an error"
echo "Response: ${RESPONSE_BODY}"
exit 1
fi

echo "✓ Slack notification sent successfully"
94 changes: 53 additions & 41 deletions .github/workflows/helper-apply.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,54 +165,66 @@ jobs:
echo "exitcode=1" >> $GITHUB_OUTPUT
fi

- name: Post Apply Results
- name: Prepare Apply Comment Body
id: prepare-apply-comment
if: always() && (steps.apply-with-plan.outcome != 'skipped' || steps.apply-fresh.outcome != 'skipped')
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const applyOutputPath = '${{ inputs.terraform-directory }}/apply-output.txt';
working-directory: ${{ inputs.terraform-directory }}
run: |
# Build comment body
HEADER="## 🚀 Terraform Apply Results"

let body = '## 🚀 Terraform Apply Results\n\n';
SUCCESS="${{ steps.apply.outputs.exitcode == '0' }}"

const success = '${{ steps.apply.outputs.exitcode }}' === '0';
if [ "$SUCCESS" == "true" ]; then
STATUS="✅ **Infrastructure changes applied successfully**"
else
STATUS="❌ **Apply failed - please check logs**"
fi

if (success) {
body += '✅ **Infrastructure changes applied successfully**\n\n';
} else {
body += '❌ **Apply failed - please check logs**\n\n';
}
# Add apply output if available
APPLY_OUTPUT=""
if [ -f apply-output.txt ]; then
APPLY_OUTPUT="<details>
<summary>📄 View Apply Output</summary>

// Add apply output if available
if (fs.existsSync(applyOutputPath)) {
const applyOutput = fs.readFileSync(applyOutputPath, 'utf8');
body += '<details>\n<summary>📄 View Apply Output</summary>\n\n';
body += '```\n' + applyOutput + '\n```\n';
body += '</details>\n';
}
\`\`\`
$(cat apply-output.txt)
\`\`\`
</details>"
fi

body += `\n🔗 [View Workflow Run](${context.payload.repository.html_url}/actions/runs/${context.runId})`;

// Try to find and comment on the merged PR
const prNumber = '${{ inputs.pr-number || steps.find-pr.outputs.pr-number }}';

if (prNumber) {
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber),
body: body
});
console.log(`Posted results to PR #${prNumber}`);
} catch (error) {
console.log(`Could not post to PR #${prNumber}: ${error.message}`);
}
} else {
console.log('No PR found to comment on');
}
FOOTER="🔗 [View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"

# Combine all parts using heredoc
{
echo "COMMENT_BODY<<EOF"
echo "$HEADER"
echo ""
echo "$STATUS"
echo ""
[ -n "$APPLY_OUTPUT" ] && echo "$APPLY_OUTPUT" && echo ""
echo "$FOOTER"
echo "EOF"
} >> $GITHUB_OUTPUT

- name: Post Apply Results to PR
if: always() && (steps.apply-with-plan.outcome != 'skipped' || steps.apply-fresh.outcome != 'skipped') && (inputs.pr-number != '' || steps.find-pr.outputs.pr-number != '')
uses: ./.github/actions/pr-comment
continue-on-error: true
with:
body-includes: 'Terraform Apply Results'
comment-body: ${{ steps.prepare-apply-comment.outputs.COMMENT_BODY }}
issue-number: ${{ inputs.pr-number || steps.find-pr.outputs.pr-number }}
private-key: ${{ secrets.ORG_GITHUB_APP_PRIVATE_KEY }}

- name: Post Apply Results as Commit Comment
if: always() && (steps.apply-with-plan.outcome != 'skipped' || steps.apply-fresh.outcome != 'skipped')
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const body = `${{ steps.prepare-apply-comment.outputs.COMMENT_BODY }}`;

// Also create a commit comment
try {
await github.rest.repos.createCommitComment({
owner: context.repo.owner,
Expand Down
Loading