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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Shell scripts must be LF so their shebang and heredocs work on Linux runners
# (Windows checkouts would otherwise commit CRLF and break `#!/usr/bin/env bash`).
*.sh text eol=lf
66 changes: 66 additions & 0 deletions actions/check_jira_key/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Check Jira Key
description: >-
Required-status-check enforcement of the UID2 Jira-key rule for IABTechLab uid2* repos
(UID2-7426). Fails a PR unless the squash commit that will land carries a UID2-<n> key or a
reasoned [no-jira - reason: <reason>] opt-out.
#
# WHY AN ACTION (not a native ruleset): IABTechLab is a GitHub team plan, where commit-metadata
# ruleset rules (commit_message_pattern) are Enterprise-gated and never evaluated. UnifiedID2 / EUID
# enforce the same rule natively (UID2-7312); IABTechLab uses this Action + a required status check.
#
# SQUASH-CONFIG ASSUMPTION: assumes squash_merge_commit_title=COMMIT_OR_PR_TITLE and
# squash_merge_commit_message=COMMIT_MESSAGES (the config all gated repos use). The workflow token
# CANNOT read squash_merge_commit_* (they return null without admin, and `administration` is not a
# grantable workflow permission), so the config is assumed, not read — with a runtime tripwire below
# that fails loudly if the settings ARE readable and differ from the assumption.
#
# ROLLOUT PREREQUISITES before wiring as a REQUIRED check (UID2-7426 — do not skip):
# 1. Every gated repo is COMMIT_OR_PR_TITLE + COMMIT_MESSAGES (enforced via terraform so it can't
# drift). uid2-android-sdk (was PR_TITLE/PR_BODY) is normalized to match.
# 2. require-squash-merge is active (else a rebase-merge can land raw keyless commits).
# 3. commit_pr_and_merge waits for checks, or the release actor is bypassed on the required-check
# rule (its immediate merge PUT would otherwise 405).
#
# Callers pin the job name `check_jira_key` so all repos produce one status-check context
# ("check_jira_key") for a single terraform required_status_checks entry.
runs:
using: composite
steps:
- name: Require a UID2 Jira key or reasoned opt-out in the landed squash commit
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ACTION_PATH: ${{ github.action_path }}
run: |
set -euo pipefail

if [ -z "${PR_NUMBER:-}" ] || [ "${PR_NUMBER}" = "null" ]; then
echo "::error::check_jira_key must run on a pull_request event (no PR number in context)."
exit 1
fi

# Runtime tripwire (zero cost on the normal path). The check ASSUMES COMMIT_OR_PR_TITLE +
# COMMIT_MESSAGES; the token usually can't read these (null -> unreadable -> trust the
# terraform/audit enforcement and proceed). But if they ARE readable and DIFFER, fail loudly
# — that turns "an admin flips the setting in the UI and the check silently evaluates the
# wrong text for months" into an immediate red check. `|| true` so a read failure is treated
# as unreadable (proceed), not a crash.
TMODE=""; MMODE=""
read -r TMODE MMODE < <(gh api "repos/${REPO}" --jq '[(.squash_merge_commit_title // "null"), (.squash_merge_commit_message // "null")] | @tsv' 2>/dev/null) || true
if [ -n "$TMODE" ] && [ "$TMODE" != "null" ] && { [ "$TMODE" != "COMMIT_OR_PR_TITLE" ] || [ "$MMODE" != "COMMIT_MESSAGES" ]; }; then
echo "::error::${REPO} squash settings are title=${TMODE}, message=${MMODE}, but this check assumes COMMIT_OR_PR_TITLE / COMMIT_MESSAGES. Restore them (UID2-7426 requires a uniform squash config) or update the check."
exit 1
fi

# Gather inputs for the pure matcher. Title fetched LIVE (not the frozen github.event payload)
# so a manual re-run after a title edit re-evaluates the current title.
PR_TITLE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.title // ""')
COMMITS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/commits?per_page=100")
COMMIT_COUNT=$(printf '%s' "$COMMITS" | jq 'length')
ALL_MSGS=$(printf '%s' "$COMMITS" | jq -r '.[].commit.message')
FIRST_LINE=$(printf '%s' "$COMMITS" | jq -r '(.[0].commit.message // "") | split("\n")[0]')

