chore: add unpinned dependency check script and CI job#2143
chore: add unpinned dependency check script and CI job#2143joshuagibeonm wants to merge 3 commits intomainfrom
Conversation
Adds script/check-unpinned-deps.sh to detect ^ ~ range operators in web/e2e package.json files. Embeds a check-deps job in ci.yml that runs on every push/PR and blocks merge via the ci gate.
There was a problem hiding this comment.
Pull request overview
Adds a repository script and CI gate to prevent unpinned (range) dependency versions from being introduced into web/package.json and e2e/package.json, improving build reproducibility.
Changes:
- Added
script/check-unpinned-deps.shto detect unpinned dependency specifiers viajq. - Added a new
check-depsjob to.github/workflows/ci.yml. - Updated the
cigate job to depend oncheck-deps.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| script/check-unpinned-deps.sh | New bash script that scans dependency sections for unpinned version patterns and exits non-zero on violations. |
| .github/workflows/ci.yml | Adds a check-deps job and gates the aggregate ci job on it. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
script/check-unpinned-deps.sh
Outdated
| local unpinned | ||
| unpinned=$(jq -r ' | ||
| ["dependencies","devDependencies","peerDependencies","optionalDependencies"] as $sections | | ||
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*xX]|>=|<=|>[^=]|<[^=]|\\|\\||^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | | ||
| sort[] // empty | ||
| ' "$pkg" 2>/dev/null || true) |
There was a problem hiding this comment.
jq failures are currently being ignored (2>/dev/null || true), which means invalid JSON or a missing jq binary will be treated the same as “no unpinned deps” and the CI check can incorrectly pass. Consider letting jq errors fail the script (or explicitly detecting the error and setting EXIT_CODE=1 with a clear message).
| local unpinned | |
| unpinned=$(jq -r ' | |
| ["dependencies","devDependencies","peerDependencies","optionalDependencies"] as $sections | | |
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*xX]|>=|<=|>[^=]|<[^=]|\\|\\||^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | | |
| sort[] // empty | |
| ' "$pkg" 2>/dev/null || true) | |
| if ! command -v jq >/dev/null 2>&1; then | |
| fail "$label: 'jq' command not found; cannot check $pkg" | |
| EXIT_CODE=1 | |
| return | |
| fi | |
| local unpinned | |
| if ! unpinned=$(jq -r ' | |
| ["dependencies","devDependencies","peerDependencies","optionalDependencies"] as $sections | | |
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*xX]|>=|<=|>[^=]|<[^=]|\\|\\||^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | | |
| sort[] // empty | |
| ' "$pkg"); then | |
| fail "$label: Failed to parse $pkg with jq" | |
| EXIT_CODE=1 | |
| return | |
| fi |
script/check-unpinned-deps.sh
Outdated
| @@ -0,0 +1,69 @@ | |||
| #!/usr/bin/env bash | |||
| # Check for unpinned dependencies in web/e2e package.json files | |||
| # Unpinned = uses range operators (^, ~, *, >=, >, <, <=) instead of exact versions | |||
There was a problem hiding this comment.
The header comment describes unpinned deps as only using range operators like ^, ~, *, >=, >, <, <=, but the actual detection logic also flags x/X wildcards, ||, and certain dist-tags. Update the comment to match the implemented rules (or narrow the rules to match the comment) so future changes don’t accidentally weaken/strengthen the policy.
| # Unpinned = uses range operators (^, ~, *, >=, >, <, <=) instead of exact versions | |
| # Unpinned = uses range operators (^, ~, *, >=, >, <, <=), x/X wildcards, || ranges, or dist-tags (latest, next, canary) instead of exact versions |
script/check-unpinned-deps.sh
Outdated
| local label="$2" | ||
|
|
||
| if [ ! -f "$pkg" ]; then | ||
| warn "$label: $pkg not found, skipping" |
There was a problem hiding this comment.
If a target package.json is missing, the script currently warns and continues, which can make the CI gate ineffective if paths change (it will “pass” without checking anything). For a merge-blocking CI job, it’s safer to treat missing files as an error (set EXIT_CODE=1) or add an explicit flag to allow skipping only when intended.
| warn "$label: $pkg not found, skipping" | |
| if [ "${ALLOW_MISSING_PACKAGE_JSON:-0}" = "1" ]; then | |
| warn "$label: $pkg not found, skipping (missing file allowed)" | |
| else | |
| fail "$label: $pkg not found" | |
| EXIT_CODE=1 | |
| fi |
script/check-unpinned-deps.sh
Outdated
| local unpinned | ||
| unpinned=$(jq -r ' | ||
| ["dependencies","devDependencies","peerDependencies","optionalDependencies"] as $sections | | ||
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*xX]|>=|<=|>[^=]|<[^=]|\\|\\||^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | |
There was a problem hiding this comment.
The regex treats any occurrence of x/X in the version as “unpinned” ([\^~*xX]), which can produce false positives for valid pinned prereleases/build metadata that contain the letter (e.g. 1.2.3-experimental.1). If the intent is to catch semver wildcard ranges like 1.x / 1.2.X, tighten the pattern to match only wildcard positions rather than any x character.
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*xX]|>=|<=|>[^=]|<[^=]|\\|\\||^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | | |
| [ $sections[] as $s | .[$s] // {} | to_entries[] | select(.value | test("[\\^~*]|>=|<=|>[^=]|<[^=]|\\|\\||^(?:[0-9]+\\.)*(?:[xX])(?:\\.[xX])*$|^(latest|next|canary)$")) | "\($s)\t\(.key)\t\(.value)" ] | |
- Fail on missing jq or unparseable JSON instead of silently passing - Fail on missing package.json instead of warning - Tighten x/X wildcard regex to avoid false positives - Update header comment to match actual detection rules - Remove unused warn() function
What
Adds
script/check-unpinned-deps.sh— a pure bash script that detects unpinned dependencies (^,~, range operators) inweb/package.jsonande2e/package.json. Embeds acheck-depsjob inci.ymlthat runs on every push/PR and blocks merge via thecigate.Why
Prevents range-versioned deps from sneaking in and breaking reproducible builds. Caught
e2e/package.jsonhaving 21 unpinned deps (fixed in #2140).Changed
script/check-unpinned-deps.sh— new script; usesjq(pre-installed on ubuntu-latest) to parse deps,columnfor aligned output.github/workflows/ci.yml— addscheck-depsjob + gatescion it