Skip to content

feat(issue-lifecycle): auto-assign issue to current user during start#65

Merged
stack72 merged 1 commit intomainfrom
feat/issue-lifecycle-auto-assign
Apr 12, 2026
Merged

feat(issue-lifecycle): auto-assign issue to current user during start#65
stack72 merged 1 commit intomainfrom
feat/issue-lifecycle-auto-assign

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Apr 12, 2026

Summary

Test Plan

🤖 Generated with Claude Code

Sync from systeminit/swamp#1167. When `start` is called, the method
reads the authenticated username from local auth, resolves it to a
userId via the eligible-assignees endpoint, and PATCHes the issue's
assignees additively. All assignment failures are best-effort warnings.

Bumps model version to 2026.04.12.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Adversarial Review

Critical / High

None.

Medium

  1. Optimistic success logging after silent PATCH failureissue_lifecycle.ts:516-529

    After calling sc.updateAssignees(...), the code unconditionally posts a lifecycle entry ("Assigned to {username}") and logs an info message. But updateAssignees delegates to patchIssue, which swallows all errors (HTTP 500, network timeout, etc.) and returns void regardless of outcome.

    Breaking scenario: The PATCH endpoint returns 500 (server error). patchIssue logs internally and returns normally. The lifecycle audit trail then records "Assigned to alice" and the local logger prints "Auto-assigned issue to alice" — but the issue's actual assignee list was never modified.

    Impact: Misleading audit trail. During incident investigation, a team member could see "Assigned to alice" in the lifecycle log and assume alice was notified/responsible, when the assignment never took effect.

    Suggested fix: Have patchIssue (or a new variant) return a boolean indicating success. Only post the lifecycle entry and info log when the PATCH actually succeeded:

    const patched = await sc.tryUpdateAssignees([...existingIds, resolvedUserId]);
    if (patched) {
      await sc.postLifecycleEntry({ step: "assigned", ... });
      context.logger.info("Auto-assigned issue to {username}", ...);
    }

Low

  1. No structural try/catch around assignment blockissue_lifecycle.ts:491-531

    The comment states "All failures are warnings — assignment must never break the triage flow." Each individual async method (loadAuthFile, resolveUserId, updateAssignees, postLifecycleEntry) has its own try/catch. However, the block as a whole is unwrapped. If a future change introduces a throwing code path in this block, start() would throw after writing the state resource (phase: "triaging"), leaving the model in an inconsistent state from the caller's perspective.

    In the current code, I verified every operation is defensively wrapped, so this is theoretical. A top-level try/catch around lines 491-531 with a warning log would structurally guarantee the stated contract at near-zero cost.

  2. Redundant auth file readissue_lifecycle.ts:493

    loadAuthFile() is called in start() to get the username. When SWAMP_API_KEY env var is not set, createSwampClubClient() (line 436) already called loadAuthFile() internally to get the API key. This results in two filesystem reads of the same file. Not a bug — just a minor inefficiency. Could be addressed by threading the username through the client or caching the auth file result.

Verdict

PASS — The code is well-structured with good defensive patterns. The auto-assignment is genuinely best-effort, error handling is consistent with existing patterns in the codebase, and test coverage is thorough (happy path, idempotency, no-username, 403 on assignees, PATCH failure). The optimistic lifecycle logging (Medium #1) is worth fixing but doesn't block — the assignment is advisory, and the lifecycle log is informational rather than decision-driving.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review

Blocking Issues

None.

Suggestions

  1. Fetch stub vs. Deno.serve: buildStartTestContext replaces globalThis.fetch with a stub instead of using Deno.serve({ port: 0 }) as recommended by CLAUDE.md. The global mutation is safe here (sequential tests, restored in finally), and a stub IS an in-memory mock client which the rules also allow — but a Deno.serve-based approach would be more future-proof if tests ever run concurrently across files.

  2. Double loadAuthFile call: createSwampClubClient already calls loadAuthFile internally to extract the API key; start() then calls it again to get the username. Two file reads per invocation. Could thread username out of createSwampClubClient or accept it as an optional param, but given the file is tiny and the second call is isolated to the auto-assign block, this is a low-priority concern.

  3. Lifecycle entry posted before confirming PATCH success: When patchIssue silently eats a 500 error, postLifecycleEntry still records "Assigned to ``" — the history entry can be inaccurate. Since everything here is best-effort this is acceptable, but worth noting for future observability work.


The core logic is correct: all error paths (missing credentials, no username, userId not in eligible list, already assigned, PATCH failure) are handled gracefully as warnings without breaking the triage flow. The six new test cases cover the happy path, idempotency, and every failure mode. No any types, named exports only, env vars restored in finally blocks throughout.

@stack72 stack72 merged commit 2bfb70d into main Apr 12, 2026
24 checks passed
@stack72 stack72 deleted the feat/issue-lifecycle-auto-assign branch April 12, 2026 00:10
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.

1 participant