Skip to content

Add workflow step catalog — community-installable step types#2394

Open
Copilot wants to merge 6 commits intomainfrom
copilot/add-community-installable-steps
Open

Add workflow step catalog — community-installable step types#2394
Copilot wants to merge 6 commits intomainfrom
copilot/add-community-installable-steps

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 28, 2026

The workflow engine shipped with a dynamic STEP_REGISTRY but no distribution mechanism for community-authored step types. This adds a full catalog system for discovering, installing, and managing custom step types, following the same patterns as the workflow/extension catalogs.

New classes (workflows/catalog.py)

  • StepRegistry — persists installed custom steps in .specify/workflows/steps/step-registry.json
  • StepCatalog — multi-source catalog stack with SHA256-based caching; resolves SPECKIT_STEP_CATALOG_URL env var → .specify/step-catalogs.yml~/.specify/step-catalogs.yml → built-in defaults (step-catalog.json + step-catalog.community.json)
  • StepCatalogError / StepValidationError / StepCatalogEntry supporting types

Dynamic step loading (workflows/__init__.py)

load_custom_steps(project_root) scans .specify/workflows/steps/, dynamically imports each package's __init__.py, finds the StepBase subclass matching the declared type_key, and registers it into STEP_REGISTRY. Broken packages are silently skipped.

CLI surface (specify workflow step …)

specify workflow step list              # built-in + installed custom types
specify workflow step add <id>          # fetch step.yml + __init__.py, validate, install
specify workflow step remove <id>       # delete package dir + registry entry
specify workflow step search [query]    # search across all configured catalogs
specify workflow step info <id>         # detail view (built-in or custom)
specify workflow step catalog list      # show active catalog sources
specify workflow step catalog add <url> # add catalog to .specify/step-catalogs.yml
specify workflow step catalog remove <n># remove by index

add validates that the downloaded step.yml's type_key matches the catalog ID and that all fetches use HTTPS before writing anything to disk.

Catalog files

  • workflows/step-catalog.json — official catalog (empty, ready for entries)
  • workflows/step-catalog.community.json — community catalog (empty, ready for entries)

Tests

27 new tests across TestStepRegistryCustom, TestStepCatalog, and TestLoadCustomSteps covering CRUD, catalog resolution (env var / project / user / default), URL validation, search, and dynamic loading edge cases (missing files, broken imports, already-registered keys).

Copilot AI requested review from Copilot and removed request for Copilot April 28, 2026 17:48
Copilot AI linked an issue Apr 28, 2026 that may be closed by this pull request
Copilot AI requested review from Copilot and removed request for Copilot April 28, 2026 17:59
Comment thread tests/test_workflows.py Fixed
Copilot AI changed the title [WIP] Add step catalog and CLI for community-installable step types Add workflow step catalog — community-installable step types Apr 28, 2026
Copilot AI requested a review from mnriem April 28, 2026 18:01
…e-effect'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 19:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a catalog/registry system for community-installable workflow step types, plus CLI commands to discover, install, and manage them alongside built-in steps.

Changes:

  • Introduces StepRegistry and StepCatalog (multi-source resolution + SHA256 cache) for step type distribution/management.
  • Adds dynamic filesystem-based loading of installed custom step packages into STEP_REGISTRY.
  • Expands CLI with specify workflow step … and adds tests and initial (empty) catalog JSON files.
Show a summary per file
File Description
workflows/step-catalog.json Adds the built-in “official” step catalog scaffold (currently empty).
workflows/step-catalog.community.json Adds the built-in “community” step catalog scaffold (currently empty).
src/specify_cli/workflows/catalog.py Implements StepRegistry + StepCatalog with config resolution and caching.
src/specify_cli/workflows/init.py Adds load_custom_steps(project_root) dynamic import/registration for installed step packages.
src/specify_cli/init.py Adds Typer subcommands for listing/searching/installing/removing steps and managing step catalogs.
tests/test_workflows.py Adds unit tests covering registry CRUD, catalog resolution/validation, search/info, and dynamic loading behavior.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

src/specify_cli/init.py:5555

  • workflow_step_remove() builds step_dir from unvalidated step_id and then shutil.rmtree(step_dir). A malicious value like ../... could delete arbitrary directories outside .specify/workflows/steps. Add the same resolved-path relative_to() guard used by workflow_remove/workflow_add before performing deletions.
    step_dir = project_root / ".specify" / "workflows" / "steps" / step_id
    if step_dir.exists():
        import shutil
        shutil.rmtree(step_dir)

  • Files reviewed: 6/6 changed files
  • Comments generated: 4

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/workflows/catalog.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
… failed-to-load display

- Add resolve()+relative_to() path traversal guards in workflow_step_add and
  workflow_step_remove to prevent directory escape via step_id
- Harden _is_url_cache_valid in both StepCatalog and WorkflowCatalog to
  coerce fetched_at to float and catch TypeError/ValueError
- Check STEP_REGISTRY and StepRegistry before installing to prevent
  collisions with built-in step types or already-installed steps
- Show 'Custom (installed, failed to load)' section in workflow step list
  for steps in the registry that failed to load into STEP_REGISTRY
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (1)

src/specify_cli/workflows/catalog.py:1021

  • StepCatalog.add_catalog()/remove_catalog() read YAML without handling yaml.YAMLError/OSError; those exceptions will bubble past the CLI handlers (which only catch StepValidationError) and produce a stack trace. Wrap YAML reads in try/except and re-raise as StepValidationError so the CLI consistently reports a user-facing error.
    def remove_catalog(self, index: int) -> str:
        """Remove a catalog source by index (0-based). Returns the removed name."""
        config_path = self.project_root / ".specify" / "step-catalogs.yml"
        if not config_path.exists():
            raise StepValidationError("No step catalog config file found.")

        data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
        if not isinstance(data, dict):
            raise StepValidationError(
                "Catalog config file is corrupted (expected a mapping)."
            )
  • Files reviewed: 6/6 changed files
  • Comments generated: 2

Comment on lines +594 to +603
def _load(self) -> dict[str, Any]:
"""Load registry from disk or create default."""
if self.registry_path.exists():
try:
with open(self.registry_path, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, ValueError):
return {"schema_version": self.SCHEMA_VERSION, "steps": {}}
return {"schema_version": self.SCHEMA_VERSION, "steps": {}}

Comment on lines +972 to +980
data: dict[str, Any] = {"catalogs": []}
if config_path.exists():
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise StepValidationError(
"Catalog config file is corrupted (expected a mapping)."
)
data = raw

@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented May 6, 2026

@copilot apply changes based on the comments in this thread

Copilot AI requested review from Copilot and removed request for Copilot May 6, 2026 22:47
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

@copilot apply changes based on the comments in this thread

Applied all changes from the review thread in commits e311afb and df061e0:

  • StepRegistry._load() — now validates the loaded JSON is a dict with a dict steps field (resets malformed steps to {}), and catches OSError/UnicodeError so unreadable files fall back to the default structure instead of crashing CLI commands.
  • StepCatalog.add_catalog() — changed yaml.safe_load(...) to yaml.safe_load(...) or {} so an empty .specify/step-catalogs.yml is treated as an empty mapping and can be initialized by the command.

Three new tests cover these cases: malformed steps key, unreadable registry file (with functional-after-fallback assertion), and empty YAML config file.

@mnriem mnriem marked this pull request as ready for review May 6, 2026 22:48
Copilot AI review requested due to automatic review settings May 6, 2026 22:48
Copilot finished work on behalf of mnriem May 6, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Workflow Step Catalog — community-installable step types

3 participants