Skip to content
Draft
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
117 changes: 92 additions & 25 deletions .github/actions/notify-slack-deploy/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Notify Slack of a deploy
description: Post a deploy notification to a Slack incoming webhook, prompting a smoke test.
description: Post the outcome of a deploy to a Slack incoming webhook.

inputs:
webhook_url:
Expand All @@ -15,6 +15,12 @@ inputs:
description: The environment that was deployed to.
required: false
default: production
status:
description: >
What happened: deployed, failed, or cancelled. Only a deploy asks for a
smoke test, so this picks the copy and the buttons.
required: false
default: deployed

runs:
using: composite
Expand All @@ -27,58 +33,119 @@ runs:
SERVICE: ${{ inputs.service }}
APP_URL: ${{ inputs.app_url }}
ENVIRONMENT: ${{ inputs.environment }}
STATUS: ${{ inputs.status }}
COMMIT_SHA: ${{ github.sha }}
ACTOR: ${{ github.actor }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# jq builds the payload so every value is escaped for us, rather than
# interpolated into a JSON heredoc.
# Reading the run's own start time needs actions:read on the calling job.
GH_TOKEN: ${{ github.token }}
RUN_API_PATH: repos/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail

# How long the run took, so the outcome message also answers "when did
# this start", and no separate start notification is needed. The run is
# over either way, so treat every step here as best effort and fall back
# to omitting the fields.
started_epoch=''
started_label=''
duration=''
started_at="$(gh api "$RUN_API_PATH" --jq '.run_started_at' 2>/dev/null || true)"
if [ -n "$started_at" ]; then
started_epoch="$(date -u -d "$started_at" +%s 2>/dev/null || true)"
fi
if [ -n "$started_epoch" ]; then
started_label="$(date -u -d "@$started_epoch" '+%H:%M UTC' 2>/dev/null || true)"
elapsed=$(( $(date -u +%s) - started_epoch ))
if [ "$elapsed" -ge 0 ]; then
duration="$(( elapsed / 60 ))m $(( elapsed % 60 ))s"
fi
fi

# jq builds the payload so every value is escaped for us, rather than
# interpolated into a JSON heredoc.
jq -n \
--arg service "$SERVICE" \
--arg environment "$ENVIRONMENT" \
--arg status "$STATUS" \
--arg short_sha "${COMMIT_SHA:0:7}" \
--arg actor "$ACTOR" \
--arg app_url "$APP_URL" \
--arg workflow_url "$WORKFLOW_URL" \
--arg started_epoch "$started_epoch" \
--arg started_label "$started_label" \
--arg duration "$duration" \
'($environment | (.[0:1] | ascii_upcase) + .[1:]) as $env_name |
(if $status == "failed" then
{
headline: "🛑 \($service) deploy to \($environment) failed",
body: "\($env_name) is unchanged and still serving the previous release.",
actor_label: "Pushed by",
link_app: false
}
elif $status == "cancelled" then
{
headline: "⏭️ \($service) deploy to \($environment) stopped",
# Job level concurrency cancels a run in progress when the next
# commit lands, which is the usual reason to see this.
body: "This run was cancelled before deploying, usually because a newer commit landed on main. \($env_name) is unchanged, and the newer run deploys instead.",
actor_label: "Pushed by",
link_app: false
}
else
{
headline: "🚀 \($service) deployed to \($environment)",
body: "🧪 *\($env_name) smoke test required*\n\nPlease verify the application directly in \($environment).",
actor_label: "Deployed by",
link_app: true
}
end) as $copy |
{
# Slack honours username on this webhook but ignores icon_emoji.
# The avatar comes from the icon set on the Slack app itself.
username: "\($service) Deploy",
text: "🚀 \($service) deployed to \($environment): smoke test required",
text: $copy.headline,
blocks: [
{
type: "header",
text: { type: "plain_text", text: "🚀 \($service) deployed to \($environment)" }
text: { type: "plain_text", text: $copy.headline }
},
{
type: "section",
text: {
type: "mrkdwn",
text: "🧪 *\($env_name) smoke test required*\n\nPlease verify the application directly in \($environment)."
}
text: { type: "mrkdwn", text: $copy.body }
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: "*Commit:*\n\($short_sha)" },
{ type: "mrkdwn", text: "*Deployed by:*\n@\($actor)" }
]
fields: (
[
{ type: "mrkdwn", text: "*Commit:*\n\($short_sha)" },
{ type: "mrkdwn", text: "*\($copy.actor_label):*\n@\($actor)" }
]
# Slack renders this date token in local time for each reader.
+ (if $started_epoch != "" then
[{ type: "mrkdwn", text: "*Started:*\n<!date^\($started_epoch)^{time}|\($started_label)>" }]
else [] end)
+ (if $duration != "" then
[{ type: "mrkdwn", text: "*Duration:*\n\($duration)" }]
else [] end)
)
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "Open \($env_name)" },
url: $app_url
},
{
type: "button",
text: { type: "plain_text", text: "View GitHub Actions" },
url: $workflow_url
}
]
elements: (
(if $copy.link_app then
[{
type: "button",
text: { type: "plain_text", text: "Open \($env_name)" },
url: $app_url
}]
else [] end)
+ [{
type: "button",
text: { type: "plain_text", text: "View GitHub Actions" },
url: $workflow_url
}]
)
}
]
}' \
Expand Down
41 changes: 34 additions & 7 deletions .github/workflows/frontend-deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,19 @@ jobs:
npm_build_environment: prod
secrets: inherit

notify-production-smoke-test:
name: Notify Production Smoke Test
needs: deploy-production
if: ${{ needs.deploy-production.result == 'success' }}
notify-production-deploy:
name: Notify Production Deploy
needs: [run-unit-tests, run-tests, deploy-production]
# Reports whatever happened, not just a success. This notification is the
# only report the deploy channel gets, so a failed or superseded run has to
# come through here too.
if: ${{ always() }}
runs-on: ubuntu-latest
# Only needs to read the repo to resolve the local action below.
permissions:
# Enough to resolve the local action below.
contents: read
# The notification reads this run to report when it started.
actions: read

steps:
- name: Cloning repo
Expand All @@ -95,15 +100,37 @@ jobs:
# Nothing runs git after this, so the token need not persist.
persist-credentials: false

- name: Resolve what to report
id: outcome
env:
UNIT_TESTS: ${{ needs.run-unit-tests.result }}
E2E_TESTS: ${{ needs.run-tests.result }}
DEPLOY: ${{ needs.deploy-production.result }}
run: |
set -eu
# A superseded run reads as cancelled rather than failed: run-tests
# cancels itself in progress when the next commit lands on main. That
# needs saying differently to a real failure, or every busy morning
# looks like a broken deploy.
if [ "$DEPLOY" = success ]; then
status=deployed
elif [ "$UNIT_TESTS" = cancelled ] || [ "$E2E_TESTS" = cancelled ] || [ "$DEPLOY" = cancelled ]; then
status=cancelled
else
status=failed
fi
echo "status=$status" >> "$GITHUB_OUTPUT"

- name: Notify Slack
# The deploy has already succeeded by this point, so a Slack outage or a
# rotated webhook should not fail the run.
# The run is over by this point either way, so a Slack outage or a
# rotated webhook should not fail it.
continue-on-error: true
uses: ./.github/actions/notify-slack-deploy
with:
webhook_url: ${{ secrets.SLACK_FRONTEND_DEPLOY_WEBHOOK }}
service: Frontend
app_url: https://app.flagsmith.com
status: ${{ steps.outcome.outputs.status }}

deploy-demo:
name: Deploy to Vercel Demo
Expand Down
Loading