Skip to content

[jsweep] Clean update_project.cjs#39183

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
signed/jsweep/update_project-a6d48dd34f42e606
Open

[jsweep] Clean update_project.cjs#39183
github-actions[bot] wants to merge 1 commit into
mainfrom
signed/jsweep/update_project-a6d48dd34f42e606

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Summary

Cleans update_project.cjs (71.7 KB, github-script context) by eliminating ~240 lines of duplication, removing minified code, and extracting named helper functions.

Changes

Extracted applyFieldUpdates helper

  • Both draft_issue and content_number branches contained identical ~120-line field update loops
  • Extracted into applyFieldUpdates(github, projectId, itemId, fields) shared helper
  • GraphQL mutation strings expanded from single-line escaped \n sequences to readable multi-line template literals

Extracted findExistingItemByContentId helper

  • IIFE used inline for item lookup replaced with named findExistingItemByContentId(github, projectId, contentId) function

Cleaned minified contentNumber block

  • Replaced Yoda conditions (null !== contentNumber, "pull_request" === output.content_type)
  • Replaced !0/!1 boolean literals with true/false
  • Replaced comma-chained const declarations and IIFE with clean readable code

for...of .entries() loop

  • for (let i = 0; i < arr.length; i++)for (const [i, viewConfig] of configuredViews.entries())

Exported utility functions for direct testing

  • normalizeUpdateProjectOutput, summarizeProjectsV2, summarizeEmptyProjectsV2List added to module.exports

Context

Execution context: github-script (uses global core, github, context)
@ts-nocheck: Not present — file already had @ts-check; no type errors

Test Improvements

  • Before: 76 tests
  • After: 98 tests (+22 new tests)

New test suites added:

  • normalizeUpdateProjectOutput (11 tests): camelCase alias normalization, non-object passthrough, snake_case precedence
  • summarizeProjectsV2 (7 tests): empty/null input, single project, closed project, multiple projects, filtering, limit
  • summarizeEmptyProjectsV2List (4 tests): empty input, totalCount context, diagnostic counts, SSO hint

Validation

All checks passed:

  • ✅ Formatting: npm run format:cjs
  • ✅ Linting: npm run lint:cjs
  • ✅ Type checking: npm run typecheck
  • ✅ Tests: npm run test:js — 98/98 passed

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • traces.example.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "traces.example.com"

See Network Configuration for more information.

Generated by 🧹 jsweep - JavaScript Unbloater · 1.3K AIC · ⌖ 63.5 AIC · ⊞ 18.7K ·

  • expires on Jun 15, 2026, 9:31 PM UTC-08:00

- Extract duplicated ~120-line field update loop into shared
  applyFieldUpdates(github, projectId, itemId, fields) helper,
  eliminating ~240 lines of duplication across draft_issue and
  content_number branches
- Extract IIFE for item lookup into named
  findExistingItemByContentId(github, projectId, contentId) function
  comma-chained consts, IIFE) with clean readable code
- Expand GraphQL mutation strings from single-line escaped \n sequences
  into readable multi-line template literals
- Replace index-based for loop with for...of .entries() on configuredViews
- Export normalizeUpdateProjectOutput, summarizeProjectsV2, and
  summarizeEmptyProjectsV2List utility functions for direct testing
- Add 22 new tests covering normalizeUpdateProjectOutput (camelCase alias
  normalization), summarizeProjectsV2 (formatting, filtering, limit),
  and summarizeEmptyProjectsV2List (diagnostics, SSO hint, empty cases)
- Tests: 76 → 98 (all passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pelikhan pelikhan marked this pull request as ready for review June 14, 2026 16:02
Copilot AI review requested due to automatic review settings June 14, 2026 16:02
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #39183 does not have the 'implementation' label and has 0 new lines of code in business logic directories (≤100 threshold).

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

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

This PR refactors the update_project.cjs github-script implementation to reduce duplication and improve readability, while expanding the unit test suite to directly cover newly exported helper utilities.

Changes:

  • Extracts shared helpers (applyFieldUpdates, findExistingItemByContentId) and replaces duplicated inline logic with named functions.
  • Cleans up previously minified/condensed logic and converts GraphQL strings into readable multiline template literals.
  • Exports utility helpers and adds/expands test coverage for output normalization and project-list summarization helpers.
Show a summary per file
File Description
actions/setup/js/update_project.cjs Refactors project-item lookup and field-update logic into helpers; improves readability and exports utilities for testing.
actions/setup/js/update_project.test.cjs Adds new test suites for the newly exported helper utilities and increases overall coverage.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 81/100 — Excellent

Analyzed 22 test(s): 22 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (22 tests analyzed)
Metric Value
New/modified tests analyzed 22
✅ Design tests (behavioral contracts) 22 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 12 (55%)
Duplicate test clusters 1
Test inflation detected No (0.53:1 ratio)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
returns non-object values unchanged update_project.test.cjs ✅ Design Edge case (null/undefined/primitive passthrough)
normalizes camelCase contentType to content_type update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase contentNumber to content_number update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase targetRepo to target_repo update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase draftTitle to draft_title update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase draftBody to draft_body update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase draftIssueId to draft_issue_id update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase temporaryId to temporary_id update_project.test.cjs ✅ Design Happy path — part of single-field duplicate cluster
normalizes camelCase fieldDefinitions to field_definitions update_project.test.cjs ✅ Design Happy path
does not overwrite existing snake_case keys with camelCase aliases update_project.test.cjs ✅ Design Edge case (conflict resolution)
handles a full camelCase payload update_project.test.cjs ✅ Design Comprehensive multi-field integration
returns (none) for empty array update_project.test.cjs ✅ Design Edge case (empty input)
returns (none) for null or non-array input update_project.test.cjs ✅ Design Edge case (null/undefined)
formats a single open project correctly update_project.test.cjs ✅ Design Happy path
formats a closed project with (closed) marker update_project.test.cjs ✅ Design Edge case (closed state)
joins multiple projects with semicolons update_project.test.cjs ✅ Design Happy path
filters out entries missing number or title update_project.test.cjs ✅ Design Edge case (invalid entries)
respects the limit parameter update_project.test.cjs ✅ Design Edge case (boundary condition)
returns (none) for empty list with no diagnostics update_project.test.cjs ✅ Design Edge case (empty diagnostics)
includes totalCount context when items exist but none readable update_project.test.cjs ✅ Design Edge case (non-zero count, zero readable)
includes diagnostic counts in output update_project.test.cjs ✅ Design Behavioral (diagnostic formatting)
includes SSO hint in message when totalCount > 0 update_project.test.cjs ✅ Design Behavioral (SSO guidance)

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests
  • 🟨 JavaScript (*.test.cjs, *.test.js): 22 tests (vitest)
⚠️ Minor Note — 1 Duplicate Cluster (not a failing issue)

i️ 7 single-field normalization tests in normalizeUpdateProjectOutput

The tests normalizes camelCase contentType, contentNumber, targetRepo, draftTitle, draftBody, draftIssueId, temporaryId all follow the same pattern: call with a single camelCase field → assert the snake_case output. This is a legitimate approach for documenting the mapping table, but could optionally be collapsed into a single table-driven test:

it.each([
  [{ contentType: "issue" }, "content_type", "issue"],
  [{ contentNumber: 42 }, "content_number", 42],
  // ...
])("normalizes camelCase %s", (input, key, expected) => {
  expect(normalizeUpdateProjectOutput(input)[key]).toBe(expected);
});

This is a style suggestion only — the current tests are still valid design tests that document the normalization contract.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 22 new tests verify behavioral contracts with no guideline violations.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · 177.5 AIC · ⌖ 35 AIC · ⊞ 26.9K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 81/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 22 new tests verify behavioral contracts against pure functions with no guideline violations.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /zoom-out, /tdd, and /improve-codebase-architecture — clean refactor with solid structural improvements; commenting with targeted suggestions (no blockers).

📋 Key Themes & Highlights

Positive Highlights

  • ✅ Excellent de-duplication: applyFieldUpdates eliminates ~240 lines of duplicate field-update logic shared across draft_issue and content_number branches
  • findExistingItemByContentId → named function replaces the inline IIFE cleanly, with JSDoc and clear pagination logic
  • ✅ GraphQL template literals are a significant readability win over the minified single-line \n-escaped strings
  • ✅ 22 new unit tests with comprehensive coverage of the three exported utilities; Arrange/Assert structure is clear throughout
  • for...of .entries() modernisation is idiomatic and removes the manual index/element split

Suggestions (no blockers)

  • Null safetyresult.node in findExistingItemByContentId can be null for invalid project IDs; a guard would convert the opaque TypeError into a clear warning (line 406)
  • Testability gapapplyFieldUpdates has complex field-type inference but no direct unit tests; extracting a pure inferFieldDataType helper would unlock exhaustive coverage without mocking (line 424)
  • Dead conditionisDateField OR-condition includes a redundant includes("_date") term (line 439)
  • Missing comment"classification" hard-code in isTextField lost its explanatory comment in the extraction (line 440)
  • Fragile testtoContain("SSO") is editorial-string sensitive; asserting on diagnostic data (totalCount=5, 0 readable project nodes) would be more stable (test line 2339)

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 406.8 AIC · ⌖ 40.3 AIC · ⊞ 27.9K

{ projectId, after: endCursor }
);

const found = result.node.items.nodes.find(item => item.content?.id === contentId);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/zoom-out] result.node can be null if projectId is invalid or inaccessible — the GraphQL node() query returns null for unknown/unauthorized IDs. This causes an unguarded TypeError on .items that was previously hidden inside the IIFE; now that it is a named function, making the failure observable is straightforward.

💡 Suggested guard
if (!result?.node?.items) {
  core.warning(`Project ${projectId} not found or inaccessible; stopping item search.`);
  break;
}
const found = result.node.items.nodes.find(item => item.content?.id === contentId);

Without this guard, an invalid projectId surfaces as an opaque TypeError in the updateProject call stack rather than a clear warning.

* @param {Record<string, unknown>} fields - Field name/value pairs to update
* @returns {Promise<void>}
*/
async function applyFieldUpdates(github, projectId, itemId, fields) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] applyFieldUpdates is the highest-risk extracted helper — it contains complex field-type inference heuristics (date/text/single-select detection, type mismatch guards, three field-creation paths) — but has no dedicated unit tests. The PR wisely exports three utility functions for direct testing; the same treatment for this helper would significantly improve regression safety.

💡 Approach

Exporting applyFieldUpdates for testing is tricky because it references the global core and calls fetchAllProjectFields. Consider extracting the pure, side-effect-free detection logic into a small helper:

/** `@returns` {"DATE"|"TEXT"|"SINGLE_SELECT"} */
function inferFieldDataType(fieldName, fieldValue, datePattern) {
  const isDateField = fieldName.toLowerCase().includes("date");
  const isTextField = fieldName.toLowerCase() === "classification" || (typeof fieldValue === "string" && fieldValue.includes("|"));
  if (isDateField && typeof fieldValue === "string" && datePattern.test(fieldValue)) return "DATE";
  if (isTextField) return "TEXT";
  return "SINGLE_SELECT";
}

This pure function can be tested exhaustively without mocking github or core, covering cases like start_date, classification, pipe-delimited values, and the fallback path.

continue;
}

const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/improve-codebase-architecture] The first condition includes("_date") is dead code: every string that contains "_date" also contains "date", so the OR short-circuits to the second term in all cases. The condition simplifies to just includes("date") with no behavioural change.

💡 Simplified
const isDateField = fieldName.toLowerCase().includes("date");

This was pre-existing in the original IIFE, but applyFieldUpdates is now the single canonical location for this logic — a good place to tidy it up.

}

const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date");
const isTextField = fieldName.toLowerCase() === "classification" || (typeof fieldValue === "string" && fieldValue.includes("|"));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/improve-codebase-architecture] The hardcoded "classification" field name is opaque — the extracted helper lost the inline comment from the original code explaining why this specific field name always gets TEXT treatment instead of SINGLE_SELECT.

💡 Suggestion
// "classification" is always treated as a free-text field rather than single-select;
// pipe-delimited values ("A|B|C") also signal a free-text field storing multi-option strings.
const isTextField =
  fieldName.toLowerCase() === "classification" ||
  (typeof fieldValue === "string" && fieldValue.includes("|"));

Alternatively, consider making the list of always-text field names configurable (e.g., a TEXT_FIELD_NAMES constant) so the special-casing is visible and extensible rather than buried in a condition.


it("includes SSO hint in message when totalCount > 0", () => {
const result = summarizeEmptyProjectsV2List({ totalCount: 5, diagnostics: { rawNodesCount: 0, nullNodesCount: 0, rawEdgesCount: 0, nullEdgeNodesCount: 0 } });
expect(result).toContain("SSO");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] toContain("SSO") tests for a specific editorial string fragment, making this test fragile to message wording changes that do not change the semantics.

💡 More stable alternative
it("includes SSO hint in message when totalCount > 0", () => {
  const result = summarizeEmptyProjectsV2List({ totalCount: 5, ... });
  // Test the data communicated, not the exact wording
  expect(result).toContain("totalCount=5");
  expect(result).toContain("0 readable project nodes");
});

Assertions on "totalCount=5" and "0 readable project nodes" are tied to the data in the diagnostic — they remain valid across message rephrasing while still confirming the diagnostic branch was taken.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

REQUEST_CHANGES — 2 correctness bugs + 2 medium concerns in new helpers

The refactoring direction is right: eliminating ~240 lines of duplication and making the logic readable are clear wins, and the 22 new tests are solid. However, the extraction introduced new code that carries forward null-safety gaps from the old minified code, plus surfaced a pre-existing consistency bug that is now more visible in the shared helper.

Blocking issues (must fix before merge)
Non-blocking (should fix)
  1. contentData null (line 1142): If the issue/PR doesn't exist in targetRepo, the TypeError from null.id is caught but the error message is opaque. A pre-emptive null check with a clear ERR_VALIDATION message would help operators.

  2. Case-sensitive single-select vs case-insensitive iteration (line 573): Inconsistency that silently drops updates depending on field type. Fix is one-liner: add .toLowerCase() normalization on both sides.

🔎 Code quality review by PR Code Quality Reviewer · 537.9 AIC · ⌖ 13.8 AIC · ⊞ 17.3K

{ projectId, after: endCursor }
);

const found = result.node.items.nodes.find(item => item.content?.id === contentId);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unguarded null dereference when project node is not a ProjectV2.

If projectId points to a deleted project, a classic Project (v1), or any non-ProjectV2 node, the ... on ProjectV2 inline fragment yields no fields. result.node becomes {} or null, so result.node.items is undefined and the chain throws TypeError: Cannot read properties of undefined (reading 'nodes'). The outer catch in updateProject rescues the exception, but the message is opaque and doesn't help diagnose the real cause.

💡 Suggested fix
const found = result.node?.items?.nodes.find(item => item.content?.id === contentId);
if (found) return found;

const pageInfo = result.node?.items?.pageInfo;
hasNextPage = pageInfo.hasNextPage;
endCursor = pageInfo.endCursor;

Or add a single guard at the top of the loop body:

}

let valueToSet;
if (field.dataType === "DATE") {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TypeError crash when createProjectV2Field returns a null projectV2Field — aborts all remaining field updates for this item.

The outer catch in updateProject saves the process, but the entire item's field update is aborted — every subsequent field in the loop is silently lost.

💡 Suggested fix
  core.warning(`Field "${fieldName}" could not be created or resolved; skipping.`);
  continue;
}

This converts a process-aborting TypeError into a per-field warning, keeping other field updates alive.

}`;
const contentResult = await github.graphql(contentQuery, { owner: contentOwner, repo: targetRepo, number: contentNumber });
const contentData = contentType === "Issue" ? contentResult.repository.issue : contentResult.repository.pullRequest;
const contentId = contentData.id;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

contentData.id crashes if the issue or PR number does not exist in the target repo.

contentResult.repository.issue (or .pullRequest) returns null when the content number is valid but doesn't exist in targetRepo (deleted issue, wrong repo, cross-repo reference). Line 1142 then reads null.id and throws. The outer catch surfaces an opaque TypeError rather than a user-actionable message like "Issue #N not found in org/repo".

💡 Suggested fix
const contentData = contentType === "Issue"
  ? contentResult.repository.issue
  : contentResult.repository.pullRequest;

  throw new Error(
    `${ERR_VALIDATION}: ${contentType} #${contentNumber} not found in ${contentOwner}/${targetRepo}.`
  );
}

This gives operators a clear diagnostic instead of an unhelpful stack trace.

}
valueToSet = { iterationId: iteration.id };
} else if (field.options) {
const option = field.options.find(o => o.name === fieldValue);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Case-sensitive single-select lookup silently drops updates that the iteration lookup would accept.

Line 565 (ITERATION) normalizes both sides with .toLowerCase() for matching. Line 573 (single-select options) uses strict === with no normalization. A YAML value of in progress will silently skip an option named In Progress and emit a warning, while the same value for an iteration field would succeed. The inconsistency is a silent data-loss trap for configurations that mix field types.

💡 Suggested fix
const option = field.options.find(
  o => o.name.toLowerCase() === String(fieldValue).toLowerCase()
);```

This mirrors the iteration lookup and eliminates the case-sensitivity gap.

</details>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant