Skip to content

feat(extensions): per-event hook lists with priority ordering#2798

Open
seiya-koji wants to merge 4 commits into
github:mainfrom
seiya-koji:feat/hook-multiple-entries-with-priority
Open

feat(extensions): per-event hook lists with priority ordering#2798
seiya-koji wants to merge 4 commits into
github:mainfrom
seiya-koji:feat/hook-multiple-entries-with-priority

Conversation

@seiya-koji
Copy link
Copy Markdown
Contributor

Description

Extension manifests allowed only one command per hook event, and hooks fired
in insertion order with no way to control sequencing. Resolves #2378.

Each hook event in extension.yml may now be a single mapping (existing form)
or a list of mappings, with an optional integer priority (>= 1, default 10;
lower runs first, stable on ties). Existing single-mapping manifests are
unaffected. This hook-entry priority is independent of the extension-level
priority used for preset/template resolution.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

18 new unit tests in tests/test_extensions.py cover list/single validation,
priority ordering, dedup, and backward compatibility. End-to-end: installed a
sample extension with a two-command after_plan list via specify extension add — both registered with priorities and resolved in priority order.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Implemented with AI assistance (Claude Code); reviewed by me before submission.

The manifest validator restricted each hook event to a single mapping,
even though HookExecutor stores entries as a list per event. This blocked
an extension from running multiple commands on one event (e.g. a
verification step plus a doc-generation step after speckit.plan), and
get_hooks_for_event returned entries in raw insertion order with no way
to influence execution order across or within extensions.

This change:

1. Validator: accept hooks.<event> as either a single mapping or a list
   of mappings. Each entry is validated individually and may carry an
   optional integer `priority` (>= 1, default 10; bool rejected).
2. Command-ref normalization: apply rename / alias->canonical rewriting
   to every entry in the list, not just the head.
3. register_hooks: expand list entries, persist `priority`, and
   purge-and-replace all entries owned by the extension on each event so a
   reinstall whose shape changed (single<->list, or a shorter list) leaves
   no orphaned entries behind.
4. get_hooks_for_event: sort enabled entries by `priority` ascending with
   a stable sort (ties keep insertion order). The existing
   normalize_priority helper is reused as the sort key so corrupted
   on-disk values fall back to the default instead of raising.

Backward compatible: existing single-mapping manifests parse and register
unchanged with priority defaulting to 10. The extension-level `priority`
used by preset/template resolution is independent of the new hook-entry
`priority`.

Implements github#2378
Copilot AI review requested due to automatic review settings June 1, 2026 20:23
@seiya-koji seiya-koji requested a review from mnriem as a code owner June 1, 2026 20:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR extends the extension hook system to support multiple hook entries per event and introduces per-entry priority ordering for deterministic execution.

Changes:

  • Added DEFAULT_HOOK_PRIORITY and priority-aware sorting for hooks.
  • Extended manifest parsing/validation to accept either a single hook mapping or a list of hook mappings.
  • Updated the extension template and expanded tests to cover list hooks, priority validation, and ordering.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/test_extensions.py Adds regression + new unit tests for list-form hooks, priority validation, and priority ordering in HookExecutor.
src/specify_cli/extensions.py Implements list-form hook normalization, validates per-entry priority, persists priority in config, and sorts hooks by priority.
extensions/template/extension.yml Documents list-form hook configuration and the new priority field.
Comments suppressed due to low confidence (1)

src/specify_cli/extensions.py:93

  • DEFAULT_HOOK_PRIORITY is introduced as the canonical default (10), but normalize_priority() still hard-codes default: int = 10 in its signature. To avoid drift if the default ever changes, consider using the constant as the default parameter (or otherwise centralize the default in one place).
def normalize_priority(value: Any, default: int = 10) -> int:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/extensions.py
Comment thread tests/test_extensions.py Outdated
- Skip non-dict hook entries before .get() so a manifest that bypasses
  validation can't crash register_hooks with AttributeError.
- Normalize `priority` on save via normalize_priority so the on-disk
  config stays clean, mirroring the read-side defense in
  get_hooks_for_event.
- Tests: cover the non-dict-entry skip and add encoding="utf-8" to the
  new tests' manifest writes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment thread src/specify_cli/extensions.py
register_hooks only purged events the new manifest still declared, so an
extension that dropped an event on reinstall left stale entries for it in
the project config. Purge this extension's entries from undeclared events
(and prune emptied events) before registering; scoped to this extension,
and a no-op for the install/update flow where unregister_hooks runs first.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/extensions.py
- normalize_priority falls back to default for bool values
- dedup deletes duplicate commands before re-insert for last-wins ties
- register_hooks purges orphans even when all hooks are dropped
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Allow multiple commands per hook event with explicit priority ordering

3 participants