Merge pull request #1482 from trheyi/main
Enhance testing framework with Registry Client SDK integration
This commit is contained in:
commit
e412d2c48d
8 changed files with 1951 additions and 3 deletions
163
.github/workflows/pr-test.yml
vendored
163
.github/workflows/pr-test.yml
vendored
|
|
@ -1367,3 +1367,166 @@ jobs:
|
|||
issue_number: issue_number,
|
||||
body: '✨DONE✨ db: ${{ matrix.db }} redis: ${{ matrix.redis }} mongo: ${{ matrix.mongo }} passed.'
|
||||
});
|
||||
|
||||
# =============================================================================
|
||||
# Registry Client SDK Tests (requires Yao Registry Docker service)
|
||||
# =============================================================================
|
||||
RegistryTest:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
yao-registry:
|
||||
image: yaoapp/registry:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env:
|
||||
REGISTRY_INIT_USER: yaoagents
|
||||
REGISTRY_INIT_PASS: yaoagents
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.25"]
|
||||
if: >
|
||||
${{ github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: "Download artifact"
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{github.event.workflow_run.id }},
|
||||
});
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr"
|
||||
})[0];
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data));
|
||||
|
||||
- name: "Read NR & SHA"
|
||||
run: |
|
||||
unzip pr.zip
|
||||
cat NR
|
||||
cat SHA
|
||||
echo HEAD=$(cat SHA) >> $GITHUB_ENV
|
||||
echo NR=$(cat NR) >> $GITHUB_ENV
|
||||
|
||||
- name: "Comment on PR"
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { NR } = process.env
|
||||
var issue_number = NR;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: '🤖 Registry Client SDK Tests running...'
|
||||
});
|
||||
|
||||
- name: Wait for Registry
|
||||
run: |
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Checkout Kun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/kun
|
||||
path: kun
|
||||
|
||||
- name: Checkout Xun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/xun
|
||||
path: xun
|
||||
|
||||
- name: Checkout Gou
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/gou
|
||||
path: gou
|
||||
|
||||
- name: Checkout V8Go
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/v8go
|
||||
path: v8go
|
||||
|
||||
- name: Unzip libv8
|
||||
run: |
|
||||
files=$(find ./v8go -name "libv8*.zip")
|
||||
for file in $files; do
|
||||
dir=$(dirname "$file")
|
||||
echo "Extracting $file to directory $dir"
|
||||
unzip -o -d $dir $file
|
||||
rm -rf $dir/__MACOSX
|
||||
done
|
||||
|
||||
- name: Checkout Demo App
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/yao-dev-app
|
||||
path: app
|
||||
|
||||
- name: Checkout Extension
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/yao-extensions-dev
|
||||
path: extension
|
||||
|
||||
- name: Move Dependencies
|
||||
run: |
|
||||
mv kun ../
|
||||
mv xun ../
|
||||
mv gou ../
|
||||
mv v8go ../
|
||||
mv app ../
|
||||
mv extension ../
|
||||
|
||||
- name: Checkout pull request HEAD commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.HEAD }}
|
||||
|
||||
- name: Setup Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run Registry Client Tests
|
||||
env:
|
||||
YAO_REGISTRY_URL: http://localhost:8080
|
||||
run: make unit-test-registry
|
||||
|
||||
- name: Codecov Report
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: "Comment on PR - Registry Tests Done"
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { NR } = process.env
|
||||
var issue_number = NR;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: '✅ Registry Client SDK Tests passed!'
|
||||
});
|
||||
|
|
|
|||
101
.github/workflows/unit-test.yml
vendored
101
.github/workflows/unit-test.yml
vendored
|
|
@ -1031,3 +1031,104 @@ jobs:
|
|||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
|
||||
# =============================================================================
|
||||
# Registry Client SDK Tests (requires Yao Registry Docker service)
|
||||
# =============================================================================
|
||||
registry-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
yao-registry:
|
||||
image: yaoapp/registry:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env:
|
||||
REGISTRY_INIT_USER: yaoagents
|
||||
REGISTRY_INIT_PASS: yaoagents
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.25"]
|
||||
steps:
|
||||
- name: Wait for Registry
|
||||
run: |
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Checkout Kun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.REPO_KUN }}
|
||||
path: kun
|
||||
|
||||
- name: Checkout Xun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.REPO_XUN }}
|
||||
path: xun
|
||||
|
||||
- name: Checkout Gou
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.REPO_GOU }}
|
||||
path: gou
|
||||
|
||||
- name: Checkout V8Go
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/v8go
|
||||
path: v8go
|
||||
|
||||
- name: Unzip libv8
|
||||
run: |
|
||||
files=$(find ./v8go -name "libv8*.zip")
|
||||
for file in $files; do
|
||||
dir=$(dirname "$file")
|
||||
echo "Extracting $file to directory $dir"
|
||||
unzip -o -d $dir $file
|
||||
rm -rf $dir/__MACOSX
|
||||
done
|
||||
|
||||
- name: Checkout Demo App
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/yao-dev-app
|
||||
path: app
|
||||
|
||||
- name: Checkout Extension
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: yaoapp/yao-extensions-dev
|
||||
path: extension
|
||||
|
||||
- name: Move Dependencies
|
||||
run: |
|
||||
mv kun ../
|
||||
mv xun ../
|
||||
mv gou ../
|
||||
mv v8go ../
|
||||
mv app ../
|
||||
mv extension ../
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run Registry Client Tests
|
||||
env:
|
||||
YAO_REGISTRY_URL: http://localhost:8080
|
||||
run: make unit-test-registry
|
||||
|
||||
- name: Codecov Report
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -72,3 +72,4 @@ agent/robot/DESIGN-V2.md
|
|||
tg-session.json
|
||||
tg-login
|
||||
tg-send
|
||||
registry/data/
|
||||
|
|
|
|||
27
Makefile
27
Makefile
|
|
@ -10,9 +10,9 @@ NOW := $(shell date +"%FT%T%z")
|
|||
OS := $(shell uname)
|
||||
|
||||
# ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*' | awk '!/\/tests\// || /openapi\/tests/')
|
||||
# Core tests (exclude AI-related: agent, aigc, openai, KB, sandbox, and integrations which require external services)
|
||||
TESTFOLDER_CORE := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|agent|kb|sandbox|integrations' | awk '!/\/tests\// || /openapi\/tests/')
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|registry' | awk '!/\/tests\// || /openapi\/tests/')
|
||||
# Core tests (exclude AI-related: agent, aigc, openai, KB, sandbox, registry, and integrations which require external services)
|
||||
TESTFOLDER_CORE := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|agent|kb|sandbox|integrations|registry' | awk '!/\/tests\// || /openapi\/tests/')
|
||||
# Agent tests (agent, aigc) - exclude agent/search/handlers/web (requires external API keys) and robot packages (tested in robot job)
|
||||
TESTFOLDER_AGENT := $(shell $(GO) list ./agent/... ./aigc/... | grep -vE 'agent/search/handlers/web|agent/robot/')
|
||||
# KB tests (kb)
|
||||
|
|
@ -174,6 +174,27 @@ unit-test-robot:
|
|||
fi; \
|
||||
done
|
||||
|
||||
# Registry Client Test (requires Yao Registry service)
|
||||
.PHONY: unit-test-registry
|
||||
unit-test-registry:
|
||||
echo "mode: count" > coverage.out
|
||||
$(GO) test -v -timeout=2m -covermode=count -coverprofile=profile.out ./registry/... > tmp.out; \
|
||||
cat tmp.out; \
|
||||
if grep -q "^--- FAIL" tmp.out; then \
|
||||
rm tmp.out; \
|
||||
exit 1; \
|
||||
elif grep -q "^FAIL" tmp.out; then \
|
||||
rm tmp.out; \
|
||||
exit 1; \
|
||||
elif grep -q "^panic:" tmp.out; then \
|
||||
rm tmp.out; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
if [ -f profile.out ]; then \
|
||||
cat profile.out | grep -v "mode:" >> coverage.out; \
|
||||
rm profile.out; \
|
||||
fi
|
||||
|
||||
# Sandbox Unit Test (requires Docker)
|
||||
.PHONY: unit-test-sandbox
|
||||
unit-test-sandbox:
|
||||
|
|
|
|||
56
registry/README.md
Normal file
56
registry/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# registry
|
||||
|
||||
Go client SDK for [Yao Registry](https://github.com/YaoApp/registry).
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/yaoapp/yao/registry"
|
||||
|
||||
// Only the server URL is required. API prefix is auto-discovered
|
||||
// via /.well-known/yao-registry on the first call.
|
||||
c := registry.New("https://registry.yaoagents.com",
|
||||
registry.WithAuth("user", "pass"), // optional, for push/delete
|
||||
)
|
||||
|
||||
// Push a .yao.zip package
|
||||
result, err := c.Push("assistants", "@yao", "hello", "1.0.0", zipBytes)
|
||||
|
||||
// Pull (by version or dist-tag)
|
||||
data, digest, err := c.Pull("assistants", "@yao", "hello", "latest")
|
||||
|
||||
// Query
|
||||
pack, err := c.GetPackument("assistants", "@yao", "hello")
|
||||
ver, err := c.GetVersion("assistants", "@yao", "hello", "1.0.0")
|
||||
list, err := c.Search("hello", "assistants", 1, 20)
|
||||
|
||||
// Dependencies
|
||||
deps, err := c.GetDependencies("assistants", "@yao", "hello", "1.0.0", true)
|
||||
|
||||
// Dist-tags
|
||||
c.SetTag("assistants", "@yao", "hello", "stable", "1.0.0")
|
||||
c.DeleteTag("assistants", "@yao", "hello", "stable")
|
||||
|
||||
// Delete
|
||||
c.DeleteVersion("assistants", "@yao", "hello", "1.0.0")
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `WithAuth(user, pass)` | Basic Auth for push/delete |
|
||||
| `WithHTTPClient(hc)` | Custom `*http.Client` |
|
||||
| `WithTimeout(d)` | HTTP timeout |
|
||||
|
||||
## Environment
|
||||
|
||||
Tests require a running registry server. Set `YAO_REGISTRY_URL` (default `http://localhost:8080`) and create user `yaoagents`/`yaoagents`.
|
||||
|
||||
```bash
|
||||
# Start registry with test user
|
||||
REGISTRY_INIT_USER=yaoagents REGISTRY_INIT_PASS=yaoagents registry start
|
||||
|
||||
# Run tests
|
||||
go test ./registry/... -v
|
||||
```
|
||||
494
registry/client.go
Normal file
494
registry/client.go
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
// Package registry provides a client SDK for the Yao Registry HTTP API.
|
||||
// It supports push, pull, search, version management, dist-tags,
|
||||
// dependency queries, and package deletion with Basic Auth.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client talks to a Yao Registry server over HTTP.
|
||||
// On first API call it discovers the API prefix via /.well-known/yao-registry
|
||||
// so callers only need to provide the server root URL (e.g. "https://registry.yaoagents.com").
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiPrefix string // resolved from well-known, e.g. "/v1"
|
||||
discoverOnce sync.Once
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Option configures a Client.
|
||||
type Option func(*Client)
|
||||
|
||||
// WithAuth sets Basic Auth credentials for push/delete operations.
|
||||
func WithAuth(username, password string) Option {
|
||||
return func(c *Client) {
|
||||
c.username = username
|
||||
c.password = password
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient overrides the default http.Client.
|
||||
func WithHTTPClient(hc *http.Client) Option {
|
||||
return func(c *Client) { c.httpClient = hc }
|
||||
}
|
||||
|
||||
// WithTimeout sets the HTTP client timeout.
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(c *Client) { c.httpClient.Timeout = d }
|
||||
}
|
||||
|
||||
// New creates a registry client. serverURL is the root URL users configure,
|
||||
// e.g. "http://localhost:8080" or "https://registry.yaoagents.com".
|
||||
// The actual API prefix is auto-discovered via /.well-known/yao-registry.
|
||||
func New(serverURL string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
baseURL: strings.TrimRight(serverURL, "/"),
|
||||
apiPrefix: "/v1", // sensible default, overridden by discovery
|
||||
httpClient: &http.Client{Timeout: 60 * time.Second},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ensureDiscovered runs well-known discovery exactly once (thread-safe).
|
||||
func (c *Client) ensureDiscovered() {
|
||||
c.discoverOnce.Do(func() {
|
||||
var info RegistryInfo
|
||||
if err := c.doGet("/.well-known/yao-registry", nil, &info); err == nil && info.Registry.API != "" {
|
||||
c.apiPrefix = strings.TrimRight(info.Registry.API, "/")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Response types ---
|
||||
|
||||
// RegistryInfo is returned by the discovery endpoint.
|
||||
type RegistryInfo struct {
|
||||
Registry struct {
|
||||
Version string `json:"version"`
|
||||
API string `json:"api"`
|
||||
} `json:"registry"`
|
||||
Types []string `json:"types"`
|
||||
}
|
||||
|
||||
// ServerInfo is returned by GET /v1/.
|
||||
type ServerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// PushResult is returned after a successful push.
|
||||
type PushResult struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
// DeleteResult is returned after a successful version delete.
|
||||
type DeleteResult struct {
|
||||
Deleted string `json:"deleted"`
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TagResult is returned after setting a dist-tag.
|
||||
type TagResult struct {
|
||||
Tag string `json:"tag"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// TagDeleteResult is returned after deleting a dist-tag.
|
||||
type TagDeleteResult struct {
|
||||
Deleted string `json:"deleted"`
|
||||
}
|
||||
|
||||
// ListResult is returned by list and search endpoints.
|
||||
type ListResult struct {
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pagesize"`
|
||||
Packages []json.RawMessage `json:"packages"`
|
||||
}
|
||||
|
||||
// Packument is the full package metadata response.
|
||||
type Packument struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
DistTags map[string]string `json:"dist_tags"`
|
||||
Versions map[string]json.RawMessage `json:"versions"`
|
||||
License string `json:"license,omitempty"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Author json.RawMessage `json:"author,omitempty"`
|
||||
Maintainers json.RawMessage `json:"maintainers,omitempty"`
|
||||
Repository json.RawMessage `json:"repository,omitempty"`
|
||||
Bugs json.RawMessage `json:"bugs,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// VersionDetail is returned for a single version query.
|
||||
type VersionDetail struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
Dependencies []Dependency `json:"dependencies"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Artifacts []Artifact `json:"artifacts,omitempty"`
|
||||
}
|
||||
|
||||
// Dependency represents a package dependency.
|
||||
type Dependency struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// DependencyList wraps the dependencies response.
|
||||
type DependencyList struct {
|
||||
Dependencies []json.RawMessage `json:"dependencies"`
|
||||
}
|
||||
|
||||
// DependentList wraps the dependents response.
|
||||
type DependentList struct {
|
||||
Dependents []json.RawMessage `json:"dependents"`
|
||||
}
|
||||
|
||||
// Artifact represents a platform-specific release artifact.
|
||||
type Artifact struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Variant string `json:"variant"`
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// APIError is returned when the server responds with an error.
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("registry: HTTP %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// --- Discovery ---
|
||||
|
||||
// Discover calls GET /.well-known/yao-registry.
|
||||
func (c *Client) Discover() (*RegistryInfo, error) {
|
||||
var info RegistryInfo
|
||||
if err := c.doGet("/.well-known/yao-registry", nil, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Info calls GET {apiPrefix}/.
|
||||
func (c *Client) Info() (*ServerInfo, error) {
|
||||
c.ensureDiscovered()
|
||||
var info ServerInfo
|
||||
if err := c.doGet(c.apiPrefix+"/", nil, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// --- List & Search ---
|
||||
|
||||
// List calls GET {apiPrefix}/:type with optional filters.
|
||||
func (c *Client) List(pkgType string, scope string, query string, page, pageSize int) (*ListResult, error) {
|
||||
c.ensureDiscovered()
|
||||
params := url.Values{}
|
||||
if scope != "" {
|
||||
params.Set("scope", scope)
|
||||
}
|
||||
if query != "" {
|
||||
params.Set("q", query)
|
||||
}
|
||||
if page > 0 {
|
||||
params.Set("page", fmt.Sprintf("%d", page))
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||
}
|
||||
var result ListResult
|
||||
if err := c.doGet(c.apiPrefix+"/"+pkgType, params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Search calls GET {apiPrefix}/search.
|
||||
func (c *Client) Search(q string, pkgType string, page, pageSize int) (*ListResult, error) {
|
||||
c.ensureDiscovered()
|
||||
params := url.Values{"q": {q}}
|
||||
if pkgType != "" {
|
||||
params.Set("type", pkgType)
|
||||
}
|
||||
if page > 0 {
|
||||
params.Set("page", fmt.Sprintf("%d", page))
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||
}
|
||||
var result ListResult
|
||||
if err := c.doGet(c.apiPrefix+"/search", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// --- Package metadata ---
|
||||
|
||||
// GetPackument calls GET {apiPrefix}/:type/:scope/:name.
|
||||
func (c *Client) GetPackument(pkgType, scope, name string) (*Packument, error) {
|
||||
c.ensureDiscovered()
|
||||
var p Packument
|
||||
path := fmt.Sprintf("%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name)
|
||||
if err := c.doGet(path, nil, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetVersion calls GET {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) GetVersion(pkgType, scope, name, version string) (*VersionDetail, error) {
|
||||
c.ensureDiscovered()
|
||||
var v VersionDetail
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
if err := c.doGet(path, nil, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// --- Dependencies ---
|
||||
|
||||
// GetDependencies calls GET {apiPrefix}/:type/:scope/:name/:version/dependencies.
|
||||
func (c *Client) GetDependencies(pkgType, scope, name, version string, recursive bool) (*DependencyList, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s/dependencies", c.apiPrefix, pkgType, scope, name, version)
|
||||
params := url.Values{}
|
||||
if recursive {
|
||||
params.Set("recursive", "true")
|
||||
}
|
||||
var dl DependencyList
|
||||
if err := c.doGet(path, params, &dl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dl, nil
|
||||
}
|
||||
|
||||
// GetDependents calls GET {apiPrefix}/:type/:scope/:name/dependents.
|
||||
func (c *Client) GetDependents(pkgType, scope, name string) (*DependentList, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/dependents", c.apiPrefix, pkgType, scope, name)
|
||||
var dl DependentList
|
||||
if err := c.doGet(path, nil, &dl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dl, nil
|
||||
}
|
||||
|
||||
// --- Push & Pull ---
|
||||
|
||||
// Push uploads a .yao.zip package via PUT {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) Push(pkgType, scope, name, version string, zipData []byte) (*PushResult, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(zipData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/zip")
|
||||
c.setAuth(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return nil, parseError(resp)
|
||||
}
|
||||
|
||||
var result PushResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Pull downloads a .yao.zip via GET {apiPrefix}/:type/:scope/:name/:version/pull.
|
||||
// The version parameter can be a semver or a dist-tag name.
|
||||
func (c *Client) Pull(pkgType, scope, name, version string) ([]byte, string, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s/pull", c.apiPrefix, pkgType, scope, name, version)
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, "", parseError(resp)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
digest := resp.Header.Get("X-Digest")
|
||||
return data, digest, nil
|
||||
}
|
||||
|
||||
// --- Tags ---
|
||||
|
||||
// SetTag calls PUT {apiPrefix}/:type/:scope/:name/tags/:tag.
|
||||
func (c *Client) SetTag(pkgType, scope, name, tag, version string) (*TagResult, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/tags/%s", c.apiPrefix, pkgType, scope, name, tag)
|
||||
body, _ := json.Marshal(map[string]string{"version": version})
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.setAuth(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseError(resp)
|
||||
}
|
||||
|
||||
var result TagResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteTag calls DELETE {apiPrefix}/:type/:scope/:name/tags/:tag.
|
||||
func (c *Client) DeleteTag(pkgType, scope, name, tag string) (*TagDeleteResult, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/tags/%s", c.apiPrefix, pkgType, scope, name, tag)
|
||||
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.setAuth(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseError(resp)
|
||||
}
|
||||
|
||||
var result TagDeleteResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// --- Delete ---
|
||||
|
||||
// DeleteVersion calls DELETE {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) DeleteVersion(pkgType, scope, name, version string) (*DeleteResult, error) {
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.setAuth(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseError(resp)
|
||||
}
|
||||
|
||||
var result DeleteResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// --- Internal helpers ---
|
||||
|
||||
func (c *Client) setAuth(req *http.Request) {
|
||||
if c.username != "" {
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) doGet(path string, params url.Values, out interface{}) error {
|
||||
u := c.baseURL + path
|
||||
if len(params) > 0 {
|
||||
u += "?" + params.Encode()
|
||||
}
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return parseError(resp)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var errResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" {
|
||||
return &APIError{StatusCode: resp.StatusCode, Message: errResp.Error}
|
||||
}
|
||||
return &APIError{StatusCode: resp.StatusCode, Message: string(body)}
|
||||
}
|
||||
1042
registry/client_test.go
Normal file
1042
registry/client_test.go
Normal file
File diff suppressed because it is too large
Load diff
70
registry/testdata/build.go
vendored
Normal file
70
registry/testdata/build.go
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Package testdata provides helpers to build .yao.zip test fixtures in memory.
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Manifest mirrors the pkg.yao structure for test fixture construction.
|
||||
type Manifest struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Dependencies []ManifestDep `json:"dependencies,omitempty"`
|
||||
Engines map[string]string `json:"engines,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author *ManifestAuthor `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// ManifestDep represents a dependency entry in pkg.yao.
|
||||
type ManifestDep struct {
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// ManifestAuthor holds author information.
|
||||
type ManifestAuthor struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
// BuildZip creates an in-memory .yao.zip with a package/pkg.yao manifest
|
||||
// and an optional set of extra files (path relative to package/) -> content.
|
||||
func BuildZip(manifest *Manifest, extraFiles map[string]string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
|
||||
data, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := w.Create("package/pkg.yao")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name, content := range extraFiles {
|
||||
f, err := w.Create("package/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.Write([]byte(content)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue