Organization-wide defaults for repositories in the havit-internal organization.
GitHub treats a repo named exactly .github in an org specially: any repo that
does not define its own version of these files falls back to what lives here.
That gives the whole org consistent issue templates, PR templates, community
health files, and a canonical label set with zero per-repo work.
.github/
├── ISSUE_TEMPLATE/
│ ├── feature.yml ← Outcome-level container; rolls up Stories
│ ├── bug_report.yml ← "Something broken" in dev, staging, or production
│ ├── user_story.yml ← A user-facing slice of work
│ ├── task.yml ← Implementation slice — a piece of a Story
│ └── config.yml ← Disables blank issues
├── pull_request_template.md ← Issues / Refs — see QA convention below
├── labels.yml ← Source of truth for sev:*/meta labels (work type is an Issue Type, not a label; workflow status is the Work status issue field, not a label)
└── workflows/
├── qa-routing.yml ← Reusable workflow — see "PR convention" below.
├── issue-status-sync.yml ← Reusable workflow — issue closed ⟷ Work status Done, both directions; reopened → In progress
├── pr-linked-status.yml ← Reusable workflow — PR linked to issue → Work status In progress
└── label-sync.yml ← Runs centrally — see "Label sync" below. CI still planned.
workflow-templates/ ← Ready-made wrappers for the reusable workflows — see "Adding the wrappers to a repo"
├── qa-routing.yml (+ .properties.json)
├── issue-status-sync.yml (+ .properties.json)
└── pr-linked-status.yml (+ .properties.json)
plugins/
└── gh-issue-templates/ ← Claude Code plugin — see "Claude Code plugin" below
| Repo | Role |
|---|---|
havit-internal/.github |
Fallback files: issue templates, PR template, .github/labels.yml. Reusable workflows also live here, under .github/workflows/ (qa-routing.yml, issue-status-sync.yml, pr-linked-status.yml, and label-sync.yml built; CI still planned). |
havit-internal/002.HFW-NewProjectTemplate-Blazor |
GitHub template repo for new Blazor projects. Not org-wide — covers the Blazor stack specifically, not every repo type. |
Templates from this repo are inherited by every repo in the org that does
not define its own. Workflow files are not inherited — a reusable workflow
defined here still has to be called explicitly from a thin wrapper in each
consuming repo (uses: havit-internal/.github/.github/workflows/<name>.yml@main).
Those wrappers can be added through the GitHub UI (Actions → New workflow) —
see "Adding the wrappers to a repo" below. No separate workflows repo is needed for this — reusable workflows can be
called from any repo, including this one. If workflow versioning or ownership
ever needs to diverge from the templates/labels here, split them out then;
until that's a real need, keeping everything in one repo is simpler.
Each wrapper is published as an org workflow template
(workflow-templates/ at the root of this repo). In the consuming repo, go
to Actions → New workflow, find the By havit-internal section, and
click Configure on QA routing, Issue status sync, or PR-linked issue
status, then commit. Nothing to copy by hand, and the uses: path to the
reusable workflow can't be mistyped.
workflow-templates/*.yml is the single source for the wrapper YAML — the
sections below describe what each workflow does and link there instead of
repeating it. Without the UI, copy the file into the consuming repo's
.github/workflows/ under the same name. The only knob is the optional
runner input: a JSON array of runner labels, default '["ubuntu-latest"]',
e.g. '["self-hosted","on-prem"]'.
Fallback is all-or-nothing per repo. As soon as a repo defines any issue template of its own (even one), the org defaults stop applying to that repo entirely — they do not merge with local templates.
Prefer keeping repos with zero local templates so they inherit the full set defined here.
- Feature — outcome-level container. Not implemented directly; holds Stories.
- Story — a vertical, user-facing slice of a Feature. The unit that gets planned and delivered.
- Task — an implementation-level piece of a Story.
- Bug — unplanned work; can reference a parent Story or Feature but doesn't require one.
Every template is a single required Description box — free-form plain text, no sections, no checklists, no prefilled content. Write whatever the issue needs; nothing is asked for that the author doesn't want to give.
Parent/child linking is manual — mention the parent issue number in the description, and use GitHub's sub-issue UI where it helps navigation.
Work type (feature / story / task / bug) is not a label — it's set via
GitHub's native org-level Issue Types (org Settings → Issue types),
which sync to every repo automatically with no workflow needed. Task,
Bug, Feature, and Story are all configured and enabled on the org,
matching the type: key each issue template sets.
The issue templates set their Issue Type via the top-level type: key in
each .github/ISSUE_TEMPLATE/*.yml file (not the labels: key). labels.yml
below is only for things Issue Types and the Work status field don't cover:
severity and triage/meta labels. Workflow status itself is not a label —
it's the org-wide Work status issue field (see "QA routing workflow"
below).
No template presets needs-triage (or any other label), and that's
deliberate — don't add labels: back. A label listed in a template is
applied by GitHub a moment after the issue is created, as a separate step
that ignores the label picker, so deselecting it in the new-issue dialog
doesn't stick: it reappears on the created issue. Untriaged is better
expressed as a filter — is:issue no:assignee — and needs-triage stays in
labels.yml for whoever wants to set it by hand.
.github/labels.yml is the canonical list for severity (sev:*) and meta
labels (needs-triage, blocked, skip-qa, etc.) — everything that isn't a
work type or a workflow status. .github/workflows/label-sync.yml applies it
to every repo in the org automatically — on push to main when
.github/labels.yml changes, on a weekly schedule (catches repos created
since the last sync), and on demand (workflow_dispatch). Do not create
ad-hoc labels in individual repos — edit this file and open a PR here
instead.
Unlike the other workflows in this repo, label-sync.yml is not
reusable / per-repo-opt-in — it runs centrally, only here, and pushes out to
every repo in the org. No wrapper needed anywhere else.
It's non-destructive: creates labels that don't exist yet in a repo, and
updates the color/description of ones that already match by name, but never
deletes or otherwise touches a label that isn't in .github/labels.yml — a
repo's own ad-hoc labels are left alone.
It needs an org-level secret, ORG_LABEL_SYNC_SECRET — a fine-grained
PAT (or GitHub App token) with Issues: Read and write across all
repositories in the org. The default GITHUB_TOKEN won't work here: it's
scoped only to the repo the workflow runs in, and this workflow writes
labels to every other repo in the org. To set it up:
- Create a fine-grained PAT.
Resource owner:
havit-internal. Repository access: All repositories (so newly created repos are covered without reconfiguring the token). Permissions → Repository permissions → Issues: Read and write. - If the org requires approval for fine-grained PATs, an org owner needs to approve it before it's usable.
- Add it as an organization-level Actions secret (Org Settings →
Secrets and variables → Actions → New organization secret), named
ORG_LABEL_SYNC_SECRET, with repository access limited tohavit-internal/.github(the only repo that needs it). - Fine-grained PATs expire (max 1 year) — note the expiry and plan to rotate it, or move to a GitHub App later if this becomes long-lived.
The qa-routing workflow (see below) piggybacks on GitHub's own closing
keywords instead of inventing a separate one. Use Closes #N, Fixes #N,
or Resolves #N — anywhere in the PR description, one or several
comma-separated (Fixes #10, #11) — for every issue this PR fixes. GitHub
auto-closes those issues on merge, and the workflow, in the same run, sets
the org-wide Work status issue field to Ready for QA and assigns
everyone in .github/QAOWNERS, so the issue doesn't just quietly close —
it still gets a QA pass.
You don't have to type a keyword at all — linking an issue via the PR's
Development panel (the "Link a pull request"/"Link an issue" UI) works
identically. The workflow reads GitHub's own resolved closingIssuesReferences
for the PR, the same data that panel displays, so a manually linked issue is
picked up exactly like a Fixes #N in the text — no body parsing involved.
Closes #N/Fixes #N/Resolves #Nin the body, or a manual link in the Development panel — This PR fixes issue N. Closed by GitHub and routed to QA on merge.Refs #N— Related issue, no action. Cross-link only, not auto-closed, not routed to QA (plain text mentions never appear inclosingIssuesReferencesunless linked one of the two ways above).
.github/workflows/qa-routing.yml is a reusable workflow. It does not run on its own —
each consuming repo needs a thin wrapper that triggers it on merge:
workflow-templates/qa-routing.yml. Add it through the UI:
Actions → New workflow → By havit-internal → Configure.
Self-hosted runners need Actions Runner v2.327.1 or newer. These workflows
run actions/github-script v9 (and label-sync runs actions/checkout v7),
which refuse to start on an older runner — the job fails before any script of
ours executes. Hosted runners like ubuntu-latest are always new enough, so
this only matters if you pass self-hosted labels. The same floor applies to
all three reusable workflows below.
What it does on merge:
- Reads the PR's
closingIssuesReferences(GraphQL) — the same resolved list GitHub shows in the PR's Development panel, covering bothCloses/Fixes/Resolves #Ntext and manually linked issues. No matches → no-op. - For each linked issue, sets the org-wide Work status issue field
(single select) to Ready for QA via the
setIssueFieldValueGraphQL mutation — a single-select field only ever holds one value, so this always replaces whatever Work status was there before (no stacking, unlike labels). Exception: an issue labeledskip-qagoes straight to Done and gets closed instead — see below. - Assigns everyone listed in that repo's
.github/QAOWNERS— see below (skipped forskip-qaissues, since there's no QA pass to assign). - Leaves a comment on the issue linking back to the merged PR.
skip-qa label: for issues with nothing for a tester to verify (purely
technical work — refactors, dependency bumps, internal tooling). Label the
issue skip-qa before the PR merges, and qa-routing sets Work status
straight to Done and closes the issue itself, instead of Ready for QA
plus a QAOWNERS assignment. skip-qa is defined in labels.yml alongside
the other meta labels.
.github/QAOWNERS format (lives in each consuming repo, not here):
one GitHub username per line, @ optional, # for comments, blank lines
ignored:
# QA owners for this repo — all are assigned on every merge
@alice
@bob
If a repo has no QAOWNERS file, the workflow still sets Work status to
Ready for QA but leaves the issue unassigned (logged as a warning in the
workflow run) — so repos can adopt this incrementally rather than needing
QAOWNERS set up before merges work at all.
Work status field IDs: the workflow references the Work status field
and its Ready for QA and Done options (the latter used by the skip-qa
path above) by GraphQL node ID (single-select fields are set by option ID,
not name). Those IDs are hardcoded as constants in qa-routing.yml — if the
field or an option is ever deleted and recreated (even with the same name),
it gets new IDs and the workflow needs updating. Look them up via
repository.issueFields for any repo in the org (the field is org-wide, so
it shows up the same everywhere).
.github/workflows/issue-status-sync.yml is a reusable workflow that keeps
an issue's open/closed state and its Work status field in sync, in both
directions. Wrapper:
workflow-templates/issue-status-sync.yml. Add it through the UI:
Actions → New workflow → By havit-internal → Configure.
What it does:
- Issue closed as completed → sets Work status to Done. A close with
any other reason (won't-fix, duplicate) is left alone — "Done" implies
actual completion, not "not planned". Exception: if a merged PR closed
the issue and the repo also runs
qa-routing, Work status is left toqa-routing(Ready for QA, or Done forskip-qa). Both workflows fire on the same merge, so without this the issue could end up Done and skip QA, depending on which run finished last. "Runsqa-routing" means a workflow file on the default branch has auses:line calling theqa-routing.ymlreusable workflow; a repo without one still gets Done on a PR close. Only a PR in the same repo counts —qa-routingskips cross-repo issues, so an issue closed by another repo's PR still gets Done here. - Issue reopened → if Work status is Done or Ready for QA, sets it to In progress — typically QA rejecting a fix. Any other status is left as is, and so is an issue that was closed again before the run got to it.
- Work status set to Done (the
field_addedactivity type, which GitHub fires whenever any issue field value is set or changed) → closes the issue as completed.
Each direction checks the other side's current state before acting (already Done? already closed?), so triggering one doesn't bounce back and forth with the other — it settles after at most one harmless extra run.
.github/workflows/pr-linked-status.yml is a reusable workflow that moves
an issue's Work status to In progress as soon as a PR is linked to
it — same closingIssuesReferences detection as qa-routing.yml (body
keyword or Development panel link, either way). Wrapper:
workflow-templates/pr-linked-status.yml. Add it through the UI:
Actions → New workflow → By havit-internal → Configure.
It skips issues whose Work status is already Ready for QA or Done,
so it never walks status backward (e.g. a small follow-up PR after QA
rejected it shouldn't undo that progress). Caveat: a PR linked purely
through the Development panel, with no further edit to the PR itself,
won't trigger this workflow until the PR's next opened/edited-type
event — there's no dedicated webhook event for "issue linked via panel"
alone.
Projects' built-in Status field is per-project — every project gets its own
copy, nothing keeps them consistent across repos, and none of the workflows
above can write to it. The org-wide Work status issue field is the
opposite: defined once, one value per issue, readable from every project and
repo, and already driven by qa-routing, issue-status-sync, and
pr-linked-status. A board here should therefore draw its columns from Work
status and leave the project's own Status unused.
That is supported: an org issue field added to a project behaves like any project single-select — it can be the board's column field as well as its Group by (swimlane) axis, and dragging a card between columns writes the issue field itself, which the workflows above then see.
One org-level project, configured once, then Settings → Templates → Copy as
template so it shows up under New project.
Fields on the project:
| Field | Source | Role |
|---|---|---|
Work status |
org issue field | board columns — Backlog / Ready / In progress / Ready for QA / Done |
Type |
native Issue Type | swimlanes — Feature / Story / Task / Bug |
Priority |
org issue field | Urgent / High / Medium / Low — sort within a column |
Iteration |
project iteration field, 1-week | sprint scoping — the @current / @next views filter on it |
Parent issue, Sub-issues progress |
built-in | Feature → Story → Task roll-up |
Status |
built-in project field | delete it — or hide it in every view if the project won't let it go — so nobody maintains two competing statuses |
Work status is org-visibility All, but Priority is Org only, and
org-only issue fields are hidden in projects that are public or internal — so
either keep the project private or flip Priority to All in the org's issue
field settings.
Views, in tab order — boards for moving work day to day, tables for planning,
triage and bulk edits (inline edits, paste down a column, sort and group
without dragging cards). Every view is sorted by Priority, and every view
except All work is also filtered to is:issue:
| View | Layout | Filter / setup | Who, when |
|---|---|---|---|
| Current iteration | board | iteration:@current; columns Work status, swimlanes Type |
whole team, daily — the default view |
| QA queue | table | work-status:"Ready for QA" — exactly what qa-routing sets on merge; shows Iteration, Linked pull requests |
testers |
| Next iteration | table | iteration:@next; shows Type, Priority, Assignees, Sub-issues progress |
sprint planning |
| Backlog & triage | table | no:iteration -work-status:Ready,"In progress","Ready for QA",Done — i.e. Work status empty or Backlog, no iteration |
triage: set type, priority, Work status, then an iteration |
| Bugs | table | type:Bug; shows Labels (for sev:*), Iteration, Work status |
deciding which bugs go into this or the next sprint |
| Features | table | type:Feature; shows Sub-issues progress, Iteration |
product and leads: are each Feature's Stories moving? |
| All work | table | no filter at all — deliberately includes PRs and drafts; grouped by Work status |
overview and search, the catch-all |
The Backlog filter excludes the other Work status values because a filter can't express "empty or Backlog" directly — if Work status gains an option, add it to that exclusion list. There's no roadmap view: its date axis can't be set through the API, so add one by hand on a project that wants a timeline.
Add the org fields from a table view (+ in the header → Add field → the org
issue fields are listed alongside project fields); set column field and Group
by from the board's view-options menu. Pick filter values from the suggestion
dropdown rather than typing qualifiers — GitHub writes the qualifier itself,
including for multi-word field names.
The views were created through the REST API
(POST /orgs/{org}/projectsV2/{number}/views; a classic token or gh login
needs the project scope, a fine-grained token or GitHub App needs the
organization Projects read-and-write permission), which takes name, layout, filter, visible fields, sort, column field
and Group by. Two gaps: it silently drops Group by Type (the native Issue
Type isn't exposed as a groupable field), so the swimlanes on Current
iteration are set by hand; and there's no call to edit an existing view's
layout settings — GraphQL updateProjectV2View only changes name, filter and
visible fields. deleteProjectV2View does work, which is how the default
Table / Board / Roadmap views were removed.
Issue fields only populate on issues owned by this org. Pull requests, draft
issues, and issues from other orgs have no Work status at all and would pile
up in a "No Work status" column. is:issue keeps PRs and drafts off the
board; PRs are still visible on the cards through Linked pull requests.
is:issue does not exclude issues from other orgs, though — if one is added
to the project it still shows up without a Work status. Auto-add only pulls
from this org's repos, so that takes adding one by hand.
Issues themselves can still lack a value. Nothing sets a Work status or an
Iteration when an issue is created — the templates here set only a type: —
so a fresh issue never reaches Current iteration; it shows up in Backlog
& triage. That view is the intake lane: everything in it with an empty Work
status is untriaged, and triage means setting type, priority and Work status,
then an iteration.
The built-in workflows that move work forward — "when an issue or PR is closed, set Status to Done" and the same for merged PRs, both on by default — write the project's Status field, which a project built this way doesn't use. They go inert, and nothing is lost: the three reusable workflows in this repo do that job one level down, on the issue itself, so it holds for issues in no project at all.
The built-in workflows that don't touch Status are unaffected and still worth using — auto-add (pull items from a repo into the project) above all, plus auto-archive.
Copying a project (or creating one from the template) brings the views, the fields and their values, draft issues, insights, and configured workflows — except auto-add workflows, which are never copied. Every new project therefore needs its own "auto-add items from repo X" workflow wired up by hand.
The copy does keep its board columns bound to the issue field Work status
— it does not fall back to a project-local single select. GitHub's docs don't
state this either way; it was checked by hand on the first project created
from the template.
This repo doubles as a Claude Code plugin marketplace (.claude-plugin/marketplace.json).
It currently ships one plugin, gh-issue-templates, with a skill that creates
GitHub issues matching the actual template a target repo renders (including
the all-or-nothing inheritance gotcha above) — field order, labels, and
native Issue Type — instead of a freeform title/body.
Install once, works in any repo:
/plugin marketplace add havit-internal/.github
/plugin install gh-issue-templates
Everything in this repo affects every repo in the org. Open a PR, get review from at least one other maintainer, and merge. Changes take effect immediately for any repo that doesn't override.