-
Notifications
You must be signed in to change notification settings - Fork 0
143 lines (117 loc) · 5.23 KB
/
Copy pathchangelog-guard.yml
File metadata and controls
143 lines (117 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
name: Changelog Guard
on:
pull_request:
push:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
guard-release-artifacts:
name: Guard release artifacts (changelog)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout (full history)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Enforce release artifact rules
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
GUARD_RELEASE_ARTIFACTS: ${{ vars.GUARD_RELEASE_ARTIFACTS }}
# Optional allowlist for release commit authors (comma-separated).
# Defaults cover common cases (GitHub Actions bot + typical semantic-release bot naming).
# RELEASE_BOT_NAMES: ${{ vars.RELEASE_BOT_NAMES }}
run: |
set -euo pipefail
PROTECTED_FILES=(
"CHANGELOG.md"
)
if [[ "${GUARD_RELEASE_ARTIFACTS:-TRUE}" != "TRUE" ]]; then
echo "ℹ️ Guard disabled via vars.GUARD_RELEASE_ARTIFACTS"
exit 0
fi
if [[ "$EVENT_NAME" == "pull_request" ]]; then
BASE_SHA="$PR_BASE_SHA"
else
BASE_SHA="$PUSH_BEFORE_SHA"
fi
HEAD_SHA="$(git rev-parse HEAD)"
mapfile -t CHANGED < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
if [[ "${#CHANGED[@]}" -eq 0 ]]; then
echo "No file changes detected."
exit 0
fi
PROTECTED_CHANGED=()
for f in "${CHANGED[@]}"; do
for p in "${PROTECTED_FILES[@]}"; do
[[ "$f" == "$p" ]] && PROTECTED_CHANGED+=("$f")
done
done
if [[ "${#PROTECTED_CHANGED[@]}" -eq 0 ]]; then
echo "OK: no protected files changed."
exit 0
fi
echo "Protected files changed:"
printf ' - %s\n' "${PROTECTED_CHANGED[@]}"
# PRs: never allowed
if [[ "$EVENT_NAME" == "pull_request" ]]; then
echo "❌ PRs may not modify release artifacts."
echo " Releases are handled by semantic-release on main (stable) and canary (pre-release)."
exit 1
fi
# Pushes: only allow on release branches (main = stable, canary = pre-release)
if [[ "$REF_NAME" != "main" && "$REF_NAME" != "canary" ]]; then
echo "❌ Release artifacts may only be modified on main or canary."
exit 1
fi
# --------------------------------------------------------------------
# Extra guards only when CHANGELOG.md changed:
# - Commit message must match semantic-release format
# - Commit author must look like a bot/app
# --------------------------------------------------------------------
if printf '%s\n' "${PROTECTED_CHANGED[@]}" | grep -qx "CHANGELOG.md"; then
HEAD_MSG="$(git log -1 --pretty=%B | tr -d '\r')"
HEAD_FIRST_LINE="$(printf '%s' "$HEAD_MSG" | head -n 1)"
AUTHOR_NAME="$(git log -1 --pretty=%an | tr -d '\r')"
AUTHOR_EMAIL="$(git log -1 --pretty=%ae | tr -d '\r')"
echo "HEAD commit first line:"
echo "$HEAD_FIRST_LINE"
echo "HEAD commit author:"
echo " name=$AUTHOR_NAME"
echo " email=$AUTHOR_EMAIL"
# Allow only: "chore(release): X.Y.Z" or "chore(release): X.Y.Z-<pre-release>"
if ! grep -Eq '^chore\(release\): [0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' <<< "$HEAD_FIRST_LINE"; then
echo "❌ CHANGELOG.md updates must be authored by semantic-release."
echo " Expected: chore(release): X.Y.Z or chore(release): X.Y.Z-canary.N"
exit 1
fi
# Author allowlist (override via vars.RELEASE_BOT_NAMES). Comma-separated.
# Default matches common bot/app patterns.
allowlist="${RELEASE_BOT_NAMES:-github-actions[bot],semantic-release-bot}"
is_allowed_author="false"
IFS=',' read -r -a allowed_names <<< "$allowlist"
for n in "${allowed_names[@]}"; do
n_trim="$(echo "$n" | xargs)"
if [[ -n "$n_trim" && "$AUTHOR_NAME" == "$n_trim" ]]; then
is_allowed_author="true"
break
fi
done
# Also allow any GitHub bot/app style author name (ends with [bot])
# and a GitHub noreply email.
if [[ "$AUTHOR_NAME" =~ \[bot\]$ && "$AUTHOR_EMAIL" == *@users.noreply.github.com ]]; then
is_allowed_author="true"
fi
if [[ "$is_allowed_author" != "true" ]]; then
echo "❌ CHANGELOG.md updates on main must be authored by a bot/app."
echo " Allowed names (vars.RELEASE_BOT_NAMES): ${allowlist}"
echo " Or any author ending with [bot] using a @users.noreply.github.com email."
exit 1
fi
fi
echo "✅ Release artifact change authorized (semantic-release)."