GitHub Slack Notifications workflow (aws/bedrock-agentcore-starter-toolkit)
The GitHub Slack Notifications workflow from aws/bedrock-agentcore-starter-toolkit, explained and optimized by Latchkey.
CI health: B - good
Point runs-on at Latchkey and get job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the GitHub Slack Notifications workflow from the aws/bedrock-agentcore-starter-toolkit repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: GitHub Slack Notifications
# Central GitHub -> Slack integration point. Two triggers feed one shared
# Slack workflow (via SLACK_WEBHOOK_URL), which branches on event_type:
# - issues opened -> notify oncall of a new issue
# - comments on closed PRs -> redirect the commenter to open an issue,
# and notify oncall (closed-PR comments are
# otherwise easy to miss)
# Every payload sends the same key set so the Slack workflow can branch
# reliably; fields that don't apply to an event are sent empty.
on:
issues:
types: [opened]
issue_comment:
types: [created]
jobs:
notify-issue-opened:
if: github.event_name == 'issues'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Send issue details to Slack
# Attacker-controlled fields are passed through env: rather than
# interpolated into the YAML payload, to prevent workflow injection.
# For issue_opened, the issue_* fields carry the data and the
# pr_*/comment_* fields are empty.
env:
REPOSITORY: ${{ github.repository }}
CREATED_AT: ${{ github.event.issue.created_at }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_URL: ${{ github.event.issue.html_url }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
ISSUE_BODY: ${{ github.event.issue.body }}
LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }}
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
event_type: "issue_opened"
repository: "${{ env.REPOSITORY }}"
created_at: "${{ env.CREATED_AT }}"
issue_number: "${{ env.ISSUE_NUMBER }}"
issue_title: ${{ toJSON(env.ISSUE_TITLE) }}
issue_url: "${{ env.ISSUE_URL }}"
issue_author: "${{ env.ISSUE_AUTHOR }}"
issue_body: ${{ toJSON(env.ISSUE_BODY) }}
labels: ${{ toJSON(env.LABELS) }}
pr_number: ""
pr_title: ""
pr_url: ""
pr_author: ""
pr_state: ""
pr_closed_at: ""
pr_merged_at: ""
comment_id: ""
comment_url: ""
comment_author: ""
comment_body: ""
closed-pr-comment-redirect:
# Only fire on comments left on PRs (issue_comment fires for issues too)
# that are already closed, and skip comments left by bots.
if: >-
github.event_name == 'issue_comment' && github.event.issue.pull_request != null && github.event.issue.state ==
'closed' && github.event.comment.user.type != 'Bot'
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: read
# Serialize per-PR so the marker-comment dedup is race-free (otherwise two
# rapid-fire comments could both see "no marker" and both post a redirect).
# cancel-in-progress is false so the second run still executes after the
# first finishes -- we want to evaluate dedup against the just-posted marker.
concurrency:
group: closed-pr-comment-${{ github.event.issue.number }}
cancel-in-progress: false
steps:
- name: Check commenter permission
id: perm
uses: actions/github-script@v9
with:
script: |
// External users on private repos can 404 here; treat any
// failure as "not a maintainer" so the redirect still fires.
let permission = 'none';
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
permission = data.permission;
} catch (err) {
core.info(`Permission lookup failed for ${context.payload.comment.user.login}: ${err.message}. Treating as non-maintainer.`);
}
const skip = ['admin', 'maintain', 'write'].includes(permission);
core.setOutput('skip', String(skip));
core.info(`Commenter ${context.payload.comment.user.login} permission=${permission} skip=${skip}`);
- name: Check for existing redirect comment
id: existing
if: steps.perm.outputs.skip != 'true'
uses: actions/github-script@v9
with:
script: |
// Marker we embed in our reply so we don't double-post on the same PR.
const marker = '<!-- closed-pr-comment-redirect -->';
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
per_page: 100,
}
);
const alreadyPosted = comments.some(c => c.body && c.body.includes(marker));
core.setOutput('already_posted', String(alreadyPosted));
- name: Post redirect comment
if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true'
# The Slack notification is the load-bearing part of this job. If
# posting the bot reply fails (rate limit, transient error), don't
# block the Slack notification.
continue-on-error: true
uses: actions/github-script@v9
env:
# Repos with issue templates use /issues/new/choose; repos without
# templates should change this to /issues/new.
ISSUES_NEW_URL: https://github.com/${{ github.repository }}/issues/new/choose
with:
script: |
const commenter = context.payload.comment.user.login;
const issuesNewUrl = process.env.ISSUES_NEW_URL;
const body = [
'<!-- closed-pr-comment-redirect -->',
'',
`Thanks for the report, @${commenter} - feedback like this is exactly`,
"how we catch the things we missed. Because this PR is already",
"closed, the team won't see follow-up comments here.",
'',
'Would you mind opening a new issue so we can track it properly?',
issuesNewUrl,
'',
'If this is a security issue, please report it privately via',
'https://aws.amazon.com/security/vulnerability-reporting/ instead',
'of a public issue.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body,
});
- name: Compute PR state
id: pr_state
if: steps.perm.outputs.skip != 'true'
uses: actions/github-script@v9
with:
script: |
// PRs surface as `issue` events; merged_at is null when closed-not-merged.
const mergedAt = context.payload.issue.pull_request &&
context.payload.issue.pull_request.merged_at;
core.setOutput('state', mergedAt ? 'merged' : 'closed');
- name: Notify Slack
# Notify oncall only on the FIRST external comment per PR (gated by
# already_posted). Subsequent comments on the same PR don't notify --
# the redirect comment has already directed the commenter to open an
# issue, and issues notify oncall via the issue path. This bounds
# notification volume regardless of how chatty a thread becomes.
if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true'
# Attacker-controlled fields are passed through env: rather than
# interpolated into the YAML payload, to prevent workflow injection.
# For closed-PR comments, the issue_* fields are empty (this isn't
# an issue) and the pr_*/comment_* fields carry the real data.
env:
REPOSITORY: ${{ github.repository }}
CREATED_AT: ${{ github.event.comment.created_at }}
PR_NUMBER: ${{ github.event.issue.number }}
PR_TITLE: ${{ github.event.issue.title }}
PR_URL: ${{ github.event.issue.html_url }}
PR_AUTHOR: ${{ github.event.issue.user.login }}
PR_CLOSED_AT: ${{ github.event.issue.closed_at }}
PR_MERGED_AT: ${{ github.event.issue.pull_request.merged_at }}
COMMENT_ID: ${{ github.event.comment.id }}
COMMENT_URL: ${{ github.event.comment.html_url }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
COMMENT_BODY: ${{ github.event.comment.body }}
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
event_type: "closed_pr_comment"
repository: "${{ env.REPOSITORY }}"
created_at: "${{ env.CREATED_AT }}"
issue_number: ""
issue_title: ""
issue_url: ""
issue_author: ""
issue_body: ""
labels: ""
pr_number: "${{ env.PR_NUMBER }}"
pr_title: ${{ toJSON(env.PR_TITLE) }}
pr_url: "${{ env.PR_URL }}"
pr_author: "${{ env.PR_AUTHOR }}"
pr_state: "${{ steps.pr_state.outputs.state }}"
pr_closed_at: "${{ env.PR_CLOSED_AT }}"
pr_merged_at: "${{ env.PR_MERGED_AT }}"
comment_id: "${{ env.COMMENT_ID }}"
comment_url: "${{ env.COMMENT_URL }}"
comment_author: "${{ env.COMMENT_AUTHOR }}"
comment_body: ${{ toJSON(env.COMMENT_BODY) }}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: GitHub Slack Notifications # Central GitHub -> Slack integration point. Two triggers feed one shared # Slack workflow (via SLACK_WEBHOOK_URL), which branches on event_type: # - issues opened -> notify oncall of a new issue # - comments on closed PRs -> redirect the commenter to open an issue, # and notify oncall (closed-PR comments are # otherwise easy to miss) # Every payload sends the same key set so the Slack workflow can branch # reliably; fields that don't apply to an event are sent empty. on: issues: types: [opened] issue_comment: types: [created] jobs: notify-issue-opened: timeout-minutes: 30 if: github.event_name == 'issues' runs-on: latchkey-small permissions: {} steps: - name: Send issue details to Slack # Attacker-controlled fields are passed through env: rather than # interpolated into the YAML payload, to prevent workflow injection. # For issue_opened, the issue_* fields carry the data and the # pr_*/comment_* fields are empty. env: REPOSITORY: ${{ github.repository }} CREATED_AT: ${{ github.event.issue.created_at }} ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_URL: ${{ github.event.issue.html_url }} ISSUE_AUTHOR: ${{ github.event.issue.user.login }} ISSUE_BODY: ${{ github.event.issue.body }} LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }} uses: slackapi/slack-github-action@v2.0.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: webhook-trigger payload: | event_type: "issue_opened" repository: "${{ env.REPOSITORY }}" created_at: "${{ env.CREATED_AT }}" issue_number: "${{ env.ISSUE_NUMBER }}" issue_title: ${{ toJSON(env.ISSUE_TITLE) }} issue_url: "${{ env.ISSUE_URL }}" issue_author: "${{ env.ISSUE_AUTHOR }}" issue_body: ${{ toJSON(env.ISSUE_BODY) }} labels: ${{ toJSON(env.LABELS) }} pr_number: "" pr_title: "" pr_url: "" pr_author: "" pr_state: "" pr_closed_at: "" pr_merged_at: "" comment_id: "" comment_url: "" comment_author: "" comment_body: "" closed-pr-comment-redirect: timeout-minutes: 30 # Only fire on comments left on PRs (issue_comment fires for issues too) # that are already closed, and skip comments left by bots. if: >- github.event_name == 'issue_comment' && github.event.issue.pull_request != null && github.event.issue.state == 'closed' && github.event.comment.user.type != 'Bot' runs-on: latchkey-small permissions: pull-requests: write issues: read # Serialize per-PR so the marker-comment dedup is race-free (otherwise two # rapid-fire comments could both see "no marker" and both post a redirect). # cancel-in-progress is false so the second run still executes after the # first finishes -- we want to evaluate dedup against the just-posted marker. concurrency: group: closed-pr-comment-${{ github.event.issue.number }} cancel-in-progress: false steps: - name: Check commenter permission id: perm uses: actions/github-script@v9 with: script: | // External users on private repos can 404 here; treat any // failure as "not a maintainer" so the redirect still fires. let permission = 'none'; try { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, repo: context.repo.repo, username: context.payload.comment.user.login, }); permission = data.permission; } catch (err) { core.info(`Permission lookup failed for ${context.payload.comment.user.login}: ${err.message}. Treating as non-maintainer.`); } const skip = ['admin', 'maintain', 'write'].includes(permission); core.setOutput('skip', String(skip)); core.info(`Commenter ${context.payload.comment.user.login} permission=${permission} skip=${skip}`); - name: Check for existing redirect comment id: existing if: steps.perm.outputs.skip != 'true' uses: actions/github-script@v9 with: script: | // Marker we embed in our reply so we don't double-post on the same PR. const marker = '<!-- closed-pr-comment-redirect -->'; const comments = await github.paginate( github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, per_page: 100, } ); const alreadyPosted = comments.some(c => c.body && c.body.includes(marker)); core.setOutput('already_posted', String(alreadyPosted)); - name: Post redirect comment if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true' # The Slack notification is the load-bearing part of this job. If # posting the bot reply fails (rate limit, transient error), don't # block the Slack notification. continue-on-error: true uses: actions/github-script@v9 env: # Repos with issue templates use /issues/new/choose; repos without # templates should change this to /issues/new. ISSUES_NEW_URL: https://github.com/${{ github.repository }}/issues/new/choose with: script: | const commenter = context.payload.comment.user.login; const issuesNewUrl = process.env.ISSUES_NEW_URL; const body = [ '<!-- closed-pr-comment-redirect -->', '', `Thanks for the report, @${commenter} - feedback like this is exactly`, "how we catch the things we missed. Because this PR is already", "closed, the team won't see follow-up comments here.", '', 'Would you mind opening a new issue so we can track it properly?', issuesNewUrl, '', 'If this is a security issue, please report it privately via', 'https://aws.amazon.com/security/vulnerability-reporting/ instead', 'of a public issue.', ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, body, }); - name: Compute PR state id: pr_state if: steps.perm.outputs.skip != 'true' uses: actions/github-script@v9 with: script: | // PRs surface as `issue` events; merged_at is null when closed-not-merged. const mergedAt = context.payload.issue.pull_request && context.payload.issue.pull_request.merged_at; core.setOutput('state', mergedAt ? 'merged' : 'closed'); - name: Notify Slack # Notify oncall only on the FIRST external comment per PR (gated by # already_posted). Subsequent comments on the same PR don't notify -- # the redirect comment has already directed the commenter to open an # issue, and issues notify oncall via the issue path. This bounds # notification volume regardless of how chatty a thread becomes. if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true' # Attacker-controlled fields are passed through env: rather than # interpolated into the YAML payload, to prevent workflow injection. # For closed-PR comments, the issue_* fields are empty (this isn't # an issue) and the pr_*/comment_* fields carry the real data. env: REPOSITORY: ${{ github.repository }} CREATED_AT: ${{ github.event.comment.created_at }} PR_NUMBER: ${{ github.event.issue.number }} PR_TITLE: ${{ github.event.issue.title }} PR_URL: ${{ github.event.issue.html_url }} PR_AUTHOR: ${{ github.event.issue.user.login }} PR_CLOSED_AT: ${{ github.event.issue.closed_at }} PR_MERGED_AT: ${{ github.event.issue.pull_request.merged_at }} COMMENT_ID: ${{ github.event.comment.id }} COMMENT_URL: ${{ github.event.comment.html_url }} COMMENT_AUTHOR: ${{ github.event.comment.user.login }} COMMENT_BODY: ${{ github.event.comment.body }} uses: slackapi/slack-github-action@v2.0.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: webhook-trigger payload: | event_type: "closed_pr_comment" repository: "${{ env.REPOSITORY }}" created_at: "${{ env.CREATED_AT }}" issue_number: "" issue_title: "" issue_url: "" issue_author: "" issue_body: "" labels: "" pr_number: "${{ env.PR_NUMBER }}" pr_title: ${{ toJSON(env.PR_TITLE) }} pr_url: "${{ env.PR_URL }}" pr_author: "${{ env.PR_AUTHOR }}" pr_state: "${{ steps.pr_state.outputs.state }}" pr_closed_at: "${{ env.PR_CLOSED_AT }}" pr_merged_at: "${{ env.PR_MERGED_AT }}" comment_id: "${{ env.COMMENT_ID }}" comment_url: "${{ env.COMMENT_URL }}" comment_author: "${{ env.COMMENT_AUTHOR }}" comment_body: ${{ toJSON(env.COMMENT_BODY) }}
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
This workflow runs 2 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.