diff --git a/cmd/wfctl/plugin_registry_sync.go b/cmd/wfctl/plugin_registry_sync.go index 5dff6ff3..34331188 100644 --- a/cmd/wfctl/plugin_registry_sync.go +++ b/cmd/wfctl/plugin_registry_sync.go @@ -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 { @@ -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 == "" { @@ -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 { diff --git a/cmd/wfctl/plugin_registry_sync_core.go b/cmd/wfctl/plugin_registry_sync_core.go index fa6031d6..e3c57afc 100644 --- a/cmd/wfctl/plugin_registry_sync_core.go +++ b/cmd/wfctl/plugin_registry_sync_core.go @@ -123,7 +123,8 @@ 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 @@ -131,7 +132,11 @@ func syncCorePluginManifests(registryDir string, plugins []coreRegistryPlugin, f 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++ } } @@ -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"` diff --git a/cmd/wfctl/plugin_registry_sync_test.go b/cmd/wfctl/plugin_registry_sync_test.go index ac858616..adbdc524 100644 --- a/cmd/wfctl/plugin_registry_sync_test.go +++ b/cmd/wfctl/plugin_registry_sync_test.go @@ -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) { @@ -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"},