Skip to content

Commit c5df7ee

Browse files
refactor: requirements.txt sync & validation (#261)
## What Did You Change? Refactored the `requirements.txt` sync and validation to avoid false negatives. ## Do You Want Support (syntax, design, etc)? No, I think I'm good. AI wrote it. I directed, reviewed, and tested: 1. [x] Dev **changes** `poetry.lock`. Bot **edits** `requirements.txt`. 2. [x] Bot **changes** `requirements.txt` from target/base branch content. Bot **allows**. 3. [x] Dev **changes** `requirements.txt`. Bot **prevents**. 4. [x] Dev **reverts** `requirements.txt` to target/base branch content. Bot **allows**. ## Any Reference Material Worth Sharing? Tested on #259. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 6573542 commit c5df7ee

8 files changed

Lines changed: 193 additions & 40 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: "Validate requirements"
2+
3+
description: |
4+
Reject direct edits to `requirements.txt` by humans: the action fails if
5+
any commit touching the file in the compare range appears to be authored by a
6+
human or otherwise not from an allowed bot (unless the commit message exactly
7+
matches the canonical bot commit message).
8+
9+
inputs:
10+
allowed_bots:
11+
description: "Comma-separated list of allowed bot author names"
12+
required: false
13+
default: "github-actions[bot],dependabot[bot]"
14+
commit_message_file:
15+
description: "Path to file that contains canonical commit message (exact match)"
16+
required: false
17+
default: ".github/commit-messages/requirements_update.txt"
18+
19+
runs:
20+
using: "composite"
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Run requirements check
28+
shell: bash
29+
env:
30+
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
31+
COMMIT_MSG_FILE: ${{ inputs.commit_message_file }}
32+
run: |
33+
bash ./.github/actions/validate-requirements/check.sh
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
5+
# Mode: "validate" (default, exits non-zero on human edit) or "detect" (outputs to GITHUB_OUTPUT)
6+
MODE="${MODE:-validate}"
7+
ALLOWED_BOTS="${ALLOWED_BOTS:-github-actions[bot],dependabot[bot]}"
8+
9+
# Determine the comparison range
10+
is_pr=""
11+
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ]; then
12+
is_pr=1
13+
fi
14+
has_base_ref=""
15+
if [ -n "${GITHUB_BASE_REF:-}" ]; then
16+
has_base_ref=1
17+
fi
18+
origin_base_ref_exists=""
19+
if [ -n "${GITHUB_BASE_REF:-}" ] && git rev-parse --verify "origin/${GITHUB_BASE_REF}" >/dev/null 2>&1; then
20+
origin_base_ref_exists=1
21+
fi
22+
if [ -n "$is_pr" ] && [ -n "$has_base_ref" ] && [ -n "$origin_base_ref_exists" ]; then
23+
BASE_REF="$(git rev-parse "origin/${GITHUB_BASE_REF}")"
24+
COMPARE_RANGE="$BASE_REF...HEAD"
25+
else
26+
COMPARE_RANGE="HEAD~1..HEAD"
27+
fi
28+
29+
# Check if requirements.txt changed
30+
if ! git diff --name-only $COMPARE_RANGE | grep -q "^requirements.txt$"; then
31+
echo "'requirements.txt' unchanged"
32+
if [ "$MODE" = "detect" ]; then
33+
echo "is_human_edit=false" >> "${GITHUB_OUTPUT:-/dev/stdout}"
34+
fi
35+
exit 0
36+
fi
37+
38+
# Check if requirements.txt differs from base (net change across all commits in range)
39+
if [ -n "$is_pr" ] && [ -n "$has_base_ref" ] && [ -n "$origin_base_ref_exists" ]; then
40+
BASE_REF_PARSED="origin/${GITHUB_BASE_REF}"
41+
if git diff --quiet "$BASE_REF_PARSED" HEAD -- requirements.txt; then
42+
echo "requirements.txt touched but matches base branch (likely reverted): OK"
43+
if [ "$MODE" = "detect" ]; then
44+
echo "is_human_edit=false" >> "${GITHUB_OUTPUT:-/dev/stdout}"
45+
fi
46+
exit 0
47+
fi
48+
fi
49+
50+
# Get latest commit that touched requirements.txt
51+
latest_sha=$(git log -1 --pretty=format:'%H' $COMPARE_RANGE -- requirements.txt || true)
52+
53+
if [ -z "$latest_sha" ]; then
54+
echo "::error::No commits found touching requirements.txt in range $COMPARE_RANGE"
55+
if [ "$MODE" = "detect" ]; then
56+
echo "is_human_edit=false" >> "${GITHUB_OUTPUT:-/dev/stdout}"
57+
fi
58+
exit 1
59+
fi
60+
61+
latest_author=$(git show -s --format='%an' "$latest_sha")
62+
latest_committer=$(git show -s --format='%cn' "$latest_sha")
63+
latest_message=$(git show -s --format='%B' "$latest_sha")
64+
latest_subject=$(echo "$latest_message" | head -n1 | sed -e 's/[[:space:]]*$//')
65+
66+
# Build a grep-friendly regex from comma-separated allowed bots
67+
allowed_regex=$(echo "$ALLOWED_BOTS" | sed 's/,/\\|/g')
68+
69+
# Check 1: author or committer is allowed bot
70+
if echo "$latest_author" | grep -qE "^($allowed_regex)$" || echo "$latest_committer" | grep -qE "^($allowed_regex)$"; then
71+
echo "Latest change by allowed bot: OK"
72+
if [ "$MODE" = "detect" ]; then
73+
echo "is_human_edit=false" >> "${GITHUB_OUTPUT:-/dev/stdout}"
74+
fi
75+
exit 0
76+
fi
77+
78+
# Check 2: commit message exactly matches canonical message
79+
if [ -n "${COMMIT_MSG_FILE:-}" ] && [ -f "$COMMIT_MSG_FILE" ]; then
80+
canonical_msg=$(sed -n '1p' "$COMMIT_MSG_FILE" | tr -d '\r')
81+
if [ "$latest_subject" = "$canonical_msg" ]; then
82+
echo "Latest commit message exactly matches canonical bot message: OK"
83+
if [ "$MODE" = "detect" ]; then
84+
echo "is_human_edit=false" >> "${GITHUB_OUTPUT:-/dev/stdout}"
85+
fi
86+
exit 0
87+
fi
88+
fi
89+
90+
# Human edit detected
91+
if [ "$MODE" = "detect" ]; then
92+
echo "is_human_edit=true" >> "${GITHUB_OUTPUT:-/dev/stdout}"
93+
echo "offender_author=$latest_author" >> "${GITHUB_OUTPUT:-/dev/stdout}"
94+
echo "offender_subject=$latest_subject" >> "${GITHUB_OUTPUT:-/dev/stdout}"
95+
echo "Human edit detected"
96+
exit 0
97+
else
98+
echo "::error::You may NOT edit 'requirements.txt'"
99+
echo "::warning::Undo your changes to requirements.txt, so robot can maintain it."
100+
echo "::notice::To pin dependencies, use 'poetry add <package-name>'."
101+
echo "Latest commit: $latest_sha"
102+
echo "Latest author: $latest_author"
103+
echo "Latest committer: $latest_committer"
104+
echo "Latest message: \"$latest_subject\""
105+
exit 1
106+
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
chore: auto-update requirements.txt [bot]

