Fix extension hook paths and native YAML scalar parity · github/spec-kit@c240663 · GitHub
Skip to content

Commit c240663

Browse files
mnriemCopilot
andcommitted
Fix extension hook paths and native YAML scalar parity
Keep shared dispatch paths rooted when extension commands render, decode Bash backslash escapes, and reject implicitly typed identifiers in native resolvers. Add cross-runtime and extension rendering regressions. Assisted-by: GitHub Copilot (model: GPT-6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 87a744a commit c240663

5 files changed

Lines changed: 91 additions & 9 deletions

File tree

‎scripts/bash/pre-hooks.sh‎

Lines changed: 11 additions & 1 deletion

‎scripts/powershell/pre-hooks.ps1‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,19 @@ function Convert-HookScalar {
2323
}
2424

2525
function Add-HookField {
26-
param([hashtable]$Hook, [string]$Text)
26+
param([hashtable]$Hook, [string]$Text, [string]$Event)
2727
if ($Text -notmatch '^([a-z_]+):(?:\s|$)(.*)$') { throw "Unsupported YAML hook field" }
2828
$key = $Matches[1]
2929
$parsed = Convert-HookScalar $Matches[2]
3030
$value = $parsed.Value
31+
if ($key -in @('extension', 'command') -and -not $parsed.Quoted -and (
32+
-not $value -or $value -match '^(true|false|yes|no|on|off|null|~)$' -or
33+
$value -match '^[+-]?(0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|[0-9][0-9_]*(\.[0-9_]*)?([eE][+-]?[0-9]+)?|\.[0-9_]+([eE][+-]?[0-9]+)?|\.([iI][nN][fF]|[nN][aA][nN]))$' -or
34+
$value -match '^[0-9]+(:[0-9]+)+$' -or
35+
$value -match '^[0-9]{4}-[0-9]{2}-[0-9]{2}([Tt ]|$)'
36+
)) {
37+
throw "hooks.$Event needs extension and command"
38+
}
3139
if ($key -in @('enabled', 'optional') -and ($parsed.Quoted -or $value -notin @('true', 'false'))) {
3240
throw "$key must be a boolean"
3341
}
@@ -118,12 +126,12 @@ function Resolve-HookConfig {
118126
if ($target) {
119127
$hook = @{ enabled = 'true'; optional = 'true'; priority = '10' }
120128
$hooks.Add($hook)
121-
Add-HookField $hook $body.Substring(2)
129+
Add-HookField $hook $body.Substring(2) $Event
122130
}
123131
continue
124132
}
125133
if ($itemIndent -ge 0 -and $indent -eq $itemIndent + 2) {
126-
if ($target) { Add-HookField $hook $body }
134+
if ($target) { Add-HookField $hook $body $Event }
127135
continue
128136
}
129137
throw "Unsupported YAML hook layout"

‎src/specify_cli/integrations/base.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -796,12 +796,12 @@ def resolve_hook_script_refs(
796796
for phase, name in (("PRE", "pre"), ("POST", "post")):
797797
if variant == "py":
798798
command = IntegrationBase.build_python_invocation(
799-
f"scripts/python/{name}_hooks.py", project_root
799+
f".specify/scripts/python/{name}_hooks.py", project_root
800800
)
801801
elif variant == "ps":
802-
command = f"scripts/powershell/{name}-hooks.ps1"
802+
command = f".specify/scripts/powershell/{name}-hooks.ps1"
803803
else:
804-
command = f"scripts/bash/{name}-hooks.sh"
804+
command = f".specify/scripts/bash/{name}-hooks.sh"
805805
content = content.replace(f"{{{phase}_HOOK_SCRIPT}}", command)
806806
return content
807807

‎tests/extensions/github/test_github_extension.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,15 @@ def test_rendered_command_points_at_a_script_that_ships(
429429
assert expected in content
430430
# The rendered path must resolve to a file the extension ships.
431431
assert (project / expected).is_file()
432-
# And it must not have been rewritten into the core script tree.
433-
assert ".specify/scripts/" not in content
432+
# The extension's own script stays local; hook dispatchers are shared.
433+
for phase in ("pre", "post"):
434+
hook_path = {
435+
"sh": f".specify/scripts/bash/{phase}-hooks.sh",
436+
"ps": f".specify/scripts/powershell/{phase}-hooks.ps1",
437+
"py": f".specify/scripts/python/{phase}_hooks.py",
438+
}[variant]
439+
assert hook_path in content
440+
assert hook_path.replace(".specify/scripts/", ".specify/extensions/github/scripts/") not in content
434441

435442
@pytest.mark.parametrize("agent", EXTENSION_REGISTRAR_AGENTS)
436443
def test_every_extension_registrar_integration_renders_the_command(
@@ -522,6 +529,9 @@ def test_rendered_skill_points_at_a_script_that_ships(self, tmp_path: Path):
522529
expected = f".specify/extensions/github/{SCRIPT_TWINS['sh']}"
523530
assert expected in content
524531
assert (project / expected).is_file()
532+
for phase in ("pre", "post"):
533+
assert f".specify/scripts/bash/{phase}-hooks.sh" in content
534+
assert f".specify/extensions/github/scripts/bash/{phase}-hooks.sh" not in content
525535

526536

527537
# -- Behaviour parity with the core command -----------------------------------

‎tests/test_hook_dispatch_scripts.py‎

Lines changed: 54 additions & 0 deletions

0 commit comments

Comments
 (0)