Repository Automations
This document is a functional overview of the always-on GitHub automations that run on the HAI-Co² repository: what fires, when, what it does, and where each one is configured. It follows the lifecycle of a change, from filing an issue to deploying a release. These are the automations that are live today. The separate, label-gated AI-assisted pathway (an AI agent, Coco, run bygh-aw that plans a labelled issue, warns about higher-risk traits, and implements it after a human comments /approve-plan) is documented in ai-pathway.md; this document covers the rest.
For the label vocabulary these automations depend on, see labels-reference.md. For the day-to-day operational view (what a maintainer watches and tunes), see maintainers-handbook.md.
How the live automations chain along the lifecycle of a change:
At a glance
1. Filing an issue: issue forms
Trigger: a user clicks New issue. The issue chooser offers four structured forms (Bug, Feature, Docs, Refactor) plus contact links that route usage questions and security reports elsewhere. Blank issues are disabled, so every issue starts from a form. Each form auto-applies itstype:* label and needs-triage (the refactor form also pre-applies path:traditional).
Note: GitHub reads these forms from the default branch only, so they appear in the UI once merged there.
Config: .github/ISSUE_TEMPLATE/.
2. Opening a PR: branch-name validation
Trigger: a PR is opened, edited, reopened, or synchronized againstmain or develop.
The workflow checks the PR’s head branch against <type>/<short-kebab-description> (allowed types: feature, bugfix, fix, hotfix, release, chore, refactor, docs, test, experiment). If it doesn’t match, the check fails with a message listing the convention. main, develop, and bot branches (dependabot/*, renovate/*, github-actions/*) are exempt, so release PRs (develop → main) and dependency-bump PRs pass.
Config: branch-name.yml. The same rule is enforced locally (opt-in) by .githooks/pre-push.
3. Opening a PR: area labeler
Trigger: a PR is opened, reopened, synchronized, or marked ready for review.actions/labeler adds area:* labels based on which paths the PR touches (backend/** → area:backend, docs/** and *.md → area:docs, .github/** → area:ci, and so on). It is add-only (sync-labels: false), so a label a triager added by hand is never stripped. It runs as pull_request_target, so it also works on fork PRs.
Config: labeler.yml (workflow) + .github/labeler.yml (path rules).
4. Every push or PR: CI (lint + test)
Trigger: any push to any branch, and every pull request. Three jobs run in parallel: frontend build + lint, frontend tests, and backend tests (with coverage). A consolidatedci-complete gate then passes only if all three succeeded, which is the single status to mark as a required check in branch protection.
Config: ci.yml (the lint/test jobs and the gate).
5. Merges to develop / main: Release Drafter
Trigger: a push (merge) to develop or main runs the drafter; pull_request_target open/edit/sync events against those branches run the autolabeler.
Release Drafter maintains a continuously-updated draft GitHub Release: every merged PR is listed, grouped into categories by its type:* label, and the next SemVer version is computed from those labels. You never write release notes by hand. Since release-drafter v7 the autolabeler is a separate action (release-drafter/release-drafter/autolabeler@v7), so each workflow has two steps: the autolabeler runs on pull_request_target and the drafter (the root action) runs on push. Two channels run independently so they don’t pollute each other:
- dev channel: watches
develop, draftsdev-vX.Y.Znotes (release-drafter-dev.yml). - prod channel: watches
main, draftsvX.Y.Znotes and lists the published container images (release-drafter-prod.yml). Its version is not guessed from labels: a push-only step looks up thedev-vX.Y.Ztag that points at the promoted commit and passes that number to the drafter, so the prod draft’s tag, title and body carry the version validated on dev (if no dev tag points atHEAD, the step warns and the label-based resolver guesses instead).
categories:maps eachtype:*label to a notes section through awhen:condition (type:enhancement→ ”✨ Features”,type:bug→ ”🐞 Bug fixes”, …), and atype: pre-excludecategory honours theskip-changeloglabel, dropping such PRs before the notes and the version are computed. This mapping is mirrored in the table in labels-reference.md §2.semver-incrementon each category chooses the bump (v7 replaced the separateversion-resolver:block):breaking/type:breaking→ major,type:enhancement/type:refactor→ minor, everything else → patch, with a trailingtype: version-resolvercategory as the patch fallback for unlabelled PRs.autolabeler:(read by the autolabeler action) infers thetype:*label from a Conventional-Commits PR title (feat:→type:enhancement,fix:/hotfix:→type:bug, …), so even an unlabelled PR is categorised correctly. This is why the PR template asks for a Conventional-Commits title.
pull_request_target so it reads the workflow from the base branch, and Release Drafter expects its config file to live on the default branch.
Config: the two release-drafter-*.yml configs and their workflows. Operational detail (keeping the two autolabeler blocks in sync, watching the draft) is in maintainers-handbook.md.
6. Pushing a tag: build, push, deploy
Trigger: pushing a SemVer tag,dev-vX.Y.Z or vX.Y.Z, after CI is green.
The same ci.yml pipeline resolves the tag to a channel, then builds and ships:
resolve-releaseclassifies the tag:dev-vX.Y.Z→ thedevGitHub Environment;vX.Y.Z→ theprodGitHub Environment.build-and-pushbuilds the backend and frontend images and pushes them to GHCR, tagged with both the version and the channel tag (:devor:latest). It also bakes a build stamp into both images as build args:APP_VERSION(the tag’sX.Y.Z, fromresolve-release),APP_CHANNEL(devorprod),APP_COMMIT(the tagged SHA) andAPP_BUILT_AT(UTC ISO-8601, taken once byresolve-releaseso both images carry the same stamp), so every image knows which release it is (GET /api/versionand the frontend footer report it).deployconnects to the target server over VPN + SSH, generates.envfiles from the channel’s.env.example(filling values from GitHub Environment secrets), ships the compose tree, and runs the deploy script. A post-deploy assertion step then polls the freshly deployed stack over SSH (for up to 90 s): the backend’sGET /api/versionand the frontend’s served<html data-app-version data-app-commit>must both report the tag’s version and commit, otherwise the job fails. A required reviewer on theprodenvironment can gate production behind a human.release-summarywrites the published image refs and deploy result to the run summary.announce-releaseruns only afterdeployhas succeeded (itneedsthat job) and posts the release to Discord through a channel webhook, via.github/scripts/discord-release.sh. The text comes from the committedfrontend/src/data/whats-new.json, as checked out at the tag, never from the GitHub Release body (still a draft at this point, and the repository is private, so its links would be dead ends for Discord readers). A prod tag posts an embed with that version’s entry to#announcements(or a one-sentence maintenance notice when the file has no entry for it); its title and its “All updates” line both link to the public/whats-newpage, which renders the same entries (NOTES_URLoverrides that target). A dev tag posts a one-line ops note to the dev channel. The webhook URLs are the repository secretsDISCORD_RELEASE_WEBHOOK_URLandDISCORD_DEV_WEBHOOK_URL, repository-level on purpose: the job declares noenvironment:, so an Environment secret would resolve to empty there without an error. When the secret for the channel is unset, the step writes a note to the run summary and exits 0. Each tag is announced once: after a successful post the script creates the git refrefs/discord-announced/<tag>(the job’s only reason forcontents: write), and a later run for the same tag sees the ref and skips, which makes a re-run after a transient failure safe. The run summary records the Discord message id.
ci.yml builds, stamps, pushes, deploys, and announces on Discord. Publishing the draft release is a separate, manual step that never deploys anything and is not what triggers the announcement; the tag push alone is what builds, deploys, and announces.
Config: ci.yml. The branching/release procedure is in CONTRIBUTING.md; the two-branch rationale is ADR-0001; the curated-notes rationale is ADR-0008.
6a. Manual dispatch: re-send a Discord announcement
Trigger: manual (workflow_dispatch) only.
discord-resend.yml runs the same announcement script for a tag you name, outside the release pipeline: to re-post after a Discord outage, to force a corrected post (force: true ignores the marker ref), or to smoke-test the payload (dry_run, on by default, prints the JSON that would be sent to the run summary and posts nothing). It reads whats-new.json as of the tag (the script itself comes from the branch you dispatch from, so tags that predate it still work), or as of notes_ref when that input is set, for an entry committed after the tag was cut; a v* tag goes to #announcements, a dev-v* tag to the dev channel. The operational how-to (first-time webhook setup, when to re-send) is in maintainers-handbook.md §4.
Config: discord-resend.yml and .github/scripts/discord-release.sh.
7. Weekly: stale bot
Trigger: a weekly schedule (Monday 06:00 UTC), or manual dispatch. Deliberately conservative: an issue/PR with no activity for 180 days is labelledstatus:stale and warned, then closed after a further 21 days unless someone comments. It processes at most 30 items per run, and exempts blocked, help wanted, good first issue, high-priority, and path:ai-automation items. This avoids the well-known anti-pattern of an aggressive stale bot driving contributors away.
Config: stale.yml.
8. Weekly: Dependabot
Trigger: a weekly schedule, per ecosystem. Dependabot opens dependency-update PRs across eight streams:github-actions, pip (backend, bumping the compiled, hash-pinned requirements.txt), npm (frontend), docker for the backend and frontend base images, and docker-compose for the deployment/dev, deployment/prod and deployment/local stacks. Minor and patch version updates are grouped into one PR per ecosystem and wait out a 7-day cooldown; security updates are grouped separately and open as soon as a fix exists. Majors for the framework and toolchain (next, react, eslint, vitest, vite, tailwindcss, the langgraph trio, the Node and Python base images) are ignored on purpose and upgraded by hand (see Dependency updates in CONTRIBUTING). Each PR is labelled type:chore plus the relevant area:*, so it flows through the labeler/Release Drafter machinery like any other change. Independently, the Dependency Audit job in CI runs npm audit --omit=dev and pip-audit on every PR and reports high/critical advisories without blocking the merge.
Config: dependabot.yml.
How labels tie it all together
A singletype:* and area:* pair drives most of the chain:
- Issue forms apply
type:*at creation. - The labeler applies
area:*on the PR from changed paths. - Release Drafter reads
type:*to place the PR in the right notes section and to compute the version bump, falling back to the PR title viaautolabeler. - The stale bot reads status/priority labels to decide what to leave alone.
.github/labels.yml is the connective tissue, and the release-drafter-*.yml categories and labeler rules must stay in sync with it. Apply the taxonomy to a repo with .github/scripts/init-labels.sh.
The AI-assisted pathway
A second, opt-in pathway dispatches labelled issues to a provider-agnostic AI agent (run bygh-aw on the repo’s own API key), gated by a path:ai-automation[-<engine>] label. The engine is chosen by label: the default path:ai-automation (Copilot, needs a seat) or explicit path:ai-automation-copilot; path:ai-automation-claude and path:ai-automation-codex are seat-free. Assigning the HAI-Coco bot account to an issue automatically adds path:ai-automation (via coco-assign-label.yml). Adding the label opens a draft PR with the plan as its first comment; /coco <change> before /approve-plan revises the plan (no code written); /approve-plan implements the plan; CI runs; /promote dev deploys to dev; /coco X after approval iterates on the code; marking the PR ready for review runs Coco’s automated review; a human approves and merges. The workflows live in .github/workflows/ as ai-{plan,implement,iterate,review,review-ondemand}-{claude,codex,copilot}.md (gh-aw sources sharing prompt bodies from .github/aw/shared/, compiled to *.lock.yml), plus the classic coco-plan-comment.yml, coco-approve-ack.yml, coco-assign-label.yml, coco-stage-labels.yml, promote-dev.yml, notify-dev-ready.yml, and notify-approved.yml. A stage:* label (managed by coco-stage-labels.yml) tracks each AI-pathway issue and PR through the lifecycle, so the Issues and Pull-requests lists show where every item is at a glance. The full how-it-works + setup is in ai-pathway.md; the rationale for having two pathways is ADR-0002.
Where to go deeper
- labels-reference.md: the canonical label taxonomy these automations key off.
- maintainers-handbook.md: the operational view (release channels, what to watch, what to tune).
- repository-foundation.md §11: how the labeler, Release Drafter, and stale bot are wired (config listings).
- ai-pathway.md: the as-built AI-assisted pathway (gh-aw, provider-agnostic).
- github-projects-copilot-automation.md: the original Copilot-based design, kept as background.