feat: expose hook contributions via specify artifact - #4348
Open
nicolehaugen wants to merge 1 commit into
Open
Conversation
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
Contributor
There was a problem hiding this comment.
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4343
Stacked on top of #4305.
Summary
Extends
specify artifact list --jsonandspecify artifact info --jsonto surface hook contributions as a fourthArtifactKindalongside 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
eventName,targetCommand,optional,priority, andregistered.strategy: replaceandlookupIdfromderive_hook_id.registeredreflects the runtime by reading.specify/extensions.ymlbindings via a newHookExecutor.is_hook_registeredhelper. Declared-but-unbound hooks still appear withregistered: false.hook:{eventName}:{targetCommand}round-trips throughartifact info; unknown hook shorthands return the sameunknown artifacterror envelope as other kinds.preset/extensionlayers, never on the built-in tier (matches_HOOK_LAYERSinsrc/specify_cli/_identifier.py).Tests
30 new tests in
tests/test_artifact_command.pycover:lookupIdparity withderive_hook_id.registeredtrue / false paths against a scaffolded.specify/extensions.yml(including malformed-config graceful degradation).hook:{event}:{command}throughinfo.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.mdextended with a Hook artifacts subsection covering shorthand,registeredsemantics, and the_HOOK_LAYERSinvariant.Assisted by GitHub Copilot (model: Claude Opus 4.7, autonomous).