Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bbd4f34
fix(workflows): auto-detect project integration instead of hardcoding…
Apr 29, 2026
903ce05
fix(workflows): tolerate non-UTF8 integration.json in auto-detect (#2…
Apr 29, 2026
cd8b207
Merge remote-tracking branch 'origin' into fix/workflow-integration-a…
Apr 29, 2026
563cf46
fix(workflows): address PR #2408 review findings
Apr 29, 2026
51f9acd
chore: merge origin/main - resolve conflicts
Apr 30, 2026
2322fdb
fix(workflows): address remaining PR #2408 review findings
Apr 30, 2026
46760ca
refactor: move INTEGRATION_JSON to top-level constants module
Apr 30, 2026
22880ea
Update src/specify_cli/__init__.py
markuswondrak Apr 30, 2026
609eade
Initial plan
Copilot Apr 30, 2026
9dc4b0b
fix(workflows): minimal-invasive auto-detect integration from project…
Copilot Apr 30, 2026
6727c14
Merge pull request #1 from markuswondrak/copilot/fixworkflow-integrat…
markuswondrak Apr 30, 2026
a811a78
fix(workflows): resolve explicit integration auto input
Apr 30, 2026
98afd4a
Update src/specify_cli/workflows/engine.py
markuswondrak Apr 30, 2026
d9f9325
fix(workflows): centralize path constants, add init-options fallback,…
Copilot May 1, 2026
f2fa165
chore: create commit per repository rules
May 1, 2026
bb8db4c
fix(paths): use SPECIFY_DIR for .specify file paths
May 1, 2026
0315bb8
Merge upstream/main into pr-2
May 1, 2026
6b781f1
Merge upstream/main into pr-2
May 3, 2026
ccb80eb
merge: resolve conflicts with fix/workflow-integration-auto-detect — …
May 3, 2026
d76ac5c
fix(lint): remove duplicate INTEGRATION_JSON import that triggered ru…
May 3, 2026
1b1031a
Merge pull request #2 from markuswondrak/copilot/fix-findling-issues
markuswondrak May 3, 2026
48c6223
Sync public workflow reference
markuswondrak May 3, 2026
d05a622
fix(workflows): drop duplicate INTEGRATION_JSON and unused import (PR…
May 3, 2026
cd76942
fix(workflows): address Copilot review feedback on integration auto-d…
May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

<!-- insert new changelog below this comment -->

## [Unreleased]

### Fixed

- fix(workflows): auto-detect project integration instead of hardcoding copilot default (#2406)
Comment thread
markuswondrak marked this conversation as resolved.
Outdated

## [0.8.2] - 2026-04-28

### Changed
Expand Down
28 changes: 28 additions & 0 deletions src/specify_cli/workflows/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,36 @@ def _resolve_inputs(
elif input_def.get("required", False):
msg = f"Required input {name!r} not provided."
raise ValueError(msg)
Comment thread
markuswondrak marked this conversation as resolved.

# Auto-detect integration from project config when set to "auto"
if resolved.get("integration") == "auto":
resolved["integration"] = self._resolve_integration_auto()

Comment thread
markuswondrak marked this conversation as resolved.
Outdated
return resolved

_INTEGRATION_JSON = ".specify/integration.json"
_AUTO_FALLBACK = "copilot"

Comment thread
markuswondrak marked this conversation as resolved.
Outdated
def _resolve_integration_auto(self) -> str:
"""Read the project integration from ``.specify/integration.json``.

Returns the stored integration key, or ``"copilot"`` when the
file is missing, unreadable, or does not contain a valid key.
This method is intentionally decoupled from the CLI layer
(no ``typer.Exit`` / ``console.print``) so the engine remains
independently testable.
"""
path = self.project_root / self._INTEGRATION_JSON
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
Comment thread
markuswondrak marked this conversation as resolved.
Outdated
return self._AUTO_FALLBACK
if isinstance(data, dict):
value = data.get("integration")
if isinstance(value, str) and value:
return value
Comment thread
markuswondrak marked this conversation as resolved.
Outdated
return self._AUTO_FALLBACK

@staticmethod
def _coerce_input(
name: str, value: Any, input_def: dict[str, Any]
Expand Down
104 changes: 104 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -1843,3 +1843,107 @@ def test_switch_workflow(self, project_dir):
assert state.status == RunStatus.COMPLETED
assert "do-plan" in state.step_results
assert "do-specify" not in state.step_results


class TestIntegrationAutoDetect:
"""Test auto-detection of project integration from .specify/integration.json.

When workflow inputs specify ``default: "auto"`` for the integration
input, the engine should resolve it by reading the project's
``.specify/integration.json`` instead of hardcoding ``"copilot"``.

Regression tests for https://github.com/github/spec-kit/issues/2406.
"""

Comment thread
markuswondrak marked this conversation as resolved.
Outdated
@staticmethod
def _make_workflow_yaml(default_integration: str = "auto") -> str:
return f"""
Comment thread
markuswondrak marked this conversation as resolved.
Outdated
schema_version: "1.0"
workflow:
id: "auto-test"
name: "Auto Test"
version: "1.0.0"
inputs:
spec:
type: string
default: "build login"
integration:
type: string
default: "{default_integration}"
steps:
- id: specify
command: speckit.specify
integration: "{{{{ inputs.integration }}}}"
input:
args: "{{{{ inputs.spec }}}}"
"""

def test_resolve_inputs_auto_reads_integration_json(self, project_dir):
"""'auto' default resolves to the integration in .specify/integration.json."""
from unittest.mock import patch
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
from specify_cli.workflows.base import StepStatus
Comment thread
markuswondrak marked this conversation as resolved.
Outdated

# Write integration.json with opencode
int_json = project_dir / ".specify" / "integration.json"
int_json.write_text(json.dumps({"integration": "opencode"}), encoding="utf-8")

definition = WorkflowDefinition.from_string(self._make_workflow_yaml())
engine = WorkflowEngine(project_dir)

with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
state = engine.execute(definition)

# The resolved integration should be "opencode", not "auto" or "copilot"
step_output = state.step_results["specify"]["output"]
assert step_output["integration"] == "opencode"

def test_resolve_inputs_auto_no_json_falls_back_to_copilot(self, project_dir):
"""When no integration.json exists, 'auto' falls back to 'copilot'."""
from unittest.mock import patch
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition

# No integration.json exists in the project_dir fixture
definition = WorkflowDefinition.from_string(self._make_workflow_yaml())
engine = WorkflowEngine(project_dir)

with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
state = engine.execute(definition)

step_output = state.step_results["specify"]["output"]
assert step_output["integration"] == "copilot"

def test_resolve_inputs_explicit_override_ignores_auto(self, project_dir):
"""Explicit --input integration=gemini takes precedence over auto."""
from unittest.mock import patch
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition

# Write integration.json with opencode (should be ignored)
int_json = project_dir / ".specify" / "integration.json"
int_json.write_text(json.dumps({"integration": "opencode"}), encoding="utf-8")

definition = WorkflowDefinition.from_string(self._make_workflow_yaml())
engine = WorkflowEngine(project_dir)

with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
state = engine.execute(definition, {"integration": "gemini"})

step_output = state.step_results["specify"]["output"]
assert step_output["integration"] == "gemini"

def test_resolve_inputs_auto_with_empty_json(self, project_dir):
"""When integration.json has no 'integration' key, fall back to 'copilot'."""
from unittest.mock import patch
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition

int_json = project_dir / ".specify" / "integration.json"
int_json.write_text(json.dumps({"version": "1.0"}), encoding="utf-8")

definition = WorkflowDefinition.from_string(self._make_workflow_yaml())
engine = WorkflowEngine(project_dir)

with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
state = engine.execute(definition)

step_output = state.step_results["specify"]["output"]
assert step_output["integration"] == "copilot"
4 changes: 2 additions & 2 deletions workflows/speckit/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ inputs:
prompt: "Describe what you want to build"
integration:
type: string
default: "copilot"
prompt: "Integration to use (e.g. claude, copilot, gemini)"
default: "auto"
prompt: "Integration to use (e.g. claude, copilot, gemini, or 'auto' to detect from project config)"
Comment thread
markuswondrak marked this conversation as resolved.
Outdated
scope:
type: string
default: "full"
Expand Down
Loading