Skip to content
43 changes: 43 additions & 0 deletions .github/workflows/community-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,50 @@ jobs:
test -f .specify/extensions/preview/extension.yml
test -f .specify/extensions/repository-governance/extension.yml
test -f .specify/presets/workflow-preset/preset.yml
test -f .specify/presets/workflow-preset/templates/behavior/behavior-scenarios-draft.json
test -f .specify/presets/workflow-preset/templates/behavior/behavior-testability-checklist.md
test -f .specify/presets/workflow-preset/templates/behavior/bdd-draft.feature
test -f .specify/presets/workflow-preset/templates/behavior/bdd-contract.feature
test -f .specify/presets/workflow-preset/templates/behavior/uif-intent.json
test -f .specify/presets/workflow-preset/templates/behavior/uif-expected.json
test -f .specify/presets/workflow-preset/templates/behavior/scenario-instances.json
test -f .specify/presets/workflow-preset/templates/behavior/data-fixtures-intent.json
test -f .specify/presets/workflow-preset/templates/behavior/data-fixtures.json
test -f .specify/presets/workflow-preset/templates/behavior/assertions.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.scenarios.draft.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.uif.intent.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.data-fixtures.intent.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.uif.expected.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.scenario-instances.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.data-fixtures.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.behavior.assertions.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.implement.manifest.v1.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.implement.handoff.v2.schema.json
test -f .specify/presets/workflow-preset/schemas/speckit.implement.receipt.v1.schema.json
test -f .specify/presets/workflow-preset/.composed/speckit.specify.md
test -f .specify/presets/workflow-preset/.composed/speckit.clarify.md
test -f .specify/presets/workflow-preset/.composed/speckit.checklist.md
test -f .specify/presets/workflow-preset/.composed/speckit.analyze.md
test -f .specify/presets/workflow-preset/.composed/speckit.plan.md
test -f .specify/presets/workflow-preset/.composed/speckit.tasks.md

test -f .claude/skills/speckit-arch-generate/SKILL.md
test -f .claude/skills/speckit-preview-html/SKILL.md
test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md
test -f .claude/skills/speckit-specify/SKILL.md
test -f .claude/skills/speckit-clarify/SKILL.md
test -f .claude/skills/speckit-checklist/SKILL.md
test -f .claude/skills/speckit-analyze/SKILL.md
test -f .claude/skills/speckit-plan/SKILL.md
test -f .claude/skills/speckit-tasks/SKILL.md
test -f .claude/skills/speckit-implement/SKILL.md

grep -q "Phase 0 Behavior Projection" .claude/skills/speckit-plan/SKILL.md
grep -q "Test Strategy Derivation" .claude/skills/speckit-tasks/SKILL.md
grep -q "Core Agent" .claude/skills/speckit-implement/SKILL.md
grep -q "Vertical Planner Agent" .claude/skills/speckit-implement/SKILL.md
grep -q "Worker Agent" .claude/skills/speckit-implement/SKILL.md

/tmp/specify-community-smoke-venv/bin/specify preset resolve plan-template | tee resolve-plan-template.txt
grep -q "workflow-preset" resolve-plan-template.txt
grep -q "Composition chain" resolve-plan-template.txt
182 changes: 182 additions & 0 deletions .github/workflows/workflow-preset-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: Workflow Preset Integration

permissions:
contents: read

on:
repository_dispatch:
types: ["workflow-preset-release"]
workflow_dispatch:
inputs:
preset_version:
description: "Workflow preset version to validate"
required: true
preset_download_url:
description: "Optional workflow preset release ZIP URL"
required: false
require_bundled_version_match:
description: "Require bundled preset version to match preset_version"
required: false
type: boolean
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.event.client_payload.preset_version || github.event.client_payload.version || inputs.preset_version || 'unknown-version' }}-${{ github.run_id }}
cancel-in-progress: false

jobs:
external-release-install:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.11"

- name: Install CLI from checkout
run: |
set -euo pipefail
uv venv /tmp/specify-workflow-preset-venv
uv pip install --python /tmp/specify-workflow-preset-venv/bin/python .

- name: Resolve workflow preset release
id: release
env:
EVENT_PRESET_VERSION: ${{ github.event.client_payload.preset_version || github.event.client_payload.version || inputs.preset_version }}
EVENT_PRESET_DOWNLOAD_URL: ${{ github.event.client_payload.preset_download_url || github.event.client_payload.download_url || inputs.preset_download_url }}
run: |
set -euo pipefail
write_github_output() {
local name="$1"
local value="$2"
local delimiter
delimiter="ghadelim_${RANDOM}_${RANDOM}_$(date +%s%N)"
while printf '%s\n' "$value" | grep -Fxq "$delimiter"; do
delimiter="ghadelim_${RANDOM}_${RANDOM}_$(date +%s%N)"
done

{
printf '%s<<%s\n' "$name" "$delimiter"
printf '%s\n' "$value"
printf '%s\n' "$delimiter"
} >> "$GITHUB_OUTPUT"
}

preset_version="${EVENT_PRESET_VERSION}"
if [ -z "$preset_version" ]; then
echo "preset_version is required from repository_dispatch payload or workflow_dispatch input" >&2
exit 1
fi
if ! printf '%s\n' "$preset_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "preset_version must be a bare semantic version like 1.2.3" >&2
exit 1
fi

preset_download_url="${EVENT_PRESET_DOWNLOAD_URL}"
if [ -z "$preset_download_url" ]; then
preset_download_url="https://github.com/bigsmartben/spec-kit-workflow-preset/releases/download/v${preset_version}/spec-kit-workflow-preset-v${preset_version}.zip"
fi

write_github_output "version" "$preset_version"
write_github_output "download_url" "$preset_download_url"

- name: Install and verify external workflow preset
env:
PRESET_VERSION: ${{ steps.release.outputs.version }}
PRESET_DOWNLOAD_URL: ${{ steps.release.outputs.download_url }}
run: |
set -euo pipefail
project_dir="$(mktemp -d)"
cd "$project_dir"

/tmp/specify-workflow-preset-venv/bin/specify init --here --ai claude --script sh --ignore-agent-tools
/tmp/specify-workflow-preset-venv/bin/specify preset remove workflow-preset
/tmp/specify-workflow-preset-venv/bin/specify preset add --from "$PRESET_DOWNLOAD_URL"

/tmp/specify-workflow-preset-venv/bin/specify preset info workflow-preset | tee preset-info.txt
grep -F "ID: workflow-preset" preset-info.txt
grep -F "Version: $PRESET_VERSION" preset-info.txt
grep -F "Status: installed" preset-info.txt

/tmp/specify-workflow-preset-venv/bin/specify preset resolve plan-template | tee preset-resolve-plan-template.txt
grep -F "plan-template" preset-resolve-plan-template.txt
grep -F "workflow-preset" preset-resolve-plan-template.txt

test -f .specify/presets/workflow-preset/preset.yml
test -f .specify/presets/workflow-preset/templates/plan-template.md
test -f .specify/presets/workflow-preset/templates/tasks-template.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove nonexistent tasks template check

This check makes the new release-validation workflow fail for the current workflow-preset artifact: the bundled preset manifest/source tree contains templates/plan-template.md and the behavior templates, but no templates/tasks-template.md (repo-wide search only finds this new workflow reference). Any workflow-preset-release dispatch for a valid release shaped like the bundled preset will exit here before verifying the command registration.

Useful? React with 👍 / 👎.

test -f .specify/presets/workflow-preset/commands/speckit.plan.md
test -f .specify/presets/workflow-preset/commands/speckit.tasks.md
test -f .specify/presets/workflow-preset/commands/speckit.implement.md

test -f .claude/skills/speckit-plan/SKILL.md
test -f .claude/skills/speckit-tasks/SKILL.md
test -f .claude/skills/speckit-implement/SKILL.md
grep -F "Core Agent" .claude/skills/speckit-implement/SKILL.md
grep -F "Vertical Planner Agent" .claude/skills/speckit-implement/SKILL.md
grep -F "Worker Agent" .claude/skills/speckit-implement/SKILL.md

bundled-regression:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.11"

- name: Install dependencies
run: |
set -euo pipefail
uv sync --all-extras --dev

- name: Check bundled workflow preset version
env:
EVENT_PRESET_VERSION: ${{ github.event.client_payload.preset_version || github.event.client_payload.version || inputs.preset_version }}
REQUIRE_BUNDLED_VERSION_MATCH: ${{ github.event.client_payload.require_bundled_version_match || inputs.require_bundled_version_match || 'false' }}
run: |
set -euo pipefail
if [ "$REQUIRE_BUNDLED_VERSION_MATCH" != "true" ]; then
echo "Bundled preset version match check skipped."
exit 0
fi
if [ -z "$EVENT_PRESET_VERSION" ]; then
echo "preset_version is required when require_bundled_version_match is true" >&2
exit 1
fi
if ! printf '%s\n' "$EVENT_PRESET_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "preset_version must be a bare semantic version like 1.2.3" >&2
exit 1
fi

bundled_version="$(uv run python -c 'import yaml; data = yaml.safe_load(open("presets/workflow-preset/preset.yml", encoding="utf-8")); print(data["preset"]["version"])')"

if [ "$bundled_version" != "$EVENT_PRESET_VERSION" ]; then
echo "Bundled workflow-preset version '$bundled_version' does not match '$EVENT_PRESET_VERSION'" >&2
exit 1
fi

- name: Run targeted CLI integration tests
run: |
set -euo pipefail
uv run pytest tests/integrations/test_cli.py -k "community_extensions_and_workflow_preset_auto_installed or workflow_preset_registers" -v

