From ad7d9129cad77749d9547c3ec7809c36e670f0b3 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Thu, 25 Jun 2026 22:23:00 -0400 Subject: [PATCH] feat: add cpm publish command for notary registry (#27) - New cmd/publish.go: reads .cgp, computes SHA-256, publishes to registry - New registry.Client.Publish() method: POST /v1/patches with JSON body - PublishRequest struct for metadata + checksum + download URL - Requires --download-url flag (notary mode, no file hosting) - Requires CPM_REGISTRY_TOKEN env var for auth - Supports --tag flag (repeatable) --- cmd/publish.go | 90 +++++++++++++++++++++++++++++++++++ internal/registry/registry.go | 39 +++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 cmd/publish.go diff --git a/cmd/publish.go b/cmd/publish.go new file mode 100644 index 0000000..ab7bced --- /dev/null +++ b/cmd/publish.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/CognitiveOS-Project/cpm/internal/archive" + "github.com/CognitiveOS-Project/cpm/internal/registry" + "github.com/spf13/cobra" +) + +var publishDownloadURL string +var publishTags []string + +var publishCmd = &cobra.Command{ + Use: "publish ", + Short: "Publish a .cgp patch to the registry", + Long: `Publish a .cgp archive to the configured registry notary. + +The SHA-256 checksum is computed locally and sent with the metadata. +The registry stores the notary record (checksum + download URL) and +redirects clients to the canonical download URL. + +Examples: + cpm publish ./my-patch-1.0.0.cgp --download-url https://github.com/.../my-patch-1.0.0.cgp + cpm publish ./my-patch-1.0.0.cgp --tag vision --download-url https://...`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("open %s: %w", path, err) + } + defer f.Close() + + m, err := archive.ReadManifest(f) + if err != nil { + return fmt.Errorf("read manifest: %w", err) + } + + f.Seek(0, 0) + hasher := sha256.New() + if _, err := io.Copy(hasher, f); err != nil { + return fmt.Errorf("checksum: %w", err) + } + checksum := hex.EncodeToString(hasher.Sum(nil)) + + regURL := resolveRegistry() + if regURL == "" { + return fmt.Errorf("no registry configured") + } + rc := registry.New(regURL) + + token := os.Getenv("CPM_REGISTRY_TOKEN") + if token == "" { + return fmt.Errorf("CPM_REGISTRY_TOKEN environment variable not set") + } + + if publishDownloadURL == "" { + return fmt.Errorf("--download-url is required (notary registry does not host files)") + } + + req := registry.PublishRequest{ + Name: m.Name, + Version: m.Version, + Description: m.Description, + Author: m.Author, + DownloadURL: publishDownloadURL, + SHA256: checksum, + Tags: publishTags, + } + if err := rc.Publish(token, req); err != nil { + return fmt.Errorf("publish: %w", err) + } + + fmt.Printf("✓ Published %s v%s (sha256=%s)\n", m.Name, m.Version, checksum) + return nil + }, +} + +func init() { + fs := publishCmd.Flags() + fs.StringVar(&publishDownloadURL, "download-url", "", "Canonical download URL for the .cgp archive") + fs.StringSliceVar(&publishTags, "tag", nil, "Tags for the package (repeatable)") + rootCmd.AddCommand(publishCmd) +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index ef43e3f..47b84e5 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -1,6 +1,7 @@ package registry import ( + "bytes" "encoding/json" "fmt" "io" @@ -105,3 +106,41 @@ func (c *Client) Download(name, version string) (io.ReadCloser, error) { } return resp.Body, nil } + +type PublishRequest struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Author string `json:"author,omitempty"` + SourceRepository string `json:"source_repository,omitempty"` + SourceIssues string `json:"source_issues,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + SHA256 string `json:"sha256,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +func (c *Client) Publish(token string, req PublishRequest) error { + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("encode: %w", err) + } + + httpReq, err := http.NewRequest("POST", c.BaseURL+"/patches", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+token) + + resp, err := c.HTTP.Do(httpReq) + if err != nil { + return fmt.Errorf("network: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 201 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("registry: %s", string(respBody)) + } + return nil +}