GitHub Actions study materials
GitHub Actions study resources

Issue Triage Automation: Idempotency, Fork Permissions, and Scheduled Drift

GitHub Actions · Put your knowledge to work in context.

All case studies

Issue Triage Automation: Idempotency, Fork Permissions, and Scheduled Drift

An open-source project uses GitHub Actions to triage issues and manage stale PRs. .github/workflows/triage.yml runs on issues (opened, edited) and uses actions/github-script@v7 to add labels when the title contains 'bug', 'docs', or 'question'. It also posts a welcome comment. The workflow uses permissions: write-all because maintainers could not get labeling to work. In the last month: (1) issues edited 5 times received 15 duplicate labels and 5 welcome comments; (2) a scheduled stale workflow (cron: '0 2 * * *') sometimes starts at 04:30 and marks active PRs stale after 14 days; (3) a contributor opened a PR from a fork and the triage workflow failed with 'Resource not accessible by integration'; (4) the repository has 2,400 open issues and API calls intermittently hit secondary rate limits.

1. Diagnose the duplicate labels and comments and rewrite the logic to be idempotent. Include event action handling and API calls.

Read the worked answer

The workflow subscribes to issues with opened and edited. Every edit re-runs the same unconditional addLabels and createComment calls, so five edits produce five additional comments and duplicate label attempts. Make each side effect idempotent. For comments, list existing comments with github.paginate(github.rest.issues.listComments, ...) and skip if a hidden marker such as <!-- triage-welcome --> already exists. For labels, compute the desired set from the title/body, fetch current labels, then add only missing labels and remove obsolete ones with removeLabel. Restrict the welcome comment to issues.opened; on edited, only reconcile labels. Use github.rest.issues.addLabels (plural) once with the diff rather than looping. Handle issues events where issue.pull_request exists if the same workflow should ignore PRs. Add concurrency: group: triage-${{ github.event.issue.number }}, cancel-in-progress: true to serialize rapid edits, but do not rely on it for correctness.

2. Why did the fork PR triage fail, and what permissions model would you use? Address `pull_request` versus `pull_request_target` and `GITHUB_TOKEN`.

Read the worked answer

GITHUB_TOKEN is scoped to the repository and its permissions are capped by the event. For pull_request from a fork, GitHub gives a read-only token and withholds repository secrets. permissions: write-all in the workflow cannot elevate a fork PR token, so labeling/commenting fails with 'Resource not accessible by integration'. Use pull_request_target for metadata-only automation, because it runs in the base repository context with a write token. But never check out or execute the PR head in that workflow. Set permissions: issues: write, pull-requests: write, contents: read and use the API only. For building/testing fork code, keep a separate pull_request workflow with contents: read, no secrets, and no write token. If a comment must include test results, have the unprivileged workflow upload an artifact, then a workflow_run workflow in the base repo downloads it and comments; treat artifact contents as untrusted text.

3. The scheduled workflow drifts and marks active PRs stale. Explain cron scheduling behavior and propose a safer staleness rule.

Read the worked answer

GitHub Actions schedule uses UTC and is best-effort. Runs can be delayed during peak load, and a delayed run still executes with the repository state at that time. A 02:00 cron starting at 04:30 is expected behavior, not a guarantee. The staleness rule likely uses created_at or a fixed 14-day cutoff; a delayed run can cross the cutoff and mark active PRs. Use updated_at and require no activity for 14 days, exclude drafts, PRs with status: blocked or do-not-close, and PRs with recent comments. Validate with a dry-run output before mutating. Add concurrency: group: stale, cancel-in-progress: false so runs do not overlap, and cap API work with operations-per-run. Prefer the actions/stale action's days-before-stale, days-before-close, exempt-issue-labels, and only-issue-labels rather than custom logic that edits on every event.