.github/workflows/requirements-validate.yml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
jobs:
1010
reject-requirements-drift:
1111
runs-on: ubuntu-latest
12+
env:
13+
COMMIT_MSG_FILE: .github/commit-messages/requirements_update.txt
1214

1315
# Skip if the last commit was from the bot (prevent unnecessary check)
1416
if: github.event.head_commit.author.name != 'github-actions[bot]'
@@ -19,23 +21,9 @@ jobs:
1921
with:
2022
fetch-depth: 0 # full history
2123

22-
- name: Check if requirements.txt was modified unexpectedly
23-
run: |
24-
# For PRs, check against base branch
25-
# For pushes, check last commit
26-
if [ "${{ github.event_name }}" = "pull_request" ]; then
27-
BASE_REF="${{ github.event.pull_request.base.sha }}"
28-
COMPARE_RANGE="$BASE_REF...HEAD"
29-
else
30-
COMPARE_RANGE="HEAD~1..HEAD"
31-
fi
32-
33-
# If requirements.txt modified in that range
34-
if git diff --name-only $COMPARE_RANGE | grep -q "^requirements.txt$"; then
35-
echo "::error::You may NOT edit 'requirements.txt'"
36-
echo "::warning::Undo your changes to requirements.txt, so robot can maintain it."
37-
echo "::notice::To pin dependencies, use 'poetry add <package-name>'."
38-
exit 1
39-
fi
24+
- name: Validate requirements
25+
uses: ./.github/actions/validate-requirements
26+
with:
27+
allowed_bots: 'github-actions[bot],dependabot[bot]'
28+
commit_message_file: ${{ env.COMMIT_MSG_FILE }}
4029

41-
echo "'requirements.txt' unchanged (or only changed by bot)"

