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
67 changes: 50 additions & 17 deletions internal/forge/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@ var _ GitHubExtensions = (*FakeClient)(nil)
// NewFakeClient returns a FakeClient with all maps initialised.
func NewFakeClient() *FakeClient {
return &FakeClient{
FileContents: make(map[string][]byte),
WorkflowRuns: make(map[string]*WorkflowRun),
Secrets: make(map[string]bool),
VariablesExist: make(map[string]bool),
VariableValues: make(map[string]string),
Errors: make(map[string]error),
DirContents: make(map[string][]DirectoryEntry),
FileContentsRef: make(map[string][]byte),
BranchRefs: make(map[string]string),
ExistingBranches: make(map[string]bool),
Refs: make(map[string]string),
ProtectedBranches: make(map[string]bool),
ProtectedBranchRules: make(map[string]*ProtectedBranchRule),
PipelineSchedules: make(map[string][]PipelineSchedule),
ForceReachableCommits: make(map[string]int),
PipelineTriggerTokens: make(map[string][]PipelineTriggerToken),
ProjectHooks: make(map[string][]ProjectHook),
FileContents: make(map[string][]byte),
WorkflowRuns: make(map[string]*WorkflowRun),
Secrets: make(map[string]bool),
VariablesExist: make(map[string]bool),
VariableValues: make(map[string]string),
Errors: make(map[string]error),
DirContents: make(map[string][]DirectoryEntry),
FileContentsRef: make(map[string][]byte),
BranchRefs: make(map[string]string),
ExistingBranches: make(map[string]bool),
Refs: make(map[string]string),
ProtectedBranches: make(map[string]bool),
ProtectedBranchRules: make(map[string]*ProtectedBranchRule),
PipelineSchedules: make(map[string][]PipelineSchedule),
ForceReachableCommits: make(map[string]int),
PipelineTriggerTokens: make(map[string][]PipelineTriggerToken),
ProjectHooks: make(map[string][]ProjectHook),
PipelineVarOverrideRoles: make(map[string]string),
}
}

Expand Down Expand Up @@ -240,6 +241,11 @@ type FakeClient struct {
// ProjectHooks stores project webhooks keyed by "owner/repo".
ProjectHooks map[string][]ProjectHook

// PipelineVarOverrideRoles stores the GitLab
// ci_pipeline_variables_minimum_override_role setting per project.
// Key: "owner/repo". Missing keys round-trip as empty string.
PipelineVarOverrideRoles map[string]string

// Directory listings for ListDirectoryContents.
DirContents map[string][]DirectoryEntry // key: "owner/repo/path@ref"

Expand Down Expand Up @@ -2488,6 +2494,33 @@ func (f *FakeClient) CreateProtectedCIVariable(_ context.Context, owner, repo, n
return nil
}

func (f *FakeClient) GetPipelineVariablesMinimumOverrideRole(_ context.Context, owner, repo string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()

if e := f.err("GetPipelineVariablesMinimumOverrideRole"); e != nil {
return "", e
}
return f.PipelineVarOverrideRoles[owner+"/"+repo], nil
}

func (f *FakeClient) SetPipelineVariablesMinimumOverrideRole(_ context.Context, owner, repo, role string) error {
f.mu.Lock()
defer f.mu.Unlock()

if e := f.err("SetPipelineVariablesMinimumOverrideRole"); e != nil {
return e
}
if err := ValidatePipelineVarOverrideRole(role); err != nil {
return err
}
if f.PipelineVarOverrideRoles == nil {
f.PipelineVarOverrideRoles = make(map[string]string)
}
f.PipelineVarOverrideRoles[owner+"/"+repo] = role
return nil
}

func (f *FakeClient) CompareCommits(_ context.Context, owner, repo, base, head string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
Expand Down
68 changes: 68 additions & 0 deletions internal/forge/fake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,13 @@ func TestFakeClient_ErrorInjection(t *testing.T) {
{"ForceCommitFileToBranch", func(fc *FakeClient) error {
return fc.ForceCommitFileToBranch(ctx, "o", "r", "b", "p", "m", []byte("c"))
}},
{"GetPipelineVariablesMinimumOverrideRole", func(fc *FakeClient) error {
_, err := fc.GetPipelineVariablesMinimumOverrideRole(ctx, "o", "r")
return err
}},
{"SetPipelineVariablesMinimumOverrideRole", func(fc *FakeClient) error {
return fc.SetPipelineVariablesMinimumOverrideRole(ctx, "o", "r", PipelineVarOverrideOwner)
}},
{"CreateOrUpdateOrgVariable", func(fc *FakeClient) error {
return fc.CreateOrUpdateOrgVariable(ctx, "o", "n", "v", nil)
}},
Expand Down Expand Up @@ -1032,6 +1039,8 @@ func TestFakeClient_ThreadSafety(t *testing.T) {
_, _ = fc.ListProjectHooks(ctx, "o", "r")
_, _ = fc.UpdateProjectHook(ctx, "o", "r", 1, ProjectHook{})
_ = fc.DeleteProjectHook(ctx, "o", "r", 1)
_ = fc.SetPipelineVariablesMinimumOverrideRole(ctx, "o", "r", PipelineVarOverrideOwner)
_, _ = fc.GetPipelineVariablesMinimumOverrideRole(ctx, "o", "r")
}(i)
}

Expand Down Expand Up @@ -2057,6 +2066,65 @@ func TestFakeClient_UpdateRepoVisibility(t *testing.T) {
})
}

