diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..f29f940 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,18 @@ +name: Release Please + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/cmd/apideck/permissions_cmd.go b/cmd/apideck/permissions_cmd.go index 26b38a0..13cd618 100644 --- a/cmd/apideck/permissions_cmd.go +++ b/cmd/apideck/permissions_cmd.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/apideck-io/cli/internal/permission" "github.com/apideck-io/cli/internal/ui" @@ -14,8 +15,8 @@ func newPermissionsCmd() *cobra.Command { Short: "Show permission configuration", RunE: func(cmd *cobra.Command, args []string) error { path := permission.DefaultPermConfigPath() - cfg, err := permission.LoadPermConfig(path) - if err != nil { + + if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Println(ui.Dim.Render("No permissions config found.")) fmt.Println(ui.Dim.Render(fmt.Sprintf("Create one at: %s", path))) fmt.Println() @@ -25,6 +26,11 @@ func newPermissionsCmd() *cobra.Command { fmt.Println(" dangerous (DELETE): blocked") return nil } + + cfg, err := permission.LoadPermConfig(path) + if err != nil { + return fmt.Errorf("failed to load permissions config: %w", err) + } fmt.Println(ui.PrimaryBold.Render("Permission Defaults:")) for level, action := range cfg.Defaults { fmt.Printf(" %-12s %s\n", level+":", action) diff --git a/internal/auth/setup.go b/internal/auth/setup.go index 7db1e81..213175e 100644 --- a/internal/auth/setup.go +++ b/internal/auth/setup.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/apideck-io/cli/internal/permission" "github.com/apideck-io/cli/internal/ui" "github.com/charmbracelet/huh" ) @@ -52,6 +53,14 @@ func RunSetup(configPath string) error { } fmt.Println(ui.SuccessMsg(fmt.Sprintf("Credentials saved to %s", configPath))) + + permPath := permission.DefaultPermConfigPath() + if err := permission.WriteDefaultConfig(permPath); err != nil { + fmt.Println(ui.Dim.Render(fmt.Sprintf("Could not create permissions config: %s", err))) + } else { + fmt.Println(ui.SuccessMsg(fmt.Sprintf("Permissions config created at %s", permPath))) + } + return nil } diff --git a/internal/permission/config.go b/internal/permission/config.go index 9ecacf4..41c33b0 100644 --- a/internal/permission/config.go +++ b/internal/permission/config.go @@ -47,3 +47,41 @@ func DefaultPermConfigPath() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".apideck-cli", "permissions.yaml") } + +// defaultPermConfigTemplate is the YAML content written by WriteDefaultConfig. +const defaultPermConfigTemplate = `# Apideck CLI permission configuration +# Controls how different API operations are handled. +# +# Permission levels: +# read - GET requests +# write - POST/PUT/PATCH requests +# dangerous - DELETE requests +# +# Actions: +# allow - execute without prompting +# prompt - ask for confirmation before executing +# block - prevent execution entirely + +defaults: + read: allow + write: prompt + dangerous: block + +# Per-operation overrides (keyed by operation ID): +# overrides: +# invoices-delete: allow # skip confirmation for this specific DELETE +# invoices-list: block # block a specific GET +` + +// WriteDefaultConfig creates a permissions config file with built-in defaults +// at the given path. It does nothing if the file already exists. +func WriteDefaultConfig(path string) error { + if _, err := os.Stat(path); err == nil { + return nil + } + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + return os.WriteFile(path, []byte(defaultPermConfigTemplate), 0600) +} diff --git a/internal/permission/engine_test.go b/internal/permission/engine_test.go index 4bf8576..291b44e 100644 --- a/internal/permission/engine_test.go +++ b/internal/permission/engine_test.go @@ -132,6 +132,36 @@ func TestLoadPermConfigMissingFile(t *testing.T) { } } +// TestWriteDefaultConfig verifies that WriteDefaultConfig creates a valid config +// and is idempotent (does not overwrite an existing file). +func TestWriteDefaultConfig(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "sub", "permissions.yaml") + + // Creates file and parent directory. + if err := WriteDefaultConfig(path); err != nil { + t.Fatalf("first call failed: %v", err) + } + + cfg, err := LoadPermConfig(path) + if err != nil { + t.Fatalf("failed to load generated config: %v", err) + } + if cfg.Defaults["read"] != "allow" || cfg.Defaults["write"] != "prompt" || cfg.Defaults["dangerous"] != "block" { + t.Errorf("unexpected defaults: %v", cfg.Defaults) + } + + // Overwrite file with custom content, then call again -- should not overwrite. + os.WriteFile(path, []byte("defaults:\n read: block\n"), 0600) + if err := WriteDefaultConfig(path); err != nil { + t.Fatalf("idempotent call failed: %v", err) + } + cfg2, _ := LoadPermConfig(path) + if cfg2.Defaults["read"] != "block" { + t.Error("WriteDefaultConfig overwrote existing file") + } +} + // TestActionString verifies Action.String() returns correct labels. func TestActionString(t *testing.T) { cases := []struct { diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..007232b --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,10 @@ +{ + "packages": { + ".": { + "release-type": "go", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true + } + }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +}