Skip to content

feat: add lint-strategy input for squash-merge workflows#35

Merged
mridang merged 6 commits into
masterfrom
feat/lint-strategy
Apr 9, 2026
Merged

feat: add lint-strategy input for squash-merge workflows#35
mridang merged 6 commits into
masterfrom
feat/lint-strategy

Conversation

@mridang
Copy link
Copy Markdown
Owner

@mridang mridang commented Apr 9, 2026

Description

Adds a new lint-strategy action input with three values:

  • commits (default) — lints every commit in the event. Identical to today's behaviour.
  • pr-title — lints only the pull request title, read straight from the webhook payload (no extra GitHub API call).
  • both — lints the PR title and every commit.

Implemented as a Strategy pattern in src/strategies/ (CommitsStrategy, PrTitleStrategy, BothStrategy + a tiny getLintStrategy() factory). Strategies sit between the existing event-specific commit fetchers and the linter, so the fetcher layer is unchanged.

When lint-strategy: pr-title is used on an event without a pull request in the payload (push, merge_group, etc.), the action emits a core.notice() and exits successfully instead of failing with "No commits found to lint". This makes the strategy safe to combine with push: branches: [main] triggers — the post-merge push run no longer fails.

commit-depth continues to limit commits only; the PR title is never trimmed.

This PR also includes several drive-by quality fixes discovered during multi-round review:

  • Restored proper noun/verb agreement in the DefaultFormatter Job Summary lines
  • Pluralized the "All N commit messages are okay" success log
  • Fixed a "warning" → "warnings" typo in the fail-on-warnings notice

Related Issue

N/A — direct contribution.

Motivation and Context

For repositories that use squash-merge, individual commits on a feature branch are discarded at merge time. The single squash commit that lands in main defaults its message to the PR title, so linting the per-commit messages is busywork — the thing that actually ends up in git history is the PR title, and that is what should be enforced.

Before this change, users had no way to point this action at the PR title. After this change, lint-strategy: pr-title does exactly that, with zero extra API calls (the title is read from the existing webhook payload).

The graceful skip on non-PR events was added after a footgun was identified: a workflow with both pull_request and push: branches: [main] triggers would fail every merge under pr-title because the post-merge push event has no PR title in its payload. The skip-with-notice behaviour makes the mixed-trigger pattern safe.

How Has This Been Tested?

  • Unit tests for all three strategy classes in test/strategies/{commits,pr-title,both,index}.test.ts, including:
    • Frozen-array assertions on every code path
    • PrTitleStrategy asserting the fetcher is never invoked (mock throws on call)
    • BothStrategy constructor injection + depth interaction (PR title not counted toward commit-depth)
    • Factory test covering all three strategy names
  • Integration tests added to test/index.test.ts covering:
    • pr-title on pull_request with valid/invalid title
    • pr-title on push and merge_group (asserts graceful skip, fetcher not called)
    • both on pull_request with valid title + commits, and invalid title + valid commits
    • Garbage lint-strategy value rejected with a clear error
  • Verification suite (all green): prettier --check, npm run lint, tsc --noEmit, knip, npm test, npm run build
  • 100% line/branch/function coverage on src/strategies/*
  • 81 tests pass across 14 suites

Documentation:

README updated:

  • New lint-strategy entry in the Inputs section documenting all three values
  • New Squash-merge workflows subsection with a copy-paste recipe and explanation of why
  • The graceful-skip behaviour is called out as a "Safe with mixed triggers" note

JSDoc on every new class, interface, method, and the input parser.

Checklist:

  • I have updated the documentation accordingly.
  • I have assigned the correct milestone or created one if non-existent.
  • I have correctly labeled this pull request.
  • I have linked the corresponding issue in this description.
  • I have requested a review from at least 2 reviewers
  • I have checked the base branch of this pull request
  • I have checked my code for any possible security vulnerabilities

🤖 Generated with Claude Code

Adds a new `lint-strategy` action input that decides which items the
action should hand to commitlint:

- `commits` (default) — current behaviour, lints every commit in the
  event. Backwards compatible: workflows that don't set the input
  see zero change.
- `pr-title` — lints only the pull request title, read directly
  from the webhook payload (no extra GitHub API calls). Useful for
  squash-merge workflows where individual commits are discarded at
  merge time and the PR title becomes the squash commit message.
- `both` — lints the PR title and every commit. Degrades gracefully
  to `commits` on events without a `pull_request` payload.

Implemented as a Strategy pattern in `src/strategies/`, orthogonal
to the existing event-based fetchers. `pr-title` skips the
`fetchCommits` callback entirely, so no `pulls.listCommits` API
round-trip happens when the strategy has no use for the commits.

To make `pr-title` safe to combine with `push: branches: [main]`
triggers (so the post-merge `push` run does not fail), `run()`
detects the `pr-title` + non-PR-payload case and emits a workflow
notice and returns success rather than failing with "No commits
found to lint".

`commit-depth` continues to apply only to commits; the PR title is
never trimmed. All strategy code is fully immutable: `readonly`
fields, `ReadonlyArray` returns, frozen arrays and items.

100% line/branch/function coverage on `src/strategies/*` plus new
integration scenarios in `test/index.test.ts` covering every
strategy on every relevant event type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mridang mridang self-assigned this Apr 9, 2026
@mridang mridang added the enhancement New feature or request label Apr 9, 2026
mridang and others added 5 commits April 9, 2026 14:08
…le docs)

Follow-up to the initial `lint-strategy` PR. Addresses four real
gotchas surfaced during a deeper audit.

1. **README squash-merge recipe was missing `edited`.** GitHub's
   default `pull_request` activity types are
   `[opened, synchronize, reopened]` — `edited` is **not** included.
   Without `edited`, a contributor who opens a PR with a bad title
   and then fixes the title in the GitHub UI would not re-trigger the
   workflow, leaving them stuck unless they pushed a new commit. The
   recipe and the input description in the README now opt in to
   `edited` and explain why.

2. **`action.yml` description was stale.** It still claimed
   `lint-strategy: pr-title` would fail with "No commits found to
   lint" on push/merge_group, even though that path was already
   replaced with a graceful skip-with-notice. Updated to match
   reality and to mirror the README's `edited` advice for
   marketplace consumers.

3. **Defensive `null` check on `pull_request`.** The early return in
   `run()` and the check inside `PrTitleStrategy.resolve()` only
   handled `pull_request === undefined`. A literal
   `"pull_request": null` in the payload would have slipped past
   both and crashed on `pr.title`. Both spots now also handle `null`,
   with new unit and integration tests covering the scenario.

4. **Notice message wording.** The previous wording named only
   `push` and `merge_group`, but the skip can also fire on
   `workflow_dispatch`, `schedule`, and other events without a PR
   context. Tightened to "events without a pull request context,
   such as a 'push' event fired after a squash-merge".

79 tests pass (up from 77), `src/strategies/*` still 100% coverage,
and `dist/main.cjs` rebuilt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Six follow-ups surfaced during a thorough audit of the lint-strategy
work, focused on documentation accuracy, formatter wording, factory
hardening, and a real test for the graceful skip notice.

- README: extend the `edited` warning to cover `lint-strategy: both`,
  which has the same UX trap as `pr-title` (PR title fixes won't
  re-trigger the workflow without `edited` in the trigger types).
- Formatter: replace the misleading hardcoded "commits were analyzed
  as part of this push" wording with neutral "messages were analyzed",
  rename the `SHA` table column to `ID` so PR-title rows don't read
  oddly, and pluralize the headline correctly.
- index.ts: pluralize "Found N commit message(s) with errors/warnings"
  so a single failing commit no longer reads as "1 commit messages".
- strategies/index.ts: add a defensive `default` branch with an
  `assertNever` cast so a future addition to `LintStrategyName` that
  forgets to update the factory throws at runtime instead of silently
  returning undefined.
- tests: add a stdout-spy test that asserts a workflow notice is
  actually emitted on the `lint-strategy=pr-title` graceful skip path,
  closing a real test gap. Also add a factory test for the new
  defensive default branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A pedantic third-pass audit of the workflows, README, and top-level
config files surfaced both pre-existing accuracy bugs and PR-related
staleness in the user-facing documentation.

- README: remove the dead `config-file` reference from the main usage
  example. The input was removed in commit 4160881 ("More changes")
  but never cleaned up — it's not declared in `action.yml`, not read
  anywhere in `src/`, and silently no-ops for users who set it.
  Cosmiconfig handles configuration auto-detection on its own.
- README: update the `lint-strategy` inline comment in the main
  example so users who switch from `commits` to `pr-title`/`both` see
  the `edited` requirement at the point of use, not just buried in
  the squash-merge subsection.
- action.yml: declare the previously-undocumented `working-directory`
  input. It was already read by `src/index.ts` and worked at runtime
  via env-var translation, but was invisible to the Marketplace card,
  IDE validators, and anyone reading `action.yml` to discover the
  available inputs.
- action.yml + package.json: update the top-level descriptions to
  mention that the action can lint pull request titles in addition
  to commits, since both descriptions were stale after the
  lint-strategy work landed.
- src/index.ts: generalize the graceful-skip notice message so it
  names both `push` (post squash-merge) and `merge_group` (merge
  queue) as expected events instead of only the push case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restore proper noun/verb agreement in DefaultFormatter summary lines
  (regression from a previous round that removed "commit/commits" without
  updating dependent verbs)
- Pluralize the "All N commit messages are okay" success log
- Drive-by typo fix: "warning" -> "warnings" in fail-on-warnings notice
The "Found N commit message(s) with errors" / "...with warnings"
messages emitted by setFailed() were using `Results.errorCount` and
`Results.warningCount`, which sum the number of individual findings
across all commits. A single commit with three errors was reported as
"Found 3 commit messages with errors", which is misleading.

Add `errorCommitsCount` and `warningCommitsCount` to `Results` (count
of distinct commits with at least one error/warning) and use them in
the failure messages. The new counts are computed in the same reduce
pass as the existing totals, so there is no extra iteration. Refactor
DefaultFormatter.formatSummary to consume `errorCommitsCount` from
Results instead of recomputing it inline.

Tests added to test/linter/result.test.ts cover the bug case
(one commit with multiple findings) for both errors and warnings, and
all existing assertions are extended to cover the two new properties.

No breaking change: the existing `errorCount` / `warningCount`
properties retain their meaning and value.
@mridang mridang merged commit 550ef9c into master Apr 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant