Skip to content

feat(wfctl): plugin install --url/--local, static registry, enhanced scaffold#329

Merged
intel352 merged 14 commits intomainfrom
feat/plugin-ecosystem-wfctl
Mar 15, 2026
Merged

feat(wfctl): plugin install --url/--local, static registry, enhanced scaffold#329
intel352 merged 14 commits intomainfrom
feat/plugin-ecosystem-wfctl

Conversation

@intel352
Copy link
Contributor

Summary

  • Add --url flag to wfctl plugin install for direct URL installs (no registry needed)
  • Add --local flag to wfctl plugin install for local directory installs
  • Add SHA-256 checksum verification from lockfile on install
  • Add Registry field to lockfile entries
  • Add StaticRegistrySource type for GitHub Pages-based registry (faster, no API rate limits)
  • Default registry config now uses static GitHub Pages with GitHub API as fallback
  • Enhanced wfctl plugin init scaffold — generates full project structure with cmd/, internal/, go.mod, .goreleaser.yml, CI workflows, Makefile, README

Test plan

  • go build ./cmd/wfctl/... compiles
  • go test ./cmd/wfctl/... -count=1 passes
  • wfctl plugin install --url <tarball> downloads and installs
  • wfctl plugin install --local ./path copies plugin from local dir
  • wfctl plugin init test-plugin -author Test generates full project structure
  • Lockfile SHA-256 verification works on wfctl plugin install (no args)

🤖 Generated with Claude Code

intel352 and others added 5 commits March 14, 2026 15:19
Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Registry field to PluginLockEntry, adds updateLockfileWithChecksum
to store SHA-256 alongside version/repository, and verifies installed
binary checksums after lockfile-based installs to detect tampering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds StaticRegistrySource that fetches plugin manifests from
{baseURL}/plugins/{name}/manifest.json and lists/searches plugins via
{baseURL}/index.json. Updates DefaultRegistryConfig to use the GitHub Pages
static registry as primary with the GitHub API as a fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --local flag to wfctl plugin install that reads plugin.json from a
local directory, copies the plugin binary and manifest to the plugin
directory, and prints the install path. Also updates TestDefaultRegistryConfig
to match the new two-registry default config (static primary + github fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 19:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new installation paths and registry capabilities to wfctl plugin tooling, plus expands the plugin scaffold generator to create a full external-plugin project layout.

Changes:

  • Added wfctl plugin install --url and --local, and introduced lockfile SHA-256 verification hooks.
  • Added a static (GitHub Pages) registry source and updated default registry config to prefer it with a GitHub API fallback.
  • Enhanced wfctl plugin init scaffold to generate a full Go project structure (cmd/, internal/, CI, GoReleaser, Makefile, README).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Expands plugin scaffolding to generate full project layout and new helper generators.
cmd/wfctl/registry_source.go Adds StaticRegistrySource for static HTTP registry (index + manifests).
cmd/wfctl/registry_config.go Extends registry config schema (static URL/token) and updates defaults to static + fallback.
cmd/wfctl/plugin_lockfile.go Adds lockfile fields and checksum verification during lockfile-based installs.
cmd/wfctl/plugin_install.go Adds --url/--local install modes; records checksums; adds checksum verification helper.
cmd/wfctl/plugin.go Adds --module flag to plugin init and passes it through to generator options.
cmd/wfctl/multi_registry*.go Wires new registry type into multi-registry and updates default-config test expectations.

Comment on lines 71 to +90
@@ -79,6 +81,14 @@ func runPluginInstall(args []string) error {
return err
}

if *directURL != "" {
return installFromURL(*directURL, pluginDirVal)
}

if *localPath != "" {
return installFromLocal(*localPath, pluginDirVal)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: plugin_install_new_test.go now covers URL install, local install, and lockfile-based install paths.

Version string `yaml:"version"`
Repository string `yaml:"repository,omitempty"`
SHA256 string `yaml:"sha256,omitempty"`
Registry string `yaml:"registry,omitempty"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: Registry field is written in updateLockfileWithChecksum and consumed in installFromLockfile.

Comment on lines +512 to +523
// verifyInstalledChecksum reads the plugin binary and verifies its SHA-256 checksum.
func verifyInstalledChecksum(pluginDir, pluginName, expectedSHA256 string) error {
binaryPath := filepath.Join(pluginDir, pluginName)
data, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read binary %s: %w", binaryPath, err)
}
h := sha256.Sum256(data)
got := hex.EncodeToString(h[:])
if !strings.EqualFold(got, expectedSHA256) {
return fmt.Errorf("binary checksum mismatch: got %s, want %s", got, expectedSHA256)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: Both URL and lockfile paths now hash the installed binary consistently with verifyInstalledChecksum.

@github-actions
Copy link

github-actions bot commented Mar 14, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:244: parsing iteration count: invalid syntax
baseline-bench.txt:323432: parsing iteration count: invalid syntax
baseline-bench.txt:627504: parsing iteration count: invalid syntax
baseline-bench.txt:891052: parsing iteration count: invalid syntax
baseline-bench.txt:1189586: parsing iteration count: invalid syntax
baseline-bench.txt:1486716: parsing iteration count: invalid syntax
benchmark-results.txt:244: parsing iteration count: invalid syntax
benchmark-results.txt:332786: parsing iteration count: invalid syntax
benchmark-results.txt:653892: parsing iteration count: invalid syntax
benchmark-results.txt:939229: parsing iteration count: invalid syntax
benchmark-results.txt:1234280: parsing iteration count: invalid syntax
benchmark-results.txt:1541787: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │       benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base              │
InterpreterCreation-4               6.852m ± 56%   8.820m ± 65%       ~ (p=0.937 n=6)
ComponentLoad-4                     3.421m ±  1%   3.481m ±  7%  +1.76% (p=0.002 n=6)
ComponentExecute-4                  1.983µ ±  0%   2.016µ ±  1%  +1.66% (p=0.002 n=6)
PoolContention/workers-1-4          1.086µ ±  1%   1.093µ ±  1%       ~ (p=0.229 n=6)
PoolContention/workers-2-4          1.083µ ±  4%   1.099µ ±  1%       ~ (p=0.054 n=6)
PoolContention/workers-4-4          1.087µ ±  1%   1.103µ ±  0%  +1.52% (p=0.002 n=6)
PoolContention/workers-8-4          1.095µ ±  1%   1.107µ ±  3%  +1.14% (p=0.004 n=6)
PoolContention/workers-16-4         1.096µ ±  3%   1.105µ ±  2%       ~ (p=0.069 n=6)
ComponentLifecycle-4                3.453m ±  0%   3.517m ±  1%  +1.85% (p=0.002 n=6)
SourceValidation-4                  2.271µ ±  1%   2.310µ ±  1%  +1.72% (p=0.002 n=6)
RegistryConcurrent-4                767.0n ±  3%   812.6n ±  4%  +5.95% (p=0.026 n=6)
LoaderLoadFromString-4              3.500m ±  1%   3.612m ±  2%  +3.21% (p=0.002 n=6)
geomean                             18.39µ         19.12µ        +3.96%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               1.944Mi ± 0%   1.944Mi ± 0%       ~ (p=0.842 n=6)
ComponentLoad-4                     2.097Mi ± 0%   2.097Mi ± 0%       ~ (p=0.844 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.942 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.859 n=6)
geomean                             15.05Ki        15.05Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.09k ± 0%   15.09k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      17.43k ± 0%   17.43k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 17.48k ± 0%   17.48k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               17.47k ± 0%   17.47k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               181.2         181.2       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  286.1n ± 4%   288.5n ± 4%       ~ (p=0.065 n=6)
CircuitBreakerExecution_Success-4          22.48n ± 0%   22.49n ± 1%       ~ (p=0.364 n=6)
CircuitBreakerExecution_Failure-4          64.34n ± 0%   64.30n ± 1%       ~ (p=0.853 n=6)
geomean                                    74.52n        74.72n       +0.27%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     863.9n ± 32%   875.4n ± 34%       ~ (p=0.699 n=6)
JQTransform_ObjectConstruction-4         1.452µ ±  1%   1.441µ ± 28%       ~ (p=0.225 n=6)
JQTransform_ArraySelect-4                3.325µ ±  1%   3.356µ ±  2%       ~ (p=0.310 n=6)
JQTransform_Complex-4                    38.62µ ±  1%   38.63µ ±  2%       ~ (p=1.000 n=6)
JQTransform_Throughput-4                 1.800µ ±  2%   1.768µ ±  1%  -1.81% (p=0.011 n=6)
SSEPublishDelivery-4                     65.85n ±  1%   65.92n ±  1%       ~ (p=0.483 n=6)
geomean                                  1.635µ         1.634µ        -0.02%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                    1.106µ ± 9%   1.111µ ± 3%       ~ (p=0.420 n=6)
SchemaValidation_AllFields-4                 1.681µ ± 8%   1.670µ ± 7%       ~ (p=0.071 n=6)
SchemaValidation_FormatValidation-4          1.605µ ± 1%   1.603µ ± 3%       ~ (p=0.853 n=6)
SchemaValidation_ManySchemas-4               1.828µ ± 2%   1.854µ ± 2%       ~ (p=0.240 n=6)
geomean                                      1.528µ        1.532µ       +0.28%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base               │
EventStoreAppend_InMemory-4                1.275µ ± 15%   1.241µ ± 17%        ~ (p=0.818 n=6)
EventStoreAppend_SQLite-4                  2.061m ±  7%   1.253m ±  4%  -39.21% (p=0.002 n=6)
GetTimeline_InMemory/events-10-4           14.18µ ±  5%   14.38µ ±  3%        ~ (p=0.310 n=6)
GetTimeline_InMemory/events-50-4           63.79µ ± 22%   78.19µ ± 21%        ~ (p=0.485 n=6)
GetTimeline_InMemory/events-100-4          127.9µ ±  1%   124.4µ ±  1%   -2.77% (p=0.002 n=6)
GetTimeline_InMemory/events-500-4          660.6µ ±  1%   642.4µ ±  1%   -2.75% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.360m ±  1%   1.323m ±  1%   -2.72% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             109.9µ ±  1%   110.7µ ±  1%        ~ (p=0.180 n=6)
GetTimeline_SQLite/events-50-4             254.3µ ±  1%   253.5µ ±  1%        ~ (p=0.310 n=6)
GetTimeline_SQLite/events-100-4            439.1µ ±  0%   428.5µ ±  1%   -2.40% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.836m ±  0%   1.807m ±  0%   -1.62% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.602m ±  1%   3.535m ±  2%   -1.86% (p=0.026 n=6)
geomean                                    230.2µ         221.8µ         -3.65%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                  809.5 ± 8%     812.0 ± 8%       ~ (p=0.937 n=6)
EventStoreAppend_SQLite-4                  1.989Ki ± 2%   1.985Ki ± 3%       ~ (p=1.000 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ± 0%  +0.00% (p=0.015 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ± 0%  +0.00% (p=0.022 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ± 0%       ~ (p=0.152 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ± 0%       ~ (p=0.567 n=6)
geomean                                    67.49Ki        67.50Ki       +0.01%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

intel352 and others added 2 commits March 14, 2026 16:00
- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile function (replaced by updateLockfileWithChecksum)
- Remove unused writePluginJSON function
- Fix TestLoadRegistryConfigDefault to test DefaultRegistryConfig() directly,
  avoiding interference from user config files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix checksum mismatch: lockfile now stores SHA-256 of installed binary
  (not the download archive) so verifyInstalledChecksum passes correctly
- Fix generator SDK import: generateMainGo, generateProviderGo, and
  generateStepsGo now import plugin/external/sdk and use correct external
  interfaces (PluginProvider.Manifest, StepProvider, StepInstance, StepResult)
- Fix docs: PLUGIN_AUTHORING.md now shows correct external SDK types and
  corrects "cd workflow-plugin-my-plugin" to "cd my-plugin"
- Add mutual exclusivity validation in runPluginInstall for --url, --local,
  and positional args
- Add registry parameter to updateLockfileWithChecksum and write it to lockfile
- Add URL validation in NewStaticRegistrySource (returns error if empty)
- Replace PluginSummary(e) struct conversion with explicit field assignment
- Update all NewStaticRegistrySource callers to handle error return
- Update TestInstallFromURL to verify binary hash (not archive hash)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 21:39
@intel352
Copy link
Contributor Author

All Copilot review comments addressed in commit 4b63275:

  • Checksum mismatch: Now hashes the installed binary (not the archive) for lockfile SHA-256
  • Generator SDK imports: Updated to use plugin/external/sdk with correct interfaces (PluginProvider, StepProvider, StepInstance)
  • Docs: Updated PLUGIN_AUTHORING.md to match actual external SDK API and correct output directory naming
  • Mutual exclusivity: Added validation that --url, --local, and positional args are mutually exclusive
  • Registry field: updateLockfileWithChecksum now accepts and persists the registry parameter
  • URL validation: NewStaticRegistrySource returns error on empty URL
  • Struct conversion: Reverted to explicit field assignment (JSON tags differ)

intel352 and others added 2 commits March 14, 2026 17:43
…ticcheck S1016)

Go struct conversion is valid even when tags differ — the fields have
identical names and types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends wfctl’s plugin workflow by adding alternative install sources (direct URL + local dir), introducing a static (GitHub Pages) registry source with GitHub API fallback, adding checksum recording/verification in the lockfile, and significantly expanding the wfctl plugin init scaffold to generate a full external-plugin project layout.

Changes:

  • Add wfctl plugin install --url and --local paths, and record/verify SHA-256 checksums via .wfctl.yaml.
  • Add StaticRegistrySource and update default registry config to prefer GitHub Pages with GitHub API fallback.
  • Enhance wfctl plugin init scaffold to generate cmd/, internal/, CI workflows, GoReleaser config, Makefile, and README, with optional --module.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Expands plugin scaffold generation to include full project structure and templates.
docs/PLUGIN_AUTHORING.md Adds plugin authoring documentation covering scaffold, install, publish, registry.
cmd/wfctl/registry_source_test.go Adds tests for the new static registry source behavior.
cmd/wfctl/registry_source.go Implements StaticRegistrySource backed by index.json + per-plugin manifests.
cmd/wfctl/registry_config.go Extends registry config schema; updates defaults to static primary + GitHub fallback.
cmd/wfctl/plugin_lockfile.go Adds Registry field to lock entries; adds checksum verification after install.
cmd/wfctl/plugin_install_new_test.go Adds unit tests for --url, --local, checksum verification, and copyFile.
cmd/wfctl/plugin_install.go Adds --url/--local, lockfile checksum writes, and hashing helper.
cmd/wfctl/plugin.go Adds wfctl plugin init --module wiring into scaffold generator options.
cmd/wfctl/multi_registry_test.go Updates tests for new default registry configuration (static + fallback).
cmd/wfctl/multi_registry.go Adds support for static registry type in multi-registry assembly.

Comment on lines +200 to +201
# The lockfile (.wfctl.yaml) is updated automatically
```
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: installFromLocal now writes lockfile via updateLockfileWithChecksum; PLUGIN_AUTHORING.md docs are accurate.

intel352 and others added 2 commits March 14, 2026 17:45
- CreateStep returns error for unknown step types instead of (nil, nil)
- hashFileSHA256 returns (string, error) instead of silently returning ""
- installFromLocal now updates lockfile with binary checksum
- installFromLockfile passes pinned version and registry to install
- Remove plugin directory on checksum mismatch (fail closed)
- Use pluginName (not manifest.Name) as lockfile key for consistency
- File mode test compares permission bits only (umask-safe)
- Makefile install-local uses wfctl plugin install --local

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 22:22
@intel352
Copy link
Contributor Author

Second round of Copilot review comments addressed in b390e13:

  • CreateStep nil panic: Returns fmt.Errorf("unknown step type: %s") instead of (nil, nil)
  • hashFileSHA256 silent failure: Now returns (string, error) — callers log warnings on failure
  • installFromLocal lockfile gap: Now computes binary checksum and writes lockfile entry
  • Lockfile install ignores version/registry: Now passes name@version and --registry from lockfile entry
  • Checksum mismatch leaves binary on disk: Now removes plugin directory on verification failure
  • Lockfile key mismatch: Uses pluginName (CLI arg) instead of manifest.Name for consistent keys
  • File mode test vs umask: Compares permission bits with mask instead of exact equality
  • Makefile install path: Uses wfctl plugin install --local . instead of hardcoded ~/.local/share/workflow/plugins/

Previously addressed (round 1):

  • SDK import paths (plugin/external/sdk), struct conversion, URL validation, mutual exclusivity, registry field, checksum semantics (binary not archive), docs updates

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands wfctl’s plugin workflows by adding direct install sources (URL/local), introducing a static (GitHub Pages) registry backend with GitHub API fallback, and upgrading wfctl plugin init scaffolding to generate a full external plugin project layout.

Changes:

  • Add wfctl plugin install --url and --local, plus lockfile checksum recording/verification.
  • Add StaticRegistrySource and update default registry config to prefer a static index with GitHub fallback.
  • Enhance plugin scaffolding and add a plugin authoring guide.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
cmd/wfctl/plugin_install.go Adds --url/--local, hashing utilities, and direct install implementations.
cmd/wfctl/plugin_lockfile.go Extends lockfile entries (registry, checksum verification on lockfile installs).
cmd/wfctl/registry_source.go Implements StaticRegistrySource backed by /index.json and per-plugin manifests.
cmd/wfctl/registry_config.go Adds static registry config fields and updates the default registry list (static + fallback).
cmd/wfctl/multi_registry.go Wires static registry type into the registry source factory.
cmd/wfctl/registry_source_test.go Adds unit tests for StaticRegistrySource.
cmd/wfctl/plugin_install_new_test.go Adds unit tests for URL/local install and checksum verification helpers.
cmd/wfctl/multi_registry_test.go Updates tests for new default registry configuration.
cmd/wfctl/plugin.go Adds wfctl plugin init --module passthrough to the generator.
plugin/sdk/generator.go Expands scaffolding to generate a full project structure (cmd/internal/go.mod/CI/etc.).
docs/PLUGIN_AUTHORING.md Adds plugin authoring documentation for scaffolding, testing, publishing, and registry usage.
docs/plans/2026-03-14-messaging-plugins-plan.md Adds an internal implementation plan document.
docs/plans/2026-03-14-messaging-plugins-design.md Adds an internal design summary document.

Comment on lines +23 to +33
`wfctl plugin init` generates a complete project:

```
workflow-plugin-my-plugin/
├── cmd/workflow-plugin-my-plugin/main.go # gRPC entrypoint
├── internal/
│ ├── provider.go # Plugin provider (registers steps/modules)
│ └── steps.go # Step implementations
├── plugin.json # Plugin manifest
├── go.mod
├── .goreleaser.yml # Cross-platform release builds
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quick start shows 'cd my-plugin' which matches the OutputDir default; no change needed.

Comment on lines 186 to 198
func TestLoadRegistryConfigDefault(t *testing.T) {
// Provide a path that does not exist — should fall back to default.
cfg, err := LoadRegistryConfig("/nonexistent/path/config.yaml")
if err != nil {
t.Fatalf("LoadRegistryConfig: %v", err)
// Test DefaultRegistryConfig directly to avoid picking up user config files.
cfg := DefaultRegistryConfig()
if len(cfg.Registries) != 2 {
t.Fatalf("expected 2 registries (static + github fallback), got %d", len(cfg.Registries))
}
if len(cfg.Registries) != 1 {
t.Fatalf("expected 1 registry (default), got %d", len(cfg.Registries))
if cfg.Registries[0].Type != "static" {
t.Errorf("first registry type: got %q, want %q", cfg.Registries[0].Type, "static")
}
if cfg.Registries[0].Owner != registryOwner {
t.Errorf("owner: got %q, want %q", cfg.Registries[0].Owner, registryOwner)
if cfg.Registries[1].Owner != registryOwner {
t.Errorf("fallback owner: got %q, want %q", cfg.Registries[1].Owner, registryOwner)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TestLoadRegistryConfigFallback (line 200) already tests LoadRegistryConfig with fallback behavior; no additional test needed.

Comment on lines 164 to 174

// Update .wfctl.yaml lockfile if name@version was provided.
if _, ver := parseNameVersion(nameArg); ver != "" {
updateLockfile(manifest.Name, manifest.Version, manifest.Repository)
// Hash the installed binary (not the archive) so verifyInstalledChecksum matches.
binaryPath := filepath.Join(pluginDirVal, pluginName, pluginName)
sha, hashErr := hashFileSHA256(binaryPath)
if hashErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not hash installed binary: %v\n", hashErr)
}
updateLockfileWithChecksum(pluginName, manifest.Version, manifest.Repository, sourceName, sha)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: normalizePluginName is called before writing the lockfile key.

Comment on lines +517 to +528
if err := ensurePluginBinary(destDir, pluginName); err != nil {
fmt.Fprintf(os.Stderr, "warning: could not normalize binary name: %v\n", err)
}

// Hash the installed binary (not the archive) so that verifyInstalledChecksum matches.
binaryPath := filepath.Join(destDir, pluginName)
checksum, hashErr := hashFileSHA256(binaryPath)
if hashErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not hash installed binary: %v\n", hashErr)
}
updateLockfileWithChecksum(pluginName, pj.Version, pj.Repository, "", checksum)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: installFromURL now returns error (not warning) and calls verifyInstalledPlugin.

// Pass just the name (no @version) so runPluginInstall does not
// call updateLockfile and inadvertently overwrite the pinned entry.
installArgs = append(installArgs, name)
if entry.Registry != "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: installFromLockfile passes empty registry string to skip the --registry flag.

intel352 and others added 2 commits March 14, 2026 18:50
- Update plugin install usage text to document --url, --local, lockfile modes
- Don't write registry="local" to lockfile (prevents --registry local error)
- Fail installFromURL on ensurePluginBinary/hash errors instead of warning
- Normalize pluginName before lockfile write for consistent keys
- Fix CreateStep example in docs to return error for unknown types
- Fix project structure dir name in docs (my-plugin/ not workflow-plugin-my-plugin/)
- Remove dead code: unused goModule/license params in generator functions
- Add TestLoadRegistryConfigFallback to verify LoadRegistryConfig default path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 23:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new wfctl plugin installation paths (URL/local + static registry) and expands the wfctl plugin init scaffold to generate a full external-plugin project layout, along with new docs and tests to support these workflows.

Changes:

  • Extend wfctl plugin install with --url and --local, plus lockfile SHA-256 verification and lockfile registry metadata.
  • Add StaticRegistrySource and make static GitHub Pages registry the default with GitHub API fallback.
  • Enhance plugin scaffolding to generate a full Go project skeleton and add plugin authoring/planning documentation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Expands plugin scaffold output (cmd/, internal/, CI, GoReleaser, Makefile, README) and adds --module support.
docs/plans/2026-03-14-messaging-plugins-plan.md Adds a detailed implementation plan for messaging plugins.
docs/plans/2026-03-14-messaging-plugins-design.md Adds a high-level design overview for messaging platform plugins.
docs/PLUGIN_AUTHORING.md Adds a plugin authoring guide (scaffold/dev/publish/registry workflow).
cmd/wfctl/registry_source_test.go Adds unit tests for the new static registry source.
cmd/wfctl/registry_source.go Introduces StaticRegistrySource implementation for static index/manifest fetching.
cmd/wfctl/registry_config.go Adds static registry config support and updates default registries (static + GitHub fallback).
cmd/wfctl/plugin_lockfile.go Adds Registry to lockfile entries and verifies installed binary checksum when installing from lockfile.
cmd/wfctl/plugin_install_new_test.go Adds unit tests for URL/local install flows, checksum verification, and copy helper behavior.
cmd/wfctl/plugin_install.go Implements --url/--local, hashing installed binary for lockfile, and related helpers.
cmd/wfctl/plugin.go Adds --module flag plumbing into plugin init scaffolding.
cmd/wfctl/multi_registry_test.go Updates tests for the new default registry configuration and fallback behavior.
cmd/wfctl/multi_registry.go Wires static registry sources into MultiRegistry.

Comment on lines +81 to +86
// Pass name@version to install the pinned version from the lockfile.
installArg := name
if entry.Version != "" {
installArg = name + "@" + entry.Version
}
installArgs = append(installArgs, installArg)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: installFromLockfile now passes name only (no @Version suffix).

Comment on lines +478 to +532
// installFromURL downloads a plugin tarball from a direct URL and installs it.
func installFromURL(url, pluginDir string) error {
fmt.Fprintf(os.Stderr, "Downloading %s...\n", url)
data, err := downloadURL(url)
if err != nil {
return fmt.Errorf("download: %w", err)
}

tmpDir, err := os.MkdirTemp("", "wfctl-plugin-*")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)

if err := extractTarGz(data, tmpDir); err != nil {
return fmt.Errorf("extract: %w", err)
}

pjData, err := os.ReadFile(filepath.Join(tmpDir, "plugin.json"))
if err != nil {
return fmt.Errorf("no plugin.json found in archive: %w", err)
}
var pj installedPluginJSON
if err := json.Unmarshal(pjData, &pj); err != nil {
return fmt.Errorf("parse plugin.json: %w", err)
}
if pj.Name == "" {
return fmt.Errorf("plugin.json missing name field")
}

pluginName := normalizePluginName(pj.Name)
destDir := filepath.Join(pluginDir, pluginName)
if err := os.MkdirAll(destDir, 0750); err != nil {
return fmt.Errorf("create plugin dir: %w", err)
}

if err := extractTarGz(data, destDir); err != nil {
return fmt.Errorf("extract to dest: %w", err)
}

if err := ensurePluginBinary(destDir, pluginName); err != nil {
return fmt.Errorf("normalize binary name: %w", err)
}

// Hash the installed binary (not the archive) so that verifyInstalledChecksum matches.
binaryPath := filepath.Join(destDir, pluginName)
checksum, hashErr := hashFileSHA256(binaryPath)
if hashErr != nil {
return fmt.Errorf("hash installed binary for lockfile: %w", hashErr)
}
updateLockfileWithChecksum(pluginName, pj.Version, pj.Repository, "", checksum)

fmt.Printf("Installed %s v%s to %s\n", pluginName, pj.Version, destDir)
return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed: installFromURL now calls verifyInstalledPlugin after ensurePluginBinary.

Comment on lines +549 to +598
// installFromLocal installs a plugin from a local directory.
func installFromLocal(srcDir, pluginDir string) error {
pjPath := filepath.Join(srcDir, "plugin.json")
pjData, err := os.ReadFile(pjPath)
if err != nil {
return fmt.Errorf("read plugin.json in %s: %w", srcDir, err)
}
var pj installedPluginJSON
if err := json.Unmarshal(pjData, &pj); err != nil {
return fmt.Errorf("parse plugin.json: %w", err)
}
if pj.Name == "" {
return fmt.Errorf("plugin.json missing name field")
}

pluginName := normalizePluginName(pj.Name)
destDir := filepath.Join(pluginDir, pluginName)
if err := os.MkdirAll(destDir, 0750); err != nil {
return fmt.Errorf("create plugin dir: %w", err)
}

// Copy plugin.json
if err := copyFile(pjPath, filepath.Join(destDir, "plugin.json"), 0640); err != nil {
return err
}

// Find and copy the binary
srcBinary := filepath.Join(srcDir, pluginName)
if _, err := os.Stat(srcBinary); os.IsNotExist(err) {
fullName := "workflow-plugin-" + pluginName
srcBinary = filepath.Join(srcDir, fullName)
if _, err := os.Stat(srcBinary); os.IsNotExist(err) {
return fmt.Errorf("no plugin binary found in %s (tried %s and %s)", srcDir, pluginName, fullName)
}
}
if err := copyFile(srcBinary, filepath.Join(destDir, pluginName), 0750); err != nil {
return err
}

// Update lockfile with binary checksum for consistency with other install paths.
installedBinary := filepath.Join(destDir, pluginName)
sha, hashErr := hashFileSHA256(installedBinary)
if hashErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not hash installed binary: %v\n", hashErr)
}
updateLockfileWithChecksum(pluginName, pj.Version, "", "", sha)

fmt.Printf("Installed %s v%s from %s to %s\n", pluginName, pj.Version, srcDir, destDir)
return nil
}
func generateGoMod(goModule string) string {
var b strings.Builder
fmt.Fprintf(&b, "module %s\n\n", goModule)
b.WriteString("go 1.22\n\n")
// - GET /index.json → the provided index entries
// - GET /plugins/<name>/manifest.json → the manifest for that plugin (if present)
//
// It returns the server and a cleanup function.
@@ -0,0 +1,493 @@
# Messaging Plugins Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Step 1:** Create repo on GitHub:
```bash
gh repo create GoCodeAlone/workflow-plugin-messaging-core --public --description "Shared messaging interfaces for workflow platform plugins" --clone
cd /Users/jon/workspace/workflow-plugin-messaging-core
Comment on lines +208 to +209
PluginSummary: PluginSummary(e),
Source: s.name,
…verwrite

- installFromURL now calls verifyInstalledPlugin() for parity with
  registry installs (addresses Copilot review comment)
- installFromLockfile passes name without @Version to prevent
  runPluginInstall from overwriting the pinned lockfile entry
  before checksum verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@intel352 intel352 merged commit 67ca0e4 into main Mar 15, 2026
15 checks passed
@intel352 intel352 deleted the feat/plugin-ecosystem-wfctl branch March 15, 2026 02:05
intel352 added a commit that referenced this pull request Mar 15, 2026
Both branches made equivalent fixes to shared wfctl files. Resolved
by taking main's versions for shared code (plugin_install, lockfile,
registry_source, generator, docs) while preserving engine-branch-only
additions (integrity, autofetch, engine auto-fetch, config).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants