Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions cmd/wfctl/plugin_registry_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import (
// 4. Rejects plugin.json.type values outside the registry allowlist
// (catches accidental scaffold re-registration per workflow#762
// Layer (d) step 5).
// 5. Compares manifest.version + downloads URLs; with --fix rewrites.
// 6. Fetches tagged plugin.json from upstream; syncs capabilities,
// 5. Skips built-in/core manifests; those are owned by "registry-sync core".
// 6. Compares manifest.version + downloads URLs; with --fix rewrites.
// 7. Fetches tagged plugin.json from upstream; syncs capabilities,
// minEngineVersion, iacProvider into registry manifest.
// 7. (--verify-capabilities) Downloads release tarball + spawns binary;
// 8. (--verify-capabilities) Downloads release tarball + spawns binary;
// reuses wfctl plugin verify-capabilities to diff runtime GetManifest
// against the registry manifest.
func runPluginRegistrySync(args []string) error {
Expand Down Expand Up @@ -123,6 +124,10 @@ func syncDefault(registryDir string, fix bool, pluginFilter string, verifyCaps b
mismatches++
continue
}
if isCoreRegistryManifestType(manifestType) {
fmt.Printf(" SKIP %s — %s manifests are synced by registry-sync core\n", pluginName, manifestType)
continue
}

repoURL, _ := raw["repository"].(string)
if repoURL == "" {
Expand Down Expand Up @@ -214,6 +219,15 @@ func syncDefault(registryDir string, fix bool, pluginFilter string, verifyCaps b
return nil
}

func isCoreRegistryManifestType(manifestType string) bool {
switch manifestType {
case "builtin", "core":
return true
default:
return false
}
}

func readJSONFile(path string) (map[string]any, error) {
data, err := os.ReadFile(path) // #nosec G304 -- operator-supplied path
if err != nil {
Expand Down
14 changes: 12 additions & 2 deletions cmd/wfctl/plugin_registry_sync_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,20 @@ func syncCorePluginManifests(registryDir string, plugins []coreRegistryPlugin, f
if err != nil {
return err
}
if !reflect.DeepEqual(current, expected) {
hasDownloads := coreManifestHasDownloads(currentRaw)
if !reflect.DeepEqual(current, expected) || hasDownloads {
if fix {
if err := writeCoreManifest(expectedPath, expected, currentRaw); err != nil {
return err
}
fmt.Fprintf(stderr, "updated %s\n", relRegistryPath(registryDir, expectedPath))
continue
}
fmt.Fprintf(stderr, "core plugin manifest drift for %s: %s\n", p.Name, relRegistryPath(registryDir, expectedPath))
if hasDownloads {
fmt.Fprintf(stderr, "core plugin manifest drift for %s: %s (builtin manifests must not include downloads)\n", p.Name, relRegistryPath(registryDir, expectedPath))
} else {
fmt.Fprintf(stderr, "core plugin manifest drift for %s: %s\n", p.Name, relRegistryPath(registryDir, expectedPath))
}
failures++
}
}
Expand All @@ -147,6 +152,11 @@ func syncCorePluginManifests(registryDir string, plugins []coreRegistryPlugin, f
return nil
}

func coreManifestHasDownloads(raw map[string]any) bool {
_, ok := raw["downloads"]
return ok
}

type coreManifest struct {
Name string `json:"name"`
Version string `json:"version"`
Expand Down
91 changes: 91 additions & 0 deletions cmd/wfctl/plugin_registry_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,34 @@ func TestPluginRegistrySync_DownloadsMatchVersion(t *testing.T) {
})
}

func TestPluginRegistrySync_DefaultSkipsBuiltinManifests(t *testing.T) {
registry := t.TempDir()
mustWrite(t, filepath.Join(registry, "plugins", "admincore", "manifest.json"), `{
"name": "workflow-plugin-admincore",
"version": "0.69.6",
"description": "Workflow admin core",
"source": "github.com/GoCodeAlone/workflow",
"repository": "https://github.com/GoCodeAlone/workflow",
"type": "builtin",
"tier": "core"
}`)

binDir := t.TempDir()
marker := filepath.Join(binDir, "gh-called")
mustWrite(t, filepath.Join(binDir, "gh"), "#!/bin/sh\ntouch "+marker+"\nexit 1\n")
if err := os.Chmod(filepath.Join(binDir, "gh"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

if err := syncDefault(registry, false, "", false); err != nil {
t.Fatalf("syncDefault returned error: %v", err)
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("default registry sync must not query GitHub for builtin manifests; marker stat err=%v", err)
}
}

// TestPluginRegistrySync_PublishGradeSemverGate verifies the shared regex
// rejects non-publish-grade tags (workflow#762 plan C2 fixture pin).
func TestPluginRegistrySync_PublishGradeSemverGate(t *testing.T) {
Expand Down Expand Up @@ -249,6 +277,69 @@ func TestPluginRegistrySyncCore_DetectsAndFixesManifestDrift(t *testing.T) {
}
}

func TestPluginRegistrySyncCore_DetectsAndFixesDownloadsOnlyDrift(t *testing.T) {
registry := t.TempDir()
manifest := filepath.Join(registry, "plugins", "corealpha", "manifest.json")
mustWrite(t, manifest, `{
"name": "workflow-plugin-core-alpha",
"version": "1.2.3",
"author": "GoCodeAlone",
"description": "current",
"source": "github.com/GoCodeAlone/workflow",
"path": "plugins/corealpha",
"type": "builtin",
"tier": "core",
"license": "MIT",
"homepage": "https://github.com/GoCodeAlone/workflow",
"repository": "https://github.com/GoCodeAlone/workflow",
"downloads": [{
"os": "linux",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow/releases/download/v0.69.6/wfctl-linux-amd64.tar.gz"
}],
"capabilities": {
"moduleTypes": ["alpha"],
"stepTypes": ["step"],
"triggerTypes": ["trigger"],
"workflowHandlers": ["handler"],
"wiringHooks": ["hook"]
}
}`)

plugins := []coreRegistryPlugin{{
Name: "workflow-plugin-core-alpha",
Version: "1.2.3",
Description: "current",
ModuleTypes: []string{"alpha"},
StepTypes: []string{"step"},
TriggerTypes: []string{"trigger"},
WorkflowHandlers: []string{"handler"},
WiringHooks: []string{"hook"},
}}

var stderr bytes.Buffer
err := syncCorePluginManifests(registry, plugins, false, &stderr)
if err == nil || !strings.Contains(err.Error(), "core manifest validation failed") {
t.Fatalf("dry-run error = %v, stderr=%s", err, stderr.String())
}

stderr.Reset()
if err := syncCorePluginManifests(registry, plugins, true, &stderr); err != nil {
t.Fatalf("fix returned error: %v", err)
}
raw, err := os.ReadFile(manifest)
if err != nil {
t.Fatal(err)
}
var got map[string]any
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatal(err)
}
if _, ok := got["downloads"]; ok {
t.Fatalf("downloads should be removed from builtin core manifest: %#v", got)
}
}

func TestPluginRegistrySync_SelectPlatformReleaseAsset(t *testing.T) {
assets := []releaseAsset{
{Name: "workflow-plugin-foo-linux-amd64.tar.gz", OS: "linux", Arch: "amd64", URL: "linux-amd64"},
Expand Down
Loading