func TestFakeClient_PipelineVariablesMinimumOverrideRole(t *testing.T) {
ctx := context.Background()

t.Run("round-trips a set value through a subsequent get", func(t *testing.T) {
fc := NewFakeClient()
err := fc.SetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo", PipelineVarOverrideOwner)
require.NoError(t, err)

got, err := fc.GetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo")
require.NoError(t, err)
assert.Equal(t, PipelineVarOverrideOwner, got)
})

t.Run("get on unset project returns empty string", func(t *testing.T) {
fc := NewFakeClient()
got, err := fc.GetPipelineVariablesMinimumOverrideRole(ctx, "org", "missing")
require.NoError(t, err)
assert.Empty(t, got)
})

t.Run("returns injected get error", func(t *testing.T) {
fc := &FakeClient{
Errors: map[string]error{"GetPipelineVariablesMinimumOverrideRole": errors.New("forbidden")},
}
_, err := fc.GetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo")
require.Error(t, err)
assert.Contains(t, err.Error(), "forbidden")
})

t.Run("returns injected set error", func(t *testing.T) {
fc := &FakeClient{
Errors: map[string]error{"SetPipelineVariablesMinimumOverrideRole": errors.New("forbidden")},
}
err := fc.SetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo", PipelineVarOverrideOwner)
require.Error(t, err)
assert.Contains(t, err.Error(), "forbidden")
})

t.Run("rejects an unknown role and does not store it", func(t *testing.T) {
fc := NewFakeClient()
err := fc.SetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo", "admin")
require.Error(t, err)
assert.ErrorIs(t, err, ErrInvalidPipelineVarOverrideRole)

got, err := fc.GetPipelineVariablesMinimumOverrideRole(ctx, "org", "repo")
require.NoError(t, err)
assert.Empty(t, got)
})

t.Run("PipelineVarOverrideRoles is initialised by NewFakeClient", func(t *testing.T) {
fc := NewFakeClient()
// Mirrors the direct-assignment seeding convention used by sibling
// maps (e.g. ProtectedBranches) elsewhere in this file; would panic
// on a nil map.
fc.PipelineVarOverrideRoles["org/repo"] = PipelineVarOverrideOwner
assert.Equal(t, PipelineVarOverrideOwner, fc.PipelineVarOverrideRoles["org/repo"])
})
}

func TestFakeClient_GetOrgPlan(t *testing.T) {
ctx := context.Background()
fc := &FakeClient{}
Expand Down
56 changes: 56 additions & 0 deletions internal/forge/forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,43 @@ const (
VarGitLabRoleRotation = "FULLSEND_GITLAB_ROLE_ROTATION"
)

// GitLab CI pipeline-variable minimum-override roles.
//
// These are the documented values of GitLab's
// ci_pipeline_variables_minimum_override_role project setting
// (GitLab >= 17.1). The setting is a minimum-role gate: only identities
// at that role or above may pass user-defined variables on pipeline
// create/trigger. GitHub has no equivalent; its client returns
// ErrNotSupported.
const (
// PipelineVarOverrideNoOneAllowed rejects every user-defined pipeline
// variable, including those sent by an Owner PAT via CreatePipeline.
PipelineVarOverrideNoOneAllowed = "no_one_allowed"
PipelineVarOverrideDeveloper = "developer"
PipelineVarOverrideMaintainer = "maintainer"
PipelineVarOverrideOwner = "owner"
)

// ErrInvalidPipelineVarOverrideRole indicates that a caller supplied a
// value for ci_pipeline_variables_minimum_override_role that is not one of
// the four documented roles.
var ErrInvalidPipelineVarOverrideRole = errors.New("invalid pipeline variable override role")

// ValidatePipelineVarOverrideRole reports an error if role is not one of
// the documented ci_pipeline_variables_minimum_override_role values
// (PipelineVarOverrideNoOneAllowed, PipelineVarOverrideDeveloper,
// PipelineVarOverrideMaintainer, PipelineVarOverrideOwner). Shared by the
// GitLab LiveClient and FakeClient so both reject typos and unknown values
// the same way before persisting or sending them.
func ValidatePipelineVarOverrideRole(role string) error {
switch role {
case PipelineVarOverrideNoOneAllowed, PipelineVarOverrideDeveloper, PipelineVarOverrideMaintainer, PipelineVarOverrideOwner:
return nil
default:
return fmt.Errorf("%w: %q", ErrInvalidPipelineVarOverrideRole, role)
}
}

// ErrNotFound indicates a requested resource was not found on the forge.
var ErrNotFound = errors.New("not found")

Expand Down Expand Up @@ -945,6 +982,25 @@ type Client interface {
// Returns ErrNotFound if the hook does not exist.
DeleteProjectHook(ctx context.Context, owner, repo string, hookID int64) error

// GetPipelineVariablesMinimumOverrideRole returns the GitLab project
// setting that gates who may pass user-defined variables when creating
// or triggering a pipeline (ci_pipeline_variables_minimum_override_role).
// Valid values are no_one_allowed, developer, maintainer, and owner.
// Returns ("", nil) if the field is absent from GitLab's response
// (e.g. an older GitLab instance or an edition that doesn't expose the
// setting) — callers cannot distinguish that case from a project whose
// role was explicitly read as empty, since GitLab never returns an
// empty string for a populated field. Returns ErrNotFound if the
// project does not exist. GitHub returns ErrNotSupported.
GetPipelineVariablesMinimumOverrideRole(ctx context.Context, owner, repo string) (string, error)
// SetPipelineVariablesMinimumOverrideRole updates that setting via
// PUT /projects/:id. role must be one of the documented GitLab values
// (see ValidatePipelineVarOverrideRole); implementations reject any
// other value, including empty string, without making a request.
// Returns ErrNotFound if the project does not exist. GitHub returns
// ErrNotSupported.
SetPipelineVariablesMinimumOverrideRole(ctx context.Context, owner, repo, role string) error

// Commit comparison
// CompareCommits compares two commits and returns their relationship
// status: "ahead" (head is ahead of base), "behind" (head is behind
Expand Down
10 changes: 10 additions & 0 deletions internal/forge/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -4117,6 +4117,16 @@ func (c *LiveClient) DeleteProjectHook(_ context.Context, _, _ string, _ int64)
return forge.ErrNotSupported
}

// GetPipelineVariablesMinimumOverrideRole is not supported on GitHub.
func (c *LiveClient) GetPipelineVariablesMinimumOverrideRole(_ context.Context, _, _ string) (string, error) {
return "", forge.ErrNotSupported
}

// SetPipelineVariablesMinimumOverrideRole is not supported on GitHub.
func (c *LiveClient) SetPipelineVariablesMinimumOverrideRole(_ context.Context, _, _, _ string) error {
return forge.ErrNotSupported
}

// ForceCommitFileToBranch is not supported on GitHub.
func (c *LiveClient) ForceCommitFileToBranch(_ context.Context, _, _, _, _, _ string, _ []byte) error {
return forge.ErrNotSupported
Expand Down
8 changes: 8 additions & 0 deletions internal/forge/github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4587,6 +4587,14 @@ func TestUnsupportedMethods(t *testing.T) {
err := client.DeleteProjectHook(ctx, "o", "r", 1)
assert.ErrorIs(t, err, forge.ErrNotSupported)
})
t.Run("GetPipelineVariablesMinimumOverrideRole", func(t *testing.T) {
_, err := client.GetPipelineVariablesMinimumOverrideRole(ctx, "o", "r")
assert.ErrorIs(t, err, forge.ErrNotSupported)
})
t.Run("SetPipelineVariablesMinimumOverrideRole", func(t *testing.T) {
err := client.SetPipelineVariablesMinimumOverrideRole(ctx, "o", "r", forge.PipelineVarOverrideOwner)
assert.ErrorIs(t, err, forge.ErrNotSupported)
})
t.Run("ForceCommitFileToBranch", func(t *testing.T) {
err := client.ForceCommitFileToBranch(ctx, "o", "r", "b", "p", "m", []byte("c"))
assert.ErrorIs(t, err, forge.ErrNotSupported)
Expand Down
Loading
Loading