Skip to content

chore: add unpinned dependency check script and CI job#2143

Open
joshuagibeonm wants to merge 3 commits intomainfrom
chore/check-unpinned-deps
Open

chore: add unpinned dependency check script and CI job#2143
joshuagibeonm wants to merge 3 commits intomainfrom
chore/check-unpinned-deps

Conversation

@joshuagibeonm
Copy link
Copy Markdown
Contributor

What

Adds script/check-unpinned-deps.sh — a pure bash script that detects unpinned dependencies (^, ~, range operators) in web/package.json and e2e/package.json. Embeds a check-deps job in ci.yml that runs on every push/PR and blocks merge via the ci gate.

Why

Prevents range-versioned deps from sneaking in and breaking reproducible builds. Caught e2e/package.json having 21 unpinned deps (fixed in #2140).

Changed

  • script/check-unpinned-deps.sh — new script; uses jq (pre-installed on ubuntu-latest) to parse deps, column for aligned output
  • .github/workflows/ci.yml — adds check-deps job + gates ci on it

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.
Copilot AI review requested due to automatic review settings April 1, 2026 06:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh to detect unpinned dependency specifiers via jq.
  • Added a new check-deps job to .github/workflows/ci.yml.
  • Updated the ci gate job to depend on check-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.

Comment on lines +37 to +42
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)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
@@ -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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
local label="$2"

if [ ! -f "$pkg" ]; then
warn "$label: $pkg not found, skipping"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
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)" ] |
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[ $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)" ] |

Copilot uses AI. Check for mistakes.
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants