Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/code-storage-go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,9 @@ func (c *Client) generateJWT(repoID string, options RemoteURLOptions) (string, e
"iat": issuedAt.Unix(),
"exp": issuedAt.Add(ttl).Unix(),
}
if len(options.Refs) > 0 {
claims["refs"] = encodeRefsClaim(options.Refs)
}
if len(options.Ops) > 0 {
claims["ops"] = options.Ops
}
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (b *CommitBuilder) Send(ctx context.Context) (CommitResult, error) {
}

ttl := resolveCommitTTL(b.options.InvocationOptions, defaultTokenTTL)
jwtToken, err := b.client.generateJWT(b.repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := b.client.generateJWT(b.repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: b.options.Refs})
if err != nil {
return CommitResult{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/diff_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (d *diffCommitExecutor) send(ctx context.Context, repoID string) (CommitRes
}

ttl := resolveCommitTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := d.client.generateJWT(repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := d.client.generateJWT(repoID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return CommitResult{}, err
}
Expand Down
14 changes: 14 additions & 0 deletions packages/code-storage-go/jwt_claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package storage

// encodeRefsClaim builds the JWT `refs` claim as an array of [pattern, ops] tuples.
func encodeRefsClaim(refs RefPolicies) []any {
out := make([]any, len(refs))
for i, rule := range refs {
opList := []string(rule.Ops)
if opList == nil {
opList = []string{}
}
out[i] = []any{rule.Pattern, opList}
}
Comment on lines +6 to +12
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug. Op is a Go type alias (type Op = string), not a defined type, so Ops is literally []string and []string(rule.Ops) is a no-op conversion that compiles fine. Verified by go build ./... && go test ./... green on this branch.

return out
}
36 changes: 36 additions & 0 deletions packages/code-storage-go/jwt_claims_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package storage

import "testing"

func TestEncodeRefsClaim(t *testing.T) {
encoded := encodeRefsClaim(RefPolicies{
{Pattern: "refs/heads/main", Ops: Ops{OpNoPush}},
{Pattern: "refs/heads/release/*", Ops: nil},
{Pattern: "*", Ops: Ops{OpNoForcePush}},
})

if len(encoded) != 3 {
t.Fatalf("expected 3 rules, got %d", len(encoded))
}

mainRule, ok := encoded[0].([]any)
if !ok {
t.Fatalf("unexpected rule type: %T", encoded[0])
}
if mainRule[0] != "refs/heads/main" {
t.Fatalf("unexpected pattern: %v", mainRule[0])
}
mainOps, ok := mainRule[1].([]string)
if !ok || len(mainOps) != 1 || mainOps[0] != OpNoPush {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the previous comment — Op is an alias (type Op = string), so mainOps[0] (type string) compared to OpNoPush (also string via the alias) is a same-type comparison. Tests pass.

t.Fatalf("unexpected ops: %v", mainRule[1])
}

releaseRule, ok := encoded[1].([]any)
if !ok {
t.Fatalf("unexpected rule type: %T", encoded[1])
}
releaseOps, ok := releaseRule[1].([]string)
if !ok || len(releaseOps) != 0 {
t.Fatalf("expected empty ops slice, got %v", releaseRule[1])
}
}
24 changes: 12 additions & 12 deletions packages/code-storage-go/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,12 @@ func (r *Repo) GetNote(ctx context.Context, options GetNoteOptions) (GetNoteResu

// CreateNote adds a git note.
func (r *Repo) CreateNote(ctx context.Context, options CreateNoteOptions) (NoteWriteResult, error) {
return r.writeNote(ctx, options.InvocationOptions, "add", options.SHA, options.Note, options.ExpectedRefSHA, options.Author)
return r.writeNote(ctx, options.InvocationOptions, "add", options.SHA, options.Note, options.ExpectedRefSHA, options.Author, options.Refs)
}

// AppendNote appends to a git note.
func (r *Repo) AppendNote(ctx context.Context, options AppendNoteOptions) (NoteWriteResult, error) {
return r.writeNote(ctx, options.InvocationOptions, "append", options.SHA, options.Note, options.ExpectedRefSHA, options.Author)
return r.writeNote(ctx, options.InvocationOptions, "append", options.SHA, options.Note, options.ExpectedRefSHA, options.Author, options.Refs)
}

// DeleteNote deletes a git note.
Expand All @@ -552,7 +552,7 @@ func (r *Repo) DeleteNote(ctx context.Context, options DeleteNoteOptions) (NoteW
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return NoteWriteResult{}, err
}
Expand Down Expand Up @@ -592,7 +592,7 @@ func (r *Repo) DeleteNote(ctx context.Context, options DeleteNoteOptions) (NoteW
return result, nil
}

func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, action string, sha string, note string, expectedRefSHA string, author *NoteAuthor) (NoteWriteResult, error) {
func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, action string, sha string, note string, expectedRefSHA string, author *NoteAuthor, refs RefPolicies) (NoteWriteResult, error) {
sha = strings.TrimSpace(sha)
if sha == "" {
return NoteWriteResult{}, errors.New("note sha is required")
Expand All @@ -604,7 +604,7 @@ func (r *Repo) writeNote(ctx context.Context, invocation InvocationOptions, acti
}

ttl := resolveInvocationTTL(invocation, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: refs})
if err != nil {
return NoteWriteResult{}, err
}
Expand Down Expand Up @@ -862,7 +862,7 @@ func (r *Repo) Grep(ctx context.Context, options GrepOptions) (GrepResult, error
// PullUpstream triggers a pull-upstream operation.
func (r *Repo) PullUpstream(ctx context.Context, options PullUpstreamOptions) error {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return err
}
Expand Down Expand Up @@ -897,7 +897,7 @@ func (r *Repo) CreateBranch(ctx context.Context, options CreateBranchOptions) (C
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return CreateBranchResult{}, err
}
Expand Down Expand Up @@ -944,7 +944,7 @@ func (r *Repo) DeleteBranch(ctx context.Context, options DeleteBranchOptions) (D
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return DeleteBranchResult{}, err
}
Expand Down Expand Up @@ -1020,7 +1020,7 @@ func (r *Repo) Merge(ctx context.Context, options MergeOptions) (MergeResult, er
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return MergeResult{}, err
}
Expand Down Expand Up @@ -1081,7 +1081,7 @@ func (r *Repo) CreateTag(ctx context.Context, options CreateTagOptions) (CreateT
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return CreateTagResult{}, err
}
Expand Down Expand Up @@ -1116,7 +1116,7 @@ func (r *Repo) DeleteTag(ctx context.Context, options DeleteTagOptions) (DeleteT
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead, PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead, PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return DeleteTagResult{}, err
}
Expand Down Expand Up @@ -1159,7 +1159,7 @@ func (r *Repo) RestoreCommit(ctx context.Context, options RestoreCommitOptions)
}

ttl := resolveCommitTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl})
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitWrite}, TTL: ttl, Refs: options.Refs})
if err != nil {
return RestoreCommitResult{}, err
}
Expand Down
50 changes: 50 additions & 0 deletions packages/code-storage-go/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,56 @@ func TestRemoteURLOps(t *testing.T) {
})
}

func TestRemoteURLRefs(t *testing.T) {
client, err := NewClient(Options{Name: "acme", Key: testKey, StorageBaseURL: "acme.code.storage"})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "repo-1", DefaultBranch: "main", client: client}

t.Run("includes refs in JWT when provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{
Refs: RefPolicies{
{Pattern: "refs/heads/main", Ops: Ops{OpNoPush}},
{Pattern: "*", Ops: Ops{OpNoForcePush}},
},
})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
refs, ok := claims["refs"].([]interface{})
if !ok {
t.Fatalf("expected refs claim to be a list, got %T", claims["refs"])
}
if len(refs) != 2 {
t.Fatalf("expected 2 ref rules, got %d", len(refs))
}
mainRule, ok := refs[0].([]interface{})
if !ok || len(mainRule) != 2 {
t.Fatalf("unexpected main rule shape: %v", refs[0])
}
if mainRule[0] != "refs/heads/main" {
t.Fatalf("unexpected pattern: %v", mainRule[0])
}
mainOps, ok := mainRule[1].([]interface{})
if !ok || len(mainOps) != 1 || mainOps[0] != "no-push" {
t.Fatalf("unexpected main ops: %v", mainRule[1])
}
})

t.Run("omits refs from JWT when not provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
if _, ok := claims["refs"]; ok {
t.Fatalf("expected no refs claim")
}
})
}

func TestListFilesEphemeral(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/files" {
Expand Down
36 changes: 36 additions & 0 deletions packages/code-storage-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,28 @@ type Op = string

const (
OpNoForcePush Op = "no-force-push"
OpNoPush Op = "no-push"
)

// Ops is a list of policy operations.
type Ops []Op

// RefPolicy is a single ordered ref-matching policy rule (first match wins).
type RefPolicy struct {
Pattern string
Ops Ops
}

// RefPolicies is an ordered list of per-ref policy rules for the JWT `refs` claim.
type RefPolicies []RefPolicy

// RemoteURLOptions configure token generation for remote URLs.
type RemoteURLOptions struct {
Permissions []Permission
TTL time.Duration
Ops Ops
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// InvocationOptions holds common request options.
Expand Down Expand Up @@ -232,6 +244,8 @@ type ArchiveOptions struct {
type PullUpstreamOptions struct {
InvocationOptions
Ref string
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// ListFilesOptions configures list files.
Expand Down Expand Up @@ -309,6 +323,8 @@ type CreateBranchOptions struct {
TargetBranch string
BaseIsEphemeral bool
TargetIsEphemeral bool
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// CreateBranchResult describes branch creation result.
Expand All @@ -324,6 +340,8 @@ type DeleteBranchOptions struct {
InvocationOptions
Name string
Ephemeral *bool
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// DeleteBranchResult describes branch deletion result.
Expand Down Expand Up @@ -357,6 +375,8 @@ type MergeOptions struct {
AllowUnrelatedHistories bool
// Squash is incompatible with MergeStrategyFFOnly.
Squash bool
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// MergeResultStatus describes a merge operation outcome.
Expand Down Expand Up @@ -422,6 +442,8 @@ type CreateTagOptions struct {
InvocationOptions
Name string
Target string
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// CreateTagResult describes tag creation result.
Expand All @@ -435,6 +457,8 @@ type CreateTagResult struct {
type DeleteTagOptions struct {
InvocationOptions
Name string
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// DeleteTagResult describes tag deletion result.
Expand Down Expand Up @@ -544,6 +568,8 @@ type CreateNoteOptions struct {
Note string
ExpectedRefSHA string
Author *NoteAuthor
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// AppendNoteOptions configures note append.
Expand All @@ -553,6 +579,8 @@ type AppendNoteOptions struct {
Note string
ExpectedRefSHA string
Author *NoteAuthor
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// DeleteNoteOptions configures note delete.
Expand All @@ -561,6 +589,8 @@ type DeleteNoteOptions struct {
SHA string
ExpectedRefSHA string
Author *NoteAuthor
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// NoteWriteResult describes note write response.
Expand Down Expand Up @@ -799,6 +829,8 @@ type CommitOptions struct {
EphemeralBase bool
Author CommitSignature
Committer *CommitSignature
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// CommitFromDiffOptions configures diff commit.
Expand All @@ -813,6 +845,8 @@ type CommitFromDiffOptions struct {
EphemeralBase bool
Author CommitSignature
Committer *CommitSignature
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// RestoreCommitOptions configures restore commit.
Expand All @@ -824,6 +858,8 @@ type RestoreCommitOptions struct {
ExpectedHeadSHA string
Author CommitSignature
Committer *CommitSignature
// Refs is evaluated in declaration order; the first matching rule wins.
Refs RefPolicies
}

// RestoreCommitResult describes restore commit.
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-go/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package storage

const (
PackageName = "code-storage-go-sdk"
PackageVersion = "0.8.0"
PackageVersion = "0.9.0"
)

func userAgent() string {
Expand Down
Loading
Loading