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
28 changes: 18 additions & 10 deletions cmd/wfctl/plugin_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,13 @@ type installedPluginJSON struct {
Type string `json:"type,omitempty"`
Capabilities *installedPluginCapabilities `json:"capabilities,omitempty"`
IaCProvider *RegistryIaCProvider `json:"iacProvider,omitempty"`
// RequiredSecrets carries through from the registry manifest so
// `wfctl secrets setup --plugin <name>` can find the declared
// secrets in the on-disk plugin.json. Earlier writeInstalledManifest
// versions dropped this field, leaving secrets setup --plugin
// reporting "declares no required_secrets[]" even when the
// upstream manifest declared it.
RequiredSecrets []PluginRequiredSecret `json:"required_secrets,omitempty"`
}

type installedPluginCapabilities struct {
Expand All @@ -1401,16 +1408,17 @@ type installedPluginCapabilities struct {
// plugin.PluginManifest so that ExternalPluginManager.LoadPlugin() can validate it.
func writeInstalledManifest(path string, m *RegistryManifest) error {
pj := installedPluginJSON{
Name: m.Name,
Version: m.Version,
Author: m.Author,
Description: m.Description,
License: m.License,
Repository: m.Repository,
Tier: m.Tier,
Tags: m.Keywords,
Type: m.Type,
IaCProvider: m.IaCProvider,
Name: m.Name,
Version: m.Version,
Author: m.Author,
Description: m.Description,
License: m.License,
Repository: m.Repository,
Tier: m.Tier,
Tags: m.Keywords,
Type: m.Type,
IaCProvider: m.IaCProvider,
RequiredSecrets: m.RequiredSecrets,
}
if m.Capabilities != nil {
pj.Capabilities = &installedPluginCapabilities{
Expand Down
72 changes: 72 additions & 0 deletions cmd/wfctl/plugin_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1182,3 +1182,75 @@ func writeCompatRegistryIndex(t *testing.T, w http.ResponseWriter, plugin, baseU
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(data)
}

func TestWriteInstalledManifest_PreservesRequiredSecrets(t *testing.T) {
// G4 regression: writeInstalledManifest used to drop the
// required_secrets[] block from the registry manifest, so the
// on-disk plugin.json had no required_secrets even when upstream
// declared them. `wfctl secrets setup --plugin <name>` then
// reported "declares no required_secrets[]" no-op.
m := &RegistryManifest{
Name: "workflow-plugin-hover", Version: "v0.2.0",
Author: "GoCodeAlone", Description: "test",
RequiredSecrets: []PluginRequiredSecret{
{Name: "HOVER_USERNAME", Sensitive: false, Description: "Hover username", Prompt: "Hover username"},
{Name: "HOVER_PASSWORD", Sensitive: true, Description: "Hover password"},
},
}
dir := t.TempDir()
path := filepath.Join(dir, "plugin.json")
if err := writeInstalledManifest(path, m); err != nil {
t.Fatalf("writeInstalledManifest: %v", err)
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
var pj installedPluginJSON
if err := json.Unmarshal(raw, &pj); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(pj.RequiredSecrets) != 2 {
t.Fatalf("required_secrets len = %d, want 2", len(pj.RequiredSecrets))
}
if pj.RequiredSecrets[0].Name != "HOVER_USERNAME" {
t.Errorf("required_secrets[0].name = %q, want HOVER_USERNAME", pj.RequiredSecrets[0].Name)
}
if !pj.RequiredSecrets[1].Sensitive {
t.Errorf("required_secrets[1].sensitive = false; want true (HOVER_PASSWORD)")
}
// Also assert the raw JSON contains the field — guards against a
// future installedPluginJSON refactor that adds Sensitive omitempty
// or otherwise hides the field at marshal time.
if !strings.Contains(string(raw), "\"required_secrets\":") {
t.Errorf("installed plugin.json missing required_secrets[] key:\n%s", string(raw))
}
}

func TestRegistryManifest_UnmarshalPreservesRequiredSecrets(t *testing.T) {
// G4 regression: RegistryManifest dropped required_secrets[] at
// json.Unmarshal time because the struct didn't carry the field.
src := `{
"name": "workflow-plugin-hover",
"version": "0.2.0",
"author": "GoCodeAlone",
"description": "test",
"type": "external",
"tier": "community",
"license": "MIT",
"required_secrets": [
{"name": "X", "sensitive": false, "description": "d", "prompt": "p"},
{"name": "Y", "sensitive": true}
]
}`
var m RegistryManifest
if err := json.Unmarshal([]byte(src), &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(m.RequiredSecrets) != 2 {
t.Fatalf("required_secrets len = %d, want 2", len(m.RequiredSecrets))
}
if m.RequiredSecrets[1].Name != "Y" || !m.RequiredSecrets[1].Sensitive {
t.Errorf("unexpected required_secrets[1]: %+v", m.RequiredSecrets[1])
}
}
7 changes: 7 additions & 0 deletions cmd/wfctl/plugin_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ type RegistryManifest struct {
Downloads []PluginDownload `json:"downloads,omitempty"`
Assets *PluginAssets `json:"assets,omitempty"`
Dependencies []PluginDependency `json:"dependencies,omitempty"`
// RequiredSecrets mirrors the plugin.json `required_secrets[]`
// block. Consumed by `wfctl secrets setup --plugin <name>` to
// prompt for + write each secret to the chosen GitHub scope.
// Without this field on the registry struct, the secrets[] list
// would be silently dropped at unmarshal time even when the
// upstream manifest declared it.
RequiredSecrets []PluginRequiredSecret `json:"required_secrets,omitempty"`
}

// RegistryCapabilities describes what module/step/trigger types a plugin provides.
Expand Down
Loading