A config-driven GitHub Action that automates GitHub Projects (v2) — sprint rollover, stale-card nudges, sub-issue gating, digests, standups, priority sorting, and Slack/email notifications — all from one YAML file.
🌐 boardly-gh.pages.dev · 💎 Prefer Ruby? cdrrazan/Boardly-ruby
Getting started · Quick start · Features · Use cases · Config · Roadmap · Contributing · Sponsor
You point the action at a Project (v2), describe your rules in .github/project-automation.yml, and schedule it. On every run it reads the board, applies your enabled features, and writes an audit trail to the Actions job summary so you can see exactly what happened.
flowchart LR
cron([⏰ Schedule / manual]) --> action[🤖 Boardly]
cfg[[📄 project-automation.yml]] --> action
action -->|GraphQL| gh[(🗂️ GitHub Project v2)]
gh --> action
action --> f1[🔁 Rollover]
action --> f2[🔔 Stale nudges]
action --> f3[🧩 Sub-issue gate]
action --> f4[🏁 Digest]
action --> f5[🗓️ Standup]
action --> f6[🔼 Priority sort]
f1 & f2 & f3 & f4 & f5 & f6 --> audit[[📋 Audit trail → Job Summary]]
| Feature | What it does | |
|---|---|---|
| 🔁 | Sprint rollover | When an iteration ends, move unfinished items into the next iteration so nothing is stranded in a closed sprint. Optionally tag each rolled card with the new sprint's label (created if missing) and strip stale labels like pulled-in. |
| Sprint start | When a sprint becomes active, promote cards you pre-parked in it (e.g. Backlog → Ready). Only touches cards parked before the sprint started, so a deliberate mid-sprint move back is respected. | |
| 👤 | Auto-assign by label | A CODEOWNERS-style map from label → owner. Unassigned tickets in a status (e.g. Ready) get the configured assignee — UI → @zach, security → @rajan. Opt-in; never overrides an existing assignee. |
| 🛬 | Sprint runway warning | GitHub never auto-creates iterations. Boardly warns (job summary + annotation) when too few future sprints are planned, so you add the next one before rollover/sprint-start run dry. |
| 🔔 | Stale-card nudges | @-mention owners when a card sits in a status past a threshold. De-duped so it never spams. |
| 🧩 | Sub-issue gating + roll-up | Block a card from staying Done while it has open sub-issues; optionally write completion % into a progress field. |
| 🏁 | Sprint digest | At iteration end, post completed-vs-carried-over counts and velocity. |
| 🗓️ | Daily standup | Post what moved in the last N hours, grouped by assignee. |
| 🔼 | Priority auto-sort | Reorder the board so higher-priority cards float to the top. |
| 📣 | Slack & email notifications | Also deliver digests, standups, and stale alerts to a Slack channel and/or over email — not just GitHub comments. |
| 📋 | Audit trail | Every action (or, in dry-run, every intended action) is written to the job summary. |
Every feature comes with a standalone, copy-pasteable recipe — who it's for, the config, and what happens. Browse them all in docs/use-cases, or jump straight in:
| # | Use case | Feature(s) |
|---|---|---|
| 01 | Carry unfinished work into the next sprint | 🔁 Rollover |
| 02 | Nudge owners about stale cards | 🔔 Stale nudges |
| 03 | Stop premature "Done" on parent issues | 🧩 Sub-issue gate |
| 04 | Show live epic progress on the board | 🧩 Sub-issue roll-up |
| 05 | Auto-post a sprint retro digest | 🏁 Digest |
| 06 | Async daily standup for a distributed team | 🗓️ Standup |
| 07 | Keep the backlog sorted by priority | 🔼 Priority sort |
| 08 | Preview everything safely with dry-run | 📋 All + audit |
| 09 | Automate a project spanning many repos | ⚙️ All |
| 10 | Different schedules per feature | ⚙️ All |
| 11 | Solo maintainer / personal project board | ⚙️ All |
| 12 | Escalate cards ignored after a nudge | 🔔 Stale + 🧩 gate |
| 13 | Send digests & alerts to Slack and email | 📣 Notifications |
| 14 | Promote pre-parked cards when a sprint starts | |
| 15 | Auto-assign tickets by label | 👤 Auto-assign |
| 16 | Get warned before you run out of sprints | 🛬 Sprint runway |
New here? Start with 01 · Sprint rollover and 08 · Dry-run preview.
New here? The Getting started guide is a full, linear zero-to-running walkthrough (~15 min). The steps below are the condensed version.
flowchart TD
A[1 · Create a token<br/>Projects + Issues scope] --> B[2 · Add config<br/>.github/project-automation.yml]
B --> C[3 · Add workflow<br/>.github/workflows/project-automation.yml]
C --> D[4 · Dry-run to preview] --> E[5 · Flip dry-run off 🎉]
-
Create a token. The default
GITHUB_TOKENgenerally can't read org Projects. Create a fine-grained PAT or GitHub App token with Projects: read & write and Issues: read & write, and save it as a secret (e.g.PROJECT_AUTOMATION_TOKEN). -
Add config at
.github/project-automation.yml— start fromproject-automation.example.yml. -
Add a workflow — see
.github/workflows/example.yml:- uses: cdrrazan/Boardly@v1 with: token: ${{ secrets.PROJECT_AUTOMATION_TOKEN }} config-path: .github/project-automation.yml dry-run: "true" # preview first; remove once it looks right
Set these as repository (or org) Actions secrets, then map them into the workflow. Only the token is required.
| Secret | Required? | What it is | Needed when |
|---|---|---|---|
PROJECT_AUTOMATION_TOKEN |
Yes | Fine-grained PAT / GitHub App token — Projects: read & write + Issues: read & write | Always (the default GITHUB_TOKEN can't read org Projects) |
SLACK_WEBHOOK_URL |
No | Slack Incoming Webhook URL | You enable notifications.slack |
SMTP_USER |
No | SMTP username | You enable notifications.email with auth |
SMTP_PASS |
No | SMTP password | You enable notifications.email with auth |
The token is passed via the token: input; the notification secrets are passed via the workflow's env: (referenced in config by env-var name, never inlined). See Notifications.
Versioning: pin to
@v1to always get the latestv1.x(bug-fixes and features, no breaking changes), or@v1.0.0to freeze an exact version.
Boardly is a published Action — there's nothing to install, fork, or host. Any repo in your org just references cdrrazan/Boardly@v1. To roll it out across a team:
-
Allowlist the Action (only if your org restricts Actions). Org admin → Settings → Actions → General → Allow select actions → add
cdrrazan/Boardly@*. Skip if your org already permits all or marketplace actions. -
Create one token with access to the org's Project (v2) — a GitHub App token (recommended for teams; not tied to a single person) or an org-scoped fine-grained PAT, with Projects: read & write + Issues: read & write.
-
Store it once as an org-level secret named
PROJECT_AUTOMATION_TOKEN(Org Settings → Secrets and variables → Actions) and share it to the repos that need it — one secret, many repos. -
Host the workflow in a single repo. A Project (v2) is owned by the org or user, not a repo, and the config targets it by
owner+number— so the scheduled workflow lives in one repo, even when the board spans many. It does not need to be added to every repo. -
Point the config at the org project and dry-run first:
# .github/project-automation.yml project: owner: my-org # your org login type: org number: 7 # from the project URL: /orgs/my-org/projects/7
For a board that pulls issues from several repositories, see the multi-repo recipe.
| Input | Default | Description |
|---|---|---|
token |
— (required) | Token with project + issues access to the target project. |
config-path |
.github/project-automation.yml |
Path to the config file. |
only |
"" |
Run just one feature: rollover, sprint-start, sprint-runway, auto-assign, stale-nudge, sub-issue-gate, digest, standup, priority-sort. Empty runs every enabled feature. |
dry-run |
false |
Log every intended action to the audit trail without making changes. |
Output: actions-count — number of mutating actions taken (or that would be taken in dry-run).
Everything is declared in one YAML file. Minimal example:
project:
owner: my-org
type: org # or "user"
number: 5
fields:
status: Status
iteration: Sprint
priority: Priority
doneStatuses: ["Done"]
features:
rollover:
enabled: true
staleNudge:
enabled: true
rules:
- status: "In Progress"
days: 3
notify: assigneesFull reference: project-automation.example.yml.
By default, digests, standups, and stale alerts are posted to GitHub. You can also deliver them to a Slack channel and/or over email by adding a notifications block. Secrets are referenced by environment-variable name — never inline the webhook URL or SMTP password in config; pass them from encrypted secrets via the workflow's env:.
# in project-automation.yml
notifications:
slack:
enabled: true
webhookEnv: SLACK_WEBHOOK_URL # env var with a Slack Incoming Webhook URL
email:
enabled: true
host: smtp.example.com
port: 587
secure: false # true for port 465
userEnv: SMTP_USER
passwordEnv: SMTP_PASS
from: "Boardly <bot@example.com>"
to: ["team@example.com"]# in your workflow — map the secrets into the environment
- uses: cdrrazan/Boardly@v1
with:
token: ${{ secrets.PROJECT_AUTOMATION_TOKEN }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}Both channels are optional and independent — enable either, both, or neither. A channel failure is logged as a warning and never aborts the run, and nothing is sent under dry-run. See the notifications recipe.
Configured but paused? To keep your creds and settings in place but stop sending, just set that channel's
enabled: false. The webhook/SMTP config stays untouched — no delivery happens until you flip it back totrue. You don't need to remove secrets to go quiet.
- "Time in status" is approximated by when the Status field value was last changed (Projects v2 exposes each field value's
updatedAt). It is not a full status-history walk. - Iterations come from the iteration field's configuration. Rollover and digest act on the most recently completed iteration; new work rolls into the first active iteration.
- Sub-issues use the native GitHub sub-issues API (
subIssuesSummary), requested with thesub_issuesGraphQL feature header. - Priority sort uses
updateProjectV2ItemPosition. Manual order only shows on a board view whose sort is set to manual — a field-sorted view overrides it.
npm install
npm run typecheck # tsc --noEmit
npm test # unit tests (node:test + tsx)
npm run build # bundle src -> dist/index.js (committed; required for JS actions)
npm run all # typecheck + test + buildThe bundled dist/index.js is committed so the action runs without a build step. Rebuild and commit it whenever you change src/.
Built with: TypeScript · Node 20 · Octokit GraphQL · Zod · ncc — see the full Tech Stack and Architecture.
Working-days awareness, escalation ladders, iteration auto-assignment, capacity warnings, and more — see ROADMAP.md.
Contributions are very welcome! Read the Contributing Guide and our Code of Conduct to get started. Found a security issue? See SECURITY.md.
Boardly is free and open source. If it saves your team time, please consider sponsoring — it directly funds maintenance and new features.
Released under the MIT License — see also our open-source note.