Skip to content
havit-internalPublic

About

Organization-wide defaults for havit-internal

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

havit-internal/.github

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.

What lives here

.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

Related org repos

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.

Adding the wrappers to a repo

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"]'.

Issue template inheritance — the gotcha

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 → Story → Task hierarchy

  • 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 vs. labels

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.

Label sync

.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:

  1. 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.
  2. If the org requires approval for fine-grained PATs, an org owner needs to approve it before it's usable.
  3. 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 to havit-internal/.github (the only repo that needs it).
  4. 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.

PR convention: Closes/Fixes/Resolves vs Refs

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 #N in 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 in closingIssuesReferences unless linked one of the two ways above).

QA routing workflow

.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:

  1. Reads the PR's closingIssuesReferences (GraphQL) — the same resolved list GitHub shows in the PR's Development panel, covering both Closes/Fixes/Resolves #N text and manually linked issues. No matches → no-op.
  2. For each linked issue, sets the org-wide Work status issue field (single select) to Ready for QA via the setIssueFieldValue GraphQL 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 labeled skip-qa goes straight to Done and gets closed instead — see below.
  3. Assigns everyone listed in that repo's .github/QAOWNERS — see below (skipped for skip-qa issues, since there's no QA pass to assign).
  4. 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).

Issue status sync workflow

.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 to qa-routing (Ready for QA, or Done for skip-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. "Runs qa-routing" means a workflow file on the default branch has a uses: line calling the qa-routing.yml reusable workflow; a repo without one still gets Done on a PR close. Only a PR in the same repo counts — qa-routing skips 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_added activity 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.

PR-linked issue status workflow

.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.

Project template: kanban on Work status, not project Status

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.

Template shape

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.

Why the views are filtered to is:issue

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.

Which built-in project automations still apply

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.

What a copy carries, and what it doesn't

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.

Claude Code plugin

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

Changing something here

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.

About

Organization-wide defaults for havit-internal

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors