diff --git a/.claude/commands/create-branch.md b/.claude/commands/create-branch.md index 0a6774e04..63b0c8dd3 100644 --- a/.claude/commands/create-branch.md +++ b/.claude/commands/create-branch.md @@ -16,7 +16,7 @@ You are running `/create-branch`. Cut a fresh branch off the latest `main`, name 1. **Sync.** Run `git fetch origin --prune`. 2. **Read the change (if any).** If the working tree already has changes, run `git status` and `git diff` and use them to infer the branch `type` and a slug. If the tree is clean, derive both from `$ARGUMENTS` (ask for a 2–4 word description if none was given). 3. **Type.** Pick from the same enum the repo's commit convention uses: `feat` (default — new component/prop/event/export), `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `revert`. Only `feat` / `fix` (or a breaking `!`) actually release (see [`release-types.md`](../rules/release-types.md)). -4. **Related issue (optional).** Ask: "Which issue does this relate to? (`ENG-1234`, or leave blank for `NO-ISSUE`)". A blank answer is fine — treat it as no issue. Do not block on it. +4. **Related issue (optional).** Ask: "Which issue does this relate to? (`ENG-1234`, or leave blank if none)". A blank answer is fine — treat it as no issue (no tag anywhere; never `[NO-ISSUE]`). Do not block on it. 5. **Create.** - With an issue: `git checkout -b /- origin/main` - Without: `git checkout -b / origin/main` diff --git a/.claude/commands/open-pr.md b/.claude/commands/open-pr.md index 9bb306b43..0dd86af03 100644 --- a/.claude/commands/open-pr.md +++ b/.claude/commands/open-pr.md @@ -26,7 +26,7 @@ Run `git diff` (staged + unstaged) and determine, from the change itself: If the diff spans multiple packages, prefer **one commit per scope** (CONTRIBUTING: "one package per commit when possible"). ### 2. Related issue (optional) -- Ask: "Which issue does this relate to? (`ENG-1234`, or leave blank for `NO-ISSUE`)". Blank → use `[NO-ISSUE]`. +- Ask: "Which issue does this relate to? (`ENG-1234`, or leave blank if none)". With an issue, the tag goes **in the subject, after the colon** (`fix(webkit): [ENG-1234] …`) — never before the type. **No issue → no tag** — never write `[NO-ISSUE]` (dead characters; commitlint rejects it). ### 3. Ensure a `main`-based feature branch - If on `main` (or not on a `main`-based feature branch), run the `/create-branch` flow (type from step 1, issue from step 2), carrying the working changes over. @@ -34,8 +34,9 @@ If the diff spans multiple packages, prefer **one commit per scope** (CONTRIBUTI ### 4. Commit by context - **Split shared docs/rules from code.** If the diff mixes code with shared docs/rules/templates (`.claude/rules/*`, `.claude/skills/*`, `.specs/_template.md`, …), tell the user those belong in a **separate PR** and offer to split them out. A component's own new `.specs/.md` stays with the component. -- Header (commitlint-valid, ≤100 chars): `[] (): ` - - Breaking: `[…] ()!: ` plus a `BREAKING CHANGE: ` footer. +- Header (commitlint-valid, ≤100 chars): `(): [] ` — the `[]` tag only when an issue exists; with none, plain `(): `. + - Breaking: `()!: [] ` plus a `BREAKING CHANGE: ` footer. + - The header **starts with the bare type**. A leading ticket tag (`[ENG-1234] fix: …`) is unparseable by release-please — the merge would release **nothing** — and commitlint now rejects it (as it rejects `[NO-ISSUE]` anywhere). - **Never** add a `Co-Authored-By` trailer or any "Generated with Claude" / attribution footer. - Let the hooks run — `commit-msg` runs commitlint and the header must pass. **Do not** use `--no-verify` to skip commitlint. If `pre-commit` (lint-staged) fails for an environmental reason, report it and ask the user before retrying. @@ -45,7 +46,7 @@ If the diff spans multiple packages, prefer **one commit per scope** (CONTRIBUTI ### 6. Push and open the PR - `git push -u origin `. - If a PR already exists for the branch, update it; otherwise `gh pr create --base main --head `. -- **Title:** the conventional commit header (or `$ARGUMENTS`). Merges are **squash merges**, so the PR title is the commit release-please parses — keep it commitlint-valid, and put `!` in the title for a breaking change. **Body:** `## Summary` (what + why) and `## Notes`; call out any new dependency and any breaking change; reference the issue. No Figma links, no attribution footer. +- **Title:** the conventional commit header (normalize `$ARGUMENTS` into that form when given). Merges are **squash merges**, so the PR title is the commit release-please parses — it must **start with the bare type**, with the ticket tag after the colon only when an issue exists (`(): [] `), and put `!` in the title for a breaking change. **Body:** `## Summary` (what + why) and `## Notes`; call out any new dependency and any breaking change; reference the issue. No Figma links, no attribution footer. - Report the PR URL. ## Rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d568c855..6da0e02af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,20 +118,20 @@ Types marked **none** never release on their own — they ride along in the next The commit parser accepts these forms: ```text -[NO-ISSUE] fix(webkit): commit message -[ENG-1231] fix(webkit): commit message +fix(webkit): [ENG-1231] commit message fix(webkit): commit message fix: commit message ``` -- **Ticket prefix** is optional. Use `[NO-ISSUE]` when there is no tracking ticket, or `[]` (e.g. `[ENG-1231]`) otherwise. +- **The header starts with the bare type.** A leading ticket tag (`[NO-ISSUE] fix(webkit): …` — the pre-2026-07 convention) is unparseable by release-please, so the merge would silently release **nothing**; commitlint rejects it (`header-no-leading-ticket`). +- **Ticket tag** — only when a tracking ticket exists: `[-]` (e.g. `[ENG-1231]`) at the start of the **subject**, right after the colon. **No ticket → no tag** — never write `[NO-ISSUE]`; it is dead characters in the 100-char budget and in changelogs, and commitlint rejects it (`subject-ticket-tag`, which also rejects malformed tags like `[eng-123]`). The tag is part of the subject, so it appears in changelogs. - **Scope** is the package name without the namespace: `webkit`, `theme`, `icons`. - **Breaking changes** use either the `!` marker (`feat(webkit)!: …`) or a `BREAKING CHANGE:` footer. Examples: -- `[ENG-1231] feat(webkit): add Dropdown component` -- `[NO-ISSUE] fix(theme): correct --ring-color for dark mode` +- `feat(webkit): [ENG-1231] add Dropdown component` +- `fix(theme): correct --ring-color for dark mode` - `chore(icons): regenerate after source update` - `feat(webkit)!: drop deprecated tone prop on Button` @@ -145,6 +145,7 @@ A husky `commit-msg` hook runs `@commitlint/cli` against [`commitlint.config.js` The config also enforces: +- The header **starts with the bare type** — its pattern now matches release-please's parser exactly. A leading `[TICKET]` tag is rejected with a pointer to the new form (`header-no-leading-ticket`), and a subject-leading tag must be a well-formed real ticket (`[ABC-123]` + space + text); `[NO-ISSUE]` is rejected — omit the tag instead (`subject-ticket-tag`). - `type` must be one of: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `revert`. Only `feat` (minor) and `fix` (patch) produce a release; every other type is accepted for hygiene and produces **no** version bump on its own. Breaking changes use the `!` marker or `BREAKING CHANGE:` footer and produce a `major` release on any type. (`hotfix` was removed 2026-07-26 — release-please never released it; use `fix`.) - `type` and `scope` must be lower-case. - `subject` cannot be empty. @@ -179,13 +180,13 @@ Open an [issue](https://github.com/aziontech/webkit/issues/new) with: ## Releasing -Releases are automated by `semantic-release` on merge to `main`. There is no manual release step. Three workflows publish independently: +Releases are automated by [release-please](https://github.com/googleapis/release-please) ([`release-please.yml`](./.github/workflows/release-please.yml)) — the Release PR model. Merging `feat` / `fix` / breaking commits to `main` creates or updates a pending **Release PR** (version bumps + changelogs); merging that Release PR creates the per-package tags and GitHub Releases, which trigger the publish workflows via `release: published`: - [`package-icons.yml`](./.github/workflows/package-icons.yml) → `@aziontech/icons` - [`package-theme.yml`](./.github/workflows/package-theme.yml) → `@aziontech/theme` - [`package-webkit.yml`](./.github/workflows/package-webkit.yml) → `@aziontech/webkit` -If you need a merge that does not produce a release, use `chore:` or `docs:` (no version bump). +There is no manual release step. If you need a merge that does not produce a release, use `chore:` or `docs:` (no version bump). ## Questions diff --git a/commitlint.config.js b/commitlint.config.js index 485d6aa76..4bd5dd8cc 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,14 +1,23 @@ /** * Commitlint config — the commit-message gate for the repo. * Merges to `main` are squash merges: release-please parses the squashed - * commit (the PR title), so keep PR titles commitlint-valid too. + * commit header (the PR title), and its conventional-commit parser is NOT + * configurable — the header must START with the bare type. A leading ticket + * tag (`[NO-ISSUE] fix: …`) is unparseable there and silently produces no + * release, so that form is rejected here; the ticket tag lives in the + * subject instead, right after the colon — and only when a real ticket + * exists. No ticket → no tag (never write "[NO-ISSUE]" — dead characters). * * Accepted forms: - * [NO-ISSUE] fix(webkit): commit message - * [ENG-1231] fix(webkit): commit message - * fix(webkit): commit message + * fix(theme): [ENG-1234] move vue-tsc to devDependencies + * feat(webkit)!: [ENG-999] drop tone prop + * fix(webkit): commit message ← no ticket → no tag * fix: commit message * + * Rejected: + * [NO-ISSUE] fix(webkit): commit message ← pre-2026-07 form — invisible to release-please + * fix(webkit): [NO-ISSUE] commit message ← no ticket → omit the tag entirely + * * Breaking changes (produce a major release): * feat(webkit)!: drop tone prop ← `!` after type/scope * feat(webkit): add x\n\nBREAKING CHANGE: drops y ← footer form @@ -24,11 +33,42 @@ export default { parserPreset: { parserOpts: { - headerPattern: /^(\[[\w-]+\]\s+)?(\w+)(?:\(([\w-]+)\))?!?:\s(.*)$/, - headerCorrespondence: ['ticket', 'type', 'scope', 'subject'] + headerPattern: /^(\w+)(?:\(([\w-]+)\))?!?:\s(.*)$/, + headerCorrespondence: ['type', 'scope', 'subject'] } }, + plugins: [ + { + rules: { + // Migration guard: the old `[TICKET] type: …` header parses as no + // type at all, so without this rule the only feedback would be a + // cryptic "type may not be empty". Point at the new form instead. + 'header-no-leading-ticket': (parsed) => { + const header = parsed.header ?? '' + return [ + !/^\[[^\]]*\]/.test(header), + 'the ticket tag moved: write "type(scope): [TICKET] subject" — a leading "[TICKET] type: …" header is invisible to release-please and releases nothing' + ] + }, + // When the subject opens with a bracket tag, it must be a real, + // well-formed ticket ([ABC-123]) followed by a space and the subject + // text. No ticket → no tag ("[NO-ISSUE]" is dead characters). + 'subject-ticket-tag': (parsed) => { + const subject = parsed.subject ?? '' + if (!subject.startsWith('[')) return [true] + if (/^\[NO-ISSUE\]/i.test(subject)) { + return [false, 'no tracking ticket → omit the tag entirely (do not write "[NO-ISSUE]")'] + } + return [ + /^\[[A-Z]+-\d+\] \S/.test(subject), + 'subject ticket tag must be "[ABC-123]" followed by a space and the subject text' + ] + } + } + } + ], rules: { + 'header-no-leading-ticket': [2, 'always'], 'type-empty': [2, 'never'], 'type-case': [2, 'always', 'lower-case'], 'type-enum': [ @@ -50,6 +90,7 @@ export default { 'scope-case': [2, 'always', 'lower-case'], 'subject-empty': [2, 'never'], 'subject-case': [0], + 'subject-ticket-tag': [2, 'always'], 'header-max-length': [2, 'always', 100] } } diff --git a/packages/webkit/docs/DOC_LINTS.md b/packages/webkit/docs/DOC_LINTS.md index 133d3e6ff..87f5ac360 100644 --- a/packages/webkit/docs/DOC_LINTS.md +++ b/packages/webkit/docs/DOC_LINTS.md @@ -1402,24 +1402,27 @@ Run `pnpm webkit:type-coverage` — `--detail` lists every offending expression. ## 10. commitlint — commit messages -Commits **drive releases**: release-please parses the same header — on `main`, the squashed **PR title** — to compute version bumps ([`release-types.md`](../../../.claude/rules/release-types.md)). Header anatomy: +Commits **drive releases**: release-please parses the same header — on `main`, the squashed **PR title** — to compute version bumps ([`release-types.md`](../../../.claude/rules/release-types.md)). Its parser is **not configurable**: the header must start with the bare type, so a ticket tag lives at the start of the **subject** — and only when a real ticket exists (no ticket → no tag). Header anatomy: ``` -[ENG-1231] feat(webkit)!: add table export -└───┬────┘ └─┬┘└──┬───┘│ └──────┬───────┘ - ticket type scope breaking subject - (optional) (opt.) (opt.) +feat(webkit)!: [ENG-1231] add table export +└─┬┘└──┬───┘│ └───┬────┘ └──────┬───────┘ +type scope breaking ticket subject + (opt.) (opt.) (optional) ``` **❌ Wrong** ```text Update button styles → no type +[NO-ISSUE] fix(webkit): focus ring → leading ticket tag releases NOTHING (header-no-leading-ticket) +fix(webkit): [NO-ISSUE] focus ring → no ticket → omit the tag entirely (subject-ticket-tag) +ENG-1231 fix(webkit): focus ring → header must start with the type: fix(webkit): [ENG-1231] focus ring +fix(webkit): [eng-123] focus ring → malformed tag; use [ABC-123] (subject-ticket-tag) Feat(webkit): add table export → type must be lower-case feature(webkit): add table export → "feature" is not in the enum; use feat feat(Webkit): add table export → scope must be lower-case fix(webkit): → subject is required -ENG-1231 fix(webkit): focus ring → ticket must be bracketed: [ENG-1231] fix(webkit): correct the paginator focus ring so that it stays visible in dark mode and in high-contrast themes → header over 100 chars (111) ``` @@ -1428,8 +1431,8 @@ fix(webkit): correct the paginator focus ring so that it stays visible in dark m ```text fix(webkit): correct paginator focus ring -[ENG-1231] feat(webkit): add table export -[NO-ISSUE] chore: bump tooling +feat(webkit): [ENG-1231] add table export +chore: bump tooling docs: describe compound API exports feat(webkit)!: drop tone prop ```