diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc918f5..6b0087c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,9 +29,6 @@ jobs: go-version-file: go.mod - uses: GoCodeAlone/setup-wfctl@v1 with: - version: v0.62.0 - # workflow#762: scaffold carries type="scaffold" + TEMPLATE.* placeholders. - # Old `plugin validate --strict-contracts` (wfctl v0.20.1) rejects both; - # validate-contract (v0.62.0) checks scaffold-compatible contract surface. - - name: Validate plugin contract (scaffold-aware) + version: v0.63.1 + - name: Validate plugin contract run: wfctl plugin validate-contract . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7dbedb8..6ff83f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,10 +42,10 @@ jobs: run: | RUNNER_ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') BIN=$(jq -r --arg arch "$RUNNER_ARCH" \ - '[.[] | select(.type=="Binary" and .goos=="linux" and .goarch==$arch and (.name|startswith("scaffold-workflow-plugin")))] | .[0].path // ""' \ + '[.[] | select(.type=="Binary" and .goos=="linux" and .goarch==$arch and (.name|startswith("workflow-plugin-compute-core")))] | .[0].path // ""' \ dist/artifacts.json) if [ -z "$BIN" ] || [ "$BIN" = "null" ]; then - echo "::warning::No matching linux/$RUNNER_ARCH scaffold binary in dist/artifacts.json; skipping verify-capabilities" + echo "::warning::No matching linux/$RUNNER_ARCH workflow-plugin-compute-core binary in dist/artifacts.json; skipping verify-capabilities" jq '.[] | {name, type, goos, goarch, path}' dist/artifacts.json exit 0 fi @@ -56,7 +56,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit "${{ github.ref_name }}" --draft=false --repo "${{ github.repository }}" - # NOTE: scaffold-workflow-plugin does NOT notify workflow-registry on release - # — it's a scaffold repo, NOT an installable plugin. The notify-workflow-registry - # job is intentionally omitted (and registry-side type-allowlist defense - # in wfctl plugin registry-sync rejects type:"scaffold" if it ever leaks in). + # Registry publication is intentionally separate from binary release until + # provider catalog plugins consume this core plugin from the registry. diff --git a/.github/workflows/scaffold-rename-test.yml b/.github/workflows/scaffold-rename-test.yml deleted file mode 100644 index a9c99cc..0000000 --- a/.github/workflows/scaffold-rename-test.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Scaffold rename test - -on: - push: - branches: [main] - pull_request: - -# Verifies scripts/rename-from-scaffold.sh produces a buildable plugin in -# both --mode iac and --mode non-iac. Includes a nested-fixture file to -# exercise the find-based bulk-sed (catches globstar regressions per -# workflow#762 plan C-P4 guard). -jobs: - rename-non-iac: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - - name: Add nested fixture file - run: | - mkdir -p internal/nested/sub - cat > internal/nested/sub/test.go <<'EOF' - // Fixture file deeper than one level - exercises the rename script's - // find loop to verify imports in nested packages get rewritten. - package sub - import _ "github.com/GoCodeAlone/scaffold-workflow-plugin/internal" - EOF - - name: Rename to test-plugin (non-iac) + build - run: | - cp -r . /tmp/scaffold-copy - cd /tmp/scaffold-copy - bash scripts/rename-from-scaffold.sh test-plugin --mode non-iac - go build ./... - test -d cmd/workflow-plugin-test-plugin - test ! -d cmd/scaffold-workflow-plugin - test ! -d cmd/scaffold-workflow-plugin-iac - test "$(jq -r .type plugin.json)" = "external" - test "$(jq -r .name plugin.json)" = "workflow-plugin-test-plugin" - - rename-iac: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - - name: Add nested fixture file - run: | - mkdir -p internal/nested/sub - cat > internal/nested/sub/test.go <<'EOF' - package sub - import _ "github.com/GoCodeAlone/scaffold-workflow-plugin/internal" - EOF - - name: Rename to test-plugin (iac) + build - run: | - cp -r . /tmp/scaffold-copy - cd /tmp/scaffold-copy - bash scripts/rename-from-scaffold.sh test-plugin --mode iac - go build ./... - test -d cmd/workflow-plugin-test-plugin - test ! -d cmd/scaffold-workflow-plugin - test ! -d cmd/scaffold-workflow-plugin-iac diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 346721e..2cf5b39 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,9 +6,9 @@ before: - "sed -i.bak 's/\"version\": \".*\"/\"version\": \"{{ .Version }}\"/' plugin.json && rm -f plugin.json.bak" builds: - - id: scaffold-workflow-plugin - main: ./cmd/scaffold-workflow-plugin - binary: scaffold-workflow-plugin + - id: workflow-plugin-compute-core + main: ./cmd/workflow-plugin-compute-core + binary: workflow-plugin-compute-core env: - CGO_ENABLED=0 goos: @@ -19,7 +19,7 @@ builds: - amd64 - arm64 ldflags: - - -s -w -X main.version={{.Version}} -X github.com/GoCodeAlone/scaffold-workflow-plugin/internal.Version={{.Version}} + - -s -w -X main.version={{.Version}} -X github.com/GoCodeAlone/workflow-plugin-compute-core/internal.Version={{.Version}} archives: - formats: [tar.gz] diff --git a/CLAUDE.md b/CLAUDE.md index 7f19626..7b5889a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,7 @@ -# CLAUDE.md — Workflow Plugin Template +# CLAUDE.md — workflow-plugin-compute-core -External gRPC plugin for the GoCodeAlone/workflow engine. +External gRPC plugin and public Go module for compute protocol and provider +catalog contracts. ## Build & Test @@ -9,40 +10,18 @@ go build ./... go test ./... -v -race -count=1 ``` -## Cross-compile for deployment +## Cross-compile ```sh -GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o scaffold-workflow-plugin ./cmd/scaffold-workflow-plugin/ +GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o workflow-plugin-compute-core ./cmd/workflow-plugin-compute-core/ ``` ## Structure -- `cmd/scaffold-workflow-plugin/main.go` — Plugin entry point (calls `sdk.Serve`) -- `internal/plugin.go` — Plugin manifest, module factories, step factories -- `internal/` — All module and step implementations -- `plugin.json` — Capability manifest for the workflow registry -- `.goreleaser.yaml` — GoReleaser v2 config for cross-platform releases -- `.github/workflows/ci.yml` — CI on push/PR (build + test) -- `.github/workflows/release.yml` — Release on v* tag push (GoReleaser) - -## Adding a Module Type - -1. Create `internal/module_example.go` implementing the module -2. Register in `internal/plugin.go` ModuleFactories() -3. Add to `plugin.json` capabilities.moduleTypes -4. Add tests in `internal/module_example_test.go` - -## Adding a Step Type - -1. Create `internal/step_example.go` implementing the step -2. Register in `internal/plugin.go` StepFactories() -3. Add to `plugin.json` capabilities.stepTypes -4. Add tests in `internal/step_example_test.go` - -## Releasing - -```sh -git tag v0.1.0 -git push origin v0.1.0 -``` -GoReleaser builds cross-platform binaries and creates a GitHub Release automatically. +- `cmd/workflow-plugin-compute-core/main.go` — external plugin entrypoint +- `internal/plugin.go` — Workflow plugin manifest +- `protocol/types.go` — public compute protocol and provider catalog types +- `plugin.json` — registry-facing plugin manifest +- `.goreleaser.yaml` — GoReleaser v2 config for releases +- `.github/workflows/ci.yml` — build, test, vet, and plugin contract validation +- `.github/workflows/release.yml` — tagged release pipeline diff --git a/README.md b/README.md index 0022421..b52bca2 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,30 @@ -# scaffold-workflow-plugin +# workflow-plugin-compute-core -This is a SCAFFOLD repo. It is NOT an installable plugin. +Public Workflow plugin and Go module for compute protocol and provider catalog +contracts. -Use it to create a new workflow plugin via GitHub's "Use this template" button. +Provider plugins use this module for shared JSON-compatible protocol types, +validation helpers, and canonical hashing. They also declare a plugin dependency +on `workflow-plugin-compute-core` in `plugin.json`, giving Workflow a registry +dependency anchor separate from runtime execution plugins. -(A future `wfctl plugin init --from-scaffold` subcommand is tracked at -[workflow#762](https://github.com/GoCodeAlone/workflow/issues/762) but -not yet implemented; use the GitHub UI path below.) +This plugin intentionally advertises no module, step, trigger, or IaC runtime +capabilities. -## After creating your new repo from this template +## Build & Test -1. **Enable GitHub Actions**: Settings → Actions → "I understand my workflows, enable them". - New repos created from a template ship with workflows DISABLED by - default; you must enable them once before any release.yml run can - succeed. - -2. **Run the rename script**: - - ```bash - bash scripts/rename-from-scaffold.sh --mode [iac|non-iac] - ``` - - This: - - Picks the IaC or non-IaC main.go variant; deletes the other. - - Renames `cmd/scaffold-workflow-plugin*/` → `cmd/workflow-plugin-/`. - - Updates `go.mod` module path. - - Bulk-sed of `scaffold-workflow-plugin` → `workflow-plugin-` - across `.go`/`.yaml`/`.md`/`plugin.json` files. - - Resets `plugin.json.type` from `"scaffold"` to `"external"`; sets `.name`. - - Removes the rename script itself + scaffold-rename-test workflow. - -3. **Edit `plugin.json`**: replace `TEMPLATE.module` / `TEMPLATE.step` / - `TEMPLATE.resource` placeholder capabilities with your plugin's actual - types. Update `minEngineVersion` if you depend on a newer workflow. - -4. **Implement your plugin** in `internal/`: - - **non-IaC mode**: extend `internal/plugin.go`'s `NewPlugin()` with - real ModuleFactories / StepFactories / TriggerFactories. Delete - `internal/iacserver.go` (unused in non-IaC mode). - - **IaC mode**: replace `internal/iacserver.go`'s stub - `pb.UnimplementedIaCProviderRequiredServer` embed with your real - IaC provider implementation (Initialize, Plan, Destroy, etc.). - Delete `internal/plugin.go`'s NewPlugin (unused in IaC mode). - -5. **Commit + tag**: - - ```bash - git add -A && git commit -m "feat: initial plugin scaffold from scaffold-workflow-plugin" - git tag v0.1.0 && git push origin main v0.1.0 - ``` - - `release.yml`'s `wfctl plugin validate-contract --for-publish` gate - verifies your tag (must be release-grade semver `^v\d+\.\d+\.\d+$`) - and contract (capabilities populated, minEngineVersion set, main.go - wires `sdk.ResolveBuildVersion`, goreleaser ldflag present). - -## Modes - -- `--mode non-iac` (default): for module/step/trigger plugins that use - `sdk.Serve`. Suitable for MOST plugins. -- `--mode iac`: for IaC provider plugins that use `sdk.ServeIaCPlugin` - and satisfy `pb.IaCProviderRequiredServer`. Use ONLY if your plugin - provisions infrastructure (cloud resources, databases, etc.). - -## What's pre-baked in (workflow#758 + #762 compliance) - -- `plugin.json.version = "0.0.0"` sentinel (release tag injected at build - time via goreleaser). -- `internal/plugin.go`'s `var Version = "0.0.0"` (ldflag-injected at - release; surfaced through `sdk.ResolveBuildVersion`). -- `release.yml` pre-build + post-build `wfctl plugin validate-contract` - gates. -- No `sync-plugin-version.yml` (the discarded sync mechanism is not - shipped in scaffolds; goreleaser's `before:` hook rewrites - `plugin.json.version` from the tag at release time). -- `sdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version))` - wired in main.go so the binary surfaces its release version through - `GetManifest` at runtime. -- `setup-wfctl@v1 with version: v0.62.0` pinned for the release pipeline. - -## Build & test (during plugin development) - -```bash +```sh go build ./... go test ./... -race -count=1 ``` -## Releasing +## Release -```bash -git tag v0.1.0 && git push origin v0.1.0 +```sh +git tag v0.1.0 +git push origin v0.1.0 ``` -`release.yml` runs `wfctl plugin validate-contract --for-publish`, -goreleaser builds cross-platform binaries, and the post-build gate -verifies the shipped tarball's `plugin.json` carries the tag. - -## References - -- Plugin release contract: [docs/PLUGIN_RELEASE_GATES.md](https://github.com/GoCodeAlone/workflow/blob/main/docs/PLUGIN_RELEASE_GATES.md) -- Plugin version discipline: [workflow#758](https://github.com/GoCodeAlone/workflow/issues/758) -- Registry sync subcommand: [workflow#762](https://github.com/GoCodeAlone/workflow/issues/762) +The release workflow validates `plugin.json`, builds cross-platform binaries +with GoReleaser, and verifies the runtime plugin manifest against the shipped +contract metadata. diff --git a/cmd/scaffold-workflow-plugin-iac/main.go b/cmd/scaffold-workflow-plugin-iac/main.go deleted file mode 100644 index 2942e32..0000000 --- a/cmd/scaffold-workflow-plugin-iac/main.go +++ /dev/null @@ -1,23 +0,0 @@ -// Command scaffold-workflow-plugin-iac is the IaC variant of the workflow -// plugin scaffold. Use this entrypoint when the plugin provisions -// infrastructure (cloud resources, etc.) — it serves the typed -// pb.IaCProvider* surface required by wfctl infra apply/plan/destroy. -// -// Instantiators run `bash scripts/rename-from-scaffold.sh --mode iac` -// to copy this file to cmd/workflow-plugin-/main.go and delete -// the non-IaC variant (cmd/scaffold-workflow-plugin/). -// -// Non-IaC plugins use cmd/scaffold-workflow-plugin/main.go instead. The -// rename script's --mode flag selects which entrypoint survives. -package main - -import ( - "github.com/GoCodeAlone/scaffold-workflow-plugin/internal" - sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" -) - -func main() { - sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{ - BuildVersion: sdk.ResolveBuildVersion(internal.Version), - }) -} diff --git a/cmd/scaffold-workflow-plugin/main.go b/cmd/scaffold-workflow-plugin/main.go deleted file mode 100644 index ed4c56b..0000000 --- a/cmd/scaffold-workflow-plugin/main.go +++ /dev/null @@ -1,19 +0,0 @@ -// Command scaffold-workflow-plugin is the NON-IaC variant of the workflow -// plugin scaffold. It runs as a subprocess and communicates with the host -// workflow engine via the go-plugin gRPC protocol. -// -// Instantiators run `bash scripts/rename-from-scaffold.sh --mode non-iac` -// to copy this file to cmd/workflow-plugin-/main.go and delete -// the IaC variant (cmd/scaffold-workflow-plugin-iac/). -package main - -import ( - "github.com/GoCodeAlone/scaffold-workflow-plugin/internal" - sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" -) - -func main() { - sdk.Serve(internal.NewPlugin(), - sdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version)), - ) -} diff --git a/cmd/workflow-plugin-compute-core/main.go b/cmd/workflow-plugin-compute-core/main.go new file mode 100644 index 0000000..36d2afc --- /dev/null +++ b/cmd/workflow-plugin-compute-core/main.go @@ -0,0 +1,14 @@ +// Command workflow-plugin-compute-core exposes compute protocol metadata as an +// external Workflow plugin dependency anchor. +package main + +import ( + "github.com/GoCodeAlone/workflow-plugin-compute-core/internal" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +func main() { + sdk.Serve(internal.NewPlugin(), + sdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version)), + ) +} diff --git a/go.mod b/go.mod index 51e255f..f065d1a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/GoCodeAlone/scaffold-workflow-plugin +module github.com/GoCodeAlone/workflow-plugin-compute-core go 1.26.0 diff --git a/internal/iacserver.go b/internal/iacserver.go deleted file mode 100644 index 2cb6b66..0000000 --- a/internal/iacserver.go +++ /dev/null @@ -1,30 +0,0 @@ -package internal - -import ( - pb "github.com/GoCodeAlone/workflow/plugin/external/proto" -) - -// IaCServer is the IaC-mode stub for the scaffold. Embeds -// pb.UnimplementedIaCProviderRequiredServer so all required RPCs (Initialize, -// Name, Version, Capabilities, Plan, Destroy, Status, Import, ResolveSizing, -// BootstrapStateBackend) return codes.Unimplemented by default. -// -// Instantiators using `bash scripts/rename-from-scaffold.sh --mode iac` -// replace this stub with their real IaC provider implementation. The -// rename script removes cmd/scaffold-workflow-plugin/ in IaC mode, so the -// non-IaC NewPlugin() entrypoint is gone — only the IaC server remains. -// -// To implement additional optional IaC contracts, embed the corresponding -// Unimplemented*Server type: -// - pb.UnimplementedIaCProviderServer -// - pb.UnimplementedIaCProviderLogCaptureServer -// - pb.UnimplementedIaCProviderFinalizerServer -type IaCServer struct { - pb.UnimplementedIaCProviderRequiredServer -} - -// NewIaCServer constructs the IaC-mode plugin server. Called from -// cmd/scaffold-workflow-plugin-iac/main.go. -func NewIaCServer() *IaCServer { - return &IaCServer{} -} diff --git a/internal/plugin.go b/internal/plugin.go index 9e2ec07..8aea58d 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -1,72 +1,31 @@ -// Package internal implements the scaffold-workflow-plugin plugin. +// Package internal implements the workflow-plugin-compute-core plugin. package internal import ( - "fmt" - sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) // Version is set at build time via -ldflags -// "-X github.com/GoCodeAlone/scaffold-workflow-plugin/internal.Version=X.Y.Z". +// "-X github.com/GoCodeAlone/workflow-plugin-compute-core/internal.Version=X.Y.Z". // Default is a bare semver so plugin loaders that validate semver accept // unreleased dev builds; goreleaser overrides with the real release tag. var Version = "0.0.0" -// TEMPLATEPlugin implements sdk.PluginProvider and optionally -// sdk.ModuleProvider, sdk.StepProvider, sdk.TriggerProvider, etc. -type TEMPLATEPlugin struct{} +// ComputeCorePlugin exposes compute-core protocol metadata. +type ComputeCorePlugin struct{} // NewPlugin returns a new plugin instance. main.go calls sdk.Serve(NewPlugin()). func NewPlugin() sdk.PluginProvider { - return &TEMPLATEPlugin{} + return &ComputeCorePlugin{} } // Manifest returns the plugin metadata used by the workflow engine for // discovery and capability negotiation. -func (p *TEMPLATEPlugin) Manifest() sdk.PluginManifest { +func (p *ComputeCorePlugin) Manifest() sdk.PluginManifest { return sdk.PluginManifest{ - Name: "scaffold-workflow-plugin", + Name: "workflow-plugin-compute-core", Version: Version, Author: "GoCodeAlone", - Description: "TEMPLATE plugin for the workflow engine", - } -} - -// ModuleTypes returns the module type names this plugin provides. -// Remove this method if the plugin does not provide any modules. -func (p *TEMPLATEPlugin) ModuleTypes() []string { - return []string{ - // "example.module_type", - } -} - -// CreateModule creates a module instance of the given type. -// Remove this method if the plugin does not provide any modules. -func (p *TEMPLATEPlugin) CreateModule(typeName, name string, config map[string]any) (sdk.ModuleInstance, error) { - switch typeName { - // case "example.module_type": - // return newExampleModule(name, config) - default: - return nil, fmt.Errorf("TEMPLATE: unknown module type %q", typeName) - } -} - -// StepTypes returns the step type names this plugin provides. -// Remove this method if the plugin does not provide any steps. -func (p *TEMPLATEPlugin) StepTypes() []string { - return []string{ - // "step.example_action", - } -} - -// CreateStep creates a step instance of the given type. -// Remove this method if the plugin does not provide any steps. -func (p *TEMPLATEPlugin) CreateStep(typeName, name string, config map[string]any) (sdk.StepInstance, error) { - switch typeName { - // case "step.example_action": - // return newExampleStep(name, config), nil - default: - return nil, fmt.Errorf("TEMPLATE: unknown step type %q", typeName) + Description: "Public compute protocol and provider catalog core for Workflow.", } } diff --git a/internal/plugin_test.go b/internal/plugin_test.go index c05d370..71f57fd 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -1,9 +1,14 @@ package internal_test import ( + "encoding/json" + "os" + "slices" + "strings" "testing" - "github.com/GoCodeAlone/scaffold-workflow-plugin/internal" + "github.com/GoCodeAlone/workflow-plugin-compute-core/internal" + "github.com/GoCodeAlone/workflow-plugin-compute-core/protocol" sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) @@ -22,4 +27,107 @@ func TestManifest_HasRequiredFields(t *testing.T) { if m.Description == "" { t.Error("manifest Description is empty") } + if strings.Contains(m.Description, "TEMPLATE") || strings.Contains(strings.ToLower(m.Description), "scaffold") { + t.Fatalf("manifest still carries scaffold placeholder text: %q", m.Description) + } +} + +func TestPluginJSON_AdvertisesProtocolCoreOnly(t *testing.T) { + data, err := os.ReadFile("../plugin.json") + if err != nil { + t.Fatalf("read plugin.json: %v", err) + } + var manifest struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Private bool `json:"private"` + Keywords []string `json:"keywords"` + Capabilities struct { + ConfigProvider bool `json:"configProvider"` + ModuleTypes []string `json:"moduleTypes"` + StepTypes []string `json:"stepTypes"` + TriggerTypes []string `json:"triggerTypes"` + } `json:"capabilities"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatalf("parse plugin.json: %v", err) + } + if manifest.Name != "workflow-plugin-compute-core" || manifest.Type != "external" || manifest.Private { + t.Fatalf("unexpected plugin identity: %+v", manifest) + } + joined := strings.Join(append(manifest.Keywords, manifest.Description), " ") + if strings.Contains(joined, "TEMPLATE") || strings.Contains(strings.ToLower(joined), "scaffold") { + t.Fatalf("plugin.json still carries scaffold placeholder text: %s", joined) + } + if manifest.Capabilities.ConfigProvider || + len(manifest.Capabilities.ModuleTypes) != 0 || + len(manifest.Capabilities.StepTypes) != 0 || + len(manifest.Capabilities.TriggerTypes) != 0 { + t.Fatalf("compute-core should not advertise runtime capabilities: %+v", manifest.Capabilities) + } + if !slices.Contains(manifest.Keywords, "provider-catalog") || !slices.Contains(manifest.Keywords, "protocol") { + t.Fatalf("protocol keywords missing: %+v", manifest.Keywords) + } +} + +func TestProtocolProviderContractSupportsProduct(t *testing.T) { + runtime := protocol.DefaultProviderRuntimeProfile("node-service-sandboxed-container", protocol.ExecutionSandboxedContainer, protocol.ProofArtifactHash) + contract := protocol.ProviderContract{ + ProtocolVersion: protocol.Version, + ID: "example-v1", + PluginID: "workflow-plugin-example", + ProviderID: "example", + ContractID: "example.client.v1", + Version: "v1.0.0", + ConfigSchemaRef: "schema://providers/example/client/v1", + ConfigSchemaDigest: protocol.CanonicalHash(map[string]string{"type": "object"}), + OperatingModes: []protocol.NetworkOperatingMode{protocol.NetworkModeNodeService}, + WorkloadKinds: []string{"node-service"}, + ExecutorProviders: []string{"node-service-sandboxed-container"}, + ExecutionSecurityTiers: []protocol.ExecutionSecurityTier{protocol.ExecutionSandboxedContainer}, + ProofTiers: []protocol.ProofTier{protocol.ProofArtifactHash}, + NetworkModes: []protocol.NetworkMode{protocol.NetworkModeDirect, protocol.NetworkModeRelay}, + RuntimeContract: protocol.ProviderRuntimeContract{Profiles: []protocol.ProviderRuntimeProfile{runtime}}, + } + if err := contract.Validate(); err != nil { + t.Fatalf("contract invalid: %v", err) + } + product := protocol.NetworkProduct{ + ProtocolVersion: protocol.Version, + ID: "example-product", + OperatingMode: protocol.NetworkModeNodeService, + OrgID: "public", + PoolID: "example", + WorkloadKinds: []string{"node-service"}, + SecurityFloor: protocol.PlacementRequirements{ + ExecutorProvider: "node-service-sandboxed-container", + ExecutionSecurityTier: protocol.ExecutionSandboxedContainer, + ProofTier: protocol.ProofArtifactHash, + }, + ProviderConfig: protocol.ProviderConfig{ + PluginID: "workflow-plugin-example", + ProviderID: "example", + ContractID: "example.client.v1", + }, + NetworkModes: []protocol.NetworkMode{protocol.NetworkModeDirect}, + PlacementConstraints: protocol.PlacementConstraints{ + Chain: "example", + Role: "client", + MinDiskBytes: 1, + }, + RewardPolicy: "points", + AbusePolicy: "example", + SettlementTarget: protocol.SettlementTarget{ + Kind: protocol.SettlementTargetTreasuryWallet, + Network: "example", + WalletRef: "wallet://example/primary", + }, + } + if err := product.Validate(); err != nil { + t.Fatalf("product invalid: %v", err) + } + if err := contract.SupportsProduct(product); err != nil { + t.Fatalf("contract should support product: %v", err) + } } diff --git a/plugin.json b/plugin.json index 5f86b2b..fc55a10 100644 --- a/plugin.json +++ b/plugin.json @@ -1,24 +1,25 @@ { - "name": "scaffold-workflow-plugin", - "version": "0.0.0", - "description": "Scaffold repo for new workflow plugins. NOT an installable plugin — see README for the instantiation flow.", - "author": "GoCodeAlone", - "license": "MIT", - "type": "scaffold", - "tier": "community", - "private": false, - "minEngineVersion": "0.62.0", - "keywords": ["scaffold", "template"], - "homepage": "https://github.com/GoCodeAlone/scaffold-workflow-plugin", - "repository": "https://github.com/GoCodeAlone/scaffold-workflow-plugin", - "capabilities": { - "configProvider": false, - "moduleTypes": ["TEMPLATE.module"], - "stepTypes": ["TEMPLATE.step"], - "triggerTypes": [], - "iacProvider": { - "resourceTypes": ["TEMPLATE.resource"] - } - }, - "contracts": [] + "name": "workflow-plugin-compute-core", + "version": "0.0.0", + "description": "Public compute protocol and provider catalog core for Workflow.", + "author": "GoCodeAlone", + "license": "MIT", + "type": "external", + "tier": "community", + "private": false, + "minEngineVersion": "0.62.0", + "keywords": [ + "compute", + "protocol", + "provider-catalog" + ], + "homepage": "https://github.com/GoCodeAlone/workflow-plugin-compute-core", + "repository": "https://github.com/GoCodeAlone/workflow-plugin-compute-core", + "capabilities": { + "configProvider": false, + "moduleTypes": [], + "stepTypes": [], + "triggerTypes": [] + }, + "contracts": [] } diff --git a/protocol/types.go b/protocol/types.go new file mode 100644 index 0000000..2342db1 --- /dev/null +++ b/protocol/types.go @@ -0,0 +1,447 @@ +package protocol + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" +) + +const Version = "compute.v1alpha1" + +type NetworkOperatingMode string + +const NetworkModeNodeService NetworkOperatingMode = "node-service" + +type RuntimeProfile string + +const RuntimeProfileServiceOCI RuntimeProfile = "service-oci-v1" + +type ContainerRuntimeTool string + +const ( + ContainerRuntimePodman ContainerRuntimeTool = "podman" + ContainerRuntimeDocker ContainerRuntimeTool = "docker" + ContainerRuntimeNerdctl ContainerRuntimeTool = "nerdctl" +) + +type RuntimePermission string + +const RuntimePermissionForbidden RuntimePermission = "forbidden" + +type UpstreamClientConformance string + +const ( + UpstreamClientConformanceShapeOnly UpstreamClientConformance = "shape-only" + UpstreamClientConformanceRealClient UpstreamClientConformance = "real-client" +) + +type ExecutionSecurityTier string + +const ExecutionSandboxedContainer ExecutionSecurityTier = "sandboxed-container" + +type ProofTier string + +const ProofArtifactHash ProofTier = "artifact-hash" + +type NetworkMode string + +const ( + NetworkModeDirect NetworkMode = "direct" + NetworkModeRelay NetworkMode = "relay" +) + +type PlacementRequirements struct { + ExecutorProvider string `json:"executor_provider,omitempty"` + ExecutionSecurityTier ExecutionSecurityTier `json:"execution_security_tier,omitempty"` + ProofTier ProofTier `json:"proof_tier,omitempty"` +} + +type SessionPolicy struct { + WarmSeconds int `json:"warm_seconds,omitempty"` + MinRenewals int `json:"min_renewals,omitempty"` + MaxBatchRequests int `json:"max_batch_requests,omitempty"` +} + +type ProviderConfig struct { + PluginID string `json:"plugin_id,omitempty"` + ProviderID string `json:"provider_id,omitempty"` + ContractID string `json:"contract_id,omitempty"` + Version string `json:"version,omitempty"` + ConfigRef string `json:"config_ref,omitempty"` +} + +type ProviderContract struct { + ProtocolVersion string `json:"protocol_version"` + ID string `json:"id"` + PluginID string `json:"plugin_id"` + ProviderID string `json:"provider_id"` + ContractID string `json:"contract_id"` + Version string `json:"version"` + DisplayName string `json:"display_name,omitempty"` + ConfigSchemaRef string `json:"config_schema_ref"` + ConfigSchemaDigest string `json:"config_schema_digest"` + OperatingModes []NetworkOperatingMode `json:"operating_modes"` + WorkloadKinds []string `json:"workload_kinds"` + ExecutorProviders []string `json:"executor_providers"` + ExecutionSecurityTiers []ExecutionSecurityTier `json:"execution_security_tiers"` + ProofTiers []ProofTier `json:"proof_tiers"` + NetworkModes []NetworkMode `json:"network_modes"` + RuntimeContract ProviderRuntimeContract `json:"runtime_contract"` +} + +func (c ProviderContract) Validate() error { + var errs []error + for _, field := range []struct { + name string + value string + }{ + {"protocol_version", c.ProtocolVersion}, + {"id", c.ID}, + {"plugin_id", c.PluginID}, + {"provider_id", c.ProviderID}, + {"contract_id", c.ContractID}, + {"version", c.Version}, + {"config_schema_ref", c.ConfigSchemaRef}, + {"config_schema_digest", c.ConfigSchemaDigest}, + } { + if strings.TrimSpace(field.value) == "" { + errs = append(errs, fmt.Errorf("%s is required", field.name)) + } + } + if c.ProtocolVersion != Version { + errs = append(errs, fmt.Errorf("protocol_version must be %q", Version)) + } + if len(c.OperatingModes) == 0 || len(c.WorkloadKinds) == 0 || len(c.ExecutorProviders) == 0 || len(c.ExecutionSecurityTiers) == 0 || len(c.ProofTiers) == 0 || len(c.NetworkModes) == 0 { + errs = append(errs, errors.New("provider contract capability lists are required")) + } + if len(c.RuntimeContract.Profiles) == 0 { + errs = append(errs, errors.New("runtime_contract.profiles is required")) + } + for i, profile := range c.RuntimeContract.Profiles { + if err := profile.Validate(); err != nil { + errs = append(errs, fmt.Errorf("runtime_contract.profiles[%d]: %w", i, err)) + } + } + return errors.Join(errs...) +} + +func (c ProviderContract) SupportsProduct(product NetworkProduct) error { + if c.PluginID != product.ProviderConfig.PluginID || + c.ProviderID != product.ProviderConfig.ProviderID || + c.ContractID != product.ProviderConfig.ContractID { + return errors.New("product provider config does not match contract") + } + if !contains(c.OperatingModes, product.OperatingMode) { + return fmt.Errorf("operating mode %q is unsupported", product.OperatingMode) + } + for _, kind := range product.WorkloadKinds { + if !contains(c.WorkloadKinds, kind) { + return fmt.Errorf("workload kind %q is unsupported", kind) + } + } + for _, mode := range product.NetworkModes { + if !contains(c.NetworkModes, mode) { + return fmt.Errorf("network mode %q is unsupported", mode) + } + } + return nil +} + +type ProviderRuntimeContract struct { + Profiles []ProviderRuntimeProfile `json:"profiles"` +} + +type ProviderRuntimeProfile struct { + ID string `json:"id"` + RuntimeProfile RuntimeProfile `json:"runtime_profile"` + ExecutorProvider string `json:"executor_provider"` + ExecutionSecurityTier ExecutionSecurityTier `json:"execution_security_tier"` + ProofTier ProofTier `json:"proof_tier"` + AllowedRuntimeTools []ContainerRuntimeTool `json:"allowed_runtime_tools,omitempty"` + ImageDigestRequired bool `json:"image_digest_required"` + RootFSDigestRequired bool `json:"rootfs_digest_required"` + AllowedMountRefs []string `json:"allowed_mount_refs,omitempty"` + WritablePaths []string `json:"writable_paths,omitempty"` + WritableRootFS RuntimePermission `json:"writable_rootfs"` + Privileged RuntimePermission `json:"privileged"` + HostNamespaces RuntimePermission `json:"host_namespaces"` + HostSocket RuntimePermission `json:"host_socket"` + SeccompDisable RuntimePermission `json:"seccomp_disable"` + NoNewPrivilegesDisable RuntimePermission `json:"no_new_privileges_disable"` + ConformanceProfiles []string `json:"conformance_profiles,omitempty"` + UpstreamClientConformance UpstreamClientConformance `json:"upstream_client_conformance,omitempty"` + HostWorkspaceSupported bool `json:"host_workspace_supported,omitempty"` +} + +func (p ProviderRuntimeProfile) Validate() error { + var errs []error + if p.ID == "" { + errs = append(errs, errors.New("id is required")) + } + if p.RuntimeProfile != RuntimeProfileServiceOCI { + errs = append(errs, fmt.Errorf("runtime_profile %q is unsupported", p.RuntimeProfile)) + } + if p.ExecutorProvider == "" { + errs = append(errs, errors.New("executor_provider is required")) + } + if p.ExecutionSecurityTier != ExecutionSandboxedContainer { + errs = append(errs, fmt.Errorf("execution_security_tier %q is unsupported", p.ExecutionSecurityTier)) + } + if p.ProofTier != ProofArtifactHash { + errs = append(errs, fmt.Errorf("proof_tier %q is unsupported", p.ProofTier)) + } + if !p.ImageDigestRequired || !p.RootFSDigestRequired { + errs = append(errs, errors.New("image and rootfs digests are required")) + } + for _, permission := range []struct { + name string + value RuntimePermission + }{ + {"writable_rootfs", p.WritableRootFS}, + {"privileged", p.Privileged}, + {"host_namespaces", p.HostNamespaces}, + {"host_socket", p.HostSocket}, + {"seccomp_disable", p.SeccompDisable}, + {"no_new_privileges_disable", p.NoNewPrivilegesDisable}, + } { + if permission.value != RuntimePermissionForbidden { + errs = append(errs, fmt.Errorf("%s must be forbidden", permission.name)) + } + } + return errors.Join(errs...) +} + +func DefaultProviderRuntimeProfile(executorProvider string, tier ExecutionSecurityTier, proof ProofTier) ProviderRuntimeProfile { + return ProviderRuntimeProfile{ + ID: executorProvider + "-" + string(tier) + "-" + string(proof) + "-runtime", + RuntimeProfile: RuntimeProfileServiceOCI, + ExecutorProvider: executorProvider, + ExecutionSecurityTier: tier, + ProofTier: proof, + AllowedRuntimeTools: []ContainerRuntimeTool{ContainerRuntimePodman, ContainerRuntimeDocker, ContainerRuntimeNerdctl}, + ImageDigestRequired: true, + RootFSDigestRequired: true, + AllowedMountRefs: []string{"workspace", "node-data"}, + WritablePaths: []string{"/tmp"}, + WritableRootFS: RuntimePermissionForbidden, + Privileged: RuntimePermissionForbidden, + HostNamespaces: RuntimePermissionForbidden, + HostSocket: RuntimePermissionForbidden, + SeccompDisable: RuntimePermissionForbidden, + NoNewPrivilegesDisable: RuntimePermissionForbidden, + ConformanceProfiles: []string{"service-oci-v1"}, + HostWorkspaceSupported: true, + UpstreamClientConformance: UpstreamClientConformanceShapeOnly, + } +} + +type NetworkProduct struct { + ProtocolVersion string `json:"protocol_version"` + ID string `json:"id"` + DisplayName string `json:"display_name,omitempty"` + Purpose string `json:"purpose,omitempty"` + OperatingMode NetworkOperatingMode `json:"operating_mode"` + OrgID string `json:"org_id"` + PoolID string `json:"pool_id"` + WorkloadKinds []string `json:"workload_kinds"` + SecurityFloor PlacementRequirements `json:"security_floor"` + SessionPolicy SessionPolicy `json:"session_policy,omitzero"` + ProviderConfig ProviderConfig `json:"provider_config,omitzero"` + NetworkModes []NetworkMode `json:"network_modes"` + PlacementConstraints PlacementConstraints `json:"placement_constraints,omitzero"` + RewardPolicy string `json:"reward_policy"` + AbusePolicy string `json:"abuse_policy"` + SettlementAccountID string `json:"settlement_account_id,omitempty"` + SettlementTarget SettlementTarget `json:"settlement_target,omitzero"` + CryptoRewardRouting CryptoRewardRoutingPolicy `json:"crypto_reward_routing,omitzero"` +} + +func (p NetworkProduct) Validate() error { + var errs []error + for _, field := range []struct { + name string + value string + }{ + {"protocol_version", p.ProtocolVersion}, + {"id", p.ID}, + {"org_id", p.OrgID}, + {"pool_id", p.PoolID}, + {"reward_policy", p.RewardPolicy}, + {"abuse_policy", p.AbusePolicy}, + } { + if strings.TrimSpace(field.value) == "" { + errs = append(errs, fmt.Errorf("%s is required", field.name)) + } + } + if p.ProtocolVersion != Version { + errs = append(errs, fmt.Errorf("protocol_version must be %q", Version)) + } + if p.OperatingMode != NetworkModeNodeService { + errs = append(errs, fmt.Errorf("operating_mode %q is unsupported", p.OperatingMode)) + } + if len(p.WorkloadKinds) == 0 || len(p.NetworkModes) == 0 { + errs = append(errs, errors.New("workload_kinds and network_modes are required")) + } + if p.SecurityFloor.ExecutorProvider == "" || p.SecurityFloor.ExecutionSecurityTier == "" || p.SecurityFloor.ProofTier == "" { + errs = append(errs, errors.New("security_floor is required")) + } + if p.ProviderConfig.PluginID == "" || p.ProviderConfig.ProviderID == "" || p.ProviderConfig.ContractID == "" { + errs = append(errs, errors.New("provider_config identity is required")) + } + if p.PlacementConstraints.Chain == "" || p.PlacementConstraints.Role == "" || p.PlacementConstraints.MinDiskBytes <= 0 { + errs = append(errs, errors.New("placement_constraints chain, role, and min_disk_bytes are required")) + } + if p.SettlementTarget.Kind == "" || p.SettlementTarget.Network == "" || p.SettlementTarget.WalletRef == "" { + errs = append(errs, errors.New("settlement_target is required")) + } + return errors.Join(errs...) +} + +type PlacementConstraints struct { + Chain string `json:"chain,omitempty"` + Role string `json:"role,omitempty"` + MinDiskBytes int64 `json:"min_disk_bytes,omitempty"` + MinMemoryBytes int64 `json:"min_memory_bytes,omitempty"` + MinBandwidthMbps int64 `json:"min_bandwidth_mbps,omitempty"` + RequiresIngress bool `json:"requires_ingress,omitempty"` + RequiredCapabilities []string `json:"required_capabilities,omitempty"` + WalletRef string `json:"wallet_ref,omitempty"` + StorageGuidance StorageGuidance `json:"storage_guidance,omitzero"` +} + +type StorageGuidance struct { + Mode string `json:"mode,omitempty"` + MinDiskBytes int64 `json:"min_disk_bytes,omitempty"` + MinDiskDisplay string `json:"min_disk_display,omitempty"` + RecommendedDiskBytes int64 `json:"recommended_disk_bytes,omitempty"` + RecommendedDiskDisplay string `json:"recommended_disk_display,omitempty"` + GrowthMarginBytes int64 `json:"growth_margin_bytes,omitempty"` + GrowthMarginDisplay string `json:"growth_margin_display,omitempty"` + DurableVolumeRequired bool `json:"durable_volume_required,omitempty"` + PreserveOnUpdate bool `json:"preserve_on_update,omitempty"` + PreserveOnUninstall bool `json:"preserve_on_uninstall,omitempty"` + PruningSupported bool `json:"pruning_supported,omitempty"` + SnapshotVerificationRequired bool `json:"snapshot_verification_required,omitempty"` +} + +type SettlementTargetKind string + +const SettlementTargetTreasuryWallet SettlementTargetKind = "treasury_wallet" + +type SettlementTarget struct { + Kind SettlementTargetKind `json:"kind,omitempty"` + AccountID string `json:"account_id,omitempty"` + Network string `json:"network,omitempty"` + WalletRef string `json:"wallet_ref,omitempty"` +} + +type CryptoRewardCustodyMode string + +const CryptoRewardCustodyTreasuryThenDistribute CryptoRewardCustodyMode = "treasury_then_distribute" + +type CryptoRewardDistributionMode string + +const CryptoRewardDistributionContributionShare CryptoRewardDistributionMode = "contribution_share" + +type CryptoRewardParticipantWalletSource string + +const CryptoRewardParticipantAccountWallet CryptoRewardParticipantWalletSource = "account_wallet" + +type CryptoRewardRoutingPolicy struct { + Network string `json:"network,omitempty"` + TreasuryAccountID string `json:"treasury_account_id,omitempty"` + TreasuryWalletRef string `json:"treasury_wallet_ref,omitempty"` + CustodyMode CryptoRewardCustodyMode `json:"custody_mode,omitempty"` + DistributionMode CryptoRewardDistributionMode `json:"distribution_mode,omitempty"` + ParticipantWalletSource CryptoRewardParticipantWalletSource `json:"participant_wallet_source,omitempty"` + ManagementFeeBps int `json:"management_fee_bps,omitempty"` +} + +type ProviderUpstreamClientRequirement struct { + ProtocolVersion string `json:"protocol_version"` + PluginID string `json:"plugin_id"` + ProviderID string `json:"provider_id"` + ContractID string `json:"contract_id"` + Version string `json:"version"` + RuntimeProfileID string `json:"runtime_profile_id"` + ConformanceProfile string `json:"conformance_profile"` + DefaultConformance UpstreamClientConformance `json:"default_conformance"` + RealClientConformance UpstreamClientConformance `json:"real_client_conformance"` + UpstreamClientName string `json:"upstream_client_name"` + VersionProbeCommand []string `json:"version_probe_command,omitempty"` + ImagePolicy ProviderUpstreamImagePolicy `json:"image_policy"` + RequiredEvidence []string `json:"required_evidence,omitempty"` + Notes []string `json:"notes,omitempty"` +} + +func (r ProviderUpstreamClientRequirement) Validate() error { + var errs []error + for _, field := range []struct { + name string + value string + }{ + {"protocol_version", r.ProtocolVersion}, + {"plugin_id", r.PluginID}, + {"provider_id", r.ProviderID}, + {"contract_id", r.ContractID}, + {"version", r.Version}, + {"runtime_profile_id", r.RuntimeProfileID}, + {"conformance_profile", r.ConformanceProfile}, + {"upstream_client_name", r.UpstreamClientName}, + } { + if strings.TrimSpace(field.value) == "" { + errs = append(errs, fmt.Errorf("%s is required", field.name)) + } + } + if r.ProtocolVersion != Version { + errs = append(errs, fmt.Errorf("protocol_version must be %q", Version)) + } + if r.DefaultConformance != UpstreamClientConformanceShapeOnly { + errs = append(errs, errors.New("default_conformance must be shape-only")) + } + if r.RealClientConformance != UpstreamClientConformanceRealClient { + errs = append(errs, errors.New("real_client_conformance must be real-client")) + } + if r.ConformanceProfile != "upstream-client-v1" { + errs = append(errs, errors.New("conformance_profile must be upstream-client-v1")) + } + if err := r.ImagePolicy.Validate(); err != nil { + errs = append(errs, err) + } + return errors.Join(errs...) +} + +type ProviderUpstreamImagePolicy struct { + DigestPinnedImageRequired bool `json:"digest_pinned_image_required"` + OperatorSuppliedImageRequired bool `json:"operator_supplied_image_required,omitempty"` + RecommendedImageRef string `json:"recommended_image_ref,omitempty"` + KnownImageRefs []string `json:"known_image_refs,omitempty"` +} + +func (p ProviderUpstreamImagePolicy) Validate() error { + if !p.DigestPinnedImageRequired { + return errors.New("digest_pinned_image_required must be true") + } + return nil +} + +func CanonicalHash(value any) string { + data, err := json.Marshal(value) + if err != nil { + data = []byte(fmt.Sprintf("%v", value)) + } + sum := sha256.Sum256(data) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +func contains[T comparable](values []T, want T) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} diff --git a/scripts/rename-from-scaffold.sh b/scripts/rename-from-scaffold.sh deleted file mode 100755 index 6fcedbb..0000000 --- a/scripts/rename-from-scaffold.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# Usage: bash scripts/rename-from-scaffold.sh [--mode iac|non-iac] -# -# Renames scaffold-workflow-plugin internals to workflow-plugin-: -# 1. Picks the IaC or non-IaC main.go variant; deletes the other. -# 2. Renames cmd/scaffold-workflow-plugin*/ → cmd/workflow-plugin-/. -# 3. Updates go.mod module path. -# 4. Bulk sed across .go/.yaml/.yml/.md/.json files (find-based; safe with -# paths containing spaces; doesn't rely on bash globstar). -# 5. Resets plugin.json: type "scaffold" → "external"; name → workflow-plugin-. -# 6. Removes the rename script itself + scaffold-rename-test workflow. -# -# Requires: jq. -# -# Tested by .github/workflows/scaffold-rename-test.yml which runs this against -# a tmp copy in both --mode iac and --mode non-iac, then `go build ./...`. -set -euo pipefail - -NEW_NAME="${1:?Usage: rename-from-scaffold.sh [--mode iac|non-iac]}" -MODE="non-iac" -if [[ "${2:-}" == "--mode" ]]; then - MODE="${3:?Mode required}" -fi -case "$MODE" in - iac|non-iac) ;; - *) echo "Mode must be iac or non-iac" >&2; exit 1 ;; -esac - -if ! command -v jq >/dev/null 2>&1; then - echo "error: jq is required" >&2 - exit 1 -fi - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -# 1+2. Pick main.go variant; delete the other; rename to workflow-plugin-. -if [[ "$MODE" == "iac" ]]; then - rm -rf cmd/scaffold-workflow-plugin - mv cmd/scaffold-workflow-plugin-iac "cmd/workflow-plugin-$NEW_NAME" -else - rm -rf cmd/scaffold-workflow-plugin-iac - mv cmd/scaffold-workflow-plugin "cmd/workflow-plugin-$NEW_NAME" -fi - -# 3. go.mod -go mod edit -module "github.com/GoCodeAlone/workflow-plugin-$NEW_NAME" - -# 4. Bulk sed via find (safe for paths with spaces; no globstar dependency). -find . \( -name '*.go' -o -name '*.yaml' -o -name '*.yml' -o -name '*.md' -o -name 'plugin.json' \) \ - -not -path './vendor/*' -not -path './_worktrees/*' -not -path './.git/*' -print0 \ - | while IFS= read -r -d '' f; do - sed -i.bak "s|scaffold-workflow-plugin|workflow-plugin-$NEW_NAME|g" "$f" - rm -f "$f.bak" - done - -# 5. plugin.json: reset type + name (jq-based; idempotent). -tmp="$(mktemp)" -jq --arg name "workflow-plugin-$NEW_NAME" '.type = "external" | .name = $name' plugin.json > "$tmp" -mv "$tmp" plugin.json - -# 6. Remove the rename script itself + scaffold-rename-test workflow. -rm -f scripts/rename-from-scaffold.sh -rm -f .github/workflows/scaffold-rename-test.yml - -echo "Renamed to workflow-plugin-$NEW_NAME ($MODE mode)." -echo "Next steps:" -echo " 1. Review changes: git status / git diff" -echo " 2. Edit plugin.json: replace TEMPLATE.* placeholders with real capabilities" -echo " 3. Commit: git add -A && git commit -m 'feat: initial plugin scaffold'" -echo " 4. Tag: git tag v0.1.0 && git push origin main v0.1.0"