Skip to content
Merged
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ go test ./internal/prompts/ -count=1 -v # Prompts

### Running E2E tests

The suite drives the **real `cmd/server` binary** over stdio against a real GitLab, so a tool call crosses a process boundary and then the network; read `test/e2e/README.md` before changing any of it. **The package decides the runtime, not a build tag**: `test/e2e/gitlab/common` for what any instance serves (176 test files), `ce` for what only an unlicensed one does (6), `ee` for Premium and Ultimate (49), all three compiled together under the one `e2e` tag. Every target stages the binary first, so none of them is a bare `go test`:
The suite drives the **real `cmd/server` binary** over stdio against a real GitLab, so a tool call crosses a process boundary and then the network; read `test/e2e/README.md` before changing any of it. **The package decides the runtime, not a build tag**: `test/e2e/gitlab/common` for what any instance serves (176 test files), `ce` for what only an unlicensed one does (7), `ee` for Premium and Ultimate (49), all three compiled together under the one `e2e` tag. Every target stages the binary first, so none of them is a bare `go test`:

```bash
make test-e2e # self-hosted, from .env (GITLAB_URL, GITLAB_TOKEN); a package the instance cannot serve skips
Expand Down
19 changes: 19 additions & 0 deletions cmd/audit_1to1/internal/structs/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ const (
// deploymentMergeRequestsInert is the reason three merge request list
// options are absent from the deployment merge request input.
deploymentMergeRequestsInert = "declared by GET /projects/:id/deployments/:deployment_id/merge_requests through merge_requests_base_params and read by nothing there: lib/api/deployments.rb presents MergeRequestBasic with current_user alone, and only serializer_options_for in lib/api/merge_requests.rb turns the option into a presenter option; exposed on the merge request list inputs, where it takes effect"
// notificationNewEpicNotProjects is the reason the shared notification
// events leave out new_epic, which client-go's options carry for all three
// update routes.
notificationNewEpicNotProjects = "declared for PUT /notification_settings and PUT /groups/:id/notification_settings and not for PUT /projects/:id/notification_settings (EE::NotificationSetting::EMAIL_EVENTS_MAPPING, ee/app/models/ee/notification_setting.rb:7-17 at v19.5.0-ee; gitlab-api-live.json, 19.5.0), so Grape drops it from a project's update on every tier; the account-wide and group inputs embed it as epicEvent and their handlers send it"
// projectChecksumFiltersUnread is the reason the project lists offer
// neither checksum filter, which client-go's options carry.
projectChecksumFiltersUnread = "declared by every route that takes the project list filters (optional_filter_params_ee, ee/lib/ee/api/helpers/projects_helpers.rb:38-42 at v19.5.0-ee) and read by nothing: the finder parameters the routes build carry neither (EE::API::Helpers#project_finder_params_ee, ee/lib/ee/api/helpers.rb:119-127), and no finder, model or service names them, so a list asked for the failed checksums answered every project on every tier"
// skypeDiscarded is the reason the user create and modify inputs offer no
// skype, which client-go's options still carry.
skypeDiscarded = "GitLab discards it: the users.skype column is ignored since 18.4 and neither POST /users nor PUT /users/:id declares the param, so a caller who set it changed nothing"
Expand Down Expand Up @@ -1114,6 +1121,18 @@ var acceptedMissingInputs = &declarationTable{name: "acceptedMissingInputs", ent
// route declares the param.
"users.CreateInput.job_title": jobTitleDiscarded,
"users.ModifyInput.job_title": jobTitleDiscarded,
// An SDK options field one of the routes it is sent to does not declare.
// The three notification updates share one options struct, and GitLab
// declares new_epic for the account-wide and the group settings and not
// for a project's (EE::NotificationSetting::EMAIL_EVENTS_MAPPING), so the
// shared events the builder copies leave it out and the two inputs that
// take it embed it on their own.
"notifications.eventFields.new_epic": notificationNewEpicNotProjects,
// SDK options fields every route that takes them declares and none reads.
// Every project list goes through userProjectFilter, so the two keys
// answer all five lists at once.
"projects.userProjectFilter.repository_checksum_failed": projectChecksumFiltersUnread,
"projects.userProjectFilter.wiki_checksum_failed": projectChecksumFiltersUnread,

// Params modeled on a nested object / slice element per the full-nested-object
// policy (the auditor flattens the SDK nested options into the parent input).
Expand Down
19 changes: 19 additions & 0 deletions cmd/audit_edition_tier/param_declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ const (
// reasonDuoNeedsAPaidTierOrAnAddOn answers the two Duo settings of a group
// update.
reasonDuoNeedsAPaidTierOrAnAddOn = "GitLab stores the setting on every Enterprise Edition tier (ee/lib/ee/api/helpers/groups_helpers.rb:102-107 at v19.5.0-ee), and GitLab Duo, which it switches, answers where the group's licensed AI features are available: Premium carries ai_chat, Ultimate ai_features, and a Free namespace has them only with a GitLab Credits add-on (EE::Group#licensed_ai_features_available?, ee/app/models/ee/group.rb:368-372, and EE::Namespace#has_active_gitlab_credits_add_on? on the root ancestor, ee/app/models/ee/namespace.rb:336-348). The response carries the setting only there (ee/lib/ee/api/entities/group.rb:47-56). A tag would take it from a Free namespace with credits, so the description says so instead (issue 1240)."
// reasonMirrorRegexOnEveryTier answers the branch regex of a push mirror
// create and update.
reasonMirrorRegexOnEveryTier = "Enterprise Edition declares mirror_branch_regex on the push mirror create and update with no license check (EE::API::Helpers::RemoteMirrorsHelpers, ee/lib/ee/api/helpers/remote_mirrors_helpers.rb:11-14 at v19.5.0-ee), lets the services save it (EE::RemoteMirrors::Attributes, ee/app/models/ee/remote_mirrors/attributes.rb:8-24) and pushes only the branches it matches on every tier (EE::RemoteMirror#options_for_update and #branches_to_sync, ee/app/models/ee/remote_mirror.rb:22-35). Community Edition does not declare it (lib/api/helpers/remote_mirrors_helpers.rb:13-14) and drops it, as it drops every Enterprise Edition parameter, which the tier model cannot tell from Free (issue 1240)."
// reasonHiddenProjectsOnEveryTier answers the hidden project filter of the
// project list.
reasonHiddenProjectsOnEveryTier = "Enterprise Edition applies include_hidden for an administrator on every tier, with no license check: the list keeps it only for a caller who can administer all resources (EE::API::Projects#verify_project_filters!, ee/lib/ee/api/projects.rb:116-121 at v19.5.0-ee), hands it to the finder (EE::API::Helpers#project_finder_params_ee, ee/lib/ee/api/helpers.rb:119-127), and EE::ProjectsFinder#by_hidden returns the hidden projects only when it is set (ee/app/finders/ee/projects_finder.rb:49-51). Community Edition does not declare it, as it declares no Enterprise Edition parameter, and its finder hides no project (issue 1240)."
// reasonDuoCodeReviewWhereAvailable answers the automatic Duo code review
// of a project update.
reasonDuoCodeReviewWhereAvailable = "GitLab keeps the setting only where Duo code review is available to the project, which the row's Ultimate does not describe: the update deletes it unless the instance's license carries review_merge_request, a Premium feature (EE::API::Helpers::ProjectsHelpers#filter_attributes_using_license!, ee/lib/ee/api/helpers/projects_helpers.rb:144-149 at v19.5.0-ee, and ee/app/models/gitlab_subscriptions/features.rb:221), and the project has Duo code review, through the Duo Agent Platform's code review flow or a Duo Enterprise add-on (EE::Project#auto_duo_code_review_settings_available? and #duo_code_review_available?, ee/app/models/ee/project.rb:741-743 and 761-777). The platform is open to a Free group with a GitLab Credits add-on (Gitlab::Llm::StageCheck.available?, ee/lib/gitlab/llm/stage_check.rb:19-32), which on GitLab.com, whose license carries every feature, keeps the setting below Premium. A tag would take it from that group, so the description says where it applies (issue 1240)."
// reasonOneAssigneeBelowPremium answers the assignees of an issue create.
reasonOneAssigneeBelowPremium = "Every tier takes the parameter, and what needs Premium is a second assignee: IssuableBaseService#filter_assignees_using_checks keeps only the first ID unless the issue allows multiple assignees (app/services/issuable_base_service.rb:117-122 at v19.5.0-ee), which Community Edition never does (Issuable#allows_multiple_assignees?, app/models/concerns/issuable.rb:189-191) and Enterprise Edition does where multiple_issue_assignees is licensed (EE::Issue#allows_multiple_assignees?, ee/app/models/ee/issue.rb:195-197), a feature of the starter list every Premium license carries (ee/app/models/gitlab_subscriptions/features.rb:67 and 363). On GitLab.com, whose license carries every feature, the namespace's plan decides (EE::Project#load_licensed_feature_available, ee/app/models/ee/project.rb:1876-1886), so a Free namespace keeps the first as an unlicensed instance does, and the description says both (issue 1240)."
)

// declaredParamAnswers holds the answer to every parameter finding that is
Expand Down Expand Up @@ -145,6 +156,14 @@ var declaredParamAnswers = map[string]paramDeclaration{
"ci_variable.group_delete filter": {Documented: tierPremium, Category: categoryNotGated, Reason: reasonVariableFilterOnEveryTier},
"ci_variable.group_update environment_scope": {Documented: tierPremium, Category: categoryNotGated, Reason: reasonUpdateScopeIsTheFilter},
"ci_variable.group_create environment_scope": {Documented: tierPremium, Category: categoryValueLevelGate, Reason: reasonVariableScopeOnCreate},

"issue.create assignee_ids": {Documented: tierPremium, Category: categoryValueLevelGate, Reason: reasonOneAssigneeBelowPremium},

"project.mirror_add mirror_branch_regex": {Documented: tierPremium, Category: categoryNotGated, Reason: reasonMirrorRegexOnEveryTier},
"project.mirror_edit mirror_branch_regex": {Documented: tierPremium, Category: categoryNotGated, Reason: reasonMirrorRegexOnEveryTier},

"project.list include_hidden": {Documented: tierPremium, Category: categoryNotGated, Reason: reasonHiddenProjectsOnEveryTier},
"project.update auto_duo_code_review_enabled": {Documented: tierUltimate, Category: categoryValueLevelGate, Reason: reasonDuoCodeReviewWhereAvailable},
}

// answeredParamFinding is a finding a declaration answers, with the answer.
Expand Down
Loading
Loading