From 4c8d09b16f75781a60ca2d5dcdca42cfeef92ffd Mon Sep 17 00:00:00 2001 From: Andrei Makarych Date: Thu, 13 Aug 2026 11:06:11 +0200 Subject: [PATCH 1/4] add optional TRIGGERED_BY_EMAIL to mention who triggered the build Commits are not always made by a person. On GitHub, squash merges and merge commits made through the web UI are committed as `GitHub `, and the author is whoever opened the pull request - which can itself be a bot. In that case neither of the two identities in the approval message points at the person waiting on the deploy, so nobody gets mentioned. Let CI pass that identity in via the new optional TRIGGERED_BY_EMAIL variable. When set, a `Triggered by` line is added to the message, mentioning the person if the email matches a Slack profile and falling back to the plain email if it does not - same behaviour as the existing committer and author lines. When it is unset, the message is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 1 + main.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9906c87..a95b7f7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Configuration is done via env variables * `SLACK_BOT_TOKEN` - Slack bot token. Mandatory parameter. scopes: channels:history, chat:write, reactions:read, users:read.email, users:read * `SLACK_APP_TOKEN` - Slack app token. Mandatory parameter. scopes: connections:write * `SLACK_CHANNEL_NAME` - Slack channel name. Also channel_id can be used +* `TRIGGERED_BY_EMAIL` - Email of the person who triggered the build, for example by pressing the merge button. Optional parameter. When set, a `Triggered by` line is added to the approval message, mentioning that person if the email matches a Slack profile. Useful because the commit itself does not always point at a person: squash merges on GitHub, for instance, are committed as `noreply@github.com` # Slack App manifect example ```yaml diff --git a/main.py b/main.py index 4185920..da25878 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,9 @@ timezone = os.environ['TIMEZONE'] production_branches = os.environ['PRODUCTION_BRANCHES'].split() slack_bot_token = os.environ["SLACK_BOT_TOKEN"] + # Not every commit is made by a person - squash merges, for instance, are committed by + # the SCM itself - so CI can tell us who triggered the build. Optional. + triggered_by_email = os.environ.get('TRIGGERED_BY_EMAIL', '') print(f'branches_to_promote: {branches_to_promote}') print(f'production_branches: {production_branches}') @@ -40,6 +43,11 @@ author_email = helpers_git.get_author_email_for_ref(current_commit_id) author_slack_id = helpers_slack.user_id_by_email(app, author_email) author_id = f'<@{author_slack_id}>' if author_slack_id is not None else author_email + triggered_by_id = None + if triggered_by_email: + triggered_by_slack_id = helpers_slack.user_id_by_email(app, triggered_by_email) + triggered_by_id = (f'<@{triggered_by_slack_id}>' + if triggered_by_slack_id is not None else triggered_by_email) commit_msg = helpers_git.get_commit_message_for_ref(current_commit_id) text_for_request = 'If approved will promote commit(s) below to branch ' @@ -49,7 +57,10 @@ details += f'Commit message: `{commit_msg}`\n' details += f'Commit id: `{current_commit_id}`\n' details += f'Committer: {commiter_id}\n' - details += f'Author: {author_id}\n\n' + details += f'Author: {author_id}\n' + if triggered_by_id is not None: + details += f'Triggered by: {triggered_by_id}\n' + details += '\n' details += helpers_time.generate_time_based_message(production_branches, branches_to_promote, timezone) # Generate separate diff blocks for every branch From 6e5d1775d83e72d4e4eb9bfb8d981e043dd0087f Mon Sep 17 00:00:00 2001 From: Yousef de baz Date: Sat, 15 Aug 2026 22:07:58 +0400 Subject: [PATCH 2/4] fix: harden triggered_by_email handling per AI review - Reject whitespace-only TRIGGERED_BY_EMAIL, not just empty string - Suppress the Triggered by line entirely for GitHub noreply addresses (bare noreply@github.com and the privacy-enabled +@users.noreply.github.com form), since neither resolves to a real person - Log Slack lookup failures instead of failing silently --- README.md | 2 +- helpers_git.py | 12 ++++++++++++ helpers_slack.py | 10 ++++++---- main.py | 4 +++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a95b7f7..2f85176 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Configuration is done via env variables * `SLACK_BOT_TOKEN` - Slack bot token. Mandatory parameter. scopes: channels:history, chat:write, reactions:read, users:read.email, users:read * `SLACK_APP_TOKEN` - Slack app token. Mandatory parameter. scopes: connections:write * `SLACK_CHANNEL_NAME` - Slack channel name. Also channel_id can be used -* `TRIGGERED_BY_EMAIL` - Email of the person who triggered the build, for example by pressing the merge button. Optional parameter. When set, a `Triggered by` line is added to the approval message, mentioning that person if the email matches a Slack profile. Useful because the commit itself does not always point at a person: squash merges on GitHub, for instance, are committed as `noreply@github.com` +* `TRIGGERED_BY_EMAIL` - Email of the person who triggered the build, for example by pressing the merge button. Optional parameter. When set, a `Triggered by` line is added to the approval message, mentioning that person if the email matches a Slack profile. Useful because the commit itself does not always point at a person: squash merges on GitHub, for instance, are committed as `noreply@github.com`. A GitHub-generated noreply address (bare `noreply@github.com` or the privacy-enabled `+@users.noreply.github.com` form) never resolves to a person, so the `Triggered by` line is omitted entirely rather than showing that address # Slack App manifect example ```yaml diff --git a/helpers_git.py b/helpers_git.py index bc82d73..4e6c3e2 100644 --- a/helpers_git.py +++ b/helpers_git.py @@ -1,6 +1,18 @@ import subprocess import helpers_slack +# GitHub-generated addresses that never resolve to a real Slack profile: +# noreply@github.com (bare merge/squash commits) and the privacy-enabled +# form +@users.noreply.github.com (bots, and any user with +# "Keep my email addresses private" turned on - this is common, not rare). +NOREPLY_DOMAIN_SUFFIX = '@users.noreply.github.com' +NOREPLY_BARE_ADDRESS = 'noreply@github.com' + + +def is_noreply_email(email): + email = (email or '').strip().lower() + return email == NOREPLY_BARE_ADDRESS or email.endswith(NOREPLY_DOMAIN_SUFFIX) + def resolve_git_ref_to_sha1(ref_name): print(f'Resolving {ref_name} to Git SHA1...') diff --git a/helpers_slack.py b/helpers_slack.py index 46dd6c9..a29d2ba 100644 --- a/helpers_slack.py +++ b/helpers_slack.py @@ -92,10 +92,12 @@ def user_id_by_email(app, email): result = app.client.users_lookupByEmail(email=email) return result['user']['id'] except SlackApiError as err: - if err.response['error'] == 'users_not_found': - return None - - return None + error_code = err.response['error'] + if error_code == 'users_not_found': + print(f'No Slack user found for email {email}') + else: + print(f'Slack lookup failed for email {email}: {error_code}') + return None def is_message_longer_than_limit(message): diff --git a/main.py b/main.py index da25878..8a1bfa7 100644 --- a/main.py +++ b/main.py @@ -44,7 +44,9 @@ author_slack_id = helpers_slack.user_id_by_email(app, author_email) author_id = f'<@{author_slack_id}>' if author_slack_id is not None else author_email triggered_by_id = None - if triggered_by_email: + triggered_by_valid = (triggered_by_email and triggered_by_email.strip() + and not helpers_git.is_noreply_email(triggered_by_email)) + if triggered_by_valid: triggered_by_slack_id = helpers_slack.user_id_by_email(app, triggered_by_email) triggered_by_id = (f'<@{triggered_by_slack_id}>' if triggered_by_slack_id is not None else triggered_by_email) From c1d77ac126b530e58211e65e5a989a3c182f2bd7 Mon Sep 17 00:00:00 2001 From: Ivan Razzhivin <69042494+irazzhivin@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:46:12 +0300 Subject: [PATCH 3/4] fix: strip TRIGGERED_BY_EMAIL and move the noreply check to helpers_slack The whitespace guard validated the stripped value but kept using the raw one, so a padded-but-valid address still broke: the Slack lookup was sent ' user@example.com ', missed, and the fallback printed the address with its surrounding whitespace - the same failure the review described, just with a non-empty address. Strip at the source instead. That also lets the guard collapse into one plain condition, since is_noreply_email() already strips and lowercases internally and the extra .strip() was redundant. Move is_noreply_email() and its constants from helpers_git to helpers_slack. Classifying an address is not a git operation; the question it answers is whether the address can resolve to a Slack profile, which is what the rest of helpers_slack is about. helpers_git also imports helpers_slack, so keeping email logic there deepens that import cycle for no reason. Verified: flake8 and pylint -E clean per lint.sh. Rendered the details block through main.py with Slack and git stubbed - a padded address now resolves to a mention where it previously printed raw, unresolvable and noreply addresses behave as before, and with the variable unset the block is byte-for-byte identical to master. Co-Authored-By: Claude Opus 5 (1M context) --- helpers_git.py | 12 ------------ helpers_slack.py | 12 ++++++++++++ main.py | 6 ++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/helpers_git.py b/helpers_git.py index 4e6c3e2..bc82d73 100644 --- a/helpers_git.py +++ b/helpers_git.py @@ -1,18 +1,6 @@ import subprocess import helpers_slack -# GitHub-generated addresses that never resolve to a real Slack profile: -# noreply@github.com (bare merge/squash commits) and the privacy-enabled -# form +@users.noreply.github.com (bots, and any user with -# "Keep my email addresses private" turned on - this is common, not rare). -NOREPLY_DOMAIN_SUFFIX = '@users.noreply.github.com' -NOREPLY_BARE_ADDRESS = 'noreply@github.com' - - -def is_noreply_email(email): - email = (email or '').strip().lower() - return email == NOREPLY_BARE_ADDRESS or email.endswith(NOREPLY_DOMAIN_SUFFIX) - def resolve_git_ref_to_sha1(ref_name): print(f'Resolving {ref_name} to Git SHA1...') diff --git a/helpers_slack.py b/helpers_slack.py index a29d2ba..8b2e538 100644 --- a/helpers_slack.py +++ b/helpers_slack.py @@ -11,6 +11,13 @@ SLACK_MESSAGE_SIZE_LIMIT = 3001 +# GitHub-generated addresses that never resolve to a real Slack profile: +# noreply@github.com (bare merge/squash commits) and the privacy-enabled +# form +@users.noreply.github.com (bots, and any user with +# "Keep my email addresses private" turned on - this is common, not rare). +NOREPLY_DOMAIN_SUFFIX = '@users.noreply.github.com' +NOREPLY_BARE_ADDRESS = 'noreply@github.com' + def init_app(slack_bot_token, approve_action_id, cancel_action_id): app = App(token=slack_bot_token) @@ -87,6 +94,11 @@ def gen_report(usernames, teams, channel, message, approval_code): json.dump(report, outfile) +def is_noreply_email(email): + email = (email or '').strip().lower() + return email == NOREPLY_BARE_ADDRESS or email.endswith(NOREPLY_DOMAIN_SUFFIX) + + def user_id_by_email(app, email): try: result = app.client.users_lookupByEmail(email=email) diff --git a/main.py b/main.py index 8a1bfa7..55cb77b 100644 --- a/main.py +++ b/main.py @@ -28,7 +28,7 @@ slack_bot_token = os.environ["SLACK_BOT_TOKEN"] # Not every commit is made by a person - squash merges, for instance, are committed by # the SCM itself - so CI can tell us who triggered the build. Optional. - triggered_by_email = os.environ.get('TRIGGERED_BY_EMAIL', '') + triggered_by_email = os.environ.get('TRIGGERED_BY_EMAIL', '').strip() print(f'branches_to_promote: {branches_to_promote}') print(f'production_branches: {production_branches}') @@ -44,9 +44,7 @@ author_slack_id = helpers_slack.user_id_by_email(app, author_email) author_id = f'<@{author_slack_id}>' if author_slack_id is not None else author_email triggered_by_id = None - triggered_by_valid = (triggered_by_email and triggered_by_email.strip() - and not helpers_git.is_noreply_email(triggered_by_email)) - if triggered_by_valid: + if triggered_by_email and not helpers_slack.is_noreply_email(triggered_by_email): triggered_by_slack_id = helpers_slack.user_id_by_email(app, triggered_by_email) triggered_by_id = (f'<@{triggered_by_slack_id}>' if triggered_by_slack_id is not None else triggered_by_email) From 3cd96b0c9ed3c9161e9d93d2f37a8263f5901e77 Mon Sep 17 00:00:00 2001 From: Yousef de baz Date: Tue, 18 Aug 2026 18:05:27 +0400 Subject: [PATCH 4/4] docs: wire TRIGGERED_BY_EMAIL into workflow examples --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f85176..1872810 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ settings: BRANCHES_TO_PROMOTE: "${{ env.GIT_DESTINATION_BRANCH }}" TIMEOUT_MINUTES: 1 REPOSITORY_URL: "${{ fromJson(steps.repo.outputs.result).html_url }}" + # Who clicked Merge - not always the commit author/committer (e.g. squash + # merges are committed by GitHub as noreply@github.com). Optional. + TRIGGERED_BY_EMAIL: "${{ github.event.pusher.email }}" run: > mkdir -p magic-button/reports && chmod 777 magic-button/reports && docker run --rm @@ -102,7 +105,7 @@ settings: -e SLACK_BOT_TOKEN -e SLACK_APP_TOKEN -e BUILD_JOB_NAME -e BUILD_JOB_URL -e CURRENT_GIT_COMMIT="$(git rev-parse HEAD)" -e REPOSITORY_NAME="$(basename $(git rev-parse --show-toplevel))" -e REPOSITORY_URL -e BRANCHES_TO_PROMOTE -e TIMEOUT_MINUTES -e TIMEZONE="Europe/Oslo" - -e PRODUCTION_BRANCHES -e SLACK_CHANNEL_NAME + -e PRODUCTION_BRANCHES -e SLACK_CHANNEL_NAME -e TRIGGERED_BY_EMAIL ghcr.io/fivexl/magic-button:${{ env.MAGIC_BUTTON_VERSION }} && ls -all magic-button/reports && cat magic-button/reports/report.json continue-on-error: true @@ -124,7 +127,7 @@ settings: script: - | - mkdir -p magic-button/reports && chmod 777 magic-button/reports && docker run --rm -v "$(pwd)/.git":/app/.git -v "$(pwd)/magic-button/reports":/app/reports -e SLACK_BOT_TOKEN -e SLACK_APP_TOKEN -e BUILD_JOB_NAME -e BUILD_JOB_URL -e CURRENT_GIT_COMMIT="$(git rev-parse HEAD)" -e REPOSITORY_NAME="$(basename $(git rev-parse --show-toplevel))" -e REPOSITORY_URL -e BRANCHES_TO_PROMOTE -e TIMEOUT_MINUTES -e TIMEZONE="Europe/Oslo" -e PRODUCTION_BRANCHES -e SLACK_CHANNEL_NAME ghcr.io/fivexl/magic-button:$MAGIC_BUTTON_VERSION && ls -all magic-button/reports && cat magic-button/reports/report.json + mkdir -p magic-button/reports && chmod 777 magic-button/reports && docker run --rm -v "$(pwd)/.git":/app/.git -v "$(pwd)/magic-button/reports":/app/reports -e SLACK_BOT_TOKEN -e SLACK_APP_TOKEN -e BUILD_JOB_NAME -e BUILD_JOB_URL -e CURRENT_GIT_COMMIT="$(git rev-parse HEAD)" -e REPOSITORY_NAME="$(basename $(git rev-parse --show-toplevel))" -e REPOSITORY_URL -e BRANCHES_TO_PROMOTE -e TIMEOUT_MINUTES -e TIMEZONE="Europe/Oslo" -e PRODUCTION_BRANCHES -e SLACK_CHANNEL_NAME -e TRIGGERED_BY_EMAIL="$GITLAB_USER_EMAIL" ghcr.io/fivexl/magic-button:$MAGIC_BUTTON_VERSION && ls -all magic-button/reports && cat magic-button/reports/report.json after_script: - >