Skip to content

Commit aaafde9

Browse files
authored
chore(ci): harden runtime angular release workflow (#7)
1 parent 41929ea commit aaafde9

9 files changed

Lines changed: 510 additions & 40 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
BASE_REF="${1:-}"
5+
HEAD_REF="${2:-HEAD}"
6+
7+
if [[ -z "${BASE_REF}" ]]; then
8+
echo "Usage: $0 <base-ref> [head-ref]"
9+
exit 2
10+
fi
11+
12+
extract_unreleased() {
13+
local ref="$1"
14+
local file="$2"
15+
git show "${ref}:${file}" 2>/dev/null | awk '
16+
BEGIN { in_section = 0 }
17+
/^## \[Unreleased\][[:space:]]*$/ { in_section = 1; next }
18+
in_section && /^## / { exit }
19+
in_section { print }
20+
' || true
21+
}
22+
23+
normalize_block() {
24+
sed 's/[[:space:]]*$//' | sed '/^[[:space:]]*$/d'
25+
}
26+
27+
CHANGED_FILES="$(git diff --name-only "${BASE_REF}...${HEAD_REF}")"
28+
29+
if [[ -z "${CHANGED_FILES}" ]]; then
30+
echo "No files changed, skipping changelog gate."
31+
exit 0
32+
fi
33+
34+
CODE_CHANGED="false"
35+
while IFS= read -r file; do
36+
[[ -z "${file}" ]] && continue
37+
if [[ "${file}" =~ ^projects/ ]] \
38+
|| [[ "${file}" =~ ^\.github/workflows/ ]] \
39+
|| [[ "${file}" =~ ^\.github/scripts/ ]] \
40+
|| [[ "${file}" == "package.json" ]] \
41+
|| [[ "${file}" == "package-lock.json" ]] \
42+
|| [[ "${file}" == "angular.json" ]] \
43+
|| [[ "${file}" =~ ^tsconfig[^/]*\.json$ ]]; then
44+
CODE_CHANGED="true"
45+
break
46+
fi
47+
done <<< "${CHANGED_FILES}"
48+
49+
if [[ "${CODE_CHANGED}" != "true" ]]; then
50+
echo "No code-impacting files changed, skipping changelog gate."
51+
exit 0
52+
fi
53+
54+
validate_changelog_file() {
55+
local file="$1"
56+
if [[ ! -f "${file}" ]]; then
57+
echo "Missing required changelog file: ${file}"
58+
exit 1
59+
fi
60+
61+
if ! grep -qx "${file}" <<< "${CHANGED_FILES}"; then
62+
echo "Code changed, but ${file} was not updated."
63+
echo "Please add release notes under '## [Unreleased]'."
64+
exit 1
65+
fi
66+
67+
local head_unreleased
68+
local base_unreleased
69+
head_unreleased="$(extract_unreleased "${HEAD_REF}" "${file}" | normalize_block || true)"
70+
base_unreleased="$(extract_unreleased "${BASE_REF}" "${file}" | normalize_block || true)"
71+
72+
if [[ -z "${head_unreleased}" ]]; then
73+
echo "${file} has no content under '## [Unreleased]'."
74+
exit 1
75+
fi
76+
77+
if ! grep -Eq '^[-*][[:space:]]+' <<< "${head_unreleased}"; then
78+
echo "${file} Unreleased section must contain at least one bullet item."
79+
exit 1
80+
fi
81+
82+
if [[ "${head_unreleased}" == "${base_unreleased}" ]]; then
83+
echo "${file} Unreleased section was not changed for a code-impacting PR."
84+
exit 1
85+
fi
86+
}
87+
88+
validate_changelog_file "CHANGELOG.md"
89+
validate_changelog_file "CHANGELOG.zh-CN.md"
90+
91+
echo "Changelog gate passed."
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
CHANGELOG_FILE="${1:-CHANGELOG.md}"
5+
OUTPUT_FILE="${2:-}"
6+
7+
if [[ ! -f "${CHANGELOG_FILE}" ]]; then
8+
echo "Changelog file not found: ${CHANGELOG_FILE}"
9+
exit 1
10+
fi
11+
12+
UNRELEASED_CONTENT="$(
13+
awk '
14+
BEGIN { in_section = 0 }
15+
/^## \[Unreleased\][[:space:]]*$/ { in_section = 1; next }
16+
in_section && /^## / { exit }
17+
in_section { print }
18+
' "${CHANGELOG_FILE}" | sed 's/[[:space:]]*$//' | sed '/^[[:space:]]*$/d'
19+
)"
20+
21+
if [[ -z "${UNRELEASED_CONTENT}" ]]; then
22+
echo "No content found under ## [Unreleased] in ${CHANGELOG_FILE}."
23+
if [[ -n "${OUTPUT_FILE}" ]]; then
24+
: > "${OUTPUT_FILE}"
25+
fi
26+
exit 2
27+
fi
28+
29+
if [[ -n "${OUTPUT_FILE}" ]]; then
30+
printf '%s\n' "${UNRELEASED_CONTENT}" > "${OUTPUT_FILE}"
31+
else
32+
printf '%s\n' "${UNRELEASED_CONTENT}"
33+
fi
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
VERSION="${1:-}"
5+
BODY_FILE="${2:-}"
6+
CHANGELOG_FILE="${3:-CHANGELOG.md}"
7+
8+
if [[ -z "${VERSION}" ]]; then
9+
echo "Usage: $0 <version> [body-file] [changelog-file]"
10+
exit 2
11+
fi
12+
13+
if [[ ! -f "${CHANGELOG_FILE}" ]]; then
14+
echo "Changelog file not found: ${CHANGELOG_FILE}"
15+
exit 1
16+
fi
17+
18+
extract_unreleased() {
19+
awk '
20+
BEGIN { in_section = 0 }
21+
/^## \[Unreleased\][[:space:]]*$/ { in_section = 1; next }
22+
in_section && /^## / { exit }
23+
in_section { print }
24+
' "${CHANGELOG_FILE}" | sed 's/[[:space:]]*$//' | sed '/^[[:space:]]*$/d'
25+
}
26+
27+
BODY_CONTENT=""
28+
if [[ -n "${BODY_FILE}" && -f "${BODY_FILE}" ]]; then
29+
BODY_CONTENT="$(sed 's/[[:space:]]*$//' "${BODY_FILE}" | sed '/^[[:space:]]*$/d' || true)"
30+
fi
31+
32+
if [[ -z "${BODY_CONTENT}" ]]; then
33+
BODY_CONTENT="$(extract_unreleased)"
34+
fi
35+
36+
if [[ -z "${BODY_CONTENT}" ]]; then
37+
echo "No release notes available from body file or Unreleased section."
38+
exit 1
39+
fi
40+
41+
TMP_REST="$(mktemp)"
42+
TMP_PREAMBLE="$(mktemp)"
43+
TMP_NEW="$(mktemp)"
44+
RELEASE_DATE="$(date -u +%Y-%m-%d)"
45+
46+
awk '
47+
BEGIN { state = 0 }
48+
state == 0 && /^## \[Unreleased\][[:space:]]*$/ {
49+
state = 1
50+
next
51+
}
52+
state == 0 {
53+
print >> preamble
54+
next
55+
}
56+
state == 1 {
57+
if (/^## /) {
58+
state = 2
59+
print >> rest
60+
}
61+
next
62+
}
63+
state == 2 {
64+
print >> rest
65+
next
66+
}
67+
' preamble="${TMP_PREAMBLE}" rest="${TMP_REST}" "${CHANGELOG_FILE}"
68+
69+
{
70+
cat "${TMP_PREAMBLE}"
71+
if [[ -s "${TMP_PREAMBLE}" ]]; then
72+
printf '\n'
73+
fi
74+
printf '## [Unreleased]\n\n'
75+
printf '## %s (%s)\n\n' "${VERSION}" "${RELEASE_DATE}"
76+
printf '%s\n\n' "${BODY_CONTENT}"
77+
cat "${TMP_REST}"
78+
} > "${TMP_NEW}"
79+
80+
mv "${TMP_NEW}" "${CHANGELOG_FILE}"
81+
rm -f "${TMP_REST}" "${TMP_PREAMBLE}"
82+
83+
echo "Changelog finalized for version ${VERSION}."

.github/workflows/ci.yml

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,68 @@
1-
name: Lint, Test, Build
1+
name: CI
22

33
on:
44
pull_request:
5-
branches: [main]
5+
branches:
6+
- main
67
push:
7-
branches: [main]
8+
branches:
9+
- main
810

9-
permissions:
10-
contents: read
11+
env:
12+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
1117

1218
jobs:
13-
ci:
19+
quality:
20+
name: Format, Test, Build
1421
runs-on: ubuntu-latest
15-
env:
16-
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
22+
permissions:
23+
contents: read
1724
steps:
1825
- name: Checkout
1926
uses: actions/checkout@v5
2027

21-
- name: Setup Node.js
28+
- name: Setup Node
2229
uses: actions/setup-node@v5
2330
with:
24-
node-version: "24"
31+
node-version: 24.3.0
2532
cache: npm
2633

34+
- name: Pin npm version
35+
run: npm i -g npm@11.4.2
36+
2737
- name: Install dependencies
2838
run: npm ci
2939

30-
- name: Build
31-
run: npm run build
40+
- name: Lockfile drift gate
41+
run: git diff --exit-code package-lock.json
42+
43+
- name: Format gate
44+
run: npm run lint
3245

3346
- name: Test
3447
run: npm run test -- --watch=false
48+
49+
- name: Build package
50+
run: npm run build
51+
52+
changelog-gate:
53+
name: Changelog gate
54+
if: ${{ github.event_name == 'pull_request' }}
55+
runs-on: ubuntu-latest
56+
permissions:
57+
contents: read
58+
steps:
59+
- name: Checkout
60+
uses: actions/checkout@v5
61+
with:
62+
fetch-depth: 0
63+
64+
- name: Validate Unreleased update for code-impacting changes
65+
run: |
66+
bash .github/scripts/check_unreleased_update.sh \
67+
"${{ github.event.pull_request.base.sha }}" \
68+
"${{ github.event.pull_request.head.sha }}"

0 commit comments

Comments
 (0)