Skip to content

feat: expose hook contributions via specify artifact - #4348

Open
nicolehaugen wants to merge 1 commit into
nicolehaugen-contribution-ids-and-artifactsfrom
nicolehaugen-expose-hook-artifacts
Open

feat: expose hook contributions via specify artifact#4348
nicolehaugen wants to merge 1 commit into
nicolehaugen-contribution-ids-and-artifactsfrom
nicolehaugen-expose-hook-artifacts

Conversation

@nicolehaugen

Copy link
Copy Markdown

Closes #4343

Stacked on top of #4305.

Summary

Extends specify artifact list --json and specify artifact info --json to surface hook contributions as a fourth ArtifactKind alongside command / template / script. Adds a parallel iterator + stack builder for hooks (their name legitimately contains :, breaking the existing (kind, name) tuple grammar); the JSON envelope remains strictly additive for the existing three kinds.

What's new

  • Hook rows carry top-level eventName, targetCommand, optional, priority, and registered.
  • Stack entries use strategy: replace and lookupId from derive_hook_id.
  • registered reflects the runtime by reading .specify/extensions.yml bindings via a new HookExecutor.is_hook_registered helper. Declared-but-unbound hooks still appear with registered: false.
  • The shorthand hook:{eventName}:{targetCommand} round-trips through artifact info; unknown hook shorthands return the same unknown artifact error envelope as other kinds.
  • Layer invariant preserved: hooks only appear on preset / extension layers, never on the built-in tier (matches _HOOK_LAYERS in src/specify_cli/_identifier.py).

Tests

30 new tests in tests/test_artifact_command.py cover:

  • Extension- and (forward-compat) preset-layer surfacing.
  • lookupId parity with derive_hook_id.
  • Active-winner selection under conflicting priorities with stable insertion-order tiebreak.
  • registered true / false paths against a scaffolded .specify/extensions.yml (including malformed-config graceful degradation).
  • Round-trip of hook:{event}:{command} through info.
  • Absence of hooks from the built-in tier.
  • No regression to command / template / script shapes.

Full suite: 6945 passed, 410 skipped. Remaining Windows-only failures are pre-existing symlink privilege issues (WinError 1314) unrelated to this change.

Docs

docs/reference/artifacts.md extended with a Hook artifacts subsection covering shorthand, registered semantics, and the _HOOK_LAYERS invariant.


Assisted by GitHub Copilot (model: Claude Opus 4.7, autonomous).

Extends `specify artifact list --json` and `specify artifact info --json` to surface hook contributions as a fourth ArtifactKind alongside command / template / script. Adds a parallel iterator + stack builder for hooks (their name legitimately contains `:`, breaking the existing (kind, name) tuple grammar), preserving strictly additive JSON envelope changes for the existing three kinds.

Hook rows carry top-level `eventName`, `targetCommand`, `optional`, `priority`, and `registered` fields. Stack entries use `strategy: replace` and `lookupId` from `derive_hook_id`. The `registered` flag mirrors the runtime by reading `.specify/extensions.yml` bindings via a new `HookExecutor.is_hook_registered` helper. Declared-but-unbound hooks still appear with `registered: false`. The shorthand `hook:{eventName}:{targetCommand}` round-trips through `artifact info`.

Includes 30 new tests covering surfacing, sort order, active-winner selection, registered semantics, the layer invariant, and no-regression on existing kinds. Docs at `docs/reference/artifacts.md` extended with a Hook artifacts subsection.

Closes #4343

Assisted-by: GitHub Copilot (model: Claude Opus 4.7, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 667230a1-e9fa-4500-a57f-c1c482be2507
@nicolehaugen
nicolehaugen requested a review from mnriem as a code owner August 27, 2026 03:54
Copilot AI balanced review requested due to automatic review settings August 27, 2026 03:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds hook contributions to the artifact introspection API alongside commands, templates, and scripts.

Changes:

  • Adds hook inventory, stack, lookup, ordering, and registration metadata.
  • Adds runtime registration helper and extensive tests.
  • Documents hook artifact fields and semantics.
Show a summary per file
File Description
src/specify_cli/artifacts/__init__.py Implements hook artifact collection and serialization.
src/specify_cli/artifacts/_commands.py Accepts hooks in CLI help and kind validation.
src/specify_cli/extensions/__init__.py Adds hook registration lookup.
tests/test_artifact_command.py Covers hook inventory and CLI behavior.
docs/reference/artifacts.md Documents hook artifacts.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (1)

docs/reference/artifacts.md:183

  • Correct the grammatical error in this field description.
| `eventName`     | The event whose fires trigger this hook (`before_specify`, `after_plan`, …)                     |
  • Files reviewed: 5/5 changed files
  • Comments generated: 7
  • Review effort level: Balanced

from .._identifier import (
PROJECT_OVERRIDE_LAYER,
IdentifierComponentError,
derive_hook_id,
layer=layer, # type: ignore[arg-type]
sourceId=str(source_id),
strategy="replace",
active=(position == 0),
continue
if entry.get("extension") != extension_id:
continue
if entry.get("enabled", True) is False:
Comment on lines +436 to +439
try:
resolver = PresetResolver(project_root)
except OSError:
return
Comment on lines +168 to +177
def to_json_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"layer": self.layer,
"sourceId": self.sourceId,
"strategy": self.strategy,
"active": self.active,
"lookupId": self.lookupId,
"priority": self.priority,
"optional": self.optional,
Comment on lines +1081 to +1088
registered = any(
hook_executor.is_hook_registered(
event_name=event_name,
extension_id=entry.sourceId,
command=command,
)
for entry in stack_entries
)
| `--json` | Required. Emit the inventory as a JSON array on stdout. |

Prints the full inventory of every visible artifact — one row per `(kind, name)` pair, including its composition `stack` — sorted by kind (`command`, then `template`, then `script`) and then by name.
Prints the full inventory of every visible artifact — one row per `(kind, name)` pair, including its composition `stack` — sorted by kind (`command`, then `template`, then `script`, then `hook`) and then by name.
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.

2 participants