Skip to content

docs(spec): M2 Spec Kit feature slice (proactive picker → Console hand-off) - #5144

Open
mlehotskylf wants to merge 3 commits into
devfrom
docs/easycla-ss-m2-speckit
Open

docs(spec): M2 Spec Kit feature slice (proactive picker → Console hand-off)#5144
mlehotskylf wants to merge 3 commits into
devfrom
docs/easycla-ss-m2-speckit

Conversation

@mlehotskylf

@mlehotskylf mlehotskylf commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What

Extracts Milestone 2 into its own self-contained Spec Kit feature folder — specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/ — based on dev, mirroring the existing m1-my-cla/ layout. This is the artifact Spec Kit (/speckit.plan/speckit.tasks/speckit.implement) will consume to plan and implement M2.

Intentionally separate from the program-level design PR (#5132), which keeps all six milestone briefs + overview + top-level spec.md. Splitting by artifact type (program design vs. implementable feature slice) keeps this PR small and free of cross-doc conflicts.

Scope reflected (revised 2026-08-04, per Heather/PM)

M2 is an additive, PR-independent "Sign a CLA" picker in the Me lens that hands off to the existing Contributor Console for the actual signing — for both the ICLA and CCLA paths. Specifically:

  • Self Serve runs no signing ceremony, makes no signing-initiation calls, and never talks to DocuSign.
  • The Contributor Console is not cut over or retired; the PR-check remediation link is unchanged (no SSM flip).
  • No per-platform (GitHub/GitLab/Gerrit) sub-milestone split — the Console runs the platform-specific ceremony.

Contents

  • spec.md — M2 feature spec: user story (P2), acceptance scenarios, FR-001…008, SC-002, scope boundaries, and three explicit open questions.
  • plan.md — implementation plan: summary, technical context, constitution check, project structure (extends M1's cla server module), complexity tracking.

Deliberately not included

research.md / data-model.md / contracts/ are not hand-written. The three genuinely unresolved questions — the Console hand-off contract, the org/repo picker scope, and the CLA-Group discovery endpoint — are exactly what the Spec Kit planning flow should settle. Inventing them here would be guesswork. They are marked as open questions in spec.md for /speckit.clarify.

Note on links

The upward links (../spec.md, ../02-milestone-sign-icla-fable.md) point at program-level docs that live in #5132 (targeting dev) and resolve once that PR merges. The m1-my-cla/ sibling folder is already on dev.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings August 4, 2026 13:31
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The pull request adds the Milestone 2 specification and implementation plan for feature-flagged CLA selection, Contributor Console handoff, ICLA/ECLA invalidation, status display, ownership enforcement, and impersonation protection.

Changes

M2 Self Serve CLA handoff

Layer / File(s) Summary
M2 behavior contract
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Defines CLA search and selection, Contributor Console handoff, account authorization, invalidation flows, status values, acceptance criteria, and scope boundaries.
Verified backend constraints
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Documents Console and backend behavior, server-side ownership enforcement, impersonation write protection, and open questions for account binding, ECLA invalidation, status evaluation, GitHub ICLA support, and CLA-Group listing.
M2 implementation plan
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
Defines the feature flag, stateless architecture, implementation scope, security constraints, testing, rollout, constitution checks, source structure, and delivery risks.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Milestone 2 specification and its proactive picker-to-Console hand-off scope.
Description check ✅ Passed The description directly explains the new M2 Spec Kit feature folder, its scope, and its implementation boundaries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/easycla-ss-m2-speckit

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

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.

Pull request overview

Intended to add the M2 Spec Kit feature slice, but the diff also includes unrelated M1 API, SSS, email, test, and operational changes.

Changes:

  • Adds M2 signing hand-off specification and plan.
  • Adds the My CLAs API and documentation.
  • Changes SSS screening, email normalization, utilities, and tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

Show a summary per file
File Description
utils/search_aws_log_group.sh Improves AWS/jq error handling.
utils/my_clas.sh Adds My CLAs API utility.
utils/lookup_all_logs.sh Fails fast on lookup errors.
utils/local-fbsd-api.sh Adds local FreeBSD runner.
utils/get_user_svc.sh Adds user-service lookup utility.
utils/get_ddb_user_identities.sh Adds DynamoDB identity lookup.
utils/downcase_emails.sh Adds email normalization script.
tests/functional/cypress/e2e/v4/cla-manager.cy.ts Changes request failure handling.
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Defines the M2 feature.
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Plans M2 implementation.
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md Adds M1 specification.
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md Adds M1 plan.
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md Documents M1 models.
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md Documents upstream contracts.
docs/MY_CLAS_API.md Documents My CLAs endpoints.
CLAUDE.md Adds repository guidance.
cla-backend-legacy/internal/api/handlers.go Adds SSS switch and normalization.
cla-backend-go/v2/user-service/client.go Adds contextual user lookups.
cla-backend-go/v2/sign/service.go Integrates SSS switch and email changes.
cla-backend-go/v2/sign/service_sss_test.go Tests SSS behavior.
cla-backend-go/v2/sign/handlers.go Updates compliance errors.
cla-backend-go/v2/my_clas/service.go Implements My CLAs logic.
cla-backend-go/v2/my_clas/service_test.go Tests My CLAs service.
cla-backend-go/v2/my_clas/repository.go Adds My CLAs data access.
cla-backend-go/v2/my_clas/handlers.go Adds My CLAs handlers.
cla-backend-go/users/repository.go Normalizes stored emails.
cla-backend-go/swagger/common/my-identity-list.yaml Defines identity response.
cla-backend-go/swagger/common/my-cla.yaml Defines CLA response item.
cla-backend-go/swagger/common/my-cla-pdf.yaml Defines PDF response.
cla-backend-go/swagger/common/my-cla-list.yaml Defines CLA list response.
cla-backend-go/swagger/cla.v2.yaml Adds My CLAs API paths.
cla-backend-go/signatures/repository.go Normalizes removed emails.
cla-backend-go/emails/prefill_test.go Tests CLA Group prefill.
cla-backend-go/emails/docusign_templates.go Updates signed-document emails.
cla-backend-go/emails/docusign_templates_test.go Tests template rendering.
cla-backend-go/config/ssm.go Loads the SSS switch.
cla-backend-go/config/config.go Adds SSS enabled configuration.
cla-backend-go/cmd/server.go Wires My CLAs and SSS.
cla-backend-go/cmd/s3_upload/main.go Updates service construction.
cla-backend-go/approval_list/repository.go Normalizes requester email.
.gitignore Updates generated-file exclusions.

Copilot AI review requested due to automatic review settings August 4, 2026 13:36

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • This source map does not match the completed M1 Me-lens seam it says M2 will extend. In the current lfx-self-serve, M1 is implemented under app/modules/profile/clas, registered as /profile/clas in modules/profile/profile.routes.ts, and surfaced through ProfileLayoutComponent/buildProfileTabs; MainLayoutComponent and app.routes.ts do not own that tab. Following this plan would create a separate top-level surface instead of extending the existing Profile/Me hub. Update the structure to name the actual profile module, profile routes, tab builder/sidebar, and feature guard integration points.
├── app/modules/                                # NEW "Sign a CLA" Me-lens surface (picker)
│   └── sign-cla/                               # CLA-Group + org/repo + sign-type selection, hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                           # EDIT: register the picker route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:27

  • FR-002 makes org/repo narrowing inside the Self Serve picker mandatory, but the corresponding open question on line 47 still allows deferring that selection to the Console. Those outcomes are mutually exclusive, so the acceptance criteria cannot be finalized or tested until one is selected. Either remove the defer-to-Console option or make this requirement conditional on the clarification result.
- **FR-002**: The picker MUST let the user select a CLA Group, and where a CLA Group spans multiple GitHub orgs/repos, MUST let the user narrow to a specific org/repo. *(Interaction depth is a [NEEDS CLARIFICATION] — see Open questions.)*

…d-off)

Extract Milestone 2 into its own implementable Spec Kit folder
(m2-sign-cla-handoff/), mirroring the existing m1-my-cla/ layout, based on
main so it is self-contained and independent of the program-level design PR
(#5132).

Reflects the 2026-08-04 revision (per Heather/PM): M2 is an additive,
PR-independent "Sign a CLA" picker in the Me lens that hands off to the
existing Contributor Console for signing — for both ICLA and CCLA paths. SS
runs no signing ceremony, makes no signing-initiation calls, and does not cut
over or retire the Console; the PR-check remediation link is unchanged.

Scaffolds spec.md + plan.md only. The hand-off contract, org/repo picker
scope, and CLA-Group discovery endpoint are left as explicit open questions
for the Spec Kit planning flow to resolve — research.md/data-model.md/
contracts/ are intentionally not hand-written.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Michal Lehotsky <[email protected]>
@mlehotskylf
mlehotskylf force-pushed the docs/easycla-ss-m2-speckit branch from 3f3efd6 to b88e3ac Compare August 4, 2026 13:41
Copilot AI review requested due to automatic review settings August 4, 2026 13:41

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (5)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:56

  • The source plan omits required Contributor Console work. In the current Console, IndividualDashboardComponent.findActiveSignature() fails when no active signature exists and tells the user to restart from a pull request. Since FR-001 requires a no-PR flow and FR-005 forbids Self Serve from initiating signing, changing only lfx-self-serve cannot make the ICLA path completable; include easycla-contributor-console changes (and any supporting contract/backend work) to bootstrap this proactive flow.
Primary repo: `linuxfoundation/lfx-self-serve`

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • These frontend paths do not match the completed M1 seam this plan says it extends. M1 is implemented under app/modules/profile/clas, registered in app/modules/profile/profile.routes.ts, and exposed through the profile layout/sidebar—not through app/modules/my-clas, main-layout, or app.routes.ts. Update this tree against the current repository so generated tasks target real integration points.
├── app/modules/                                # NEW "Sign a CLA" Me-lens surface (picker)
│   └── sign-cla/                               # CLA-Group + org/repo + sign-type selection, hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                           # EDIT: register the picker route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:28

  • This constitution path is absent from the current branch, and PR #5132 also does not add the .specify tooling. As written, the plan claims to have evaluated a file that will not exist after these changes merge.
`.specify/memory/constitution.md` is the unratified template — no project-specific gates exist. Default gates applied:

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:17

  • The documented test stack is stale: the current lfx-self-serve app runs Vitest for unit tests and Playwright for E2E; it does not use Jest/Karma or Cypress. Correcting this prevents the generated tasks from targeting nonexistent harnesses.

This issue also appears in the following locations of the same file:

  • line 28
  • line 56
  • line 60
**Testing**: lfx-self-serve conventions — Jest/Karma unit tests for services/components, server route tests, Cypress/E2E per repo norms (verify exact harness during implementation).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:27

  • FR-002 already mandates that org/repo narrowing occurs in the Self Serve picker, but acceptance scenario 3 and the open question retain “defer to the Console” as a valid outcome; FR-008 likewise assumes org/repo is passed during hand-off. These normative requirements cannot all hold if clarification chooses deferral. Make them conditional on the design decision or remove the deferral alternative before generating tasks.
- **FR-002**: The picker MUST let the user select a CLA Group, and where a CLA Group spans multiple GitHub orgs/repos, MUST let the user narrow to a specific org/repo. *([NEEDS CLARIFICATION]: interaction depth.)*

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (6)
cla-backend-go/v2/my_clas/service_test.go (1)

23-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding error paths to fakeRepo.

Every fakeRepo method returns a nil error, so no test covers the failure branches. Two branches carry real behavior:

  • authorizeIdentity must fail closed when GetUsersByLFUsername returns an error.
  • GetMyClas must propagate a GetUserCLASignatures error instead of returning a partial list.

Add an error field per method and two short tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/my_clas/service_test.go` around lines 23 - 70, Extend
fakeRepo with configurable errors for GetUsersByLFUsername and
GetUserCLASignatures, returning the corresponding error from those methods. Add
focused tests verifying authorizeIdentity fails closed when LF username lookup
errors and GetMyClas propagates signature lookup errors without returning
partial results.
cla-backend-go/v2/user-service/client.go (2)

295-296: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Escape the URL components.

lfUsername is placed in a query string and userSFID is placed in a path segment, both with plain fmt.Sprintf. Neither value is escaped. Today lfUsername comes from the authenticated principal and userSFID comes from the user-service response, so this is not exploitable from a request parameter. Escaping keeps the URLs correct if either source changes.

♻️ Proposed refactor
-	url := fmt.Sprintf("https://%s/user-service/v1/users?username=%s", usc.apiGwURL, lfUsername)
+	url := fmt.Sprintf("https://%s/user-service/v1/users?username=%s", usc.apiGwURL, neturl.QueryEscape(lfUsername))
-		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
+		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, neturl.PathEscape(userSFID), pageSize, offset)

Import net/url as neturl to avoid shadowing by the local url variables.

Also applies to: 362-362

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/user-service/client.go` around lines 295 - 296, Escape the
dynamic URL components in the user lookup request and the related request around
userSFID. Use net/url imported as neturl, encode lfUsername as a query parameter
and userSFID as a path segment before constructing each URL, while preserving
the existing request behavior.

306-307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse one http.Client instead of creating one per call.

Both new functions create &http.Client{Timeout: 30 * time.Second} on each invocation. Each client gets its own default transport, so connections are never reused and idle connections accumulate per call. Declare one package-level client and share it.

♻️ Proposed refactor
// package level, next to the other package vars
var userServiceHTTPClient = &http.Client{Timeout: 30 * time.Second}
-	httpClient := &http.Client{Timeout: 30 * time.Second}
-	response, err := httpClient.Do(request)
+	response, err := userServiceHTTPClient.Do(request)

Apply the same change in ListUserIdentities at line 358.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/user-service/client.go` around lines 306 - 307, Create a
single package-level http.Client variable with the 30-second timeout at the top
of the client.go file alongside other package-level declarations. Replace the
inline httpClient instantiation at line 306-307 with a reference to this shared
package-level client. Apply the same change to the httpClient creation in the
ListUserIdentities function at line 358 to reuse the single shared client
instance across both functions.
cla-backend-go/v2/my_clas/repository.go (1)

137-188: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding the users-table scan cost.

GetUsersBySecondaryEmails runs a full scan of cla-<stage>-users for every request that includes secondaryEmail. The scan reads all items and returns full items even though only a few attributes are used by toUserModels.

Two cheap improvements:

  • Add a ProjectionExpression for the attributes toUserModels reads, to cut read units and payload size.
  • Add a metric or log of the consumed capacity, so the cost of this endpoint stays visible in production.

The swagger description already warns callers, and the service only scans values the caller owns, so this is a cost concern rather than an abuse path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/my_clas/repository.go` around lines 137 - 188, Update
GetUsersBySecondaryEmails to request only the attributes consumed by
toUserModels via a DynamoDB ProjectionExpression, using the existing expression
attribute-name handling as needed. Enable consumed-capacity reporting on the
ScanInput and record the returned capacity through the repository’s established
metric or logging mechanism for each scan page.
cla-backend-go/v2/my_clas/handlers.go (1)

28-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External · Exploitability: Difficult

Reachability path
● Entry
  cla-backend-go/cmd/server.go:486
  Configure
│
▼
● Sink
  cla-backend-go/v2/my_clas/handlers.go

Remove authUserEmail from the My CLAs log fields if application logs are subject to PII controls.

The field is emitted on warning and error paths in all three handlers. The success paths do not write these fields. Apply the same change at lines 62–67 and 102–107.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/my_clas/handlers.go` around lines 28 - 33, Remove the
authUserEmail field from the logrus.Fields definitions in all three My CLAs
handlers, including the blocks near GetMyClas and the corresponding sections
around lines 62–67 and 102–107. Keep the remaining functionName, XREQUESTID, and
authentication username fields unchanged.
cla-backend-go/v2/my_clas/service.go (1)

509-523: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Case-variant Ownership Widening (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External · Exploitability: Theoretical

Reachability path
● Entry
  cla-backend-legacy/internal/api/handlers.go:54
  Handlers: Ported building blocks (incrementally used by endpoints as they are rewritten from Python).
│
▼
● Sink
  cla-backend-go/v2/my_clas/service.go

Defensive refactor: Remove the caller-supplied spelling from the approved-username list.

Append only the canonical variants proven to belong to the user; this keeps the ownership guarantee exact and avoids future case-sensitive identity collisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/my_clas/service.go` around lines 509 - 523, Update
appendAllowedUsernames so approved usernames append only the canonical variants
returned by canon, removing the caller-supplied value from the destination list
while preserving skipped handling and deduplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cla-backend-go/approval_list/repository.go`:
- Line 90: Validate the normalized requester email in the request-writing flow
before constructing attributes or calling PutItem: trim whitespace and reject an
empty result with the repository’s established validation error pattern. Only
invoke addStringSliceAttribute for a non-empty value, preserving the lowercased
email behavior.

In `@cla-backend-go/config/config.go`:
- Around line 146-148: Update the comment for the Enabled field to accurately
describe behavior when the SSS kill switch is disabled: persisted SSS-origin
is_sanctioned blocks do not block compliance, while manual or admin-origin
blocks still do. Keep the documented behavior aligned with both compliance
implementations and the existing test expectations.

In `@cla-backend-go/signatures/repository.go`:
- Line 3345: Normalize every entry in params.RemoveEmailApprovalList with
TrimSpace and ToLower before passing it to buildApprovalAttributeList and
approvalList.ApprovalList, while preserving the existing normalized lookup
behavior. Update the removal-path updateApprovalTable call to pass
params.RemoveEmailApprovalList instead of params.AddEmailApprovalList.
- Line 3345: The email normalization in the loop over
params.RemoveEmailApprovalList converts whitespace-only entries to empty
strings, which causes GetProjectCompanyEmployeeSignatures to execute without an
email filter and return all company employee signatures instead of a filtered
result. This leads to unintended removal of signatures. Add a validation check
immediately after the email = strings.ToLower(strings.TrimSpace(email)) line to
skip the iteration if email is empty before the goroutine is spawned, preventing
the unfiltered query and subsequent invalidateSignatures call.

In `@cla-backend-go/users/repository.go`:
- Around line 390-393: After calling normalizeEmails() and assigning the result
to the normalized variable, check if the list is empty before assigning to
expressionAttributeValues. When normalized is empty, either add a REMOVE
operation for the `#UES` attribute to clear user_emails instead of assigning the
empty SS, or validate and reject the input before the UpdateItem call rather
than attempting to send an empty string set to DynamoDB.
- Around line 390-393: Update the Debug logging in the Save function, including
the log statement near normalizeEmails and the other sensitive-field logs
identified in the comment, so email addresses and related PII are not emitted.
Log only non-sensitive metadata such as collection counts, while preserving the
existing query-building behavior.

In `@cla-backend-go/v2/my_clas/service.go`:
- Around line 225-227: Update the sorting logic around result.Clas to parse each
SignedOn value and compare the parsed timestamps chronologically before any
FormatTimeString processing. Define and consistently apply behavior for invalid
or unsupported timestamps, then format the values afterward without using the
formatted strings for ordering.

In `@cla-backend-go/v2/user-service/client.go`:
- Around line 361-404: Bound the pagination loop around the user-identity
listing flow by tracking the number of pages and enforcing a finite maximum page
count. Also break immediately when identityList.Data is empty, while preserving
the existing short-page termination condition and response handling.

In `@docs/MY_CLAS_API.md`:
- Around line 599-608: Update the documented access policy for non-admin
GetMyClas and GetMyClaPdf to require immutable githubId/gitlabId corroboration
for historical records and signed PDFs, or explicitly record approved privacy
and security sign-off for the alias-based exception before release; do not
present the current-possession behavior as an unconditional accepted product
decision.

In
`@specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md`:
- Around line 3-5: Replace the superseded M1 contract in
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md:3-5
with the authoritative /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and
/v4/my-clas/identities definitions. Update
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md:5-5 for current
signature fields, ECLA classification, response fields, and validity; remove
obsolete Self Serve aggregation and authorization requirements from
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md:8-10; and align
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md:21-27 with valid,
secondary email, GitLab/Gerrit support, and the current endpoint scope.

In `@tests/functional/cypress/e2e/v4/cla-manager.cy.ts`:
- Line 710: The failOnStatusCode property in the CLA Manager creation request is
unconditionally set to false, which masks unexpected HTTP errors. Make this
setting conditional based on the allowFail parameter so that failOnStatusCode is
false only when allowFail is true, allowing the test to properly fail on
unexpected status codes when allowFail is false.

In `@utils/downcase_emails.sh`:
- Around line 1-2: Add the required Linux Foundation copyright header
immediately after the shebang in utils/downcase_emails.sh lines 1-2, followed by
the MIT SPDX identifier. Add the Linux Foundation copyright header after the
shebang in utils/lookup_all_logs.sh lines 1-2, preserving its existing
applicable SPDX header.
- Around line 10-26: Update the update-item operation in the normalization loop
to condition the write on user_emails still matching the scanned item’s original
user_emails.SS value, while preserving the normalized :e update. Capture
conditional-check failures separately from successful updates and report the
affected uid for manual retry instead of treating them as successful writes.
- Line 10: Update the script around the DynamoDB scan to set a restrictive
umask, create the JSON output with mktemp, and store its path for reuse instead
of writing directly to the stage-named file. Register an EXIT trap to remove the
temporary file on termination, and ensure the scan command writes to that
temporary path while preserving its existing query options.

In `@utils/get_user_svc.sh`:
- Around line 51-59: Update the profile lookup curl invocation in
get_user_svc.sh to include the -f/--fail flag alongside the existing silent and
show-error options. Ensure HTTP 4xx/5xx responses cause curl to fail so the
surrounding profile and pagination flows do not treat error responses as empty
results.

In `@utils/local-fbsd-api.sh`:
- Around line 5-10: Update the setup and build flow in the local fBSD launch
script to enable strict shell failure handling and run the `cd cla-backend-go`
plus `go build` commands in a subshell. Ensure any failed directory change or
build terminates the script before sourcing environment files or executing the
binary.

---

Nitpick comments:
In `@cla-backend-go/v2/my_clas/handlers.go`:
- Around line 28-33: Remove the authUserEmail field from the logrus.Fields
definitions in all three My CLAs handlers, including the blocks near GetMyClas
and the corresponding sections around lines 62–67 and 102–107. Keep the
remaining functionName, XREQUESTID, and authentication username fields
unchanged.

In `@cla-backend-go/v2/my_clas/repository.go`:
- Around line 137-188: Update GetUsersBySecondaryEmails to request only the
attributes consumed by toUserModels via a DynamoDB ProjectionExpression, using
the existing expression attribute-name handling as needed. Enable
consumed-capacity reporting on the ScanInput and record the returned capacity
through the repository’s established metric or logging mechanism for each scan
page.

In `@cla-backend-go/v2/my_clas/service_test.go`:
- Around line 23-70: Extend fakeRepo with configurable errors for
GetUsersByLFUsername and GetUserCLASignatures, returning the corresponding error
from those methods. Add focused tests verifying authorizeIdentity fails closed
when LF username lookup errors and GetMyClas propagates signature lookup errors
without returning partial results.

In `@cla-backend-go/v2/my_clas/service.go`:
- Around line 509-523: Update appendAllowedUsernames so approved usernames
append only the canonical variants returned by canon, removing the
caller-supplied value from the destination list while preserving skipped
handling and deduplication.

In `@cla-backend-go/v2/user-service/client.go`:
- Around line 295-296: Escape the dynamic URL components in the user lookup
request and the related request around userSFID. Use net/url imported as neturl,
encode lfUsername as a query parameter and userSFID as a path segment before
constructing each URL, while preserving the existing request behavior.
- Around line 306-307: Create a single package-level http.Client variable with
the 30-second timeout at the top of the client.go file alongside other
package-level declarations. Replace the inline httpClient instantiation at line
306-307 with a reference to this shared package-level client. Apply the same
change to the httpClient creation in the ListUserIdentities function at line 358
to reuse the single shared client instance across both functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87870c4c-e5ee-4c24-ba79-049c40eeac83

📥 Commits

Reviewing files that changed from the base of the PR and between 7c70261 and 4990e8e.

📒 Files selected for processing (41)
  • .gitignore
  • CLAUDE.md
  • cla-backend-go/approval_list/repository.go
  • cla-backend-go/cmd/s3_upload/main.go
  • cla-backend-go/cmd/server.go
  • cla-backend-go/config/config.go
  • cla-backend-go/config/ssm.go
  • cla-backend-go/emails/docusign_templates.go
  • cla-backend-go/emails/docusign_templates_test.go
  • cla-backend-go/emails/prefill_test.go
  • cla-backend-go/signatures/repository.go
  • cla-backend-go/swagger/cla.v2.yaml
  • cla-backend-go/swagger/common/my-cla-list.yaml
  • cla-backend-go/swagger/common/my-cla-pdf.yaml
  • cla-backend-go/swagger/common/my-cla.yaml
  • cla-backend-go/swagger/common/my-identity-list.yaml
  • cla-backend-go/users/repository.go
  • cla-backend-go/v2/my_clas/handlers.go
  • cla-backend-go/v2/my_clas/repository.go
  • cla-backend-go/v2/my_clas/service.go
  • cla-backend-go/v2/my_clas/service_test.go
  • cla-backend-go/v2/sign/handlers.go
  • cla-backend-go/v2/sign/service.go
  • cla-backend-go/v2/sign/service_sss_test.go
  • cla-backend-go/v2/user-service/client.go
  • cla-backend-legacy/internal/api/handlers.go
  • docs/MY_CLAS_API.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
  • tests/functional/cypress/e2e/v4/cla-manager.cy.ts
  • utils/downcase_emails.sh
  • utils/get_ddb_user_identities.sh
  • utils/get_user_svc.sh
  • utils/local-fbsd-api.sh
  • utils/lookup_all_logs.sh
  • utils/my_clas.sh
  • utils/search_aws_log_group.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (14)
cla-backend-go/approval_list/repository.go (1)

90-90: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- repository.go ---'
sed -n '1,180p' cla-backend-go/approval_list/repository.go
printf '%s\n' '--- addStringSliceAttribute definitions and uses ---'
rg -n -C 4 'addStringSliceAttribute|user_emails|requesterEmail' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 31945


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- helper ---'
cat -n cla-backend-go/approval_list/helpers.go
printf '%s\n' '--- AddCclaApprovalRequest callers ---'
rg -n -C 8 'AddCclaApprovalRequest|requester_email|requesterEmail' cla-backend-go/approval_list cla-backend-go | head -n 260
printf '%s\n' '--- module and local AWS SDK availability ---'
rg -n 'aws-sdk-go|go [0-9]+\.[0-9]+' cla-backend-go/go.mod go.mod 2>/dev/null || true
find . -path '*aws-sdk-go*attributevalue*' -o -path '*aws-sdk-go*dynamodbattribute*' | head -n 40

Repository: linuxfoundation/easycla

Length of output: 26332


🌐 Web query:

AWS DynamoDB String Set empty string member ValidationException

💡 Result:

In Amazon DynamoDB, you cannot store an empty Set (including String Sets, Number Sets, and Binary Sets). Attempting to perform a PutItem or UpdateItem operation with an empty set will result in a ValidationException [1][2][3]. It is important to distinguish between two related concepts: 1. Empty Sets are Not Allowed: DynamoDB explicitly prohibits sets that contain no elements [2][3]. If you attempt to persist a set with no members, the service will reject the request with a ValidationException, stating that a string set (or number/binary set) may not be empty [4][5]. 2. Empty Strings Within a Set: While the set itself cannot be empty, DynamoDB does permit the inclusion of empty string values as members within a non-empty String Set [4][6][3]. To resolve a ValidationException caused by an empty set, you should either: - Ensure the set contains at least one valid element before sending the request [7]. - If the attribute is optional and the set is empty, remove the attribute from the item entirely before sending the request to DynamoDB [7]. Note that while Lists and Maps allow empty structures, Sets strictly require at least one member to be considered valid by the DynamoDB API [8][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- AWS SDK module cache ---'
find "${GOMODCACHE:-/root/go/pkg/mod}" -path '*aws-sdk-go*' -type f 2>/dev/null | head -n 20 || true
printf '%s\n' '--- SDK v1.36.27 AttributeValue validation ---'
curl -fsSL https://raw.githubusercontent.com/aws/aws-sdk-go/v1.36.27/service/dynamodb/api.go |
  rg -n -C 12 'func \(s \*AttributeValue\) Validate|SS.*Validate|StringSet|empty'
printf '%s\n' '--- approval-list request argument definitions and validation ---'
sed -n '1,190p' cla-backend-go/approval_list/service.go
rg -n -C 8 'ContributorEmail|ContributorName|Request.*Access|approval.*request|allowlist|approval_list' cla-backend-go/approval_list cla-backend-go/swagger | head -n 320

Repository: linuxfoundation/easycla

Length of output: 50379


🌐 Web query:

site:docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html empty string values set

💡 Result:

In most programming contexts and data structures, an empty string is treated as a valid, distinct string value, and thus it can be contained within a set [1]. Because sets enforce the uniqueness of their elements, an empty string is considered a unique element that is distinct from other strings or null values [1]. For example, in systems like Amazon DynamoDB, empty string values are explicitly permitted within string sets [1]. While DynamoDB does not support empty sets themselves, it allows a string set to include an empty string as one of its members [1]. In common programming languages such as Python, Java, or JavaScript, the standard Set data structure similarly treats an empty string as a valid element [1]. You can include an empty string in a set without it being interpreted as an indicator of an empty set or a null value. Example of adding an empty string to a set: # Python my_set = {"apple", ""} print("" in my_set) # Returns True // JavaScript let mySet = new Set(["apple", ""]); console.log(mySet.has("")); // Returns true // Java Set mySet = new HashSet<>; mySet.add(""); System.out.println(mySet.contains("")); // Returns true

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- approval-list service request path ---'
sed -n '1,175p' cla-backend-go/approval_list/service.go
printf '%s\n' '--- ContributorEmail declarations and validation ---'
rg -n -C 10 'ContributorEmail|contributor_email|contributorEmail' cla-backend-go/approval_list cla-backend-go/gen cla-backend-go/swagger | head -n 280
printf '%s\n' '--- request schema required fields ---'
rg -n -C 8 'ContributorEmail|contributor_email|contributor-email' cla-backend-go/swagger cla-backend-go/gen | head -n 220

Repository: linuxfoundation/easycla

Length of output: 24118


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
inputs = ["", "   ", "\t\n", " [email protected] "]
for raw in inputs:
    normalized = raw.strip().lower()
    attribute = {"SS": [normalized]}
    print({"input": repr(raw), "normalized": repr(normalized), "attribute": attribute,
           "set_member_count": len(attribute["SS"])})
PY

Repository: linuxfoundation/easycla

Length of output: 564


Reject an empty normalized requester email before writing the request.

strings.TrimSpace converts empty input to "", and the helper creates SS: [""]. DynamoDB accepts this non-empty string set, so PutItem can persist an invalid user_emails value. Return a validation error before PutItem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/approval_list/repository.go` at line 90, Validate the
normalized requester email in the request-writing flow before constructing
attributes or calling PutItem: trim whitespace and reject an empty result with
the repository’s established validation error pattern. Only invoke
addStringSliceAttribute for a non-empty value, preserving the lowercased email
behavior.
cla-backend-go/config/config.go (1)

146-148: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the persisted SSS-origin behavior correctly.

The comment states that persisted is_sanctioned values still block. Both compliance implementations return false for an SSS-origin persisted block when sssEnabled is false. The new test also requires that behavior. Limit this statement to manual or admin blocks, or change the runtime behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/config/config.go` around lines 146 - 148, Update the comment
for the Enabled field to accurately describe behavior when the SSS kill switch
is disabled: persisted SSS-origin is_sanctioned blocks do not block compliance,
while manual or admin-origin blocks still do. Keep the documented behavior
aligned with both compliance implementations and the existing test expectations.
cla-backend-go/signatures/repository.go (1)

3345-3345: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'buildApprovalAttributeList|RemoveEmailApprovalList|ApprovalList =|invalidateSignatures|updateApprovalTable' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository update path ---'
sed -n '3298,3365p' cla-backend-go/signatures/repository.go

printf '%s\n' '--- list removal helper ---'
rg -n -C 8 'func RemoveItemsFromList|RemoveItemsFromList' cla-backend-go

printf '%s\n' '--- approval-table implementation ---'
rg -n -C 12 'func .*updateApprovalTable|updateApprovalTable' cla-backend-go/signatures

printf '%s\n' '--- user search implementation ---'
rg -n -C 10 'func .*SearchUsers|SearchUsers\(' cla-backend-go | head -200

Repository: linuxfoundation/easycla

Length of output: 47562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact removal semantics ---'
sed -n '110,155p' cla-backend-go/utils/utils.go

printf '%s\n' '--- approval-table removal path ---'
sed -n '3860,3925p' cla-backend-go/signatures/repository.go

printf '%s\n' '--- user search query construction ---'
sed -n '1255,1345p' cla-backend-go/users/repository.go

printf '%s\n' '--- converter behavior probe ---'
python3 - <<'PY'
existing = ["[email protected]"]
remove = [" [email protected] "]
updated = [value.strip() for value in existing]
result = [value for value in updated if value not in remove]
print({"existing": existing, "remove": remove, "updated": updated, "result": result})
PY

Repository: linuxfoundation/easycla

Length of output: 6907


Normalize and pass the removal email list to every consumer.

  • utils.RemoveItemsFromList uses exact equality. " [email protected] " does not remove stored [email protected]; line 3345 only normalizes the goroutine-local lookup. Normalize the removal slice before buildApprovalAttributeList and approvalList.ApprovalList.
  • The removal call to updateApprovalTable passes params.AddEmailApprovalList. Pass params.RemoveEmailApprovalList so removal-only requests deactivate the correct records.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/signatures/repository.go` at line 3345, Normalize every entry
in params.RemoveEmailApprovalList with TrimSpace and ToLower before passing it
to buildApprovalAttributeList and approvalList.ApprovalList, while preserving
the existing normalized lookup behavior. Update the removal-path
updateApprovalTable call to pass params.RemoveEmailApprovalList instead of
params.AddEmailApprovalList.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'SearchUsers|criteria\.UserEmail|GetProjectCompanyEmployeeSignatures|invalidateSignatures' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 50380


🏁 Script executed:

# Get context around line 3345 in signatures/repository.go
sed -n '3330,3360p' cla-backend-go/signatures/repository.go

# Get the GetProjectCompanyEmployeeSignatures function
sed -n '2200,2250p' cla-backend-go/signatures/repository.go

# Get the invalidateSignatures function signature and implementation
rg -n 'func.*invalidateSignatures' cla-backend-go/signatures/repository.go -A 25

Repository: linuxfoundation/easycla

Length of output: 5556


🏁 Script executed:

# Verify SearchUsers returns non-nil for empty input
sed -n '1255,1275p' cla-backend-go/users/repository.go

# Verify the condition check in the goroutine
sed -n '3344,3365p' cla-backend-go/signatures/repository.go

# Check buildApprovalAttributeList to see if it normalizes
rg -n 'buildApprovalAttributeList' cla-backend-go/signatures/repository.go -A 10

Repository: linuxfoundation/easycla

Length of output: 6297


Reject blank removal entries before starting the removal goroutines.

Line 3345 normalizes whitespace-only input to an empty string. When email becomes "", SearchUsers("user_emails", "", false) returns a non-nil empty result (not an error or nil), so the condition on line 3354 does not trigger early return. The code then calls GetProjectCompanyEmployeeSignatures with criteria.UserEmail = "". Since the filter is only applied when criteria.UserEmail != "" (line 2220), the query executes without the email filter and may return all employee signatures for the company and project. These unfiltered signatures are then passed to invalidateSignatures, causing unintended removals.

Add a check to reject empty normalized emails before the goroutine starts:

for _, email := range params.RemoveEmailApprovalList {
    email = strings.ToLower(strings.TrimSpace(email))
    if email == "" {
        log.WithFields(f).Debugf("skipping empty removal entry")
        continue
    }
    go func(email string) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/signatures/repository.go` at line 3345, The email
normalization in the loop over params.RemoveEmailApprovalList converts
whitespace-only entries to empty strings, which causes
GetProjectCompanyEmployeeSignatures to execute without an email filter and
return all company employee signatures instead of a filtered result. This leads
to unintended removal of signatures. Add a validation check immediately after
the email = strings.ToLower(strings.TrimSpace(email)) line to skip the iteration
if email is empty before the goroutine is spawned, preventing the unfiltered
query and subsequent invalidateSignatures call.
cla-backend-go/users/repository.go (1)

390-393: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 6 'normalizeEmails|AttributeValue.*SS|UpdateItem|user_emails' cla-backend-go/users/repository.go

Repository: linuxfoundation/easycla

Length of output: 6571


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Save implementation ---'
sed -n '300,465p' cla-backend-go/users/repository.go
printf '%s\n' '--- normalizeEmails implementation ---'
sed -n '888,925p' cla-backend-go/users/repository.go
printf '%s\n' '--- user email model and Save callers ---'
rg -n -C 4 'type User|Emails \[\]string|\.Save\(|Save\(.*User|user\.Emails' cla-backend-go/users cla-backend-go -g '*.go' | head -n 260
printf '%s\n' '--- tests mentioning email updates ---'
rg -n -C 5 'normalizeEmails|user_emails|Emails' cla-backend-go -g '*_test.go' | head -n 260

Repository: linuxfoundation/easycla

Length of output: 42023


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- UserUpdate definition and API schema references ---'
rg -n -C 8 'type UserUpdate|UserUpdate struct|emails.*json|Emails.*json|emails:' cla-backend-go -g '*.go' -g '*.yaml' -g '*.yml' -g '*.json' | head -n 320
printf '%s\n' '--- update-user handler validation and generated parameters ---'
sed -n '1,165p' cla-backend-go/users/handlers.go
printf '%s\n' '--- module SDK version ---'
rg -n 'aws-sdk-go|go [0-9]' cla-backend-go/go.mod go.mod 2>/dev/null || true
printf '%s\n' '--- focused structural behavior probe ---'
python3 - <<'PY'
def normalize(emails):
    if emails is None:
        return None
    seen = set()
    out = []
    for email in emails:
        email = email.strip().lower()
        if not email or email in seen:
            continue
        seen.add(email)
        out.append(email)
    return out

for value in (None, [], [""], [" ", "\t"], ["[email protected]", " [email protected] "]):
    normalized = normalize(value)
    print(repr(value), "=>", repr(normalized),
          "non_nil_branch=", value is not None,
          "empty_SS=", value is not None and len(normalized) == 0)
PY

Repository: linuxfoundation/easycla

Length of output: 23625


🌐 Web query:

AWS DynamoDB UpdateItem AttributeValue SS empty set validation error

💡 Result:

In Amazon DynamoDB, set type attributes (String Set [SS], Number Set [NS], and Binary Set [BS]) cannot be empty [1][2][3]. If you attempt to update an item with an empty set, DynamoDB will reject the request with a ValidationException [1][2][4]. This restriction is a fundamental design constraint of the DynamoDB service [5][4]. The service requires that set attributes contain at least one element [1][2]. To resolve or avoid this validation error, consider the following approaches: 1. Remove the attribute: If the set is empty, it is often appropriate to remove the attribute from the item entirely rather than setting it to an empty collection [6][4]. 2. Validate before sending: Implement logic in your application or client-side code to check if the set is empty before performing the UpdateItem operation [6]. If the set is empty, skip the update or remove the attribute using a REMOVE expression [6]. 3. Ensure at least one element: If the business logic requires the attribute to exist, ensure that the set contains at least one valid element before sending the request [6]. Note that this behavior is specific to set types. While String and Binary scalar attributes also have length requirements (must be greater than zero), set types are uniquely restricted from being empty [1][2][5]. Attempting to send an AttributeValue structure that is empty or does not contain a supported, non-empty data type will trigger a ValidationException [4].

Citations:


Handle empty normalized email lists before assigning AttributeValue.SS.

When user.Emails is non-nil but normalizeEmails returns no values, Save sends an empty SS to DynamoDB. DynamoDB rejects empty string sets, so the update fails and cannot clear user_emails. Use REMOVE #UES`` when an empty list means clear; otherwise reject the input before UpdateItem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/users/repository.go` around lines 390 - 393, After calling
normalizeEmails() and assigning the result to the normalized variable, check if
the list is empty before assigning to expressionAttributeValues. When normalized
is empty, either add a REMOVE operation for the `#UES` attribute to clear
user_emails instead of assigning the empty SS, or validate and reject the input
before the UpdateItem call rather than attempting to send an empty string set to
DynamoDB.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, read the exact code at the specified lines
cat -n cla-backend-go/users/repository.go | sed -n '380,400p'

Repository: linuxfoundation/easycla

Length of output: 1405


🏁 Script executed:

# Check the context of this function and normalizeEmails implementation
ast-grep outline cla-backend-go/users/repository.go

Repository: linuxfoundation/easycla

Length of output: 2707


🏁 Script executed:

# Search for log level configuration in the codebase
rg -i "log.*level|debug" cla-backend-go --type go -B 2 -A 2 | head -100

Repository: linuxfoundation/easycla

Length of output: 7545


🏁 Script executed:

# Look for server/initialization configuration
fd config -e yaml -e yml -e go | head -20

Repository: linuxfoundation/easycla

Length of output: 288


🏁 Script executed:

# Check if there are any log redaction or sanitization mechanisms
rg -i "redact|sanitiz|mask" cla-backend-go --type go

Repository: linuxfoundation/easycla

Length of output: 2230


🏁 Script executed:

# Examine the config.go file for log level settings
cat -n cla-backend-go/config/config.go | head -100

Repository: linuxfoundation/easycla

Length of output: 4094


🏁 Script executed:

# Search for logrus or log level initialization
rg -i "logrus|setlevel|debug" cla-backend-go --type go | grep -i "level\|init\|new" | head -30

Repository: linuxfoundation/easycla

Length of output: 3346


🏁 Script executed:

# Check server.go for log initialization and configuration
cat -n cla-backend-go/cmd/server.go | sed -n '1,100p'

Repository: linuxfoundation/easycla

Length of output: 5137


🏁 Script executed:

# Look for environment variables controlling log levels
rg -i "log_level|loglevel" cla-backend-go --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 526


🏁 Script executed:

# Check if Debug logs are sent to DataDog or other persistent services
rg -i "datadog|cloudwatch|stderr|stdout" cla-backend-go --type go | head -40

Repository: linuxfoundation/easycla

Length of output: 3882


🏁 Script executed:

# Check the complete logging/logger.go to understand log level defaults and output
cat -n cla-backend-go/logging/logger.go

Repository: linuxfoundation/easycla

Length of output: 6979


🏁 Script executed:

# Look for log output configuration (file, stdout, etc.)
rg "AddHook|SetOutput" cla-backend-go --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Check if logs are sent to Datadog or other centralized services
rg "Hook|Formatter" cla-backend-go/logging --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 1681


🏁 Script executed:

# Check for deployment/infrastructure configuration that sets LOG_LEVEL
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.tf" -o -name "*.json" -o -name "*.env*" \) | xargs grep -l "LOG_LEVEL" 2>/dev/null | head -20

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Check for any log redaction or sanitization at the application level
rg "email|Email" cla-backend-go/logging --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Verify if there's any configuration in the actual Save function that calls this code
cat -n cla-backend-go/users/repository.go | sed -n '331,470p'

Repository: linuxfoundation/easycla

Length of output: 7207


Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External

Redact the email list before logging, or log only the count.

Line 391 logs the full normalized email addresses at Debug level using %v. The default log level in the codebase is Debug (logging/logger.go:77), and no deployment-level LOG_LEVEL override is configured. In production Lambda or container environments, Debug logs are typically captured and retained in CloudWatch Logs or similar log aggregation services, creating a persistent PII copy. Replace the log statement with a redacted version—for example, log only the count of emails (len(normalized)) or redact the addresses to [email address] format. This pattern applies to other Debug logs in the Save function that also log sensitive fields (lines 369, 376, 383, 398, 405, 412, 419, 426).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/users/repository.go` around lines 390 - 393, Update the Debug
logging in the Save function, including the log statement near normalizeEmails
and the other sensitive-field logs identified in the comment, so email addresses
and related PII are not emitted. Log only non-sensitive metadata such as
collection counts, while preserving the existing query-building behavior.
cla-backend-go/v2/my_clas/service.go (1)

225-227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect FormatTimeString to confirm the output format is lexicographically sortable.
set -euo pipefail

rg -n --type=go -C15 'func FormatTimeString\s*\(' cla-backend-go/utils

# Show the layout constants it uses.
rg -n --type=go 'time\.RFC3339|2006-01-02|Jan  2|Layout' cla-backend-go/utils | head -40

Repository: linuxfoundation/easycla

Length of output: 3377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- utils parsing ---'
cat -n cla-backend-go/utils/utils.go | sed -n '40,115p'

printf '%s\n' '--- service context ---'
cat -n cla-backend-go/v2/my_clas/service.go | sed -n '180,245p'

printf '%s\n' '--- SignedOn declarations and assignments ---'
rg -n --type=go -C4 'SignedOn|signedOn|FormatTimeString|my-cla-list|sorted by signedOn' cla-backend-go/v2 cla-backend-go | head -240

printf '%s\n' '--- contract references ---'
rg -n -i --glob '*.{yaml,yml,go,md}' 'cla-list|signedOn|signed.on|chronolog|sort' . | head -240

Repository: linuxfoundation/easycla

Length of output: 48068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- timestamp writers ---'
rg -n --type=go -C10 'func .*AddSignedOn|AddSignedOn\(|SignatureCreated|DateCreated:|date_created' cla-backend-go/v2/signatures cla-backend-go/signatures cla-backend-go/v2/sign cla-backend-go/v2/my_clas | head -280

printf '%s\n' '--- my-cla sorting tests and fixtures ---'
cat -n cla-backend-go/v2/my_clas/service_test.go | sed -n '1,220p'
rg -n --type=go -C8 'GetMyCla|ResultCount|SignedOn|signedOn' cla-backend-go/v2/my_clas/service_test.go

printf '%s\n' '--- API contract and response model ---'
cat -n cla-backend-go/swagger/common/my-cla-list.yaml | sed -n '1,65p'
cat -n docs/MY_CLAS_API.md | sed -n '235,255p;305,380p'

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- AddSignedOn implementation ---'
rg -n --type=go -C18 'func \(.*\) AddSignedOn' cla-backend-go/signatures

printf '%s\n' '--- all direct signed_on assignments near write paths ---'
rg -n --type=go -C3 'SignedOn:\s|signed_on.*currentTime|AddSignedOn' cla-backend-go/signatures cla-backend-go/v2/sign cla-backend-go/v2/dynamo_events | head -220

printf '%s\n' '--- contract wording ---'
cat -n cla-backend-go/swagger/common/my-cla-list.yaml | sed -n '25,38p'
cat -n docs/MY_CLAS_API.md | sed -n '238,248p'

Repository: linuxfoundation/easycla

Length of output: 19120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from datetime import datetime, timezone

# Model the relevant Go behavior:
# ParseDateTime accepts RFC3339-like values, then FormatTimeString emits
# time.RFC3339, which has second precision and UTC normalization.
values = [
    "2025-01-01T00:00:00.900000Z",
    "2025-01-01T00:00:00.100000Z",
]
formatted = [
    datetime.fromisoformat(v.replace("Z", "+00:00"))
    .astimezone(timezone.utc)
    .strftime("%Y-%m-%dT%H:%M:%SZ")
    for v in values
]

print("raw chronological order:", values[0] > values[1])
print("formatted values:", formatted)
print("formatted comparator distinguishes them:", formatted[0] != formatted[1])
assert values[0] != values[1]
assert formatted[0] == formatted[1]
PY

Repository: linuxfoundation/easycla

Length of output: 305


Sort by parsed time before formatting.

FormatTimeString removes subsecond precision and returns the original string for unsupported values. String sorting can therefore violate chronological order. Sort by parsed timestamps and define the invalid-value behavior before formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/my_clas/service.go` around lines 225 - 227, Update the
sorting logic around result.Clas to parse each SignedOn value and compare the
parsed timestamps chronologically before any FormatTimeString processing. Define
and consistently apply behavior for invalid or unsupported timestamps, then
format the values afterward without using the formatted strings for ordering.
cla-backend-go/v2/user-service/client.go (1)

361-404: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the pagination loop.

The loop only stops when a page returns fewer than pageSize records. If the upstream ignores offset and keeps returning exactly 100 records, the loop repeats until the request context is cancelled. The 30-second timeout applies per request, not to the loop.

Add a maximum page count, and stop when a page returns no records.

🛡️ Proposed guard
 	const pageSize = int64(100)
+	const maxPages = 100
 	var identities []*models.UserIdentity
-	for offset := int64(0); ; offset += pageSize {
+	for page := 0; page < maxPages; page++ {
+		offset := int64(page) * pageSize
 		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
@@
 		identities = append(identities, identityList.Data...)
-		if int64(len(identityList.Data)) < pageSize {
+		if int64(len(identityList.Data)) < pageSize {
 			break
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	const pageSize = int64(100)
	const maxPages = 100
	var identities []*models.UserIdentity
	for page := 0; page < maxPages; page++ {
		offset := int64(page) * pageSize
		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
		request, reqErr := http.NewRequestWithContext(ctx, "GET", url, nil)
		if reqErr != nil {
			log.WithFields(f).WithError(reqErr).Warn("problem building new request")
			return nil, reqErr
		}

		request.Header.Set("X-API-KEY", usc.apiKey)
		request.Header.Set("Authorization", "Bearer "+tok)
		request.Header.Set("Content-Type", "application/json")

		response, doErr := httpClient.Do(request)
		if doErr != nil {
			log.WithFields(f).WithError(doErr).Warn("problem listing user identities")
			return nil, doErr
		}

		data, readErr := io.ReadAll(response.Body)
		closeErr := response.Body.Close()
		if closeErr != nil {
			log.WithFields(f).WithError(closeErr).Warn("error closing body")
		}
		if readErr != nil {
			log.WithFields(f).WithError(readErr).Warn("problem reading the user identities response")
			return nil, readErr
		}

		if response.StatusCode != http.StatusOK {
			log.WithFields(f).Warnf("problem listing user identities - status: %d", response.StatusCode)
			return nil, fmt.Errorf("listing user identities for user %s failed with status: %d", userSFID, response.StatusCode)
		}

		var identityList models.UserIdentityList
		if unmarshalErr := json.Unmarshal(data, &identityList); unmarshalErr != nil {
			log.WithFields(f).WithError(unmarshalErr).Warn("problem decoding the user identities response")
			return nil, unmarshalErr
		}

		identities = append(identities, identityList.Data...)
		if int64(len(identityList.Data)) < pageSize {
			break
		}
	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/user-service/client.go` around lines 361 - 404, Bound the
pagination loop around the user-identity listing flow by tracking the number of
pages and enforcing a finite maximum page count. Also break immediately when
identityList.Data is empty, while preserving the existing short-page termination
condition and response handling.
docs/MY_CLAS_API.md (1)

599-608: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

sed -n '383,520p' cla-backend-go/v2/my_clas/service.go
rg -n "authorizeIdentity|GithubID|GithubUsername|historical|reassign|recycl|GetMyClas" cla-backend-go/v2/my_clas docs/MY_CLAS_API.md

Repository: linuxfoundation/easycla

Length of output: 19136


🏁 Script executed:

sed -n '280,380p' cla-backend-go/v2/my_clas/service.go
sed -n '440,575p' cla-backend-go/v2/my_clas/service_test.go
sed -n '120,220p' cla-backend-go/v2/my_clas/service.go

Repository: linuxfoundation/easycla

Length of output: 12677


🏁 Script executed:

sed -n '200,290p' cla-backend-go/v2/my_clas/service.go
sed -n '55,115p' cla-backend-go/v2/my_clas/handlers.go
rg -n "Pdf|pdf|Download|download" cla-backend-go/v2/my_clas

Repository: linuxfoundation/easycla

Length of output: 10902


Historical CLA Disclosure Through Reassigned Identity (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External · Exploitability: Moderate

Require privacy/security approval or immutable identity corroboration before release.

The non-admin GetMyClas and GetMyClaPdf paths authorize email and SCM username values from the authenticated LF account’s identities. A reassigned alias can expose the previous holder’s historical records and signed ICLA PDF. Require immutable githubId/gitlabId corroboration, or document explicit privacy and security approval for this exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/MY_CLAS_API.md` around lines 599 - 608, Update the documented access
policy for non-admin GetMyClas and GetMyClaPdf to require immutable
githubId/gitlabId corroboration for historical records and signed PDFs, or
explicitly record approved privacy and security sign-off for the alias-based
exception before release; do not present the current-possession behavior as an
unconditional accepted product decision.
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md (1)

3-5: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one authoritative M1 contract after PR #5125.

These artifacts still contain pre-PR #5125 endpoints, client-side authorization, old signature classification, and old status models while their banners say those designs are superseded.

  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md#L3-L5: replace the old endpoint contract with /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and /v4/my-clas/identities.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md#L5-L5: update the signature fields, ECLA classification, response fields, and validity model.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md#L8-L10: remove obsolete Self Serve aggregation and authorization requirements.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md#L21-L27: align requirements with valid, secondary email, GitLab/Gerrit support, and current endpoint scope.
📍 Affects 4 files
  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md#L3-L5 (this comment)
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md#L5-L5
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md#L8-L10
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md#L21-L27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md`
around lines 3 - 5, Replace the superseded M1 contract in
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md:3-5
with the authoritative /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and
/v4/my-clas/identities definitions. Update
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md:5-5 for current
signature fields, ECLA classification, response fields, and validity; remove
obsolete Self Serve aggregation and authorization requirements from
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md:8-10; and align
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md:21-27 with valid,
secondary email, GitLab/Gerrit support, and the current endpoint scope.
tests/functional/cypress/e2e/v4/cla-manager.cy.ts (1)

710-710: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore conditional use of allowFail for failOnStatusCode.

This line sets failOnStatusCode: false unconditionally. This removes the ability to fail the test on unexpected HTTP error status codes. If the test relies on allowFail to distinguish expected failures from unexpected ones, this change masks real errors in the CLA Manager creation request.

Restore the previous conditional behavior, unless the intent is to always ignore status codes for this specific request.

🔧 Proposed fix
-      failOnStatusCode: false,
+      failOnStatusCode: allowFail ? false : true,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/cypress/e2e/v4/cla-manager.cy.ts` at line 710, The
failOnStatusCode property in the CLA Manager creation request is unconditionally
set to false, which masks unexpected HTTP errors. Make this setting conditional
based on the allowFail parameter so that failOnStatusCode is false only when
allowFail is true, allowing the test to properly fail on unexpected status codes
when allowFail is false.
utils/downcase_emails.sh (3)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required source headers to both shell scripts.

The CI header checker applies to shell files.

  • utils/downcase_emails.sh#L1-L2: add the Linux Foundation copyright header and # SPDX-License-Identifier: MIT after the shebang.
  • utils/lookup_all_logs.sh#L1-L2: add the Linux Foundation copyright header after the shebang and preserve the applicable SPDX header.

As per coding guidelines, source files under cla-backend-go and applicable utility scripts must include the required Linux Foundation copyright header, and new source files must include the MIT SPDX identifier.

📍 Affects 2 files
  • utils/downcase_emails.sh#L1-L2 (this comment)
  • utils/lookup_all_logs.sh#L1-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/downcase_emails.sh` around lines 1 - 2, Add the required Linux
Foundation copyright header immediately after the shebang in
utils/downcase_emails.sh lines 1-2, followed by the MIT SPDX identifier. Add the
Linux Foundation copyright header after the shebang in utils/lookup_all_logs.sh
lines 1-2, preserving its existing applicable SPDX header.

Source: Coding guidelines


10-10: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n utils/downcase_emails.sh

Repository: linuxfoundation/easycla

Length of output: 1932


Sensitive Data Exposure (CWE-312): Cleartext Storage of Sensitive Information

Reachability: Internal

Write user email data to a secure temporary file with automatic cleanup.

Line 10 writes all user email addresses to ${STAGE}_user_emails.json without mode restrictions and without cleanup. Email addresses are personally identifiable information. A shared workstation or CI artifact process can expose this data through the persistent file.

Apply umask 077 before the aws dynamodb scan command, create a temporary file using mktemp, and register an EXIT trap to remove the file when the script terminates:

Proposed fix
+umask 077
+email_dump="$(mktemp "${TMPDIR:-/tmp}/${STAGE}_user_emails.XXXXXX.json")"
+trap 'rm -f "$email_dump"' EXIT
-aws dynamodb scan ... > "${STAGE}_user_emails.json"
-cat "${STAGE}_user_emails.json" | jq -c ...
+aws dynamodb scan ... > "$email_dump"
+jq -c '...' < "$email_dump"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/downcase_emails.sh` at line 10, Update the script around the DynamoDB
scan to set a restrictive umask, create the JSON output with mktemp, and store
its path for reuse instead of writing directly to the stage-named file. Register
an EXIT trap to remove the temporary file on termination, and ensure the scan
command writes to that temporary path while preserving its existing query
options.

10-26: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent concurrent email updates from being overwritten.

The scan and update form an unguarded read-modify-write. If the application changes user_emails after Line 10, Line 23 replaces that newer value with the stale normalized set.

Add a condition that requires user_emails to equal the scanned value. Report conditional failures for manual retry.

Proposed fix
     uid=$(jq -r '.user_id.S' <<<"$item")
+    oldss=$(jq -c '.user_emails.SS' <<<"$item")
     newss=$(jq -c '[.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique' <<<"$item")
+    values=$(jq -cn --argjson old "$oldss" --argjson new "$newss" \
+      '{":old":{"SS":$old},":e":{"SS":$new}}')
 ...
         --update-expression 'SET user_emails = :e' \
-        --expression-attribute-values "{\":e\":{\"SS\":$newss}}" && echo "ok"
+        --condition-expression 'user_emails = :old' \
+        --expression-attribute-values "$values" && echo "ok"

As per path instructions, operational scripts can mutate DynamoDB in the AWS environment selected by the active credentials, including production.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

aws dynamodb scan --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" --projection-expression 'user_id, user_emails' --filter-expression 'attribute_exists(user_emails)' --output json > "${STAGE}_user_emails.json"
cat "${STAGE}_user_emails.json" | jq -c '.Items[] | select(.user_emails.SS != null) | ([.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique) as $n | select(($n | length > 0) and ($n != (.user_emails.SS | sort)))' \
| while IFS= read -r item; do
    uid=$(jq -r '.user_id.S' <<<"$item")
    oldss=$(jq -c '.user_emails.SS' <<<"$item")
    newss=$(jq -c '[.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique' <<<"$item")   # lower + trim + drop-empty + dedupe
    values=$(jq -cn --argjson old "$oldss" --argjson new "$newss" \
      '{":old":{"SS":$old},":e":{"SS":$new}}')
    if [ "$newss" = "[]" ]
    then
      echo "skip $uid (no valid emails after normalize)" >&2
      continue
    fi
    echo "user $uid -> $newss"
    if [ "$APPLY" = "1" ]
    then
      aws dynamodb update-item --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" \
        --key "{\"user_id\":{\"S\":\"$uid\"}}" \
        --update-expression 'SET user_emails = :e' \
        --condition-expression 'user_emails = :old' \
        --expression-attribute-values "$values" && echo "ok"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/downcase_emails.sh` around lines 10 - 26, Update the update-item
operation in the normalization loop to condition the write on user_emails still
matching the scanned item’s original user_emails.SS value, while preserving the
normalized :e update. Capture conditional-check failures separately from
successful updates and report the affected uid for manual retry instead of
treating them as successful writes.

Source: Path instructions

utils/get_user_svc.sh (1)

51-59: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /repo && pwd

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# First, check if the file exists and examine lines 51-59 and 70-73
if [ -f "utils/get_user_svc.sh" ]; then
  echo "=== Lines 51-59 ==="
  sed -n '51,59p' utils/get_user_svc.sh
  echo ""
  echo "=== Lines 70-73 ==="
  sed -n '70,73p' utils/get_user_svc.sh
else
  echo "File not found"
fi

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Create a simple script to verify curl behavior with different flags
python3 - <<'PY'
import subprocess
import sys

# According to curl documentation:
# -s: silent mode (no progress meter)
# -S: show errors even in silent mode
# -f: fail on server errors (HTTP 4xx, 5xx)

# Test 1: Check curl man page or help for -f flag behavior
result = subprocess.run(['curl', '--help'], capture_output=True, text=True)
help_text = result.stdout + result.stderr

# Find the -f flag documentation
for line in help_text.split('\n'):
    if ' -f,' in line or line.strip().startswith('-f'):
        print(f"curl -f documentation: {line}")
        
# Also check explicit behavior description
print("\n--- Key curl behavior ---")
print("curl -sS: silent mode, but still return 0 for HTTP error codes")
print("curl -fsS: fail (non-zero exit) on HTTP 4xx/5xx, silent mode")
print("\nThe -f flag is critical for catching HTTP errors in scripts.")
PY

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Get the full context of the file around those lines
head -80 utils/get_user_svc.sh | tail -35

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Also check git log for Signed-off-by requirement
cd /repo && git log --oneline -10 -- utils/get_user_svc.sh 2>/dev/null || echo "No git history available"

Repository: linuxfoundation/easycla

Length of output: 1932


🌐 Web query:

curl -f flag fail on HTTP errors exit code documentation

💡 Result:

When using curl, the command line flag to make it return a non-zero exit code on HTTP errors (specifically for HTTP response codes 400 or greater) is -f or --fail [1][2]. When this flag is used, if the server returns an HTTP status code indicating an error (400 or above), curl will exit with error code 22 [3][1]. By default, curl does not consider HTTP response codes to be failures and will continue processing or return a success exit code (0) even if the server returns an error page [1][2]. Key details regarding this behavior: 1. No Output: When using --fail, curl does not output the response body when an HTTP error occurs [1][2]. 2. Alternative for Content: If you want to receive an error code 22 on HTTP failures while still saving or outputting the response body (the error page returned by the server), use --fail-with-body instead of --fail [4][1][2]. 3. Scope: Note that --fail is not a global option and is contained by --next [4][2]. If you are performing multiple transfers, you may need to ensure the flag is applied correctly to the relevant URLs. 4. Limitations: The --fail option is not entirely fail-safe; in some cases, such as certain authentication scenarios (e.g., response codes 401 and 407), the request may still appear to succeed in curl's logic [1][2]. If you are writing scripts, checking for exit code 22 is the standard way to detect these specific HTTP-based failures when the --fail flag is active [3].

Citations:


🌐 Web query:

curl -sS vs -fsS difference HTTP error handling

💡 Result:

In the context of curl, HTTP error handling is governed by a distinction between transport-level failures and application-level HTTP status codes [1][2]. By default, curl only considers transport-level issues (such as inability to resolve a host, timeout, or lost connection) as failures [1][2]. If a server successfully processes a request and returns an HTTP status code in the 4xx or 5xx range, curl treats this as a successful transfer, meaning it does not return a non-zero exit code [3][1][4]. To change this behavior and force curl to exit with a non-zero status when receiving 4xx or 5xx HTTP responses, you must explicitly enable error handling via command-line flags [1][5]: 1. --fail (-f): Instructs curl to exit with error code 22 if the server returns an HTTP status code of 400 or higher [1][6][5]. When used, curl suppresses the response body, which is often useful if you only need to verify success [6][2]. 2. --fail-with-body: Similar to --fail, this also causes curl to exit with error code 22 for HTTP errors >= 400, but it preserves and outputs the response body [6][5][2]. This is preferred for debugging or when the server's error response contains necessary information [2]. At the library level (libcurl), the behavior is similar. The library does not trigger an error for HTTP 4xx/5xx responses unless the CURLOPT_FAILONERROR option is set to 1L [3]. When enabled, libcurl will return the error code CURLE_HTTP_RETURNED_ERROR [3][7]. In summary, the key difference is that curl separates the success of the underlying HTTP protocol exchange (which is always successful if a response is received) from the meaning of the HTTP status code itself (which may represent an application-level error) [1][2]. Users must explicitly bridge this gap if they want HTTP errors to trigger shell exit codes [8][2].

Citations:


Add curl -f flag to fail on HTTP errors.

By default, curl -sS returns exit code 0 for HTTP 4xx and 5xx responses. The profile lookup then prints no user-service profile found and exits 0 instead of failing. The pagination loop also treats error responses as the final empty page.

Add the -f flag to make curl exit with code 22 on HTTP errors >= 400.

Proposed fix
-profile="$(curl -sS -G -XGET "${hdr[@]}" --data-urlencode "username=${lfid}" "${GW}/user-service/v1/users")"
+if ! profile="$(curl -fsS -G -XGET "${hdr[@]}" \
+  --data-urlencode "username=${lfid}" \
+  "${GW}/user-service/v1/users")"
+then
+  echo "user-service profile request failed" >&2
+  exit 3
+fi
 ...
-  page="$(curl -sS -XGET "${hdr[@]}" "${GW}/user-service/v1/users/${sfid}/identities?pageSize=100&offset=${offset}")"
+  if ! page="$(curl -fsS -XGET "${hdr[@]}" \
+    "${GW}/user-service/v1/users/${sfid}/identities?pageSize=100&offset=${offset}")"
+  then
+    echo "user-service identities request failed at offset ${offset}" >&2
+    exit 3
+  fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/get_user_svc.sh` around lines 51 - 59, Update the profile lookup curl
invocation in get_user_svc.sh to include the -f/--fail flag alongside the
existing silent and show-error options. Ensure HTTP 4xx/5xx responses cause curl
to fail so the surrounding profile and pagination flows do not treat error
responses as empty results.
utils/local-fbsd-api.sh (1)

5-10: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop when directory setup or the build fails.

If cd or go build fails, the script continues. It can source the wrong environment file and execute an existing stale binary.

Use strict mode and perform the build in a subshell.

Proposed fix
 #!/bin/bash
+set -euo pipefail
 ...
-cd cla-backend-go
-go build -o bin/cla-fbsd main.go
-cd ..
+(cd cla-backend-go && go build -o bin/cla-fbsd main.go)
 source setenv.sh
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

set -euo pipefail
(cd cla-backend-go && go build -o bin/cla-fbsd main.go)
source setenv.sh
# source setenv-prod.sh.secret
GH_ORG_VALIDATION=false PORT=8080 ./cla-backend-go/bin/cla-fbsd
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)


[info] 7-7: Use a ( subshell ) to avoid having to cd back.

(SC2103)


[info] 8-8: Not following: setenv.sh was not specified as input (see shellcheck -x).

(SC1091)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/local-fbsd-api.sh` around lines 5 - 10, Update the setup and build flow
in the local fBSD launch script to enable strict shell failure handling and run
the `cd cla-backend-go` plus `go build` commands in a subshell. Ensure any
failed directory change or build terminates the script before sourcing
environment files or executing the binary.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Line 20: Resolve the selection contract across FR-002, Acceptance Scenario 3,
and Open Question 1: choose either Self Serve narrowing the CLA Group to a
relevant org/repo or deferring that selection to Contributor Console. Update all
three references consistently so the requirement, scenario, and open question
describe the same behavior, and remove the ambiguity.
- Line 43: Update the scope wording to use the required “Approved List”
terminology, replacing the hyphenated “approval-list” reference while preserving
the existing M4 exclusion meaning. Do not introduce “whitelist” or alter any
other scope items.
- Line 9: Preserve the blockquote continuity at the blank line on line 9 of the
specification by prefixing that blank line with “>”, or remove the blank line
entirely so both notes render as one blockquote.
- Line 14: Standardize the corporate agreement terminology across the user
story, acceptance scenarios, and FR-004 by choosing one canonical sign-type
value consistent with project_ccla_enabled. If ECLA remains user-facing,
explicitly define its mapping to CCLA for the agreement picker and Contributor
Console hand-off, and apply that mapping consistently throughout the
specification.
- Line 33: Update FR-008 and the hand-off contract specification to require
server-side identity derivation from the Console session, never client-supplied
user IDs or query parameters. Define carried context such as CLA Group ID, sign
type, and org/repo as either session-bound through a server-validated one-time
signed token or non-identifying metadata, and verify the Console session model
enforces ownership before opening the signing flow.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 396bf8f3-cc63-4e00-acd5-75c6e024fe95

📥 Commits

Reviewing files that changed from the base of the PR and between 4990e8e and b88e3ac.

📒 Files selected for processing (2)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md

This is the extracted, implementable slice for Milestone 2. Program-wide context, assumptions, and resolved decisions live in the parent spec; this file is what `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` operate on.

> **Scope (revised 2026-08-04, per Heather/PM)**: M2 is purely additive — it retires nothing and cuts nothing over. Self Serve never runs the DocuSign ceremony; it hands off to the existing Contributor Console for signing. See the User Story below.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve the blockquote on Line 9.

Add > to the blank line, or remove the blank line. Otherwise, Markdown rendering can split the two notes into separate blocks.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 9-9: Blank line inside blockquote

(MD028, no-blanks-blockquote)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
9, Preserve the blockquote continuity at the blank line on line 9 of the
specification by prefixing that blank line with “>”, or remove the blank line
entirely so both notes render as one blockquote.

Source: Linters/SAST tools


## User Story (P2)

A contributor who wants to sign a CLA — without first hitting a failing PR check — logs into LFX Self Serve and, under the Me lens, opens "Sign a CLA". They pick which CLA Group they need to sign for (and, where relevant, narrow to a specific GitHub org/repo), choose ICLA or ECLA (gated by what that CLA Group allows), and are handed off to the existing Contributor Console, pre-scoped to that selection, to complete the actual signing there. Signing itself — the DocuSign ceremony — happens in the Console, exactly as it does today.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Define one canonical name for the corporate sign type.

The PR objective and project_ccla_enabled use CCLA, but the user story and acceptance scenario use ECLA. FR-004 mixes both terms. Select one canonical value, or define the ECLA to CCLA mapping for the picker and Console hand-off.

Also applies to: 19-19, 28-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
14, Standardize the corporate agreement terminology across the user story,
acceptance scenarios, and FR-004 by choosing one canonical sign-type value
consistent with project_ccla_enabled. If ECLA remains user-facing, explicitly
define its mapping to CCLA for the agreement picker and Contributor Console
hand-off, and apply that mapping consistently throughout the specification.

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated

**In**: new Me-lens "Sign a CLA" picker (CLA Group + optional org/repo + ICLA/ECLA), SS server route(s) to enumerate CLA Groups and construct the Console hand-off, feature flag.

**Out**: any DocuSign/webhook/PDF backend changes; native signing ceremony in SS; PR-redirect cutover (`CLAContributorv2Base` SSM flip); CCLA/approval-list management (M4). Also out: corporate org-selection UX polish for the CCLA path — Heather flagged this for M3, so do not over-invest here ahead of that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required Approved List terminology.

Replace approval-list with Approved List.

As per coding guidelines, use the terminology Approved List; do not use whitelist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
43, Update the scope wording to use the required “Approved List” terminology,
replacing the hyphenated “approval-list” reference while preserving the existing
M4 exclusion meaning. Do not introduce “whitelist” or alter any other scope
items.

Source: Coding guidelines

…ied facts

Verified against easycla-contributor-console and cla-backend-go:

- Hand-off reuses the Console's existing decision-screen deep link
  (/#/cla/project/{claGroupID}/user/{userID}, ?redirect= optional) — no new
  hand-off contract. The ICLA/ECLA choice, legal guidance, and flag gating
  stay in the Console; SS does not re-implement them.
- Drop org/repo selection: the signing unit is the CLA Group; org/repo names
  are at most search metadata in the picker.
- userID resolved server-side via existing GET /v4/user-from-token
  (lookup-or-create by LF username/email).

Adds constraints: simple/straightforward design, independently deliverable
in ~2 weeks.

Rewrites open questions from code evidence:
1. GitHub identity binding (primary) — proactive ICLA must land on a user
   record the PR check can match (GitHub ID → username → email); user-from-token
   records carry no GitHub identity. Recommended: require M1's GitHub-account
   linking for the ICLA path.
2. Proactive-ICLA active-signature gap — Console + backend assume PR-derived
   context on the GitHub ICLA path (active-signature record, repository_id/
   pull_request_id for the DocuSign callback); Gerrit path proves a no-PR
   shape works. ECLA path has no PR dependency.
3. CLA-Group listing endpoint (unchanged).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Michal Lehotsky <[email protected]>
Copilot AI review requested due to automatic review settings August 4, 2026 14:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md (1)

39-42: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make the success criterion cover the signing outcome.

SC-002 measures only arrival at the decision screen. It can pass while Scenario 3 fails during identity binding or active-signature creation. Add a criterion for ICLA completion and PR-check matching, or state that M2 success stops at the hand-off.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` around
lines 39 - 42, Update success criterion SC-002 in the Success Criteria section
to cover the full signing outcome, including successful ICLA completion and
matching PR checks after identity binding and active-signature creation;
alternatively, explicitly redefine M2 success as ending at the Contributor
Console hand-off.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Line 19: Resolve the M2 specification inconsistency by either adding GitHub
identity binding and no-PR active-signature support to the M2 requirements and
flow, or removing/defering Scenario 3 and FR-008 with explicit interim behavior.
Update the affected sections consistently, including the contributor handoff
flow and all referenced requirements, so proactive ICLA signing can produce a
PR-matchable completed record.

---

Nitpick comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Around line 39-42: Update success criterion SC-002 in the Success Criteria
section to cover the full signing outcome, including successful ICLA completion
and matching PR checks after identity binding and active-signature creation;
alternatively, explicitly redefine M2 success as ending at the Contributor
Console hand-off.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e83e6017-54a3-4ed6-a8ff-902e3a515d4b

📥 Commits

Reviewing files that changed from the base of the PR and between b88e3ac and f03162f.

📒 Files selected for processing (2)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md


## User Story (P2)

A contributor who wants to sign a CLA — without first hitting a failing PR check — logs into LFX Self Serve and, under the Me lens, opens "Sign a CLA". They pick which CLA Group they need to sign for and are handed off to the existing Contributor Console's decision screen for that CLA Group, where they choose Individual or Corporate contributor (as PR-referred contributors do today) and complete the signing there. The DocuSign ceremony, the ICLA/ECLA choice, and all signing logic stay in the Console, exactly as today.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Resolve the proactive ICLA dependency before accepting Scenario 3.

Scenario 3 and FR-008 require a completed ICLA that the PR check can match. FR-004 can return a newly created LF-only record, while the verified facts state that the GitHub ICLA path still requires a PR-created active-signature record. Make the GitHub identity binding and no-PR active-signature changes part of M2, or remove/defer Scenario 3 and FR-008 and define the interim behavior.

Also applies to: 25-25, 33-37, 61-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
19, Resolve the M2 specification inconsistency by either adding GitHub identity
binding and no-PR active-signature support to the M2 requirements and flow, or
removing/defering Scenario 3 and FR-008 with explicit interim behavior. Update
the affected sections consistently, including the contributor handoff flow and
all referenced requirements, so proactive ICLA signing can produce a
PR-matchable completed record.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:32

  • FR-002/FR-003 silently resolve two questions that the PR description and linked program PR #5132 still call unresolved: whether org/repo narrowing is required and what the no-PR Console hand-off contract is. The parent spec currently requires selection/pre-scoping by sign type and allows org/repo narrowing, whereas this slice removes both without recording a subsequent product decision. Keep these as clarification items, or update the PR description and parent artifacts with the decision that supersedes them before treating this as the extracted implementation contract.
- **FR-002**: The picker MUST let the user find and select a CLA Group — a simple searchable list (project/CLA Group names). No org/repo selection step: the signing unit is the CLA Group, and the Console hand-off URL carries only `claGroupID` + `userID`. Org/repo names MAY be used as search metadata to help users find the right CLA Group, nothing more.
- **FR-003**: On selection, Self Serve MUST hand off to the Contributor Console's existing decision-screen URL — `{console}/#/cla/project/{claGroupID}/user/{userID}` — the same shape the PR-check link uses, without the optional `?redirect=` param (there is no PR to return to). The ICLA/ECLA choice, its legal guidance text, and the `project_icla_enabled`/`project_ccla_enabled` gating stay in the Console; SS MUST NOT re-implement them.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • These Angular paths describe the pre-implementation M1 plan rather than the current Self Serve layout. M1 landed as app/modules/profile/clas, registered in app/modules/profile/profile.routes.ts, with its feature-gated tab wired through the profile layout/sidebar; there is no app/modules/my-clas surface to mirror. Targeting main-layout.component.ts and top-level app.routes.ts would bypass the established Profile & Account integration, so update the structure after deciding whether Sign a CLA is a profile child or another current Me-lens surface.
├── app/modules/sign-cla/                        # NEW Me-lens page: searchable CLA Group list → hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                            # EDIT: register the route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:79

  • An ECLA-only fallback does not satisfy this PR's stated M2 scope. The spec's acceptance scenario 3 and FR-008 require proactive ICLA completion, and the PR description explicitly includes both ICLA and CCLA paths; deferring the no-PR ICLA change would ship a picker whose Individual path is known to fail. Make resolution of this gap a launch blocker, or formally revise the milestone scope and acceptance criteria with PM approval.
2. **Proactive-ICLA gap** (spec open question 2) — Console + backend delta; decide in/out of M2 at `/speckit.clarify`. ECLA works proactively with zero changes, so a worst-case fallback exists but weakens the milestone.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:23

  • “No EasyCLA writes” is incompatible with FR-004 because user-from-token is explicitly lookup-or-create and may also refresh stored identity fields. Narrow this prohibition to signing-related writes while explicitly allowing the required provisioning operation.
**Constraints**: no EasyCLA writes and no signing-initiation calls from SS (FR-005); server-side identity derivation only (never trust client-supplied user IDs); PR-check remediation link untouched (FR-006); feature-flagged dark launch; 2-week delivery budget.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:18

  • The storage statement conflicts with the required GET /v4/user-from-token lookup-or-create flow. That middleware updates an existing identity and calls CreateUser when no record exists (cla-backend-go/cmd/server.go:933-1068), so EasyCLA DynamoDB is not untouched. Distinguish “no SS-owned storage/schema” from the permitted EasyCLA user-provisioning write.
**Storage**: none in SS (stateless: list CLA Groups, resolve `userID`, redirect). EasyCLA DynamoDB + S3 untouched.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:19

  • The current M1 CLA server service/controller tests use Vitest (cla.service.spec.ts and clas.controller.spec.ts), not Jest/Karma. Leaving the harness unresolved sends /speckit.tasks toward the wrong tooling even though the exact adjacent tests are already available; name Vitest for server tests and reference the current Angular harness separately.
**Testing**: lfx-self-serve conventions — Jest/Karma unit tests for services/components, server route tests, Cypress/E2E per repo norms (verify exact harness during implementation).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:12

  • This says there are two clarification items, but the spec defines three and the plan later tracks CLA-Group listing as open question 3. Include the listing source here so the summary does not omit a required Phase 0 decision.
The SS-side build is deliberately thin: one page, one or two server routes, one redirect. The milestone's real design work is the two `/speckit.clarify` items in [spec.md](spec.md): **GitHub identity binding** (a proactive ICLA must land on a user record the PR check can match — recommended: require M1's GitHub-account linking for the ICLA path) and the **proactive-ICLA active-signature gap** (Console + backend assume PR-derived context on the GitHub ICLA path; the Gerrit path proves a no-PR shape already works). Both are scoped and evidenced in the spec's "Verified Console/backend facts" section; neither is resolved here.

`.specify/memory/constitution.md` is the unratified template — no project-specific gates exist. Default gates applied:

- **Simplicity**: no new services, storage, state, or contracts — the hand-off is the Console's existing deep-link URL, and the ICLA/ECLA choice is not duplicated in SS. PASS.
- **Security**: `userID` derived server-side from the session via `user-from-token`; SS makes no signing calls and exposes no arbitrary-user lookup; the Console/backend remain the enforcement point for signing. PASS.
…count auth

Incorporate the M2 UI mockup (easyclav2-migration-planning, MyCLAs v8) as the
UI source of truth. M2 is now M1's My CLAs page extended with:

- "+ Sign a CLA" inline search (project / CLA group / repo source incl.
  GitLab & Gerrit) → hand-off to the Console decision screen, unchanged
  from the prior spec, plus a pre-hand-off account-authorization step
  (mockup's note) — adopted as the direction for the identity-binding
  question; per-platform mechanics remain open.
- CLA invalidation per row: ICLA via existing PUT /v4/cla-group/{id}/user/
  {id}/icla (verified: handler has no ownership check → SS enforces
  self-only); ECLA ("Confirm you no longer work at {company}?") needs a new
  backend endpoint — flagged as the main schedule risk.
- Invalidation blocked server-side during SS impersonation, reusing the
  existing impersonation-readonly middleware (FR-009).
- Status column (Valid / Needs attention / Invalidated) with
  "Request approval →" deep link into the Console; approval-criteria
  evaluation source is an open question.

Scope decision (2026-08-05): all mockup features are M2; schedule risks
noted in plan.md with independent degradation order if the 2-week budget
forces a cut.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Michal Lehotsky <[email protected]>
Copilot AI review requested due to automatic review settings August 5, 2026 08:07

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (4)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:9

  • This is not currently an extraction of the linked program-level M2 contract. PR #5132's revised User Story 2/FR-011–012 requires Self Serve to choose the sign type and, where applicable, org/repo before handing off, and it does not include invalidation or status. This slice instead delegates sign-type choice, excludes org/repo selection, and adds two substantial backend-backed features. Align the parent PR and this slice before Spec Kit consumes competing definitions of M2.
This is the extracted, implementable slice for Milestone 2. Program-wide context, assumptions, and resolved decisions live in the parent spec; this file is what `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` operate on.

> **Scope (revised 2026-08-04, per Heather/PM; UI per mockup v8)**: M2 extends M1's **My CLAs** page with three additions — a proactive "Sign a CLA" entry that hands off to the existing Contributor Console, per-CLA **invalidation**, and a richer **status** column. Self Serve never runs the DocuSign ceremony; nothing is cut over or retired; the PR-check remediation link is unchanged.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:42

  • GET /v4/user-from-token cannot by itself satisfy FR-004: it resolves only LF username/email and may create an LF-only record, while M1 deliberately aggregates platform-linked records because commit attribution uses GitHub/GitLab/Gerrit identities. Passing that LF-only user ID to the Console can bind the new signature to a record that contribution checks do not resolve. Make user-ID selection depend on the authorized platform identity/binding contract rather than mandating this endpoint before that contract is settled.
- **FR-003**: Self Serve MUST resolve the EasyCLA `userID` server-side from the session identity via the existing `GET /v4/user-from-token` endpoint (lookup-or-create). No client-supplied user IDs.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:85

  • The evaluation source is already implemented: GET /v4/my-clas evaluates each ECLA against its current CCLA and approval lists and returns valid (v2/my_clas/service.go:214-219,665-714). The actual missing contract is a backend-derived reason that distinguishes invalidation from an approval-criteria failure for the explanatory note. Reframe this question accordingly to avoid designing a duplicate evaluation endpoint.
3. **Status evaluation** — source for "no longer matches approval criteria": extend `GET /v4/my-clas` vs. a separate check; must not require SS to re-implement approval-list logic. Second schedule risk.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:82

  • The proactive ICLA entry cannot ride existing endpoints: this spec establishes that the GitHub ICLA path requires a PR-created active-signature record and repository/PR metadata. Deferring the no-PR Console/backend delta while retaining sign entry would make the required ICLA path fail, so the fallback must treat that delta as mandatory or explicitly narrow the shipped hand-off to ECLA.
If the budget forces a cut, the mockup's pieces degrade independently: sign entry + ICLA invalidation ride existing endpoints; ECLA invalidation and status enrichment are the deferrable slices — decide at `/speckit.plan`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md (1)

9-11: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve the blockquote through the blank line.

Line 10 is a blank line between two blockquote lines. Markdownlint still reports MD028. Prefix the blank line with > or remove it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` around
lines 9 - 11, Update the blockquote containing the Scope and Link note so the
blank line between them is also prefixed with ">"; preserve both quoted
paragraphs while eliminating the MD028 markdownlint violation.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md`:
- Around line 23-24: Clarify the write-boundary constraint in the plan to
account for the lookup-or-create behavior of GET /v4/user-from-token and any
identity-linking updates during authorization. Explicitly distinguish permitted
user-resolution or identity-state side effects from the prohibited
signing-initiation writes, while preserving the invalidation-only restriction
for business-action writes from Self Serve.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Line 24: Update the “See status” requirement to define a status mapping for
every record returned by GET /v4/my-clas, including invalid, expired, revoked,
pending, and historical ICLAs/ECLAs. Specify the corresponding UI status and
permitted action for each case, or explicitly exclude unsupported records,
ensuring non-valid records cannot appear as Valid or expose an inappropriate
“Request approval” action.
- Line 55: Replace the non-canonical approval-list terminology with “Approved
List” at all affected sites: update the status requirement in
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md lines 55-55
and 67-67, and the ECLA invalidation risk in
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md lines 77-77.
Preserve the surrounding requirements and wording.
- Line 22: Update the “Sign a CLA” flow specification to define binding the
authorized GitHub, GitLab, or Gerrit identity to the EasyCLA user record created
by GET /v4/user-from-token before handing off to the Contributor Console.
Require the bound identity to match the provider identity used for commit
lookup, and reject the hand-off when the binding is absent or mismatched.
- Line 31: Define the ECLA invalidation contract before accepting Scenario 4:
specify whether it affects one ECLA, all company ECLAs, or the CCLA association;
define resulting signature_approved and Approved List states, emitted event type
and payload, HTTP response, and duplicate-request behavior. Update Scenario 4
and FR-008 to consistently reflect those semantics.

---

Duplicate comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Around line 9-11: Update the blockquote containing the Scope and Link note so
the blank line between them is also prefixed with ">"; preserve both quoted
paragraphs while eliminating the MD028 markdownlint violation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9fe41e2f-6368-427f-b467-a2d8977ff54b

📥 Commits

Reviewing files that changed from the base of the PR and between f03162f and f312516.

📒 Files selected for processing (2)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md

Comment on lines +23 to +24
**Constraints**: only writes from SS are invalidation calls (FR-007/FR-008) — no signing-initiation calls (FR-005); server-side identity derivation and ownership enforcement (never trust client-supplied user IDs); invalidation blocked during impersonation (FR-009); PR-check remediation link untouched (FR-006); feature-flagged dark launch; 2-week delivery budget.
**Scale/Scope**: all LFX contributors; extends 1 existing Me-lens page, ~3–4 server routes (CLA-Group search, `userID` resolution/hand-off, ICLA invalidate, ECLA invalidate), 1 new upstream endpoint (ECLA invalidation) + possibly a status/listing extension per clarify outcomes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Correct the write boundary.

The plan says that only invalidation calls write from Self Serve. FR-003 invokes GET /v4/user-from-token, whose verified behavior is lookup-or-create. Account authorization may also update identity state. Clarify whether the constraint excludes user resolution and identity-linking side effects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md` around
lines 23 - 24, Clarify the write-boundary constraint in the plan to account for
the lookup-or-create behavior of GET /v4/user-from-token and any
identity-linking updates during authorization. Explicitly distinguish permitted
user-resolution or identity-state side effects from the prohibited
signing-initiation writes, while preserving the invalidation-only restriction
for business-action writes from Self Serve.


A contributor opens **My CLAs** (M1's page) in the Self Serve Me lens. There they can:

1. **Sign a CLA** — click "+ Sign a CLA", search by project name, CLA group name, or linked repo source (GitHub, GitLab, or Gerrit), pick a CLA Group, and continue to the existing Contributor Console decision screen to complete the signing (ICLA/ECLA choice and the DocuSign ceremony stay in the Console). Before the Console opens, they are asked to authorize the account they'll contribute with (GitHub, GitLab, or Gerrit).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
wc -l specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
cat -n specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
printf '%s\n' '--- related identifiers and endpoints ---'
rg -n -i 'FR-004|user-from-token|authorize|authorization|identity|commit author|signing identity|GitHub|GitLab|Gerrit|Console hand-off|handoff' specs/001-easycla-ss-integration-fable .

Repository: linuxfoundation/easycla

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- specification ---'
cat -n specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
printf '%s\n' '--- exact endpoint references ---'
rg -n -i --glob '!vendor/**' --glob '!node_modules/**' \
  'user-from-token|FR-004|commit author|signing identity|handoff|hand-off' \
  specs/ cla-backend-go cla-frontend 2>/dev/null | head -n 300
printf '%s\n' '--- user endpoint implementations and models ---'
rg -l 'user-from-token|UserFromToken' . --glob '*.go' --glob '*.yaml' --glob '*.yml' --glob '*.json' | head -n 100

Repository: linuxfoundation/easycla

Length of output: 23902


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- user-from-token handler and user mapping ---'
sed -n '960,1035p' cla-backend-go/cmd/server.go
ast-grep outline cla-backend-go/v2/current_user/handlers.go
cat -n cla-backend-go/v2/current_user/handlers.go
printf '%s\n' '--- M1 identity flow ---'
rg -n -i -C 4 'account.?link|identity|github|gitlab|gerrit|authorize|authorization|user-from-token' \
  specs/001-easycla-ss-integration-fable/m1-my-cla apps/lfx-one 2>/dev/null | head -n 400
printf '%s\n' '--- commit-author matching ---'
sed -n '660,730p' cla-backend-go/github/github_repository.go
sed -n '1180,1370p' cla-backend-go/github/github_repository.go

Repository: linuxfoundation/easycla

Length of output: 35635


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- complete legacy lookup-or-create path ---'
sed -n '1000,1105p' cla-backend-go/cmd/server.go
printf '%s\n' '--- v2 UserFromContext path ---'
rg -n -C 8 'func .*UserFromContext|UserFromContext\\(' cla-backend-go/v2/current_user cla-backend-go/users
printf '%s\n' '--- user model identity fields and lookups ---'
rg -n -C 5 'UserGithubID|UserGithubUsername|UserGitlabID|UserGitlabUsername|GetUserByGitHubID|GetUserByGitHubUsername' \
  cla-backend-go/users cla-backend-go/gen/v2/models cla-backend-go/gen/v1/models | head -n 500
printf '%s\n' '--- Self Serve linked-identity implementation ---'
rg -n -i --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.json' \
  'identity|identities|github.*link|link.*github|auth0|social' apps/lfx-one 2>/dev/null | head -n 400

Repository: linuxfoundation/easycla

Length of output: 2536


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- v2 UserFromContext path ---'
rg -n -F -C 8 'UserFromContext' cla-backend-go/v2/current_user cla-backend-go/users
printf '%s\n' '--- user identity fields and lookups ---'
rg -n -F -C 5 \
  -e 'UserGithubID' -e 'UserGithubUsername' -e 'UserGitlabID' -e 'UserGitlabUsername' \
  -e 'GetUserByGitHubID' -e 'GetUserByGitHubUsername' \
  cla-backend-go/users cla-backend-go/gen/v2/models cla-backend-go/gen/v1/models | head -n 500
printf '%s\n' '--- Self Serve linked-identity implementation ---'
rg -n -i --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.json' \
  'identity|identities|github.*link|link.*github|auth0|social' apps/lfx-one 2>/dev/null | head -n 400

Repository: linuxfoundation/easycla

Length of output: 18725


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

server = Path("cla-backend-go/cmd/server.go").read_text()
repo = Path("cla-backend-go/github/github_repository.go").read_text()
spec = Path("specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md").read_text()

creation = server[server.index("newUser := &models.User{"):server.index("log.WithFields(f).Debug(\"creating new user\")")]
required_creation = ["LfEmail:", "LfUsername:", "Username:"]
for field in required_creation:
    assert field in creation, f"missing expected field: {field}"
for provider_field in ["GithubID:", "GithubUsername:", "GitlabID:", "GitlabUsername:"]:
    assert provider_field not in creation, f"unexpected provider identity field: {provider_field}"

matching = repo[repo.index("if commitAuthorID != \"\""):repo.index("if user == nil {", repo.index("if commitAuthorID != \"\""))]
assert matching.index("GetUserByGitHubID") < matching.index("GetUserByGitHubUsername")
assert matching.index("GetUserByGitHubUsername") < matching.index("GetUserByEmail")

fr004 = spec[spec.index("**FR-004**"):spec.index("\n", spec.index("**FR-004**"))]
assert "account-authorization" in fr004
assert "bound" not in fr004.lower(), "FR-004 already specifies an explicit binding"
assert "block" not in fr004.lower(), "FR-004 already specifies a hand-off block"

print("lookup-or-create fields: LF identity only")
print("commit matching precedence: GitHub ID -> username -> email")
print("FR-004: authorization is required, but binding and hand-off gating are unspecified")
PY

Repository: linuxfoundation/easycla

Length of output: 345


Bind the authorized provider identity before the Console hand-off.

GET /v4/user-from-token can create a user with only LF identity fields. Define how the selected GitHub, GitLab, or Gerrit identity is attached to the EasyCLA user record. Reject the hand-off if the binding is missing or does not match the identity used for commit lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
22, Update the “Sign a CLA” flow specification to define binding the authorized
GitHub, GitLab, or Gerrit identity to the EasyCLA user record created by GET
/v4/user-from-token before handing off to the Contributor Console. Require the
bound identity to match the provider identity used for commit lookup, and reject
the hand-off when the binding is absent or mismatched.


1. **Sign a CLA** — click "+ Sign a CLA", search by project name, CLA group name, or linked repo source (GitHub, GitLab, or Gerrit), pick a CLA Group, and continue to the existing Contributor Console decision screen to complete the signing (ICLA/ECLA choice and the DocuSign ceremony stay in the Console). Before the Console opens, they are asked to authorize the account they'll contribute with (GitHub, GitLab, or Gerrit).
2. **Invalidate a CLA** — each signed CLA row offers an Invalidate action with a confirmation modal. For an ICLA: "This will mark your ICLA for {project} as invalid… This action cannot be undone." For an ECLA: "Confirm you no longer work at {company}?" — confirming ends their coverage under that company's CCLA.
3. **See status** — each row shows a status: Valid, or "Needs attention" with a note (e.g. an ECLA that is signed but no longer matches the company's approval criteria), with a "Request approval →" link into the Console's existing request-authorization flow where applicable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Define a status mapping for every My CLAs record.

GET /v4/my-clas returns current and historical ICLAs and ECLAs with computed validity. FR-010 defines only Valid, Needs attention, and Invalidated. Define the handling for existing invalid, expired, revoked, pending, and historical records, or exclude them explicitly. Otherwise, the UI can show Valid for a non-valid record and expose the wrong action.

Also applies to: 55-56

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
24, Update the “See status” requirement to define a status mapping for every
record returned by GET /v4/my-clas, including invalid, expired, revoked,
pending, and historical ICLAs/ECLAs. Specify the corresponding UI status and
permitted action for each case, or explicitly exclude unsupported records,
ensuring non-valid records cannot appear as Valid or expose an inappropriate
“Request approval” action.

1. **Given** a logged-in contributor on My CLAs, **When** they open "+ Sign a CLA", search, and select a CLA Group, **Then** they land on the Contributor Console's decision screen for that CLA Group (`/#/cla/project/{claGroupID}/user/{userID}`), with the ICLA/ECLA choice and its legal guidance presented by the Console — not re-implemented in SS.
2. **Given** the contributor selected a CLA Group, **When** they continue to sign, **Then** they are first asked to authorize the account they want to use (GitHub, GitLab, or Gerrit) so the resulting signature is bound to the identity their contributions come from (see Open questions for mechanics).
3. **Given** a contributor with a signed ICLA, **When** they click Invalidate and confirm, **Then** the ICLA is invalidated via the existing EasyCLA endpoint, the row's status changes to Invalidated, and the action is recorded in the EasyCLA event log.
4. **Given** a contributor with a valid ECLA, **When** they click Invalidate, **Then** the modal asks them to confirm they no longer work at that company, and confirming ends their ECLA coverage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target spec outline ---'
ast-grep outline specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md --match 'Open Question' --view expanded || true

printf '%s\n' '--- target spec excerpts ---'
cat -n specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md | sed -n '1,110p'

printf '%s\n' '--- related terms and references ---'
rg -n -i --glob '*.{md,txt,yaml,yml,go,py,sh}' \
  'invalidate|invalidat|FR-008|Open Question 2|ECLA|CCLA|company coverage|returned status|duplicate' \
  specs .github 2>/dev/null | head -300

Repository: linuxfoundation/easycla

Length of output: 31625


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- ECLA invalidation implementation candidates ---'
rg -n -i --glob '*.{go,yaml,yml,md}' \
  'invalidate.*(ecla|ccla)|(ecla|ccla).*invalidate|InvalidatedSignature|approval.?list|approval list|employee.*coverage|coverage' \
  . | head -400

printf '%s\n' '--- relevant backend files ---'
fd -i -t f 'handlers.go|service.go|cla.v2.yaml|swagger' . | head -200

printf '%s\n' '--- specification references near the cited Open Question ---'
rg -n -C 5 'ECLA invalidation endpoint|define semantics|duplicate|returned status|approval-list effects|events' \
  specs/001-easycla-ss-integration-fable

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- invalidation symbols and routes ---'
rg -n -C 8 \
  'func .*Invalidate|invalidateICLA|InvalidateCCLA|Invalidate.*Signature|InvalidatedSignature|signature.invalidated|/icla|/ecla' \
  cla-backend-go cla-backend-legacy --glob '*.go' --glob '*.yaml' | head -500

printf '%s\n' '--- current My CLAs contract semantics ---'
cat -n cla-backend-go/swagger/common/my-cla.yaml | sed -n '1,130p'
cat -n docs/MY_CLAS_API.md | sed -n '220,310p'

printf '%s\n' '--- roadmap context ---'
cat -n roadmap.md | sed -n '1,55p'

Repository: linuxfoundation/easycla

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- ICLA endpoint definition and handler ---'
sed -n '3628,3685p' cla-backend-go/swagger/cla.v2.yaml
rg -n -C 12 'invalidateICLA|InvalidateICLA' cla-backend-go --glob '*.go'

printf '%s\n' '--- signature invalidation service methods ---'
rg -n 'Invalidate|invalidate|signature_approved|ApprovalList' cla-backend-go/signatures --glob '*.go' | head -250

printf '%s\n' '--- approval-list removal call sites ---'
rg -n -C 10 'Remove.*Approval|remove.*approval|Invalidate.*Records|signature_approved.*false|Set.*Approved|Update.*Approved' \
  cla-backend-go/signatures cla-backend-go/approval_list cla-backend-go/project --glob '*.go' | head -350

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- ICLA invalidation implementation ---'
sed -n '350,435p' cla-backend-go/v2/signatures/service.go
sed -n '1248,1284p' cla-backend-go/v2/signatures/handlers.go

printf '%s\n' '--- approval-list update orchestration ---'
sed -n '474,560p' cla-backend-go/signatures/service.go
sed -n '900,980p' cla-backend-go/signatures/service.go

printf '%s\n' '--- employee-signature and approval-list repository paths ---'
rg -n -C 8 \
  'CreateOrUpdateEmployeeSignature|UpdateApprovalList|InvalidateSignature|signatureApproved|SignatureApproved|approved.*false|Remove.*Contributor' \
  cla-backend-go/signatures/repository.go cla-backend-go/signatures/service.go --glob '*.go' | head -350

printf '%s\n' '--- read-only deterministic endpoint probe ---'
python3 - <<'PY'
from pathlib import Path
import re

swagger = Path("cla-backend-go/swagger/cla.v2.yaml").read_text()
paths = re.findall(r"^  (/[^:\n]+):\s*$", swagger, re.MULTILINE)
print("ICLA invalidation paths:", [p for p in paths if "icla" in p.lower() and ("user" in p.lower() or "invalidate" in p.lower())])
print("ECLA invalidation paths:", [p for p in paths if "ecla" in p.lower() and ("user" in p.lower() or "invalidate" in p.lower())])
print("Invalidation operation IDs:", re.findall(r"^\s+operationId:\s*(\S*invalidate\S*)\s*$", swagger, re.MULTILINE | re.IGNORECASE))
PY

Repository: linuxfoundation/easycla

Length of output: 46183


Define ECLA invalidation semantics before accepting the flow.

Specify whether invalidation targets one ECLA, all ECLAs for the company, or the CCLA association. Define its signature_approved and Approved List effects, event type and payload, HTTP response, and duplicate-request behavior. Update Scenario 4 and FR-008 to match the contract.

🧰 Tools
🪛 LanguageTool

[style] ~31-~31: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...recorded in the EasyCLA event log. 4. Given a contributor with a valid ECLA, **Wh...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
31, Define the ECLA invalidation contract before accepting Scenario 4: specify
whether it affects one ECLA, all company ECLAs, or the CCLA association; define
resulting signature_approved and Approved List states, emitted event type and
payload, HTTP response, and duplicate-request behavior. Update Scenario 4 and
FR-008 to consistently reflect those semantics.


**Status**

- **FR-010**: Each row MUST show a status: Valid; "Needs attention" with an explanatory note (e.g. ECLA signed but no longer matching the company's approval-list criteria); Invalidated after invalidation. *([NEEDS CLARIFICATION]: where the approval-criteria evaluation comes from.)*

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required Approved List terminology.

Replace the non-canonical hyphenated approval term with Approved List at each site. This repeats the issue from the previous review.

  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md#L55-L55: Update the status requirement.
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md#L67-L67: Update the scope boundary.
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md#L77-L77: Update the ECLA invalidation risk.

As per coding guidelines, use the terminology Approved List.

📍 Affects 2 files
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md#L55-L55 (this comment)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md#L67-L67
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md#L77-L77
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` at line
55, Replace the non-canonical approval-list terminology with “Approved List” at
all affected sites: update the status requirement in
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md lines 55-55
and 67-67, and the ECLA invalidation risk in
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md lines 77-77.
Preserve the surrounding requirements and wording.

Source: Coding guidelines

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