.github/workflows/requirments-sync.yml

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,37 @@ jobs:
4949
echo "has_change=true" >> $GITHUB_OUTPUT
5050
fi
5151
52-
commit-requirements-delta:
52+
prevent-bot-commit-if-human-edit:
5353
runs-on: ubuntu-latest
5454
needs: detect-requirements-delta
5555
if: needs.detect-requirements-delta.outputs.has_change == 'true'
56+
outputs:
57+
is_human_edit: ${{ steps.check.outputs.is_human_edit }}
58+
offender_author: ${{ steps.check.outputs.offender_author }}
59+
offender_subject: ${{ steps.check.outputs.offender_subject }}
60+
env:
61+
ALLOWED_BOTS: 'github-actions[bot],dependabot[bot]'
62+
COMMIT_MSG_FILE: .github/commit-messages/requirements_update.txt
63+
64+
steps:
65+
- name: Checkout code
66+
uses: actions/checkout@v4
67+
with:
68+
ref: ${{ github.head_ref || github.ref_name }}
69+
fetch-depth: 0
70+
71+
- name: Check for human edits
72+
id: check
73+
env:
74+
MODE: detect
75+
run: bash ./.github/actions/validate-requirements/check.sh
76+
77+
commit-requirements-delta:
78+
runs-on: ubuntu-latest
79+
needs: [detect-requirements-delta, prevent-bot-commit-if-human-edit]
80+
if: needs.detect-requirements-delta.outputs.has_change == 'true' && needs.prevent-bot-commit-if-human-edit.outputs.is_human_edit == 'false'
81+
env:
82+
COMMIT_MSG_FILE: .github/commit-messages/requirements_update.txt
5683

5784
steps:
5885
- name: Checkout code
@@ -83,6 +110,11 @@ jobs:
83110
if git diff --staged --quiet; then
84111
echo "No changes to requirements.txt"
85112
else
86-
git commit -m "chore: auto-update requirements.txt [bot]"
113+
commit_msg=$(sed -n '1p' "$COMMIT_MSG_FILE" 2>/dev/null | tr -d '\r')
114+
if [ -z "$commit_msg" ]; then
115+
echo "::error::Missing or empty canonical commit message file: $COMMIT_MSG_FILE"
116+
exit 1
117+
fi
118+
git commit -m "$commit_msg"
87119
git push
88120
fi

.github/workflows/validate-requirements.yml

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,18 @@ on:
77
jobs:
88
check-requirements:
99
runs-on: ubuntu-latest
10+
env:
11+
COMMIT_MSG_FILE: .github/commit-messages/requirements_update.txt
1012

1113
steps:
1214
- name: Checkout code
1315
uses: actions/checkout@v4
1416
with:
15-
fetch-depth: 2
17+
fetch-depth: 0
1618

17-
- name: Check if requirements.txt was modified unexpectedly
18-
run: |
19-
# Get author of last commit
20-
AUTHOR=$(git log -1 --pretty=format:'%an')
21-
22-
# Check if requirements.txt was modified in last commit
23-
if git diff --name-only HEAD~1 HEAD | grep -q "^requirements.txt$"; then
24-
if [ "$AUTHOR" != "github-actions[bot]" ]; then
25-
echo "❌ ERROR: You may NOT edit `requirements.txt`"
26-
echo "To pin dependencies, use `poetry add <package-name>`."
27-
echo "Please remove your changes to requirements.txt, so the robot can maintain it."
28-
exit 1
29-
fi
30-
fi
19+
- name: Validate requirements
20+
uses: ./.github/actions/validate-requirements
21+
with:
22+
allowed_bots: 'github-actions[bot],dependabot[bot]'
23+
commit_message_file: ${{ env.COMMIT_MSG_FILE }}
3124

32-
echo "✅ SUCCESS: `requirements.txt` not modified unexpectedly"

poetry.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ mkdocs-exclude-search==0.6.6 ; python_version >= "3.10" and python_version < "3.
115115
mkdocs-include-markdown-plugin==5.1.0 ; python_version >= "3.10" and python_version < "3.13" \
116116
--hash=sha256:4a1b8d79a0e1b6fd357ca8013a6d1701c755ada4acb74ee97b0642d1afe6756e \
117117
--hash=sha256:e9ca188ab1d86f5fc4a6b96ce8c85acf6e25f114897868041056ec7945f29f65
118-
mkdocs-tacc==1.0.0 ; python_version >= "3.10" and python_version < "3.13" \
119-
--hash=sha256:5d9f1d4a4b871526f74e92bda8eb52584ece817d1eef5d4064ef40fe6adcf99d \
120-
--hash=sha256:cbd107eab1ff1659bc164c84f17055f367097a0b3dfe2ec3b41ef34850f7181c
118+
mkdocs-tacc==1.0.1 ; python_version >= "3.10" and python_version < "3.13" \
119+
--hash=sha256:08b8e0b1ab5bdcdfea493c2f18077723b36569afdd71a23a5d097fb7942799ea \
120+
--hash=sha256:f14ac8d3833bb43447cdb91210f6179e85fe5bef92f7d948ac9c50e41ec8e6c0
121121
mkdocs==1.4.3 ; python_version >= "3.10" and python_version < "3.13" \
122122
--hash=sha256:5955093bbd4dd2e9403c5afaf57324ad8b04f16886512a3ee6ef828956481c57 \
123123
--hash=sha256:6ee46d309bda331aac915cd24aab882c179a933bd9e77b80ce7d2eaaa3f689dd

0 commit comments

Comments
 (0)