Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8f6390
fix: bootstrap phase 5 sets four repository settings left at GitHub d…
TzuH-Hsu Sep 2, 2026
3a43bc8
feat: bootstrap phase 6 reports and offers repository security settings
TzuH-Hsu Sep 2, 2026
1ca709d
feat: bootstrap phase 9 requires an explicit licence choice
TzuH-Hsu Sep 2, 2026
1859a3c
feat: runner selection moves to the RUNNER_LABELS repository variable
TzuH-Hsu Sep 2, 2026
7b5891e
ci: re-trigger with RUNNER_LABELS set (temporary)
TzuH-Hsu Sep 2, 2026
ef03a77
feat: ship the coarse-Type fallback labels as a commented-out opt-in
TzuH-Hsu Sep 2, 2026
a4fbc91
fix: three P1 licence-phase defects from Codex review
TzuH-Hsu Sep 5, 2026
d2f859b
fix: blank line before the NOTICE fence (MD031)
TzuH-Hsu Sep 5, 2026
403c42b
Merge branch 'feat/28-licence-phase' into feat/31-runner-labels
TzuH-Hsu Sep 5, 2026
8cc46f9
fix: prohibit self-hosted runners on a public repository (P1)
TzuH-Hsu Sep 5, 2026
56cadf6
Merge remote-tracking branch 'origin/main' into fix/24-repo-settings
TzuH-Hsu Sep 5, 2026
978df54
Merge branch 'fix/24-repo-settings' into feat/26-security-phase
TzuH-Hsu Sep 5, 2026
0c2ed50
Merge branch 'feat/26-security-phase' into feat/28-licence-phase
TzuH-Hsu Sep 5, 2026
8504ce6
Merge branch 'feat/28-licence-phase' into feat/31-runner-labels
TzuH-Hsu Sep 5, 2026
c792c57
Merge branch 'feat/31-runner-labels' into feat/33-coarse-type-fallback
TzuH-Hsu Sep 5, 2026
656e4ef
fix: two P2s in the security phase from Codex review
TzuH-Hsu Sep 5, 2026
d3fa7d8
Merge branch 'feat/26-security-phase' into feat/28-licence-phase
TzuH-Hsu Sep 5, 2026
c1938dd
fix: two P2s in the licence phase from Codex review
TzuH-Hsu Sep 5, 2026
5430d51
Merge branch 'feat/28-licence-phase' into feat/31-runner-labels
TzuH-Hsu Sep 5, 2026
94fc957
Merge branch 'feat/31-runner-labels' into feat/33-coarse-type-fallback
TzuH-Hsu Sep 5, 2026
fb9d4ab
fix: two P2s in the coarse-Type fallback from Codex review
TzuH-Hsu Sep 5, 2026
d7d11d3
Merge remote-tracking branch 'origin/main' into feat/33-coarse-type-f…
TzuH-Hsu Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions .github/PROJECT_FIELDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Every issue/PR attribute lives in **exactly one place**. Never dual-write the sa

| Attribute | Home | Values / format |
| --- | --- | --- |
| Type (coarse) | **Native issue type** | `Bug` / `Feature` / `Task` — set by the issue form |
| Type (coarse) | **Native issue type** | `Bug` / `Feature` / `Task` — set by the issue form (no organization? see "Personal accounts") |
| Type (subtype) | `type:*` **labels** | `chore` / `ops` / `docs` / `security` — Task subtypes only |
| Priority | `priority:*` **labels** | `p0` critical / `p1` milestone-blocking / `p2` important / `p3` polish |
| Area | `area:*` **labels** | starter set: `docs`, `skills`, `ci`, `governance` — rename to your domains |
Expand All @@ -18,16 +18,48 @@ Every issue/PR attribute lives in **exactly one place**. Never dual-write the sa
| Dependencies | **Native issue relationships** | GitHub blocked-by / blocking |
| Epic membership | **Native sub-issues** | parent issue with sub-issues; no `epic:*` labels |

## Personal accounts
## When native issue types are unavailable

Native issue types require an organization — on personal-account repos the
coarse Type row above has no native home (`repos/{repo}/issue-types` 404s).
The issue form used at creation (bug/feature/task) still captures intent,
but GitHub silently ignores its `type:` key with no org to back it. Adopters
on personal accounts who need queryable coarse type may extend the `type:*`
labels with `bug`/`feature` as the Type home instead — this is still
single-home: labels become the *one* home for Type when native types are
unavailable, never both at once.
Native issue types began as an organization-only feature and have since been
rolled out to personal accounts as well, so **check rather than assume**:

```bash
gh api repos/{owner}/{repo}/issue-types --jq '.[].name'
```

If that 404s or comes back empty, the issue forms' top-level `type:` key is
silently ignored — the form still captures intent at creation, but nothing
stores it. Bootstrap phase 2 runs this check for you and says which case you
are in.

The coarse Type row above therefore has **no home by default** on a personal
account. There are two supported resolutions, and you pick exactly one:

1. **Accept no coarse Type** (the default; nothing to configure). Task
subtypes, priority, area, status and milestone all still work — you simply
cannot filter by Bug vs Feature.
2. **Adopt the label fallback.** Uncomment the `type:bug` / `type:feature`
block in `.github/labels.yml` and re-run `scripts/bootstrap.sh`. Labels
become the *one* home for coarse Type, and the authority-map row above reads
`type:bug` / `type:feature` **labels** instead of Native issue type.

This is still single-home, and it is enforced in both directions:

- The two labels ship **commented out**, never active, so an organization repo
cannot end up holding a native `Bug` type *and* a `type:bug` label.
- Bootstrap phase 2 checks both directions. On a 404 it reports whether the
fallback is in use; when native types *are* available and the fallback is
also declared, it reports a single-home violation and tells you to re-comment
the block.
- If you later transfer the repo into an organization, removing those two
entries is part of enabling native types — not optional cleanup.
- The labels are applied **by hand**. `.github/workflows/issue-labeler.yml`
never adds or removes them: the coarse Type comes from the form's top-level
`type:` key, which is not part of the issue body the labeler parses.
Comment on lines +56 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude fallback types from form-managed removals

When the fallback is enabled and an issue with a manually applied coarse-Type label is later edited, .github/workflows/issue-labeler.yml:119-138 classifies every type:* label as form-managed but only derives desired labels from the Task subtype field, so it removes type:bug or type:feature. The assertion here that the workflow never removes these labels is therefore false, and opted-in type metadata silently disappears; narrow the managed predicate to the subtype labels or explicitly exempt the fallback labels.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

- There is no `type:task`. An issue with neither label is a Task, and every
Task already carries exactly one required
`type:chore|ops|docs|security` subtype — so the Task query stays a single
positive predicate rather than a negation.

## Rules

Expand Down
42 changes: 42 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
# `description:` entries only, in that order. No YAML quoting, no
# multi-line block scalars, no inline comments after values.
#
# Whole-line comments are fine, and are load-bearing here: the coarse-Type
# fallback block below ships commented out on purpose. Comments *after* a value
# on the same line are still unsupported.
#
# Descriptions must not contain a ": " sequence — `make lint-docs` runs
# `yamllint .` over this file, so `description: Coarse Type: Bug` is invalid
# YAML even though the awk parser would happily accept it.
#

labels:
- name: type:chore
Expand All @@ -28,6 +36,40 @@ labels:
color: d93f0b
description: Security subtype (native type stays Task)

# --- Coarse Type fallback (personal accounts only) -------------------------
# Uncomment ONLY if `gh api repos/{owner}/{repo}/issue-types` 404s or is empty
# for this repo. When it does, the coarse Type (Bug/Feature/Task) has no native
# home and the issue forms' top-level `type:` key is silently ignored.
#
# Native issue types began as an organization-only feature and have since been
# rolled out to personal accounts too, so check the endpoint rather than
# assuming from your account type -- bootstrap phase 2 checks it for you.
#
# Uncomment the two entries below ONLY on a personal-account repo where you

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the fallback on endpoint availability

On an organization-owned repository where the issue-types endpoint is unavailable—a case docs/setup/bootstrap.md:97-99 explicitly allows—this instruction forbids enabling the only fallback even though the ADR identifies endpoint availability, not account type, as the deciding condition. Such repositories are left without a coarse-Type home; permit the labels whenever the endpoint is 404/empty and forbid them only when native types are actually returned.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

# want a queryable coarse Type. Labels then become the *one* home for it — see
# "Personal accounts" in .github/PROJECT_FIELDS.md.
#
# On an ORGANIZATION repo, leave them commented out. Native issue types are
# the home there, and activating these would give coarse Type two homes — the
# dual-write ADR-0003 forbids. Bootstrap phase 2 checks both directions and
# warns if it sees the wrong one.
#
# Task needs no label: an issue with neither of these is a Task, and every
# Task already carries exactly one required type:chore|ops|docs|security
# subtype, so the Task query stays a positive predicate.
#
# These are applied BY HAND. The labeler workflow never adds or removes them:
# the coarse Type comes from the form's top-level `type:` key, which is not
# part of the issue body it parses.
#
# - name: type:bug
# color: d73a4a
# description: Coarse Type Bug — label fallback where native issue types are unavailable
#
# - name: type:feature
# color: a2eeef
# description: Coarse Type Feature — label fallback where native issue types are unavailable

- name: priority:p0
color: b60205
description: Critical — drop everything
Expand Down
38 changes: 38 additions & 0 deletions docs/adr/ADR-0006-coarse-type-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ADR-0006: Coarse Type on accounts without native issue types

- **Status**: Accepted
- **Date**: 2026-09-02

## Context

The metadata contract (ADR-0003) gives coarse Type — Bug, Feature, Task — exactly one home: GitHub's native issue type, set by the issue form. Native issue types launched as an **organization-only** feature: on a personal-account repository `repos/{repo}/issue-types` returned 404 and the form's top-level `type:` key was silently ignored, leaving coarse Type with no home at all. GitHub has since rolled them out to user accounts — verified on this repository, a `User`-owned repo whose issue types were created 2026-08-21 — so the gap is narrower than when it was first reported, but it has not closed: the endpoint can still be unavailable, and the template must degrade correctly when it is. The condition to test is the endpoint, not the account type.

`.github/PROJECT_FIELDS.md` already allowed `type:bug` / `type:feature` labels as a fallback, but `.github/labels.yml` shipped no such labels. An adopter following that advice had to invent names, colours and descriptions and re-derive the single-home reasoning. This is the second pass at the same gap — commit `0f912c0` documented it once already.

The obvious fix, shipping the two labels active, is worse than it looks. An organization repo has native issue types; an active `type:bug` label would give coarse Type two homes, the exact dual-write ADR-0003 forbids. And an org adopter cannot undo it by deleting the label on GitHub, because the next `scripts/bootstrap.sh` sync recreates anything `labels.yml` still declares. The only durable fix is editing `labels.yml` — so the adopter edits that file either way.

## Decision

The two labels ship **commented out** in `.github/labels.yml`, with the reasoning inline, and bootstrap phase 2 enforces the contract in both directions.

1. **Commented, not active.** The adopter edits `labels.yml` regardless; the edit should fall on the account type that *wants* the label, not the one that must not have it. It also keeps the label budget in `skills/labels-and-taxonomy/SKILL.md` literally true, and makes activation a deliberate, reviewable act.
2. **Phase 2 checks both directions.** On the 404 path it reports whether the fallback is in use and offers it if not. On the success path — native types available *and* the fallback declared — it reports a single-home violation. That second half is what a "ship them active" design cannot provide at all.
3. **Phase 2 stays read-only.** It creates no labels and can still never `fail`; it is advisory.
4. **The check reads the declared set, not the live repo.** `parse_labels_yml` on `labels.yml` keeps `--dry-run` honest: a dry run creates nothing, so a live-repo query would report "no fallback" for a repo that is about to get one.
5. **No `type:task`.** An issue with neither label is a Task, and every Task already carries exactly one required `type:chore|ops|docs|security` subtype, so the Task query stays a positive predicate rather than a negation.
6. **The labels are applied by hand.** The labeler workflow never touches them.

## Consequences

- Personal-account adopters get correct names, colours, descriptions and reasoning, and opt in with one edit.
- Organization adopters do nothing and cannot accidentally acquire a second Type home.
- `.github/labels.yml` now depends on whole-line comments being invisible to its awk parser. That is verified — `/^[[:space:]]*-[[:space:]]*name:/` cannot match a `#`-led line — and the file header now says the comments are load-bearing so nobody "tidies" them into active entries.
- Bootstrap phase 2 grows a second failure mode to report, and the org branch of it cannot be exercised from a personal-account maintainer's own E2E.
- This does **not** supersede ADR-0003. It implements the escape hatch ADR-0003's authority map already allowed, and keeps single-home intact by making the two homes mutually exclusive rather than concurrent.

## Alternatives considered

- **Ship the labels active with a "delete these on org accounts" comment.** Rejected: a comment is not an enforcement mechanism, org repos get the dual-write by default, and deleting the label on GitHub is undone by the next sync.
- **Have phase 2 create the labels on the 404 path.** Rejected on mechanism, not principle: `main()` runs `phase_labels` before `phase_issue_types`, so the next run's phase 1 sees an undeclared label and prunes it (the default answer is yes), phase 2 recreates it, forever. Avoiding the loop would need a second `autorelease:*`-style prune exclusion for a class of labels no file declares, and it breaks `labels.yml`'s single-source-of-truth claim.
- **A Project `Type` single-select field.** Rejected: `docs/setup/project-views.md` caps the board at two custom fields, and a Project field is not queryable from `gh issue list`, which is the actual requirement.
- **Teach the labeler to apply them.** Deferred, not rejected. The coarse Type comes from the form's top-level `type:` key, which never appears in the issue body, and `context.payload.issue.type` is null on personal accounts. The only body-derived signal is heading-sniffing, which breaks the moment an adopter renames a form field — and adopters are told to customise these forms. Doing it properly needs a form-identity marker in all three forms plus labeler logic to read it.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ Routine choices (a library patch bump, a wording tweak) do not get ADRs. When in
| [ADR-0003](ADR-0003-metadata-single-home.md) | Metadata single-home policy | Accepted |
| [ADR-0004](ADR-0004-adopter-licence-choice.md) | The adopter's licence is an explicit bootstrap decision | Accepted |
| [ADR-0005](ADR-0005-runner-selection-variable.md) | Runner selection is an adopter variable, not a workflow edit | Accepted |
| [ADR-0006](ADR-0006-coarse-type-fallback.md) | Coarse Type on accounts without native issue types | Accepted |
39 changes: 28 additions & 11 deletions docs/setup/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,26 @@ Checks whether native issue types (`Bug`/`Feature`/`Task`) are available via
`gh api repos/{repo}/issue-types`. This endpoint isn't available on every
plan/org configuration.