export PR_TITLE COMMIT_COUNT ALL_MSGS FIRST_LINE
bash "${ACTION_PATH}/match.sh"
56 changes: 56 additions & 0 deletions actions/check_jira_key/match.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Pure matcher for the check_jira_key composite action (UID2-7426). Reads its inputs from the
# environment and does NO network I/O, so it is unit-testable (tests/match.test.sh). action.yaml
# gathers the inputs (gh api) + the squash-config tripwire, then calls this.
#
# Assumes the repo squashes with squash_merge_commit_title=COMMIT_OR_PR_TITLE and
# squash_merge_commit_message=COMMIT_MESSAGES (see action.yaml). Under that config the squash commit
# GitHub lands is: subject = the single commit's first line (1-commit PR) or the PR title
# (multi-commit); body = all commit messages concatenated.
#
# Inputs (env): PR_TITLE, COMMIT_COUNT, ALL_MSGS, FIRST_LINE.
set -uo pipefail
export LC_ALL=C.UTF-8 # grep -P (PCRE) errors under a non-UTF-8 locale

# SINGLE SOURCE OF TRUTH regex — kept byte-identical to the self-assertion in
# actions/commit_pr_and_merge/action.yaml (enforced by tests/regex_identity.test.sh) and to
# local.jira_key_pattern in uid2-okta-configuration. The \b blocks FOOUID2-1; \S blocks a blank reason.
PATTERN='\bUID2-[0-9]+|\[no-jira - reason:\s*\S[^\]]*\]'

PR_TITLE="${PR_TITLE:-}"
COMMIT_COUNT="${COMMIT_COUNT:-0}"
ALL_MSGS="${ALL_MSGS:-}"
FIRST_LINE="${FIRST_LINE:-}"

# Reconstruct the squash commit under the assumed config.
if [ "$COMMIT_COUNT" -eq 1 ]; then SUBJECT="$FIRST_LINE"; else SUBJECT="$PR_TITLE"; fi
BODY="$ALL_MSGS"

# Search subject and body INDEPENDENTLY — never concatenate. For a single-commit PR the subject
# (first line) is contained in the body (its full message); concatenating could create a spurious
# cross-boundary match (e.g. a blank "[no-jira - reason: ]" pairing its own "]" with the body copy).
if printf '%s' "$SUBJECT" | grep -Pq "$PATTERN" || printf '%s' "$BODY" | grep -Pq "$PATTERN"; then
echo "✓ A UID2-<n> key or reasoned [no-jira - reason: …] opt-out is present in the commit that will land."
exit 0
fi

if [ "$COMMIT_COUNT" -eq 1 ]; then
WHATLANDS="your **single commit's message**"
FIX="reword the commit (e.g. \`git commit --amend\`) to include it"
else
WHATLANDS="the **PR title** or one of your **commit messages**"
FIX="put it in the PR title or a commit message"
fi
echo "::error title=Missing Jira key::The squash commit that will merge to the default branch has no UID2-<n> key and no reasoned opt-out."
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### ❌ Jira-key check failed"
echo ""
echo "Every change merged to the default branch must carry, **in the commit that lands**, either a \`UID2-<n>\` key (uppercase) or a reasoned \`[no-jira - reason: <reason>]\` opt-out (lowercase, reason mandatory)."
echo ""
echo "Your PR has **${COMMIT_COUNT}** commit(s), so what lands is ${WHATLANDS}."
echo ""
echo "Fix: ${FIX}."
} >> "$GITHUB_STEP_SUMMARY"
fi
exit 1
32 changes: 32 additions & 0 deletions actions/check_jira_key/tests/match.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Unit tests for ../match.sh — run: bash actions/check_jira_key/tests/match.test.sh
# Auto-run in CI by .github/workflows/test-scripts.yaml (actions/**/tests/*.sh).
set -uo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/match.sh"
fail=0

# check: desc expected_rc PR_TITLE COMMIT_COUNT ALL_MSGS FIRST_LINE
# expected_rc 0 = pass (key/opt-out present in what lands), 1 = fail (missing)
check() {
local desc="$1" exp="$2" title="$3" count="$4" allmsgs="$5" first="$6" rc
PR_TITLE="$title" COMMIT_COUNT="$count" ALL_MSGS="$allmsgs" FIRST_LINE="$first" \
bash "$SCRIPT" >/dev/null 2>&1
rc=$?
if [ "$rc" -eq "$exp" ]; then echo "ok - $desc"; else echo "FAIL - $desc: expected rc=$exp got $rc"; fail=1; fi
}

check "single-commit, key in commit" 0 "misc" 1 "UID2-1234 fix" "UID2-1234 fix"
check "single-commit, key ONLY in PR title" 1 "UID2-1234 fix" 1 "fix thing" "fix thing"
check "CI release marker in commit" 0 "[CI Pipeline] Released" 1 "[CI Pipeline] Released 5.70.207 [no-jira - reason: automated release v5.70.207]" "[CI Pipeline] Released 5.70.207 [no-jira - reason: automated release v5.70.207]"
check "multi-commit, key in PR title" 0 "UID2-7056 deploy" 2 "$(printf 'fix a\nfix b')" "fix a"
check "multi-commit, key in one commit" 0 "chore" 2 "$(printf 'bump x\nUID2-9 tweak')" "bump x"
check "single-commit, key in commit body" 0 "fix" 1 "$(printf 'fix thing\n\nUID2-99 in body')" "fix thing"
check "no key anywhere" 1 "update stuff" 2 "$(printf 'update stuff\nmore')" "update stuff"
check "blank opt-out reason fails" 1 "x" 1 "cleanup [no-jira - reason: ]" "cleanup [no-jira - reason: ]"
check "reasoned opt-out in commit" 0 "x" 1 "cleanup [no-jira - reason: automated dep bump]" "cleanup [no-jira - reason: automated dep bump]"
check "FOOUID2-1 must not satisfy" 1 "x" 1 "FOOUID2-1 sneaky" "FOOUID2-1 sneaky"
check "tf-modules bump marker" 0 "Update tf" 1 "chore: update terraform_modules_version to v1.453 [no-jira - reason: automated tf-modules bump v1.453]" "chore: update terraform_modules_version to v1.453 [no-jira - reason: automated tf-modules bump v1.453]"

echo ""
if [ "$fail" -eq 0 ]; then echo "match.test.sh: ALL PASS"; else echo "match.test.sh: FAILURES"; fi
exit "$fail"
26 changes: 26 additions & 0 deletions actions/check_jira_key/tests/regex_identity.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Drift guard (UID2-7426): the SSOT Jira-key regex must be byte-identical across its in-repo copies
# — the check_jira_key matcher and the commit_pr_and_merge self-assertion. This test READS the real
# files (not a copy), so it cannot itself drift. Auto-run by .github/workflows/test-scripts.yaml.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
MATCH="$HERE/../match.sh"
CPAM="$HERE/../../commit_pr_and_merge/action.yaml"
fail=0

# Extract the single-quoted regex string that contains "UID2-" from a file (ERE, no PCRE needed).
extract() { grep -oE "'[^']*UID2-[^']*'" "$1" | head -1 | sed "s/^'//; s/'\$//"; }

A="$(extract "$MATCH")"
B="$(extract "$CPAM")"
echo "check_jira_key/match.sh : $A"
echo "commit_pr_and_merge/action: $B"

if [ -z "$A" ]; then echo "FAIL - could not extract regex from match.sh"; fail=1; fi
if [ -z "$B" ]; then echo "FAIL - could not extract regex from commit_pr_and_merge/action.yaml"; fail=1; fi
if [ -n "$A" ] && [ "$A" = "$B" ]; then
echo "ok - SSOT regex is byte-identical across both copies"
else
echo "FAIL - SSOT regex drift between the two in-repo copies"; fail=1
fi
exit "$fail"
2 changes: 1 addition & 1 deletion actions/commit_pr_and_merge/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ runs:
# merge rejection across every @v3 consumer ("all releases blocked").
# Assert against the exact pattern so drift fails here instead. Pin a
# UTF-8 locale: grep -P (PCRE) errors under a non-UTF-8 locale.
if ! printf '%s' "$message" | LC_ALL=C.UTF-8 grep -Pq 'UID2-[0-9]+|\[no-jira - reason:\s*\S[^\]]*\]'; then
if ! printf '%s' "$message" | LC_ALL=C.UTF-8 grep -Pq '\bUID2-[0-9]+|\[no-jira - reason:\s*\S[^\]]*\]'; then
echo "::error::composed commit message does not satisfy the require_jira_key ruleset: ${message}"
exit 1
fi
Expand Down