- name: Run targeted preset tests
run: |
set -euo pipefail
uv run pytest tests/test_presets.py -k "workflow_preset or bundled_preset" -v
45 changes: 44 additions & 1 deletion tests/integrations/community_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
"speckit.repository-governance.refresh",
)
DEFAULT_PRESET_ID = "workflow-preset"
DEFAULT_PRESET_COMMANDS = (
"speckit.specify",
"speckit.clarify",
"speckit.checklist",
"speckit.analyze",
"speckit.plan",
"speckit.tasks",
"speckit.implement",
)


def _copied_extension_files(extension_id: str) -> list[str]:
Expand Down Expand Up @@ -91,6 +100,38 @@ def _registered_extension_command_files(
return files


def _registered_workflow_preset_command_files(
integration_key: str,
*,
skills_mode: bool = False,
commands_dir: str | None = None,
) -> list[str]:
integration = get_integration(integration_key)
config = dict(integration.registrar_config)
if skills_mode:
config["dir"] = f"{integration.config['folder']}skills"
config["extension"] = "/SKILL.md"
if commands_dir is not None:
config["dir"] = commands_dir
command_dir = Path(config["dir"])
extension = config["extension"]

files: list[str] = []
for command_name in DEFAULT_PRESET_COMMANDS:
output_name = CommandRegistrar._compute_output_name(
integration_key, command_name, config
)
if extension == "/SKILL.md":
files.append((command_dir / output_name / "SKILL.md").as_posix())
else:
files.append((command_dir / f"{output_name}{extension}").as_posix())

if integration_key == "copilot" and not skills_mode:
files.append(f".github/prompts/{command_name}.prompt.md")

return files


def bundled_community_default_files(
integration_key: str,
*,
Expand All @@ -115,4 +156,6 @@ def bundled_community_default_files(
commands_dir=commands_dir,
)
)
return sorted(files)
# workflow-preset wraps/replaces core commands whose output paths are already
# accounted for by each integration's base command inventory.
return sorted(set(files))
55 changes: 55 additions & 0 deletions tests/integrations/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,61 @@ def test_community_extensions_and_workflow_preset_auto_installed(self, tmp_path)
preset_registry = json.loads((project / ".specify" / "presets" / ".registry").read_text())
assert "workflow-preset" in preset_registry["presets"]

def test_workflow_preset_registers_commands_and_composes_wrappers(self, tmp_path):
"""workflow-preset registers commands and composes wrapped core templates."""
from typer.testing import CliRunner
from specify_cli import app

project = tmp_path / "workflow-preset-registration"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
runner = CliRunner()
result = runner.invoke(app, [
"init", "--here", "--ai", "claude", "--script", "sh",
"--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)

assert result.exit_code == 0, f"init failed: {result.output}"

preset_dir = project / ".specify" / "presets" / "workflow-preset"
assert (preset_dir / "preset.yml").exists()
assert (preset_dir / "schemas" / "speckit.implement.handoff.v2.schema.json").exists()
assert (preset_dir / "templates" / "behavior" / "bdd-draft.feature").exists()
assert (preset_dir / ".composed" / "speckit.plan.md").exists()
assert (preset_dir / ".composed" / "speckit.tasks.md").exists()
assert (preset_dir / ".composed" / "speckit.analyze.md").exists()

preset_registry = json.loads((project / ".specify" / "presets" / ".registry").read_text())
workflow_entry = preset_registry["presets"]["workflow-preset"]
expected_preset_commands = {
"speckit.specify",
"speckit.clarify",
"speckit.checklist",
"speckit.analyze",
"speckit.plan",
"speckit.tasks",
"speckit.implement",
}
assert set(workflow_entry["registered_commands"]["claude"]) == expected_preset_commands

plan_skill = project / ".claude" / "skills" / "speckit-plan" / "SKILL.md"
tasks_skill = project / ".claude" / "skills" / "speckit-tasks" / "SKILL.md"
implement_skill = project / ".claude" / "skills" / "speckit-implement" / "SKILL.md"
for skill_path in (plan_skill, tasks_skill, implement_skill):
assert skill_path.exists(), f"{skill_path} was not registered"

assert "Phase 0 Behavior Projection" in plan_skill.read_text(encoding="utf-8")
assert "test strategy derivation" in tasks_skill.read_text(encoding="utf-8").lower()
implement_text = implement_skill.read_text(encoding="utf-8")
assert "Core Agent" in implement_text
assert "Vertical Planner Agent" in implement_text
assert "Worker Agent" in implement_text
assert "speckit.implement.handoff.v2" in implement_text

def test_no_git_keeps_community_defaults(self, tmp_path):
"""--no-git skips only git; bundled community defaults still install."""
from typer.testing import CliRunner
Expand Down
Loading
Loading