Skip to content

Maintenance & Housekeeping #35

Maintenance & Housekeeping

Maintenance & Housekeeping #35

Workflow file for this run

name: Maintenance & Housekeeping
on:
schedule:
# Run daily at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
stale-issues:
runs-on: ubuntu-latest
name: Close Stale Issues
permissions:
issues: write
contents: read
steps:
- name: Close Stale Issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const staleLabel = 'on-hold';
const staleAfterDays = 30;
const warningAfterDays = 28;
const now = new Date();
const staleDate = new Date(now.getTime() - staleAfterDays * 24 * 60 * 60 * 1000);
const warningDate = new Date(now.getTime() - warningAfterDays * 24 * 60 * 60 * 1000);
// Get all open issues
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
for (const issue of issues) {
// Skip PRs
if (issue.pull_request) continue;
const lastActivity = new Date(issue.updated_at);
const daysSinceUpdate = (now - lastActivity) / (1000 * 60 * 60 * 24);
// Check if issue is stale (30+ days with no activity)
if (daysSinceUpdate > staleAfterDays && !issue.labels.some(l => l.name === 'blocked')) {
// Check if already warned
const hasStaleComment = (await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 5
})).data.some(c => c.body.includes('This issue is stale'));
if (!hasStaleComment) {
// Add warning
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ **Stale Issue Warning**\n\nThis issue has been inactive for ${Math.floor(daysSinceUpdate)} days. It will be closed in 2 days if no activity occurs.\n\n**To keep it open:**\n- Update the issue description if anything has changed\n- Leave a comment with progress or blockers\n- Label it as \`on-hold\` if it's blocked\n\nThank you for your contribution!`
});
// Add stale label if it exists
try {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['stale']
});
} catch (err) {
console.log(`Could not add stale label: ${err.message}`);
}
}
}
// Close if stale for 30+ days without activity
if (daysSinceUpdate > staleAfterDays + 2) {
await github.rest.issues.update({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🔒 **Closed as Stale**\n\nThis issue has been closed due to inactivity. If you'd like to continue working on it:\n\n1. Comment \`/reopen\` and we'll reopen it\n2. Update the issue description with current status\n3. Label it appropriately\n\nStale issues can be reopened anytime by the original author or maintainers.`
});
}
}
stale-prs:
runs-on: ubuntu-latest
name: Review Stale PRs
permissions:
pull-requests: write
contents: read
steps:
- name: Notify on Stale PRs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const staleAfterDays = 14;
const now = new Date();
const staleDate = new Date(now.getTime() - staleAfterDays * 24 * 60 * 60 * 1000);
// Get all open PRs
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
for (const pr of prs) {
const lastActivity = new Date(pr.updated_at);
const daysSinceUpdate = (now - lastActivity) / (1000 * 60 * 60 * 24);
// Notify if PR is stale (14+ days without activity)
if (daysSinceUpdate > staleAfterDays && daysSinceUpdate < staleAfterDays + 1) {
await github.rest.issues.createComment({
issue_number: pr.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⏰ **PR Update Needed**\n\nThis PR has been waiting for ${Math.floor(daysSinceUpdate)} days. Please:\n\n- Address review comments if any\n- Rebase on latest \`test\` branch\n- Resolve any conflicts\n- Push updates\n\nIf this PR is no longer needed, please close it. Thank you!`
});
}
}
cleanup-merged-branches:
runs-on: ubuntu-latest
name: Cleanup Merged Branches
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Delete merged branches
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: branches } = await github.rest.repos.listBranches({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
const protectedBranches = ['main', 'test', 'develop'];
for (const branch of branches) {
// Skip protected branches
if (protectedBranches.includes(branch.name)) continue;
try {
// Check if branch is merged into main or test
const compareMain = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: 'main',
head: branch.name
});
const compareTest = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: 'test',
head: branch.name
});
// Delete if fully merged (behind by 0 commits)
if ((compareMain.status === 'identical' || compareMain.behind_by === 0) ||
(compareTest.status === 'identical' || compareTest.behind_by === 0)) {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch.name}`
});
console.log(`✅ Deleted merged branch: ${branch.name}`);
}
} catch (error) {
// Branch might not exist or other error, skip it
console.log(`ℹ️ Could not process branch ${branch.name}: ${error.message}`);
}
}
vulnerability-alert:
runs-on: ubuntu-latest
name: Check Dependency Vulnerabilities
permissions:
contents: read
security-events: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'
- name: Run security audit
continue-on-error: true
run: |
npm ci --legacy-peer-deps 2>&1 || true
npm audit --production 2>&1 || echo "Audit completed"
- name: Notify on vulnerabilities
uses: actions/github-script@v7
if: failure()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '🔒 Security: Dependency Vulnerabilities Detected',
body: `⚠️ **Automated Security Alert**\n\nDependency vulnerabilities have been detected in the latest scan.\n\n**Action Required:**\n1. Run \`npm audit\` to see details\n2. Run \`npm audit fix\` for automatic fixes\n3. Review high/critical vulnerabilities\n4. Create PR to fix issues\n\nSee [SECURITY.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/.github/SECURITY.md) for guidelines.`,
labels: ['security', 'priority-high']
});