Skip to content

feat(roadmap-planner): W1 — member allowlist + GitLab instance-wide sweep#189

Merged
danielfbm merged 2 commits into
mainfrom
feat/metrics-w1-member-allowlist
May 19, 2026
Merged

feat(roadmap-planner): W1 — member allowlist + GitLab instance-wide sweep#189
danielfbm merged 2 commits into
mainfrom
feat/metrics-w1-member-allowlist

Conversation

@danielfbm

Copy link
Copy Markdown
Contributor

First workstream of the 2026-05-19 metrics audit follow-up — see #188 for the audit report and full plan.

Summary

  • GitLab with_shared=false on ListGroupProjects (root cause of audit finding B1 — container-platform/*, alauda/artifacts etc. shouldn't have been ingested under devops/**).
  • Pass B per-member instance-wide MR sweep behind gitlab.member_instance_sweep so allowlisted members' work in projects outside the configured groups finally lands in the rollup.
  • W1 allowlist derived from team_analytics.{github,gitlab}_username_prefills ∪ {bot} minus the new team_analytics.member_denylist. The aggregator and /api/contributions/members + /team both filter on it when enabled. Activation is opt-in — empty config means pre-W1 behaviour.

What's in this diff

Layer File Change
Config internal/config/config.go TeamAnalytics.MemberDenylist []string, GitLab.MemberInstanceSweep bool
Helpers internal/contributions/allowlist.go (new) BuildAllowlist, Allowlist.Contains/IDs/Enabled
Aggregator internal/contributions/aggregator.go SetAllowlist; per-INSERT AND <member> IN (?, …) fragment; post-rebuild cleanup
API internal/api/handlers/contributions.go, internal/api/routes.go SetAllowlist + filterAllowlist in ListMembers / TeamOverview
GitLab client internal/gitlab/client.go with_shared=false on group projects; ListInstanceMergeRequests; MergeRequest.ProjectPath() helper
GitLab sync internal/gitlab/sync.go MemberInstanceSweep, AllowedMemberIDs; new sweepInstanceByMember Pass B
Wiring cmd/server/main.go Compose allowlist at startup; thread it into the aggregator, handlers, and the GitLab syncer
Tests aggregator_test.go, allowlist_test.go, gitlab/sync_test.go Allowlist truth table; aggregator filter on/off; Pass B httptest fixture + dedup + sweep-off no-op
Docs config.example.yaml, docs/team-analytics/CHANGES.md (new) W1 config knobs documented; CHANGES.md entry with activation rules + rollback paths

Activation rules

Prefills Denylist Allowlist state
empty empty disabled — pre-W1 SQL shape
populated any enabled — denylist subtracts
empty populated disabled (refuses to collapse to {bot} — operator must declare who counts)

Visible effects when enabled in prod

  • zhwang, chaozhou, gxjiao, lmhe (denylist seeds from the audit) disappear from the team overview + every per-member chart.
  • With gitlab.member_instance_sweep: true, MRs filed by allowlisted members in container-platform/*, alauda/artifacts, etc. start landing under their proper repo_id.

Rollback

  • Flip gitlab.member_instance_sweep: false to disable Pass B without touching the allowlist.
  • Empty the prefill maps to revert the aggregator to its pre-W1 SQL shape.
  • Drop denylist entries to re-include suppressed members on the next rebuild.

Test plan

  • go test ./... from backend/ — all green
  • go vet ./... — clean
  • TestBuildAllowlist covers empty / prefills-only / denylist-subtraction / case-fold / denylist-alone
  • TestAggregatorAllowlist proves the filter excludes non-allowlisted authors and the toggle-off sentinel restores pre-W1 shape
  • TestSyncPassBMemberSweep proves Pass B ingests two MRs from two namespaces, dedupes on re-sync, and no-ops when MemberInstanceSweep is false
  • Dev deploy to edge-int/devops-tooling/roadmap-planner-dev — to validate against the live prod-shape database
  • Operator smoke: with prefills configured + denylist seeds applied, verify denied members vanish from the dashboard
  • Operator smoke: with member_instance_sweep: true, verify Pass B picks up allowlisted members' MRs from outside devops/**

Sequencing

W1 is the foundation for W2 (bot consolidation) and W3 (already folded into this PR via the denylist). Per docs/team-analytics/audit-2026-05-19/PLAN.md.

🤖 Generated with Claude Code

@alaudabot

alaudabot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Code Review

Property Value
Model opencode/minimax-m2.5-free
Style strict
Issues Found 0
Config Source centralized
Profile ❌ Not Found
Personalized Prompt ❌ No
Prompt Path .github/review/profiles/alaudadevops/toolbox/pr-review.md
Alauda Skills ✅ base-acp-operator-list, base-acp-operator-release, base-authoring, base-m365, base-ocp-operator-list, base-skill-setup, builders-alauda-component-e2e-release, builders-alauda-component-upgrade, builders-alauda-pipeline, builders-claudetask-submit, builders-component-knowledge, builders-confluence, builders-dev-mesh-qa, builders-edge-ci-trace, builders-gitlab-ops, builders-helm-operator-generator, builders-install-cluster-plugin, builders-jira, builders-notify-wecom, builders-olm-operator-lifecycle, builders-prd-to-testcase, builders-publish-errata, builders-roadmap-studio, builders-story-split, builders-violet, builders-webapp-testing, cross-repo-add-mirror, cross-repo-publish, devops-add-bug-release-notes, devops-autodns, devops-bundle-csv-baseline-diff, devops-candidate-version-supervisor, devops-connectors-acceptance-test, devops-connectors-explore, devops-connectors-poc-case, devops-connectors-review, devops-connectors-unit-test, devops-connectors-upgrade-test, devops-connectors-write-user-docs, devops-creating-tekton-pipelines, devops-fix-go-vulns, devops-fork-alauda-binary-release, devops-gen-advanced-form-descriptors, devops-jira-rfd-acceptance, devops-knowledge-adoption, devops-pr-review, devops-refresh-containerfile-digests, devops-refresh-containerfile-tags, devops-replace-strings, devops-scan-docker-keywords, devops-sync-alauda-github-releases, devops-tekton-dynamic-form-optimizer, devops-tekton-operator-task-e2e, devops-tekton-pipeline-delivery, devops-tekton-refresh-results-tag, devops-tekton-task-delivery, devops-tekton-task-overview-template, devops-tekton-task-version-upgrade, devops-tekton-upgrade-notes, devops-tool-report-troubleshoot, devops-ui-e2e-code-audit, devops-ui-e2e-fix-base-on-report, devops-ui-e2e-regression-and-fix, devops-ui-generate-e2e-from-feature, devops-ui-pre-setup, devops-upgrade-go, devops-upstream-backport-cve, devops-upstream-upgrade
Reviewed at 2026-05-19 22:25:03 UTC

Summary

This PR implements the W1 milestone for roadmap-planner: member allowlist filtering and GitLab instance-wide MR sweep. The implementation adds configuration options for member_instance_sweep and member_denylist, creates a new allowlist system in allowlist.go, modifies the aggregator to filter rollups by allowlist, and adds a Pass B sync that captures MRs outside the configured GitLab groups. The code is generally well-written with proper SQL injection protection via parameterized queries.

Review Statistics

Category Count
Critical Issues 0
Warnings 1
Suggestions 2
Files Reviewed 13

Warnings

  • aggregator.go:486-492 (style/parameterization): The memberExpr parameter in allowFragment() is directly interpolated into SQL: fmt.Sprintf("\n\t\t AND %s IN (%s)", memberExpr, placeholders). While currently all callers pass hardcoded strings like "COALESCE(m.id, pr.author_id)", this pattern could become a SQL injection vulnerability if memberExpr ever comes from configuration. Consider validating against an allowlist of permitted expressions or documenting that callers must not pass user input.

Suggestions

  • aggregator.go:433-456 (performance/batch-delete): The cleanup DELETE query runs on every rebuild when the allowlist is enabled. For large allowlists (100+ members), this could become slow. Consider adding an index on member_id in the member_week_metrics table or batching the cleanup differently.

  • allowlist.go:78-85 (docs/clarity): In BuildAllowlist, if the same member ID key exists in both GitHubLoginPrefills and GitLabUsernamePrefills with different values, the later value silently overwrites. Document this behavior or merge the values explicitly.

Positive Feedback

  • Well-structured code with clear separation of concerns (allowlist.go, aggregator.go, sync.go)
  • Proper SQL injection protection using parameterized queries throughout
  • Good test coverage including TestAggregatorAllowlist, TestBuildAllowlist, and TestSyncPassBMemberSweep
  • Excellent inline documentation explaining the W1 design decisions
  • The with_shared=false fix addresses the root cause of audit finding B1
  • Graceful fallback behavior (empty allowlist preserves pre-W1 behavior)
  • Error handling in Pass B is fail-soft - one member's API failure doesn't stop the entire sweep

Review completed by alaudabot


ℹ️ About this review

This review was automatically generated using the run-actions workflow.

  • Shared prompt: .github/prompts/code-review.md
  • Config source: centralized
  • Profile path: Not Found
  • Profile ref: e75e733e9aa1b417a8b3c6441e53495dbcb418ad
  • No repository-specific prompt configured
  • Alauda skills: base-acp-operator-list, base-acp-operator-release, base-authoring, base-m365, base-ocp-operator-list, base-skill-setup, builders-alauda-component-e2e-release, builders-alauda-component-upgrade, builders-alauda-pipeline, builders-claudetask-submit, builders-component-knowledge, builders-confluence, builders-dev-mesh-qa, builders-edge-ci-trace, builders-gitlab-ops, builders-helm-operator-generator, builders-install-cluster-plugin, builders-jira, builders-notify-wecom, builders-olm-operator-lifecycle, builders-prd-to-testcase, builders-publish-errata, builders-roadmap-studio, builders-story-split, builders-violet, builders-webapp-testing, cross-repo-add-mirror, cross-repo-publish, devops-add-bug-release-notes, devops-autodns, devops-bundle-csv-baseline-diff, devops-candidate-version-supervisor, devops-connectors-acceptance-test, devops-connectors-explore, devops-connectors-poc-case, devops-connectors-review, devops-connectors-unit-test, devops-connectors-upgrade-test, devops-connectors-write-user-docs, devops-creating-tekton-pipelines, devops-fix-go-vulns, devops-fork-alauda-binary-release, devops-gen-advanced-form-descriptors, devops-jira-rfd-acceptance, devops-knowledge-adoption, devops-pr-review, devops-refresh-containerfile-digests, devops-refresh-containerfile-tags, devops-replace-strings, devops-scan-docker-keywords, devops-sync-alauda-github-releases, devops-tekton-dynamic-form-optimizer, devops-tekton-operator-task-e2e, devops-tekton-pipeline-delivery, devops-tekton-refresh-results-tag, devops-tekton-task-delivery, devops-tekton-task-overview-template, devops-tekton-task-version-upgrade, devops-tekton-upgrade-notes, devops-tool-report-troubleshoot, devops-ui-e2e-code-audit, devops-ui-e2e-fix-base-on-report, devops-ui-e2e-regression-and-fix, devops-ui-generate-e2e-from-feature, devops-ui-pre-setup, devops-upgrade-go, devops-upstream-backport-cve, devops-upstream-upgrade

@@ -371,6 +375,15 @@ func startGitLabSync(ctx context.Context, cfg *config.Config, store storage.Stor
syncer := glclient.NewSyncer(client, store, specs, glclient.DefaultLinker(projectKey), backfill)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (bug/missing-feature): The GitLab syncer receives AllowedMemberIDs from the allowlist, but the GitHub syncer path in startGitHubSync has no equivalent allowlist filter. GitHub PRs authored by non-allowlisted members will still be inserted into pull_requests and rolled up — inconsistent dashboard counts when both sources are active.

Suggested change
syncer := glclient.NewSyncer(client, store, specs, glclient.DefaultLinker(projectKey), backfill)
// TODO(W2): wire GitHub syncer AuthorAllowlist to filter GitHub PRs the same way
// Pass B filters GitLab MRs, so GitHub counts are consistent across sources.

@@ -335,6 +377,177 @@ func (s *Syncer) Sync(ctx context.Context) error {
return firstErr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning (performance/rate-limit): sweepInstanceByMember loops over every allowlisted member issuing up to 20 pages of ListInstanceMergeRequests each. With 20 members that's ≤400 API calls per sync cycle. A 429 rate-limit response sets firstErr and the sweep continues silently, meaning data loss is invisible to operators. Consider adding retry-with-backoff or at minimum a passBRateLimited log counter.

// operator typo'd the denylist/prefill key. Either way Pass
// B has nothing to fetch for them; skip silently.
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion (performance/optimization): The note-fetching loop calls ListMRNotes per MR for every non-skipped MR. Members with hundreds of MRs trigger many sequential HTTP calls. Consider adding a MaxNotesRequests guard or batching the requests.

continue
}
mrs, err := s.client.ListInstanceMergeRequests(ctx, ListInstanceMergeRequestsOptions{
AuthorUsername: m.GitLabUsername,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion (refactor/observable): MRs with unreadable projectPath are silently skipped (Warn log only). The totalMRs counter only increments on successful upsert, so silent skips are invisible in the totals. Consider adding a passBSkipped counter surfaced in the run struct.

placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = "?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning (performance/scalability): The cleanup DELETE generates a query with len(ids) bound parameters via strings.Join(placeholders, ","). SQLite's SQLITE_MAX_VARIABLE_NUMBER defaults to 999 — a team with >999 allowlisted members will cause db.ExecContext to return SQLITE_TOOMUCH and the rebuild will fail. Consider batching the cleanup into multiple DELETE statements of ≤999 rows each.

// Preserve the YAML-side pillar names (case-sensitive),
// but merge back the viper-loaded denylist + prefills
// so they aren't lost when the YAML file omits them.
if len(preserve.TeamAnalytics.MemberDenylist) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion (refactor/robustness): Using len(...) == 0 for nil-slice safety. If struct fields can ever be nil (not just empty slices), consider if field == nil || len(field) == 0 for belt-and-suspenders robustness.

on upgrade until the operator opts in):

```yaml
gitlab:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning (docs/inconsistent): The member_denylist example uses GitHub-style names (gxjiao, lmhe) but the field holds Jira member ids (slugified emails — the same keys as the prefill maps). Operators copying the example will get no matches. Suggest Jira-id-shaped examples (e.g. gxjiao@alauda.cn) and a note that entries must match prefill-map keys exactly.

danielfbm and others added 2 commits May 19, 2026 22:22
…weep

Implements the first workstream of the 2026-05-19 metrics audit follow-up
(PLAN.md → W1). Three behavioural changes, all gated behind config so
existing installs upgrade with no observable change until they opt in:

- GitLab `ListGroupProjects` now sends `with_shared=false`. The default
  `true` was what surfaced `container-platform/*` and `alauda/artifacts`
  MRs under the configured `devops/**` ingest in prod — root cause of
  the audit's B1 finding (59% of merged MRs were out-of-scope noise).
- New `Syncer.MemberInstanceSweep` Pass B: for every member in the
  derived allowlist, fetch `/api/v4/merge_requests?scope=all&author_username=<u>`
  and ingest MRs that Pass A missed (members file in projects outside
  `gitlab.groups`). De-dupes against Pass A via the existing
  `<group/proj>!<iid>` storage PK.
- New `contributions.BuildAllowlist(cfg)`: union of the prefill maps
  (`team_analytics.github_login_prefills` ∪ `gitlab_username_prefills`)
  minus `team_analytics.member_denylist`. The synthetic `bot` member
  is always included so W2's consolidation row stays visible. The
  aggregator and the `/api/contributions/members` + `/team` handlers
  both filter on the allowlist when it's enabled.

Activation rules:
  - empty prefills + empty denylist → allowlist disabled, pre-W1 SQL
  - any prefill configured → allowlist on, denylist subtracts
  - denylist only → stays disabled (refuse to collapse to {bot})

Tests:
  - `BuildAllowlist` table-driven coverage (empty, denylist subtraction,
    case-folding, denylist-only safety).
  - Aggregator integration test: 3 PRs / 3 authors, only allowlisted
    author survives; toggle filter off → all three return.
  - GitLab Pass B integration test (httptest): two members, one
    allowlisted with 2 MRs across different namespaces; verifies
    ingest + dedup on re-sync + no-op when sweep disabled.

Config additions (all default off):
  gitlab.member_instance_sweep: false
  team_analytics.member_denylist: []

Doc: `docs/team-analytics/CHANGES.md` documents the activation rules,
rollback paths, and audit cross-reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an "allowlist cleanup" INFO log after the post-rebuild
`DELETE FROM member_week_metrics WHERE member_id NOT IN (allowlist)`
sweep so we can confirm whether the cleanup actually executes in prod
and how many stale rows it removes per rebuild.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@danielfbm danielfbm force-pushed the feat/metrics-w1-member-allowlist branch from cbad2e3 to 627a4c0 Compare May 19, 2026 22:23
@danielfbm danielfbm merged commit 7e6b27a into main May 19, 2026
6 of 8 checks passed
@danielfbm danielfbm deleted the feat/metrics-w1-member-allowlist branch May 19, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants