Add init workflow step to bootstrap projects like specify init#2838
Add init workflow step to bootstrap projects like specify init#2838Copilot wants to merge 6 commits into
init workflow step to bootstrap projects like specify init#2838Conversation
init workflow step to bootstrap projects like specify init
There was a problem hiding this comment.
Pull request overview
Adds a new workflow step type (type: init) so workflows can bootstrap (or merge into) a Spec Kit project by invoking the bundled specify init logic in-process, enabling end-to-end “scaffold → run steps” workflows without requiring a manual pre-step.
Changes:
- Introduces
InitStep(src/specify_cli/workflows/steps/init/) that buildsspecify initargv from step config, runs it viaCliRunner, and capturesexit_code/stdout/stderr. - Registers the new step type in the workflow step registry and engine’s valid-step-type fallback, and updates architecture/README docs.
- Adds tests covering argv building, workflow-default integration fallback, directory creation via
project, invalid integration failure, and validation.
Show a summary per file
| File | Description |
|---|---|
| workflows/README.md | Documents the new init step type and provides a YAML example. |
| workflows/ARCHITECTURE.md | Updates step-type counts/table and module tree to include init. |
| tests/test_workflows.py | Adds test coverage for the new InitStep. |
| src/specify_cli/workflows/steps/init/init.py | Implements InitStep execution, expression resolution, and validation. |
| src/specify_cli/workflows/engine.py | Adds init to the engine’s valid step-type fallback set. |
| src/specify_cli/workflows/init.py | Registers InitStep in the built-in STEP_REGISTRY. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 6/6 changed files
- Comments generated: 3
| base = context.project_root or os.getcwd() | ||
| try: | ||
| not_empty = any(os.scandir(base)) | ||
| except OSError: | ||
| not_empty = False |
There was a problem hiding this comment.
Fixed in the latest commit — replaced any(os.scandir(base)) with a with os.scandir(base) as it: context manager so the iterator is always closed even when any() short-circuits.
| try: | ||
| result = runner.invoke(app, argv, catch_exceptions=True) | ||
| finally: | ||
| os.chdir(prev_cwd) |
There was a problem hiding this comment.
Fixed — wrapped os.chdir(prev_cwd) in the finally block with try/except OSError: pass so a stale working directory can't bypass returning the captured StepResult.
| script = config.get("script") | ||
| if ( | ||
| isinstance(script, str) | ||
| and "{{" not in script | ||
| and script not in VALID_SCRIPT_TYPES | ||
| ): |
There was a problem hiding this comment.
Fixed — validate() now checks not isinstance(script, str) first (before the string-value check), so non-string values like script: true are rejected immediately with a clear error message.
| finally: | ||
| try: | ||
| os.chdir(prev_cwd) | ||
| except OSError: |
| integration = config.get("integration") or context.default_integration | ||
| integration = self._resolve(integration, context) | ||
|
|
| def validate(self, config: dict[str, Any]) -> list[str]: | ||
| errors = super().validate(config) | ||
| script = config.get("script") | ||
| if script is not None and not isinstance(script, str): | ||
| errors.append( | ||
| f"Init step {config.get('id', '?')!r}: 'script' must be a string " | ||
| f"({' or '.join(repr(s) for s in VALID_SCRIPT_TYPES)})." | ||
| ) | ||
| elif ( | ||
| isinstance(script, str) | ||
| and "{{" not in script | ||
| and script not in VALID_SCRIPT_TYPES | ||
| ): | ||
| errors.append( | ||
| f"Init step {config.get('id', '?')!r}: 'script' must be " | ||
| f"{' or '.join(repr(s) for s in VALID_SCRIPT_TYPES)}." | ||
| ) | ||
| return errors |
- Use `with os.scandir(...)` context manager so the iterator is always closed even when `any()` short-circuits, preventing file-descriptor leaks in long-running workflow runs. - Guard `os.chdir(prev_cwd)` in the `finally` block with a try/except so an `OSError` (e.g. directory deleted) doesn't bypass returning the captured `StepResult`. - Reject non-string `script` values in `validate()` with a clear error message, rather than silently passing them through to become `--script True` at runtime.
1746289 to
fc8d6c3
Compare
Workflows had no way to scaffold a project; users had to run
specify initmanually before any workflow could drive the spec-driven process. This adds aninitstep type so a workflow can bootstrap (or merge into) a project itself.Changes
InitStep(type: init) insrc/specify_cli/workflows/steps/init/— runs the bundledspecify initcommand in-process againstcontext.project_root, capturingexit_code/stdout/stderrand returningFAILEDon non-zero exit.project,here,integration,script,force,no_git,ignore_agent_tools,preset,branch_numbering; string fields resolve{{ }}expressions.--ignore-agent-toolssince workflows run unattended.validate()rejectsscriptvalues other thansh/ps.STEP_REGISTRYand the engine's valid-step-types fallback.workflows/ARCHITECTURE.mdandworkflows/README.mdupdated (step table, module tree, new "Init Steps" section).Example