diff --git a/.github/ISSUE_TEMPLATE/workflow_step_submission.yml b/.github/ISSUE_TEMPLATE/workflow_step_submission.yml new file mode 100644 index 0000000000..70120d472c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/workflow_step_submission.yml @@ -0,0 +1,254 @@ +name: Workflow Step Type Submission +description: Submit a custom workflow step package for community catalog intake +title: "[Workflow Step]: Add " +labels: ["triage-must-have"] +body: + - type: markdown + attributes: + value: | + Thanks for contributing a workflow step type! This form captures the package, release, and catalog metadata needed for manual community catalog review. + + **Before submitting:** + - Review the [Community Workflow Step Types guide](https://github.com/github/spec-kit/blob/main/docs/community/workflow-steps.md) + - Publish a package containing root-level `step.yml` and `__init__.py` files + - Ensure `step.type_key`, the catalog ID, and the matching `StepBase.type_key` are identical + - Create a GitHub release with a versioned step package archive + - Provide version-pinned URLs and SHA-256 digests for every catalog-downloaded file + - Test the release artifact with `specify workflow step add --from ` + + **After submitting:** GitHub automatically applies only the `triage-must-have` intake verdict. This phase is intake-only: no validation workflow or draft pull request is triggered. Maintainers currently review the metadata manually and, when accepted, update the community catalog through the normal reviewed pull request process. + + - type: input + id: step-id + attributes: + label: Step Type ID + description: Exact catalog key and `step.type_key`; it must be one safe path component and must not collide with a built-in step type + placeholder: "e.g., deploy" + validations: + required: true + + - type: input + id: step-name + attributes: + label: Step Type Name + description: Human-readable name declared in `step.yml` + placeholder: "e.g., Deploy" + validations: + required: true + + - type: input + id: version + attributes: + label: Version + description: PEP 440-compatible release version; it must match `step.version` in the submitted `step.yml` + placeholder: "e.g., 1.0.0" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: Concise description of the workflow behavior this step type implements + placeholder: Deploys the current project to a configured environment and returns the deployment URL + validations: + required: true + + - type: input + id: author + attributes: + label: Author + description: Name or organization declared for the package + placeholder: "e.g., Jane Doe or Acme Corp" + validations: + required: true + + - type: input + id: repository + attributes: + label: Repository URL + description: Public source repository containing the submitted package + placeholder: "https://github.com/your-org/spec-kit-step-deploy" + validations: + required: true + + - type: input + id: download-url + attributes: + label: Download URL + description: URL to the versioned `.zip`, `.tar.gz`, or `.tgz` step package archive for this release + placeholder: "https://github.com/your-org/spec-kit-step-deploy/releases/download/v1.0.0/deploy-1.0.0.zip" + validations: + required: true + + - type: input + id: step-yml-url + attributes: + label: step.yml URL + description: Exact tag-pinned HTTPS URL used as the catalog `step_yml_url` (or `url`) + placeholder: "https://raw.githubusercontent.com/your-org/spec-kit-step-deploy/v1.0.0/step.yml" + validations: + required: true + + - type: input + id: init-url + attributes: + label: __init__.py URL + description: Exact tag-pinned HTTPS URL used as the catalog `init_url` + placeholder: "https://raw.githubusercontent.com/your-org/spec-kit-step-deploy/v1.0.0/__init__.py" + validations: + required: true + + - type: textarea + id: extra-files + attributes: + label: Extra File URLs + description: >- + JSON mapping of every additional package-relative file path to its exact tag-pinned HTTPS URL. + Paths must use forward slashes, be relative and non-empty, contain no empty, `.` or `..` segments, + and must not case-insensitively alias `step.yml` or `__init__.py`. Enter `{}` when there are no extra files. + render: json + placeholder: | + { + "helpers.py": "https://raw.githubusercontent.com/your-org/spec-kit-step-deploy/v1.0.0/helpers.py" + } + validations: + required: true + + - type: textarea + id: file-sha256 + attributes: + label: Per-file SHA-256 Digests + description: JSON mapping containing exactly `step.yml`, `__init__.py`, and every Extra File URL path, each with a 64-character hexadecimal SHA-256 digest + render: json + placeholder: | + { + "step.yml": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "__init__.py": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "helpers.py": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + validations: + required: true + + - type: input + id: license + attributes: + label: License + description: SPDX license identifier for the package; the repository must include the corresponding license text + placeholder: "e.g., MIT or Apache-2.0" + validations: + required: true + + - type: input + id: speckit-compatibility + attributes: + label: Spec Kit Compatibility + description: Spec Kit version constraint tested with this release; the current step catalog does not yet enforce this constraint + placeholder: "e.g., >=0.11.0" + validations: + required: true + + - type: textarea + id: runtime-dependencies + attributes: + label: Runtime and Tool Dependencies + description: List required Python packages, executables, services, environment variables, or platform constraints; enter `None` when the package has no external dependencies + placeholder: | + - Python package: requests>=2.32 + - Executable: kubectl>=1.30 + - Environment variable: KUBECONFIG + validations: + required: true + + - type: dropdown + id: step-type-count + attributes: + label: Number of Provided Step Types + description: One installed package currently registers exactly one `step.type_key` + options: + - "1" + validations: + required: true + + - type: textarea + id: step-types-provided + attributes: + label: Step Type Provided + description: Name the single `StepBase` subclass and its exact `type_key` + placeholder: "DeployStep — deploy" + validations: + required: true + + - type: input + id: documentation + attributes: + label: Documentation URL + description: Tag-pinned documentation covering configuration, outputs, failure behavior, side effects, and an example workflow + placeholder: "https://github.com/your-org/spec-kit-step-deploy/blob/v1.0.0/README.md" + validations: + required: true + + - type: input + id: changelog + attributes: + label: Changelog URL + description: Tag-pinned changelog for this release; leave empty only for an initial release with no prior versions + placeholder: "https://github.com/your-org/spec-kit-step-deploy/blob/v1.0.0/CHANGELOG.md" + + - type: textarea + id: testing-details + attributes: + label: Testing Details + description: Describe positive and negative configuration tests, execution/status/output tests, and any resume, nested, or concurrent behavior that applies + placeholder: | + **Tested with:** Spec Kit 0.11.0 on macOS and Ubuntu + + **Evidence:** + - Valid configuration completes with the documented outputs + - Invalid configuration returns a failed result with an actionable error + - Release artifact installs with `specify workflow step add deploy --from ...` + validations: + required: true + + - type: checkboxes + id: attestations + attributes: + label: Required Attestations + description: Confirm the submitted release satisfies the current external step-package contract + options: + - label: The release artifact contains regular, non-symlink `step.yml` and `__init__.py` files at the package root or under exactly one top-level directory + required: true + - label: The catalog ID, `step.type_key`, and the matching `StepBase.type_key` are identical and do not collide with a built-in step type + required: true + - label: The submitted version matches `step.version`, the download URL identifies that versioned release, and every catalog file URL is version-pinned + required: true + - label: The SHA-256 mapping covers exactly every file the catalog installer downloads + required: true + - label: The release artifact installs successfully with `specify workflow step add --from ` + required: true + - label: Runtime and tool dependencies, side effects, configuration, outputs, and failure behavior are documented + required: true + - label: The repository includes the submitted license and complete source for the released package + required: true + - label: I understand that custom workflow steps execute Python with the user's privileges and that a community catalog listing is not a code audit, endorsement, or support commitment + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Optional links or notes that help maintainers verify the submitted metadata + placeholder: Release notes, compatibility notes, or related issue links + + - type: textarea + id: ai-disclosure + attributes: + label: AI Disclosure + description: >- + Per our [Contributing guidelines](https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#ai-contributions-in-spec-kit), + any AI assistance used to create this issue must be disclosed. If you used AI, name the agent/tool, model(s), + settings/mode (reasoning effort; autonomous vs. human-supervised), and extent. Otherwise leave this as "N/A". + This does not change how your issue is handled — it just gives maintainers visibility into model/agent usage. + value: "N/A" + validations: + required: true diff --git a/docs/community/overview.md b/docs/community/overview.md index 48b0d42d7b..3a03856ac6 100644 --- a/docs/community/overview.md +++ b/docs/community/overview.md @@ -24,6 +24,13 @@ Bundles compose extensions, presets, workflows, and steps into role or team stac [Browse community bundles →](bundles.md) +## Workflow step types + +Workflow step types add reusable executable behavior that workflows can select +with `type:`. + +[Submit a community workflow step type →](workflow-steps.md) + ## Walkthroughs Step-by-step guides that show Spec-Driven Development in action across different scenarios, languages, and frameworks. @@ -41,5 +48,6 @@ Community projects that extend, visualize, or build on Spec Kit — including VS To publish your own work, follow the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md), the [Presets Publishing Guide](https://github.com/github/spec-kit/blob/main/presets/PUBLISHING.md), -or the [Community Bundles guide](bundles.md). For help choosing a component type, -see [Customize Spec Kit](../guides/customization.md). +the [Community Bundles guide](bundles.md), or the +[Community Workflow Step Types guide](workflow-steps.md). For help choosing a +component type, see [Customize Spec Kit](../guides/customization.md). diff --git a/docs/community/workflow-steps.md b/docs/community/workflow-steps.md new file mode 100644 index 0000000000..530fb514fc --- /dev/null +++ b/docs/community/workflow-steps.md @@ -0,0 +1,129 @@ +# Community Workflow Step Types + +> [!NOTE] +> Community workflow step types are independently created and maintained by +> their authors. Maintainers verify submission metadata and package shape; they +> do **not review, audit, endorse, or support the executable Python code**. +> Installation validates and copies the package without importing its Python +> code. Later `specify workflow add`, `run`, and `resume` commands load installed +> step packages, executing import-time code with the user's privileges. Review +> the source before using an installed step. + +Custom workflow step packages add a `type:` that workflows can use beyond the +built-in step types. The canonical intake path is the +[Workflow Step Type Submission](https://github.com/github/spec-kit/issues/new?template=workflow_step_submission.yml) +issue form. + +This is currently an **intake-only** process. Opening the form applies only the +`triage-must-have` verdict. It does not trigger catalog validation, create a +branch, or open a draft pull request. Maintainers review submissions manually +and, when accepted, update +[`workflows/step-catalog.community.json`](https://github.com/github/spec-kit/blob/main/workflows/step-catalog.community.json) +through the normal reviewed pull request process. + +The built-in community catalog is **discovery-only**. Accepted entries can +appear in `specify workflow step search` and `info`, but +`specify workflow step add ` will not install executable code from that +source. After reviewing the source, install the submitted versioned archive +directly with `--from`, or use a separately configured step catalog that you +explicitly allow for installation. + +## Package Contract + +One installed package provides one workflow step type: + +```text +my-step/ +├── step.yml +├── __init__.py +└── helpers.py # optional; every catalog-downloaded extra file is declared +``` + +The package must satisfy the current installer and loader contract: + +- `step.yml` and `__init__.py` are regular, non-symlink files at the package + root. An archive may wrap the package in exactly one top-level directory. +- `step.type_key`, the catalog key/ID, and the matching + `StepBase.type_key` are identical. +- The ID is one safe path component. The installer rejects separators, leading + dots, trailing dots or spaces, reserved names, control characters, and + Windows-invalid filename characters. +- `__init__.py` defines a `StepBase` subclass matching that single type key. + Additional classes do not make the package provide additional registered + step types. +- Extra-file paths use forward slashes, are relative and non-empty, contain no + empty, `.` or `..` segments, and do not case-insensitively alias `step.yml` + or `__init__.py`. +- The package stays within the current installer limits: 512 retained entries, + 32 directory levels, and 50 MiB of retained content. +- `.git`, `__pycache__`, and `.DS_Store` are excluded from installation. + +See [Custom step packages](../reference/workflows.md#custom-step-packages) for +the complete install, validation, and runtime behavior. + +## Submission Contract + +The issue form separates fields consumed by the current package/catalog code +from provenance and disclosure fields used during manual review. + +| Submission data | Current contract | +| --- | --- | +| Step type ID | Catalog key, `step.type_key`, and matching `StepBase.type_key` | +| Name, version, description, author | `step.yml` metadata and catalog/installed registry metadata | +| Download URL | Versioned release archive used to test the supported direct-URL installation path | +| `step.yml`, `__init__.py`, and extra-file URLs | Exact HTTPS file URLs consumed when installing from an explicitly install-allowed catalog; extra-file keys follow the package-relative path restrictions above | +| Per-file SHA-256 mapping | Catalog `sha256`; keys must be exactly the files the installer downloads | +| Provided type name/count | Loader invariant: one matching type key per installed package | +| Repository, license, documentation, changelog | Source provenance and manual-review evidence; these are not `step.yml` fields | +| Spec Kit compatibility | Required intake disclosure; the step catalog accepts a `requires` mapping but does not yet define or enforce a Spec Kit compatibility schema | +| Runtime and tool dependencies | Required intake disclosure; the CLI does not resolve or install dependencies for custom steps | + +Use a versioned release URL for the archive, consistent with the other +community submission forms. Use version-pinned URLs for every catalog file; the +required per-file SHA-256 mapping detects changed bytes even if a referenced +tag is later moved. Do not submit branch URLs, `releases/latest` URLs, or other +floating targets. Exact-release installation from an install-allowed catalog +requires a valid release version and a 64-character hexadecimal SHA-256 digest +for `step.yml`, `__init__.py`, and every `extra_files` path. + +The direct `--from` archive installer does not currently accept or verify an +archive digest. The submitted Download URL and Testing Details fields provide +versioned-release and installation-test evidence, not an immutable content +guarantee; the per-file digests protect the separate catalog-installation path. + +## Prepare a Submission + +1. Create a public source repository with the complete package, license, + documentation, and changelog where applicable. +2. Test valid and invalid configuration, `StepStatus`, outputs, errors, and any + relevant resume, nested-step, or concurrent execution behavior. +3. Publish a versioned release archive (`.zip`, `.tar.gz`, or `.tgz`) and test + it: + + ```bash + specify workflow step add my-step \ + --from https://github.com/your-org/my-step/releases/download/v1.0.0/my-step-1.0.0.zip + ``` + +4. Publish tag-pinned HTTPS URLs for `step.yml`, `__init__.py`, and every extra + package file. Compute the SHA-256 digest of each downloaded file. +5. File the canonical submission issue with the exact release metadata and + test evidence. + +## Current Review Scope + +Maintainers currently check the form manually for: + +- complete identity, release, repository, license, and documentation metadata; +- consistency between the package ID, manifest type key, provided class, and + version; +- versioned release and catalog file URLs; +- complete per-file checksum and extra-file mappings; +- disclosed compatibility and runtime dependencies; and +- evidence that the released archive follows the supported package shape and + installation path. + +There is no workflow-step submission validator or draft-PR generator in this +phase. A future automation phase will need a canonical step catalog entry +schema, including defined compatibility and dependency shapes, before all +submission fields can be machine-validated rather than reviewed as text. diff --git a/docs/reference/workflows.md b/docs/reference/workflows.md index b99fbea42c..9e524c1c85 100644 --- a/docs/reference/workflows.md +++ b/docs/reference/workflows.md @@ -594,6 +594,9 @@ my-step/ └── helpers.py # optional nested modules and data files ``` +To prepare a public package for community catalog intake, see +[Community Workflow Step Types](../community/workflow-steps.md). + `step.yml` declares the step's identity. `step.type_key` must exactly match the `` passed on the command line — the ID is never inferred from package content: diff --git a/docs/toc.yml b/docs/toc.yml index 4889ceeb5b..8761b8a491 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -102,6 +102,8 @@ href: community/presets.md - name: Bundles href: community/bundles.md + - name: Workflow Step Types + href: community/workflow-steps.md - name: Walkthroughs href: community/walkthroughs.md - name: Friends diff --git a/tests/test_github_workflows.py b/tests/test_github_workflows.py index 59776b9c64..d6cf317296 100644 --- a/tests/test_github_workflows.py +++ b/tests/test_github_workflows.py @@ -525,14 +525,154 @@ def test_bundle_submission_form_applies_only_automatic_intake_verdict(): assert "bundle-submission" not in bundle_form["labels"] +def test_workflow_step_submission_form_applies_only_automatic_intake_verdict(): + forms_dir = REPO_ROOT / ".github" / "ISSUE_TEMPLATE" + workflow_step_form = yaml.safe_load( + (forms_dir / "workflow_step_submission.yml").read_text(encoding="utf-8") + ) + + assert workflow_step_form["labels"] == ["triage-must-have"] + assert not { + "enhancement", + "needs-triage", + "workflow-step-submission", + "validation-passed", + "validation-failed", + } & set(workflow_step_form["labels"]) + + +def test_workflow_step_submission_form_has_valid_complete_field_contract(): + forms_dir = REPO_ROOT / ".github" / "ISSUE_TEMPLATE" + workflow_step_form = yaml.safe_load( + (forms_dir / "workflow_step_submission.yml").read_text(encoding="utf-8") + ) + fields = [item for item in workflow_step_form["body"] if "id" in item] + field_ids = [field["id"] for field in fields] + + assert len(field_ids) == len(set(field_ids)) + assert all(re.fullmatch(r"[A-Za-z0-9_-]+", field_id) for field_id in field_ids) + assert set(field_ids) == { + "step-id", + "step-name", + "version", + "description", + "author", + "repository", + "download-url", + "step-yml-url", + "init-url", + "extra-files", + "file-sha256", + "license", + "speckit-compatibility", + "runtime-dependencies", + "step-type-count", + "step-types-provided", + "documentation", + "changelog", + "testing-details", + "attestations", + "additional-context", + "ai-disclosure", + } + required_ids = { + "step-id", + "step-name", + "version", + "description", + "author", + "repository", + "download-url", + "step-yml-url", + "init-url", + "extra-files", + "file-sha256", + "license", + "speckit-compatibility", + "runtime-dependencies", + "step-type-count", + "step-types-provided", + "documentation", + "testing-details", + "ai-disclosure", + } + assert { + field["id"] + for field in fields + if field.get("validations", {}).get("required") is True + } == required_ids + field_by_id = {field["id"]: field for field in fields} + assert field_by_id["step-type-count"][ + "attributes" + ]["options"] == ["1"] + assert all( + option["required"] is True + for option in field_by_id["attestations"]["attributes"]["options"] + ) + bundle_form = yaml.safe_load( + (forms_dir / "bundle_submission.yml").read_text(encoding="utf-8") + ) + bundle_fields = { + item["id"]: item for item in bundle_form["body"] if "id" in item + } + assert field_by_id["download-url"]["attributes"]["label"] == ( + bundle_fields["download-url"]["attributes"]["label"] + ) + download_description = field_by_id["download-url"]["attributes"]["description"] + assert "versioned" in download_description + assert "immutable" not in download_description + assert "immutable" not in yaml.safe_dump(workflow_step_form).lower() + extra_files_description = field_by_id["extra-files"]["attributes"]["description"] + assert "forward slashes" in extra_files_description + assert "relative and non-empty" in extra_files_description + assert "no empty, `.` or `..` segments" in extra_files_description + assert "case-insensitively alias `step.yml` or `__init__.py`" in ( + extra_files_description + ) + feature_form = yaml.safe_load( + (forms_dir / "feature_request.yml").read_text(encoding="utf-8") + ) + feature_fields = { + item["id"]: item for item in feature_form["body"] if "id" in item + } + assert field_by_id["ai-disclosure"] == feature_fields["ai-disclosure"] + + +def test_workflow_step_submission_form_documents_intake_only_phase(): + forms_dir = REPO_ROOT / ".github" / "ISSUE_TEMPLATE" + workflow_step_form = yaml.safe_load( + (forms_dir / "workflow_step_submission.yml").read_text(encoding="utf-8") + ) + introduction = workflow_step_form["body"][0]["attributes"]["value"] + + assert "This phase is intake-only" in introduction + assert "no validation workflow or draft pull request is triggered" in introduction + assert "update the community catalog through the normal reviewed pull request" in ( + introduction + ) + + def test_other_issue_forms_do_not_apply_automatic_intake_verdict(): forms_dir = REPO_ROOT / ".github" / "ISSUE_TEMPLATE" - for form_name in ( + automatic_intake_forms = { + "bundle_submission.yml", + "extension_submission.yml", + "preset_submission.yml", + "workflow_step_submission.yml", + } + other_forms = sorted( + path + for path in forms_dir.glob("*.yml") + if path.name != "config.yml" and path.name not in automatic_intake_forms + ) + + assert [path.name for path in other_forms] == [ "agent_request.yml", "bug_report.yml", "feature_request.yml", - ): - form = yaml.safe_load((forms_dir / form_name).read_text(encoding="utf-8")) + ] + for form_path in other_forms: + form = yaml.safe_load(form_path.read_text(encoding="utf-8")) assert "triage-must-have" not in form["labels"]