Manual: **Organization settings → Repository → Issue types** (an
organization account is required — personal accounts don't expose native
issue types). If unavailable, the issue forms' `type:` key is silently
ignored by GitHub; the form still works, it just won't set a native type.
On personal-account repos, see the "Personal accounts" note in
`.github/PROJECT_FIELDS.md` for the label-based fallback.
Manual: **Organization settings → Repository → Issue types** on an org repo, or
**Settings → Issue types** on a personal account — these began as an
organization-only feature and have since been rolled out to user accounts, so
check the endpoint rather than assuming from your account type. If they really
are unavailable, the issue forms' `type:` key is silently ignored by GitHub; the
form still works, it just won't set a native type.
On a personal account you then choose one of two things, and the phase tells
you which you currently have:

1. **Accept no coarse Type.** Subtypes, priority, area, status and milestone all
still work; you just cannot filter Bug vs Feature. Nothing to configure.
2. **Adopt the label fallback.** Uncomment the `type:bug` / `type:feature` block
in `.github/labels.yml` and re-run. Labels become the *one* home for coarse
Type.

They ship commented out so an organization repo — which already has native
issue types — cannot end up holding both homes at once. The phase checks that
direction too: with native types available *and* the fallback declared, it
reports a single-home violation. See "Personal accounts" in
`.github/PROJECT_FIELDS.md`.

### 3. Milestone

Expand Down Expand Up @@ -374,11 +388,14 @@ admin, or set the four toggles by hand in **Settings → Advanced Security**.
doesn't request the `project` scope. Fix: `gh auth refresh -s project`, then
re-run.

**`issue-types` endpoint returns 404 / empty** — native issue types are an
organization-account feature; personal-account repos and some plans don't
expose them. The issue forms still work, but their `type:` key has no
effect until issue types are enabled at the org level (or the repo is
transferred into an org that has them).
**`issue-types` endpoint returns 404 / empty** — the repo has no native issue
types, so the issue forms' `type:` key has no effect. These began as an
organization-only feature and have since been rolled out to personal accounts
as well, so check **Settings → Issue types** (or the org equivalent) before
concluding you cannot have them. If they genuinely are unavailable and you want
a queryable coarse Type, uncomment the `type:bug` / `type:feature` block in
`.github/labels.yml` and re-run — those labels are applied by hand, never by the
labeler workflow.

**Ruleset name conflict** — if a ruleset named `main-branch-protection`
already exists, the script skips phase 8 rather than overwriting it (syncing
Expand Down
2 changes: 1 addition & 1 deletion docs/template/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Why each piece of this repository exists, and what it costs to keep. A component
| `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` | Per-agent entry points (≤10 lines each) | Near zero — pointers only |
| `skills/` (15 modules) | On-demand knowledge for humans + agents | Content reviews; consistency enforced by `scripts/check-skills.sh` |
| `.github/PROJECT_FIELDS.md` | Metadata single-home authority map | Update only when taxonomy changes (rare, ADR-worthy) |
| `.github/labels.yml` | Labels as code; bootstrap re-run = sync | Edit alongside label changes; allowlist in `issue-labeler.yml` must match |
| `.github/labels.yml` | Labels as code; bootstrap re-run = sync; carries the commented-out coarse-Type fallback (ADR-0006) | Edit alongside label changes; `FORM_MANAGED_TYPES` in `issue-labeler.yml` must match, and the commented block must stay commented |
| `.github/ISSUE_TEMPLATE/` (3 forms) | Set native types; feed the labeler | Sync option lists with `labels.yml` |
| `.github/PULL_REQUEST_TEMPLATE.md` | Validation ladder + RISK convention at point of use | Near zero |
| `.github/workflows/ci.yml` | L0 gate; installs tools via `make ci-tools`, runs `make ci-pr`. The only required status check — see its header before touching `on:` or `runs-on:` | SHA-pin bumps via Dependabot |
Expand Down
Loading