Skip to content

Commit 34fce5d

Browse files
committed
changelog and tagging
1 parent 1e9559b commit 34fce5d

1 file changed

Lines changed: 119 additions & 64 deletions

File tree

.github/workflows/next-changelog.yml

Lines changed: 119 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,86 +23,141 @@ jobs:
2323
env:
2424
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2525
run: |
26-
# Use the GitHub API to fetch changed files
27-
files=$(gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path')
28-
29-
# Sanitize to avoid code injection
30-
sanitized_files=$(echo "$files" | sed 's/[^a-zA-Z0-9._/-]/_/g')
31-
32-
# Store the sanitized list of files in a temporary file to avoid env variable issues
33-
echo "$sanitized_files" > modified_files.txt
34-
35-
- name: Fetch PR message
26+
# Use the GitHub API to fetch the list of changed files, one per
27+
# line. Stored as a file so the verify step can read it without
28+
# passing PR-controlled strings through a shell command.
29+
gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path' > modified_files.txt
30+
echo "Changed files:"
31+
cat modified_files.txt
32+
33+
- name: Fetch PR description
3634
id: pr-message
3735
env:
3836
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3937
run: |
40-
# Use the GitHub API to fetch the PR message
41-
pr_message=$(gh pr view ${{ github.event.pull_request.number }} --json body -q '.body')
42-
43-
# Sanitize the PR message to avoid code injection, keeping the equal sign
44-
sanitized_pr_message=$(echo "$pr_message" | sed 's/[^a-zA-Z0-9._/-=]/_/g')
45-
46-
# Store the sanitized PR message
47-
echo "$sanitized_pr_message" > pr_message.txt
48-
49-
- name: Verify NEXT_CHANGELOG.md was modified or PR message contains NO_CHANGELOG=true
38+
# Fetch the PR body; the verify step reads this as a literal string
39+
# in Python, so shell escape rules never apply to its content.
40+
gh pr view ${{ github.event.pull_request.number }} --json body -q '.body' > pr_body.txt
41+
42+
- name: Verify NEXT_CHANGELOG.md was modified for each touched package
5043
run: |
51-
# Read the sanitized files and PR message from the temporary files
52-
modified_files=$(cat modified_files.txt)
53-
pr_message=$(cat pr_message.txt)
54-
55-
# Check if NEXT_CHANGELOG.md exists in the list of changed files
56-
echo "Changed files: $modified_files"
57-
if ! echo "$modified_files" | grep -q "NEXT_CHANGELOG.md"; then
58-
echo "NEXT_CHANGELOG.md not modified."
59-
60-
# Check if PR message contains NO_CHANGELOG=true
61-
if echo "$pr_message" | grep -q "NO_CHANGELOG=true"; then
62-
echo "NO_CHANGELOG=true found in PR message. Skipping changelog check."
63-
exit 0
64-
else
65-
echo "WARNING: file NEXT_CHANGELOG.md not changed. If this is expected, add NO_CHANGELOG=true to the PR message."
66-
exit 1
67-
fi
68-
fi
44+
python3 <<'PYEOF'
45+
import os
46+
import re
47+
import sys
48+
49+
# Read inputs written by the previous steps. Reading as literal
50+
# strings keeps PR-controlled content safely inert.
51+
modified = {l for l in open('modified_files.txt').read().splitlines() if l}
52+
pr_body = open('pr_body.txt').read()
53+
54+
# Parse NO_CHANGELOG directive from the PR body.
55+
# NO_CHANGELOG=true -> skip every package.
56+
# NO_CHANGELOG=<pkg1>,<pkg2>,... -> skip only the listed packages.
57+
skip_all = False
58+
skip_packages = set()
59+
m = re.search(r'NO_CHANGELOG=(\S+)', pr_body)
60+
if m:
61+
val = m.group(1).strip()
62+
if val == 'true':
63+
skip_all = True
64+
else:
65+
skip_packages = {p.strip().rstrip('/') for p in val.split(',') if p.strip()}
66+
67+
if skip_all:
68+
print("NO_CHANGELOG=true found in PR body. Skipping check.")
69+
sys.exit(0)
70+
71+
# Discover packages the same way tagging.py does: any directory that
72+
# contains a .package.json is a release unit with its own changelog.
73+
packages = []
74+
for dirpath, dirnames, filenames in os.walk('.'):
75+
dirnames[:] = [d for d in dirnames if d not in ('.git', 'node_modules', 'dist')]
76+
if '.package.json' in filenames:
77+
rel = os.path.relpath(dirpath, '.')
78+
packages.append('' if rel == '.' else rel)
79+
80+
# Sub-package prefixes so a root-level package doesn't double-count
81+
# files that live under a nested package.
82+
sub_prefixes = tuple(p + '/' for p in packages if p)
6983
70-
- name: Comment on PR with instructions if needed
71-
if: failure() # This step will only run if the previous step fails (i.e., if NEXT_CHANGELOG.md was not modified and NO_CHANGELOG=true was not in the PR message)
84+
# For each package with modified files (excluding its own bookkeeping
85+
# files), require that its NEXT_CHANGELOG.md was also modified.
86+
missing = []
87+
for pkg in packages:
88+
prefix = pkg + '/' if pkg else ''
89+
next_changelog = prefix + 'NEXT_CHANGELOG.md'
90+
changelog = prefix + 'CHANGELOG.md'
91+
package_file = prefix + '.package.json'
92+
in_pkg = [
93+
f for f in modified
94+
if (not pkg or f.startswith(prefix))
95+
and f not in (changelog, package_file)
96+
]
97+
if not pkg:
98+
in_pkg = [f for f in in_pkg if not f.startswith(sub_prefixes)]
99+
if not in_pkg:
100+
continue
101+
if next_changelog not in modified:
102+
missing.append((pkg or '.', next_changelog))
103+
104+
# Honor per-package NO_CHANGELOG overrides.
105+
missing = [(pkg, chg) for pkg, chg in missing if pkg not in skip_packages]
106+
107+
if missing:
108+
# Build the PR comment body and also emit it to the workflow log
109+
# so failures are debuggable without clicking into the PR.
110+
lines = [
111+
"<!-- NEXT_CHANGELOG_INSTRUCTIONS -->",
112+
"One or more packages were modified without updating their `NEXT_CHANGELOG.md`:",
113+
"",
114+
]
115+
for pkg, chg in missing:
116+
lines.append(f"- `{pkg}` — expected a change to `{chg}`")
117+
lines.append("")
118+
lines.append("If this is intentional, add one of the following to the PR description and rerun this job:")
119+
lines.append("")
120+
lines.append("- `NO_CHANGELOG=true` — skip the check for every package.")
121+
suggested = ",".join(pkg for pkg, _ in missing)
122+
lines.append(f"- `NO_CHANGELOG={suggested}` — skip only the packages listed above.")
123+
body = "\n".join(lines) + "\n"
124+
with open('pr_comment.md', 'w') as f:
125+
f.write(body)
126+
sys.stdout.write(body)
127+
sys.exit(1)
128+
129+
print("All modified packages have NEXT_CHANGELOG.md entries.")
130+
PYEOF
131+
132+
- name: Comment on PR with the list of missing packages
133+
if: failure() # Only runs if the verify step failed.
72134
env:
73135
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74136
run: |
75-
# Check if a comment exists with the instructions
137+
# Replace any previous instructions comment so the list reflects the
138+
# current PR state instead of accumulating stale entries.
76139
previous_comment_ids=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
77140
--jq '.[] | select(.body | startswith("<!-- NEXT_CHANGELOG_INSTRUCTIONS -->")) | .id')
78141
echo "Previous comment IDs: $previous_comment_ids"
79-
80-
# If no previous comment exists, add one with instructions
81-
if [ -z "$previous_comment_ids" ]; then
82-
echo "Adding instructions comment."
83-
gh pr comment ${{ github.event.pull_request.number }} --body \
84-
"<!-- NEXT_CHANGELOG_INSTRUCTIONS -->
85-
Please ensure that the NEXT_CHANGELOG.md file is updated with any relevant changes.
86-
If this is not necessary for your PR, please include the following in your PR description:
87-
NO_CHANGELOG=true
88-
and rerun the job."
89-
fi
142+
for id in $previous_comment_ids; do
143+
gh api "repos/${{ github.repository }}/issues/comments/$id" --method DELETE
144+
done
145+
gh pr comment ${{ github.event.pull_request.number }} --body-file pr_comment.md
90146
91147
- name: Delete instructions comment on success
92-
if: success() # This step will only run if the previous check passed (i.e., if NEXT_CHANGELOG.md was modified or NO_CHANGELOG=true is in the PR message)
148+
if: success() # Only runs if the verify step passed.
93149
env:
94150
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95151
run: |
96-
# Check if there is a previous instructions comment
152+
# Clean up any previous instructions comment now that the check is
153+
# green.
97154
previous_comment_ids=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
98155
--jq '.[] | select(.body | startswith("<!-- NEXT_CHANGELOG_INSTRUCTIONS -->")) | .id')
99-
100-
# If a comment exists, delete it
101-
if [ -n "$previous_comment_ids" ]; then
102-
echo "Deleting previous instructions comment."
103-
for comment_id in $previous_comment_ids; do
104-
gh api "repos/${{ github.repository }}/issues/comments/$comment_id" --method DELETE
105-
done
106-
else
107-
echo "No instructions comment found to delete."
108-
fi
156+
if [ -n "$previous_comment_ids" ]; then
157+
echo "Deleting previous instructions comments: $previous_comment_ids"
158+
for id in $previous_comment_ids; do
159+
gh api "repos/${{ github.repository }}/issues/comments/$id" --method DELETE
160+
done
161+
else
162+
echo "No instructions comment found to delete."
163+
fi

0 commit comments

Comments
 (0)