أتمتة مراجعة تحديثات npm قبل ما الديون تتراكم
مستوى القارئ: متوسط
هتكسب نظام بسيط يخلي تحديثات npm تظهر كل أسبوع في GitHub Issue واضح، بدل ما تكتشف بعد شهرين إن عندك 27 حزمة متأخرة.
المشكلة باختصار
الطريقة الشائعة إن الفريق يعمل npm outdated لما يحصل bug أو vulnerability. الطريقة دي بتفشل لأن المراجعة بتبقى رد فعل، مش عادة ثابتة. لو عندك مشروع React أو Next.js فيه 80 dependency، تأخير المراجعة 30 يوم ممكن يخلي تحديث major واحد يتحول إلى 6 تحديثات متداخلة.
الافتراض هنا إن عندك مشروع Node.js موجود على GitHub، وبتستخدم package-lock.json، وعايز تنبيه للمراجعة فقط. الأوتوميشن مش هيعمل upgrade تلقائي. وده مقصود.
مثال سريع قبل الشرح الدقيق
ركز في السيناريو ده: عندك dashboard داخلي، والفريق بيعمل release كل أسبوعين. بدل ما حد يفتح التيرمنال يدويًا، GitHub Actions يشتغل كل يوم إثنين الساعة 07:10 UTC، ينفذ npm ci، ثم npm outdated --json، ولو لقى تحديثات يفتح issue بعنوان ثابت. المالك يراجع، يقرر: minor آمن، major يحتاج PR منفصل، أو تجاهل مؤقت.
الرقم العملي: المراجعة الشهرية تترك نافذة drift حوالي 30 يوم. الأوتوميشن الأسبوعي يقللها إلى 7 أيام. ده تقليل تقريبي 76% في زمن بقاء التحديثات بدون نظر، لكنه لا يعني 76% أمان أعلى. هو فقط يقلل زمن الغفلة.
ملف GitHub Actions القابل للنسخ
اعمل ملف جديد في .github/workflows/npm-outdated-review.yml. أفضل طريقة هنا إنك تفتح issue للمراجعة، مش PR تلقائي، لأن latest ممكن يكسر compatibility حتى لو الاختبارات خضراء.
name: Weekly npm outdated review
on:
schedule:
- cron: "10 7 * * 1"
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
outdated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install locked dependencies
run: npm ci
- name: Collect outdated packages
run: npm outdated --json > outdated.json || true
- name: Build issue body
run: |
node <<'NODE'
const fs = require('fs');
const raw = fs.existsSync('outdated.json') ? fs.readFileSync('outdated.json', 'utf8').trim() : '';
const data = raw ? JSON.parse(raw) : {};
const rows = Object.entries(data).map(([name, info]) => {
return `| ${name} | ${info.current} | ${info.wanted} | ${info.latest} | ${info.type || ''} |`;
});
if (rows.length === 0) {
fs.writeFileSync('issue.md', '');
process.exit(0);
}
const body = [
'## npm outdated weekly review',
'',
'| package | current | wanted | latest | type |',
'|---|---:|---:|---:|---|',
...rows,
'',
'Decision rule:',
'- Patch/minor: update in one PR if tests are stable.',
'- Major: open a separate PR with changelog review.',
'- Unknown owner: assign the package area owner first.'
].join('\n');
fs.writeFileSync('issue.md', body);
NODE
- name: Open review issue
if: hashFiles('issue.md') != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ ! -s issue.md ]; then
echo "No outdated packages found"
exit 0
fi
existing=$(gh issue list --state open --label dependency-review --json title --jq '.[] | select(.title == "Weekly npm outdated review") | .title' | head -n 1)
if [ -n "$existing" ]; then
echo "Review issue already open"
exit 0
fi
gh issue create \
--title "Weekly npm outdated review" \
--label "dependency-review" \
--body-file issue.md