From 64b5fb63dbcf9059eccbd8f96982578d0536ba54 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 6 Jun 2026 20:59:24 -0400 Subject: [PATCH 1/2] fix: stabilize plugin scaffold versions Closes #871 --- cmd/wfctl/plugin.go | 2 +- cmd/wfctl/plugin_init_test.go | 3 + .../plugin/.github/workflows/release.yml.tmpl | 8 +- cmd/wfctl/templates/plugin/main.go.tmpl | 4 +- cmd/wfctl/templates/plugin/plugin.go.tmpl | 5 +- .../.github/workflows/release.yml.tmpl | 8 +- cmd/wfctl/templates/ui-plugin/main.go.tmpl | 4 +- cmd/wfctl/templates/ui-plugin/plugin.go.tmpl | 5 +- plugin/doc.go | 8 ++ plugin/external/sdk/doc.go | 16 +++ plugin/external/sdk/interfaces.go | 47 ++++++-- plugin/sdk/doc.go | 8 ++ plugin/sdk/generator.go | 83 ++++++++++---- plugin/sdk/generator_test.go | 101 +++++++++++++++++- plugin/sdk/manifest.go | 4 +- 15 files changed, 262 insertions(+), 44 deletions(-) create mode 100644 plugin/doc.go create mode 100644 plugin/external/sdk/doc.go create mode 100644 plugin/sdk/doc.go diff --git a/cmd/wfctl/plugin.go b/cmd/wfctl/plugin.go index 8b3d53373..3ea435e20 100644 --- a/cmd/wfctl/plugin.go +++ b/cmd/wfctl/plugin.go @@ -91,7 +91,7 @@ func runPluginInit(args []string) error { } fs := flag.NewFlagSet("plugin init", flag.ExitOnError) author := fs.String("author", "", "Plugin author (required)") - ver := fs.String("version", "0.1.0", "Plugin version") + ver := fs.String("version", "0.0.0", "Deprecated; release versions are injected from Git tags") desc := fs.String("description", "", "Plugin description") license := fs.String("license", "", "Plugin license") output := fs.String("output", "", "Output directory (defaults to plugin name)") diff --git a/cmd/wfctl/plugin_init_test.go b/cmd/wfctl/plugin_init_test.go index 6e8242bfa..539b383eb 100644 --- a/cmd/wfctl/plugin_init_test.go +++ b/cmd/wfctl/plugin_init_test.go @@ -178,6 +178,9 @@ func TestRunPluginInit_PluginJSON(t *testing.T) { if pj["version"] == nil || pj["version"].(string) == "" { t.Error("plugin.json: missing or empty version") } + if pj["version"].(string) != "0.0.0" { + t.Errorf("version: got %q, want stable plugin.json sentinel %q", pj["version"], "0.0.0") + } if pj["author"] == nil || pj["author"].(string) == "" { t.Error("plugin.json: missing or empty author") } diff --git a/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl b/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl index 5864c818e..7d8c3adc0 100644 --- a/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl +++ b/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl @@ -13,14 +13,20 @@ jobs: - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: '1.26.4' + - name: Setup wfctl + uses: GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1 + - name: Validate release contract before packaging + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . - name: Build plugin binaries run: | mkdir -p dist for GOOS in linux darwin; do for GOARCH in amd64 arm64; do - GOOS=$GOOS GOARCH=$GOARCH go build -o dist/{{.Name}}-$GOOS-$GOARCH ./... + GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "-s -w -X main.Version=${GITHUB_REF_NAME}" -o dist/{{.Name}}-$GOOS-$GOARCH ./... done done + - name: Validate packaged release contract + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . - name: Create release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: diff --git a/cmd/wfctl/templates/plugin/main.go.tmpl b/cmd/wfctl/templates/plugin/main.go.tmpl index 05dcf7848..c646e352a 100644 --- a/cmd/wfctl/templates/plugin/main.go.tmpl +++ b/cmd/wfctl/templates/plugin/main.go.tmpl @@ -5,5 +5,7 @@ import ( ) func main() { - sdk.Serve(&{{.NameCamel}}Plugin{}) + sdk.Serve(&{{.NameCamel}}Plugin{}, + sdk.WithBuildVersion(sdk.ResolveBuildVersion(Version)), + ) } diff --git a/cmd/wfctl/templates/plugin/plugin.go.tmpl b/cmd/wfctl/templates/plugin/plugin.go.tmpl index 049ec423d..44b95a92f 100644 --- a/cmd/wfctl/templates/plugin/plugin.go.tmpl +++ b/cmd/wfctl/templates/plugin/plugin.go.tmpl @@ -6,6 +6,9 @@ import ( "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// Version is injected by the release build so runtime manifests report the tag. +var Version = "dev" + // {{.NameCamel}}Plugin implements sdk.PluginProvider, sdk.ModuleProvider, and sdk.StepProvider. type {{.NameCamel}}Plugin struct{} @@ -13,7 +16,7 @@ type {{.NameCamel}}Plugin struct{} func (p *{{.NameCamel}}Plugin) Manifest() sdk.PluginManifest { return sdk.PluginManifest{ Name: "{{.Name}}", - Version: "0.1.0", + Version: sdk.ResolveBuildVersion(Version), Author: "{{.Author}}", Description: "{{.Description}}", } diff --git a/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl b/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl index 331076f03..78861ab93 100644 --- a/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl +++ b/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl @@ -16,17 +16,23 @@ jobs: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' + - name: Setup wfctl + uses: GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1 - name: Build UI run: | cd ui && npm ci && npm run build && cd .. + - name: Validate release contract before packaging + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . - name: Build plugin binaries run: | mkdir -p dist for GOOS in linux darwin; do for GOARCH in amd64 arm64; do - GOOS=$GOOS GOARCH=$GOARCH go build -o dist/{{.Name}}-$GOOS-$GOARCH ./... + GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "-s -w -X main.Version=${GITHUB_REF_NAME}" -o dist/{{.Name}}-$GOOS-$GOARCH ./... done done + - name: Validate packaged release contract + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . - name: Create release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: diff --git a/cmd/wfctl/templates/ui-plugin/main.go.tmpl b/cmd/wfctl/templates/ui-plugin/main.go.tmpl index 05dcf7848..c646e352a 100644 --- a/cmd/wfctl/templates/ui-plugin/main.go.tmpl +++ b/cmd/wfctl/templates/ui-plugin/main.go.tmpl @@ -5,5 +5,7 @@ import ( ) func main() { - sdk.Serve(&{{.NameCamel}}Plugin{}) + sdk.Serve(&{{.NameCamel}}Plugin{}, + sdk.WithBuildVersion(sdk.ResolveBuildVersion(Version)), + ) } diff --git a/cmd/wfctl/templates/ui-plugin/plugin.go.tmpl b/cmd/wfctl/templates/ui-plugin/plugin.go.tmpl index fd616ea25..22513ba65 100644 --- a/cmd/wfctl/templates/ui-plugin/plugin.go.tmpl +++ b/cmd/wfctl/templates/ui-plugin/plugin.go.tmpl @@ -8,6 +8,9 @@ import ( "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// Version is injected by the release build so runtime manifests report the tag. +var Version = "dev" + //go:embed ui/dist var uiAssets embed.FS @@ -18,7 +21,7 @@ type {{.NameCamel}}Plugin struct{} func (p *{{.NameCamel}}Plugin) Manifest() sdk.PluginManifest { return sdk.PluginManifest{ Name: "{{.Name}}", - Version: "0.1.0", + Version: sdk.ResolveBuildVersion(Version), Author: "{{.Author}}", Description: "{{.Description}}", } diff --git a/plugin/doc.go b/plugin/doc.go new file mode 100644 index 000000000..497dbfd29 --- /dev/null +++ b/plugin/doc.go @@ -0,0 +1,8 @@ +// Package plugin contains Workflow's plugin manifest, registry, loading, and +// installation primitives. +// +// Host-side code normally starts with PluginManifest values loaded from +// plugin.json files, then uses Manager, Loader, or Registry implementations to +// resolve plugin metadata and executables. Plugin authors usually consume the +// higher-level packages under plugin/external/sdk and plugin/sdk instead. +package plugin diff --git a/plugin/external/sdk/doc.go b/plugin/external/sdk/doc.go new file mode 100644 index 000000000..052109cec --- /dev/null +++ b/plugin/external/sdk/doc.go @@ -0,0 +1,16 @@ +// Package sdk is the public runtime SDK for out-of-process Workflow plugins. +// +// Plugin binaries implement PluginProvider plus optional provider interfaces +// such as StepProvider, TypedStepProvider, ModuleProvider, ContractProvider, or +// CLIProvider. A typical main function constructs the provider and calls Serve: +// +// func main() { +// sdk.Serve(internal.NewProvider(), +// sdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version)), +// ) +// } +// +// Plugins that also expose CLI commands or build hooks can use ServePluginFull. +// IaC provider plugins should use ServeIaCPlugin so typed IaC gRPC services are +// registered and advertised consistently. +package sdk diff --git a/plugin/external/sdk/interfaces.go b/plugin/external/sdk/interfaces.go index b6588f591..dd585f43b 100644 --- a/plugin/external/sdk/interfaces.go +++ b/plugin/external/sdk/interfaces.go @@ -1,5 +1,3 @@ -// Package sdk provides the public API for building external workflow plugins. -// Plugin authors implement the interfaces defined here and call Serve() to run. package sdk import ( @@ -10,7 +8,7 @@ import ( "google.golang.org/protobuf/types/known/anypb" ) -// PluginProvider is the main interface plugin authors implement. +// PluginProvider is the minimum interface every external plugin implements. type PluginProvider interface { // Manifest returns the plugin's metadata. Manifest() PluginManifest @@ -21,14 +19,24 @@ type ContractProvider interface { ContractRegistry() *pb.ContractRegistry } -// PluginManifest describes the plugin. +// PluginManifest is the runtime metadata a plugin returns to the host. +// +// For release-built plugins, prefer sdk.WithBuildVersion with +// ResolveBuildVersion so Version reflects the Git tag injected by the build +// instead of the committed plugin.json sentinel. type PluginManifest struct { - Name string - Version string - Author string - Description string - ConfigMutable bool // whether tenants can override the config fragment - SampleCategory string // non-empty means this is a sample/app plugin + // Name is the canonical plugin name, usually workflow-plugin-. + Name string + // Version is the operator-visible runtime version. + Version string + // Author identifies the organization or person that maintains the plugin. + Author string + // Description is shown in registry and documentation output. + Description string + // ConfigMutable reports whether tenants can override the config fragment. + ConfigMutable bool + // SampleCategory marks sample/app plugins for grouped presentation. + SampleCategory string } // AssetProvider allows plugins to serve embedded static assets (e.g., UI files). @@ -163,13 +171,32 @@ type TypedServiceInvoker interface { InvokeTypedMethod(method string, input *anypb.Any) (*anypb.Any, error) } +// TelemetryAttrs aliases the host telemetry attribute map type for plugin APIs. type TelemetryAttrs = telemetry.Attrs + +// TelemetryMetricKind aliases the host metric kind enum for plugin APIs. type TelemetryMetricKind = telemetry.MetricKind + +// TelemetryMetricRecord aliases the host metric record type for plugin APIs. type TelemetryMetricRecord = telemetry.MetricRecord + +// TelemetryMetricRecorder aliases the host metric recorder interface for plugin APIs. type TelemetryMetricRecorder = telemetry.MetricRecorder + +// TelemetryMetricEmitter aliases the host metric emitter interface for plugin APIs. type TelemetryMetricEmitter = telemetry.MetricEmitter + +// TelemetryLogRecord aliases the host log record type for plugin APIs. type TelemetryLogRecord = telemetry.LogRecord + +// TelemetryLogEmitter aliases the host log emitter interface for plugin APIs. type TelemetryLogEmitter = telemetry.LogEmitter + +// TelemetrySpanEvent aliases the host span event type for plugin APIs. type TelemetrySpanEvent = telemetry.SpanEvent + +// TelemetrySpanRecorder aliases the host span recorder interface for plugin APIs. type TelemetrySpanRecorder = telemetry.SpanRecorder + +// TelemetryTraceAnnotator aliases the host trace annotator interface for plugin APIs. type TelemetryTraceAnnotator = telemetry.TraceAnnotator diff --git a/plugin/sdk/doc.go b/plugin/sdk/doc.go new file mode 100644 index 000000000..ed6a3833d --- /dev/null +++ b/plugin/sdk/doc.go @@ -0,0 +1,8 @@ +// Package sdk contains authoring tools for Workflow plugins. +// +// The package is used by wfctl plugin init and by tests that need to generate +// realistic plugin repositories. TemplateGenerator creates a buildable plugin +// project with plugin.json, Go code, release workflows, GoReleaser metadata, +// and contract descriptor files. Runtime plugin binaries should use +// github.com/GoCodeAlone/workflow/plugin/external/sdk. +package sdk diff --git a/plugin/sdk/generator.go b/plugin/sdk/generator.go index 50fdeed88..14b816309 100644 --- a/plugin/sdk/generator.go +++ b/plugin/sdk/generator.go @@ -16,8 +16,9 @@ type TemplateGenerator struct{} const ( workflowReleasedVersion = "v0.18.15" workflowStrictContractsVersion = "v0.19.0-alpha.5" - workflowMinimumGoVersion = "1.26.0" - defaultPluginGoVersion = "1.22" + workflowMinimumGoVersion = "1.26.4" + defaultPluginGoVersion = "1.26.4" + pluginManifestVersionSentinel = "0.0.0" ) // NewTemplateGenerator creates a new TemplateGenerator. @@ -25,18 +26,42 @@ func NewTemplateGenerator() *TemplateGenerator { return &TemplateGenerator{} } -// GenerateOptions configures what gets generated. +// GenerateOptions configures a generated plugin project. +// +// The committed plugin.json version is always the stable "0.0.0" sentinel. +// Release versions are injected into the generated binary from Git tags via +// GoReleaser ldflags and surfaced at runtime through sdk.ResolveBuildVersion. type GenerateOptions struct { - Name string - Version string - Author string - Description string - License string - OutputDir string - WithContract bool - LegacyContracts bool - GoModule string // e.g. "github.com/MyOrg/workflow-plugin-foo" - WorkflowReplace string // optional local replace path for github.com/GoCodeAlone/workflow + // Name is the plugin manifest name. It may include the workflow-plugin- + // prefix; generated type names and step names use the normalized suffix. + Name string + // Version is deprecated. Generated plugin.json files use the "0.0.0" + // sentinel and release builds inject the real tag into the binary. + Version string + // Author is required and is also used to build the default Go module path. + Author string + // Description is written to plugin.json and README.md. When empty, a + // generic description is used. + Description string + // License is copied into plugin.json when supplied. + License string + // OutputDir is the directory to create. It defaults to Name. + OutputDir string + // WithContract adds the legacy dynamic field contract to plugin.json. + WithContract bool + // LegacyContracts forces the older map-based step scaffold. By default the + // generator emits strict typed contracts when it can resolve a local + // workflow source checkout. + LegacyContracts bool + // GoModule overrides the default github.com//workflow-plugin- + // module path. + GoModule string + // WorkflowReplace is an optional local replace path for + // github.com/GoCodeAlone/workflow. + WorkflowReplace string + // MessageContracts are descriptor-only protobuf contracts to include in + // plugin.contracts.json for plugins that publish messages without serving a + // step for them. MessageContracts []MessageContract } @@ -47,7 +72,7 @@ func (g *TemplateGenerator) Generate(opts GenerateOptions) error { return fmt.Errorf("plugin name is required") } if opts.Version == "" { - opts.Version = "0.1.0" + opts.Version = pluginManifestVersionSentinel } if opts.Author == "" { return fmt.Errorf("author is required") @@ -75,7 +100,7 @@ func (g *TemplateGenerator) Generate(opts GenerateOptions) error { // Validate the name manifest := &plugin.PluginManifest{ Name: opts.Name, - Version: opts.Version, + Version: pluginManifestVersionSentinel, Author: opts.Author, Description: opts.Description, License: opts.License, @@ -189,7 +214,7 @@ func generateProjectStructure(opts GenerateOptions) error { } // .goreleaser.yml - if err := writeFile(filepath.Join(opts.OutputDir, ".goreleaser.yml"), generateGoReleaserYML(binaryName), 0600); err != nil { + if err := writeFile(filepath.Join(opts.OutputDir, ".goreleaser.yml"), generateGoReleaserYML(binaryName, goModule), 0600); err != nil { return err } @@ -239,7 +264,9 @@ func generateMainGo(goModule, shortName string) string { b.WriteString("\t\"github.com/GoCodeAlone/workflow/plugin/external/sdk\"\n") b.WriteString(")\n\n") b.WriteString("func main() {\n") - fmt.Fprintf(&b, "\tsdk.Serve(internal.New%sProvider())\n", toCamelCase(shortName)) + fmt.Fprintf(&b, "\tsdk.Serve(internal.New%sProvider(),\n", toCamelCase(shortName)) + b.WriteString("\t\tsdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version)),\n") + b.WriteString("\t)\n") b.WriteString("}\n") return b.String() } @@ -273,6 +300,8 @@ func generateProviderGo(opts GenerateOptions, shortName string) string { b.WriteString("\t\"github.com/GoCodeAlone/workflow/plugin/external/sdk\"\n") b.WriteString("\t\"google.golang.org/protobuf/types/known/anypb\"\n") b.WriteString(")\n\n") + b.WriteString("// Version is injected by the release build so runtime manifests report the tag.\n") + b.WriteString("var Version = \"dev\"\n\n") fmt.Fprintf(&b, "// %s implements sdk.PluginProvider, sdk.TypedStepProvider, and sdk.ContractProvider.\n", typeName) fmt.Fprintf(&b, "type %s struct{}\n\n", typeName) fmt.Fprintf(&b, "// New%s creates a new %s.\n", typeName, typeName) @@ -283,7 +312,7 @@ func generateProviderGo(opts GenerateOptions, shortName string) string { fmt.Fprintf(&b, "func (p *%s) Manifest() sdk.PluginManifest {\n", typeName) b.WriteString("\treturn sdk.PluginManifest{\n") fmt.Fprintf(&b, "\t\tName: %q,\n", "workflow-plugin-"+shortName) - fmt.Fprintf(&b, "\t\tVersion: %q,\n", opts.Version) + b.WriteString("\t\tVersion: sdk.ResolveBuildVersion(Version),\n") fmt.Fprintf(&b, "\t\tAuthor: %q,\n", opts.Author) fmt.Fprintf(&b, "\t\tDescription: %q,\n", opts.Description) b.WriteString("\t}\n") @@ -389,6 +418,8 @@ func generateLegacyProviderGo(opts GenerateOptions, shortName string) string { b.WriteString("\t\"fmt\"\n\n") b.WriteString("\t\"github.com/GoCodeAlone/workflow/plugin/external/sdk\"\n") b.WriteString(")\n\n") + b.WriteString("// Version is injected by the release build so runtime manifests report the tag.\n") + b.WriteString("var Version = \"dev\"\n\n") fmt.Fprintf(&b, "// %s implements sdk.PluginProvider and sdk.StepProvider.\n", typeName) fmt.Fprintf(&b, "type %s struct{}\n\n", typeName) fmt.Fprintf(&b, "// New%s creates a new %s.\n", typeName, typeName) @@ -399,7 +430,7 @@ func generateLegacyProviderGo(opts GenerateOptions, shortName string) string { fmt.Fprintf(&b, "func (p *%s) Manifest() sdk.PluginManifest {\n", typeName) b.WriteString("\treturn sdk.PluginManifest{\n") fmt.Fprintf(&b, "\t\tName: %q,\n", "workflow-plugin-"+shortName) - fmt.Fprintf(&b, "\t\tVersion: %q,\n", opts.Version) + b.WriteString("\t\tVersion: sdk.ResolveBuildVersion(Version),\n") fmt.Fprintf(&b, "\t\tAuthor: %q,\n", opts.Author) fmt.Fprintf(&b, "\t\tDescription: %q,\n", opts.Description) b.WriteString("\t}\n") @@ -549,7 +580,7 @@ func workflowModuleDeclared(dir string) bool { return false } -func generateGoReleaserYML(binaryName string) string { +func generateGoReleaserYML(binaryName, goModule string) string { var b strings.Builder b.WriteString("version: 2\n\n") b.WriteString("builds:\n") @@ -558,6 +589,8 @@ func generateGoReleaserYML(binaryName string) string { fmt.Fprintf(&b, " main: ./cmd/%s\n", binaryName) b.WriteString(" env:\n") b.WriteString(" - CGO_ENABLED=0\n") + b.WriteString(" ldflags:\n") + fmt.Fprintf(&b, " - -s -w -X %s/internal.Version={{.Version}}\n", goModule) b.WriteString(" goos:\n") b.WriteString(" - linux\n") b.WriteString(" - darwin\n") @@ -611,6 +644,8 @@ on: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -618,13 +653,19 @@ jobs: - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: '%s' + - name: Setup wfctl + uses: GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1 + - name: Validate release contract before packaging + run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" --plugin . - name: Run GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Validate packaged release contract + run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" --plugin . notify-registry: if: startsWith(github.ref, 'refs/tags/v') diff --git a/plugin/sdk/generator_test.go b/plugin/sdk/generator_test.go index a777fba9c..f9ba68928 100644 --- a/plugin/sdk/generator_test.go +++ b/plugin/sdk/generator_test.go @@ -35,8 +35,8 @@ func TestTemplateGeneratorGenerate(t *testing.T) { if manifest.Name != "my-plugin" { t.Errorf("Name = %q, want %q", manifest.Name, "my-plugin") } - if manifest.Version != "1.0.0" { - t.Errorf("Version = %q, want %q", manifest.Version, "1.0.0") + if manifest.Version != "0.0.0" { + t.Errorf("Version = %q, want %q", manifest.Version, "0.0.0") } // Check component was created @@ -390,8 +390,101 @@ func TestTemplateGeneratorDefaults(t *testing.T) { } manifest, _ := plugin.LoadManifest(filepath.Join(outputDir, "plugin.json")) - if manifest.Version != "0.1.0" { - t.Errorf("default version = %q, want %q", manifest.Version, "0.1.0") + if manifest.Version != "0.0.0" { + t.Errorf("default version = %q, want %q", manifest.Version, "0.0.0") + } +} + +func TestTemplateGeneratorReleaseVersionComesFromBuildMetadata(t *testing.T) { + dir := t.TempDir() + outputDir := filepath.Join(dir, "versioned-plugin") + + gen := NewTemplateGenerator() + err := gen.Generate(GenerateOptions{ + Name: "versioned-plugin", + Version: "1.2.3", + Author: "TestOrg", + Description: "Version metadata test", + OutputDir: outputDir, + GoModule: "github.com/TestOrg/workflow-plugin-versioned-plugin", + }) + if err != nil { + t.Fatalf("Generate error: %v", err) + } + + manifest, err := plugin.LoadManifest(filepath.Join(outputDir, "plugin.json")) + if err != nil { + t.Fatalf("LoadManifest error: %v", err) + } + if manifest.Version != "0.0.0" { + t.Fatalf("plugin.json version = %q, want stable sentinel %q", manifest.Version, "0.0.0") + } + + providerData, err := os.ReadFile(filepath.Join(outputDir, "internal", "provider.go")) + if err != nil { + t.Fatalf("read provider.go: %v", err) + } + providerSrc := string(providerData) + for _, want := range []string{ + `var Version = "dev"`, + `Version: sdk.ResolveBuildVersion(Version)`, + } { + if !strings.Contains(providerSrc, want) { + t.Errorf("provider.go missing %q:\n%s", want, providerSrc) + } + } + + mainData, err := os.ReadFile(filepath.Join(outputDir, "cmd", "workflow-plugin-versioned-plugin", "main.go")) + if err != nil { + t.Fatalf("read main.go: %v", err) + } + mainSrc := string(mainData) + if !strings.Contains(mainSrc, "sdk.WithBuildVersion(sdk.ResolveBuildVersion(internal.Version))") { + t.Errorf("main.go should pass the ldflag-injected build version to the SDK:\n%s", mainSrc) + } + + goreleaserData, err := os.ReadFile(filepath.Join(outputDir, ".goreleaser.yml")) + if err != nil { + t.Fatalf("read .goreleaser.yml: %v", err) + } + if !strings.Contains(string(goreleaserData), "-X github.com/TestOrg/workflow-plugin-versioned-plugin/internal.Version={{.Version}}") { + t.Errorf(".goreleaser.yml should inject internal.Version, got:\n%s", goreleaserData) + } +} + +func TestTemplateGeneratorReleaseWorkflowUsesContractGateAndCurrentActions(t *testing.T) { + dir := t.TempDir() + outputDir := filepath.Join(dir, "release-plugin") + + gen := NewTemplateGenerator() + err := gen.Generate(GenerateOptions{ + Name: "release-plugin", + Author: "TestOrg", + Description: "Release workflow test", + OutputDir: outputDir, + }) + if err != nil { + t.Fatalf("Generate error: %v", err) + } + + if _, err := os.Stat(filepath.Join(outputDir, ".github", "workflows", "sync-plugin-version.yml")); !os.IsNotExist(err) { + t.Fatalf("sync-plugin-version.yml should not be generated, stat err: %v", err) + } + + data, err := os.ReadFile(filepath.Join(outputDir, ".github", "workflows", "release.yml")) + if err != nil { + t.Fatalf("read release.yml: %v", err) + } + content := string(data) + for _, want := range []string{ + "GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1", + "wfctl plugin validate-contract --for-publish --tag \"${{ github.ref_name }}\" --plugin .", + "goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2", + "version: '~> v2'", + } { + if !strings.Contains(content, want) { + t.Errorf("release.yml missing %q:\n%s", want, content) + } } } diff --git a/plugin/sdk/manifest.go b/plugin/sdk/manifest.go index a6ec14da0..f0ee4b5c1 100644 --- a/plugin/sdk/manifest.go +++ b/plugin/sdk/manifest.go @@ -1,5 +1,5 @@ -// Package sdk hosts the plugin SDK manifest schema and helpers used by -// wfctl to discover plugin capabilities. +// This file hosts the plugin SDK manifest schema and helpers used by wfctl to +// discover plugin capabilities. // // The SDK manifest is intentionally additive over [plugin.PluginManifest]; // it captures only the fields that wfctl validates before typed runtime From 8379e434f733745e74cb19aa2cbe53194ef0f0a4 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 6 Jun 2026 21:15:14 -0400 Subject: [PATCH 2/2] fix: use positional plugin contract validation --- cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl | 4 ++-- .../templates/ui-plugin/.github/workflows/release.yml.tmpl | 4 ++-- plugin/sdk/generator.go | 4 ++-- plugin/sdk/generator_test.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl b/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl index 7d8c3adc0..461cbd75f 100644 --- a/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl +++ b/cmd/wfctl/templates/plugin/.github/workflows/release.yml.tmpl @@ -16,7 +16,7 @@ jobs: - name: Setup wfctl uses: GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1 - name: Validate release contract before packaging - run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" . - name: Build plugin binaries run: | mkdir -p dist @@ -26,7 +26,7 @@ jobs: done done - name: Validate packaged release contract - run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" . - name: Create release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: diff --git a/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl b/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl index 78861ab93..a8a457fe7 100644 --- a/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl +++ b/cmd/wfctl/templates/ui-plugin/.github/workflows/release.yml.tmpl @@ -22,7 +22,7 @@ jobs: run: | cd ui && npm ci && npm run build && cd .. - name: Validate release contract before packaging - run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" . - name: Build plugin binaries run: | mkdir -p dist @@ -32,7 +32,7 @@ jobs: done done - name: Validate packaged release contract - run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ "{{" }} github.ref_name {{ "}}" }}" . - name: Create release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: diff --git a/plugin/sdk/generator.go b/plugin/sdk/generator.go index 14b816309..d70132665 100644 --- a/plugin/sdk/generator.go +++ b/plugin/sdk/generator.go @@ -656,7 +656,7 @@ jobs: - name: Setup wfctl uses: GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1 - name: Validate release contract before packaging - run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" . - name: Run GoReleaser uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: @@ -665,7 +665,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Validate packaged release contract - run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" --plugin . + run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" . notify-registry: if: startsWith(github.ref, 'refs/tags/v') diff --git a/plugin/sdk/generator_test.go b/plugin/sdk/generator_test.go index f9ba68928..a9895f525 100644 --- a/plugin/sdk/generator_test.go +++ b/plugin/sdk/generator_test.go @@ -478,7 +478,7 @@ func TestTemplateGeneratorReleaseWorkflowUsesContractGateAndCurrentActions(t *te content := string(data) for _, want := range []string{ "GoCodeAlone/setup-wfctl@bcd880980f5bbe8d192d0c20ff6279d25331f956 # v1", - "wfctl plugin validate-contract --for-publish --tag \"${{ github.ref_name }}\" --plugin .", + "wfctl plugin validate-contract --for-publish --tag \"${{ github.ref_name }}\" .", "goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2", "version: '~> v2'", } {