Skip to content

[jsweep] Clean upload_assets.cjs#41371

Merged
pelikhan merged 3 commits into
mainfrom
signed/jsweep/upload-assets-2026-06-25-53e04f72f54d86d4
Jun 25, 2026
Merged

[jsweep] Clean upload_assets.cjs#41371
pelikhan merged 3 commits into
mainfrom
signed/jsweep/upload-assets-2026-06-25-53e04f72f54d86d4

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Caution

agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.

Details

Potential security threats were detected in the agent output.

Review the workflow run logs for details.

test

Generated by PR Description Updater for #41371 · 91.6 AIC · ⌖ 12.7 AIC · ⊞ 4.6K ·

- Add @typedef for UploadAssetItem to replace inline @param {any} annotation
- Replace minified test file with readable, well-structured tests
- Extract makeAsset and mockBranchMissing helpers to reduce duplication
- Add 7 new test cases (14 total, up from 7):
  - Missing GH_AW_ASSETS_BRANCH env var
  - No upload-asset items in output
  - SHA mismatch detection
  - Invalid asset entry (missing required fields)
  - All declared assets missing
  - Staged mode does not push
  - Non-upload_asset item types are ignored

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

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

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

No ADR enforcement needed: PR #41371 does not have the 'implementation' label and has 0 new lines of code in business logic directories.

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 actions/setup/js/upload_assets.cjs github-script helper to use a named JSDoc typedef for upload-asset items, and rewrites the corresponding Vitest suite into a clearer, helper-based structure while expanding coverage across key validation and edge cases.

Changes:

  • Added a JSDoc @typedef (UploadAssetItem) and used it to type the filtered uploadItems array in upload_assets.cjs.
  • Rewrote upload_assets.test.cjs from a minified style into an idiomatic Vitest structure with reusable helpers.
  • Expanded the test matrix to cover env validation, type filtering, SHA mismatch, invalid entries, “all missing” failure, staged-mode no-push behavior, and assets-dir resolution.
Show a summary per file
File Description
actions/setup/js/upload_assets.cjs Introduces a named upload-item typedef and applies it to the filtered upload-assets list for improved readability/type clarity under @ts-check.
actions/setup/js/upload_assets.test.cjs Full test rewrite for readability + new/expanded cases validating branch/env behavior, SHA validation, missing/invalid asset handling, staged-mode push suppression, and assets dir sourcing.

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 github-actions Bot mentioned this pull request Jun 25, 2026

@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 /tdd and /zoom-out — approving with minor suggestions.

📋 Key Themes & Highlights

Positive Highlights

  • ✅ Excellent test refactor: readable structure replacing the comma-expression minified style is a clear win for maintainability
  • makeAsset() and mockBranchMissing() helpers eliminate boilerplate cleanly — exactly the right DRY move
  • ✅ 7 new test cases covering the important failure paths (missing env var, SHA mismatch, invalid entries, staged mode)
  • crypto promoted to a top-level import instead of repeated require() inside each test body
  • @typedef for UploadAssetItem is a good type-safety improvement over the previous @param {any} workaround

Suggestions (non-blocking)

  1. @type cast syntax (upload_assets.cjs:82) — wrap the expression in parentheses for idiomatic JSDoc casting
  2. Zombie tempFilePath variable (upload_assets.test.cjs:24) — never read after assignment; safe to remove
  3. Vague assertion (upload_assets.test.cjs:271) — stringContaining("missing") could pass on unrelated error messages; tighten to "declared assets were missing"
  4. Cleanup without try/finally (multiple tests) — if an assertion throws, process.cwd() temp files leak; the assets dir resolution test already models the right pattern
  5. Helper underuse in staged-mode test (upload_assets.test.cjs:292) — duplicates the rev-parse throw from mockBranchMissing() instead of composing with it

All issues are style/robustness level — no correctness or security problems found.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 83.8 AIC · ⌖ 7.63 AIC · ⊞ 6.5K

Comment thread actions/setup/js/upload_assets.cjs Outdated

// Find all upload-asset items
const uploadItems = result.items.filter(/** @param {any} item */ item => item.type === "upload_asset");
const uploadItems = /** @type {UploadAssetItem[]} */ result.items.filter(item => item.type === "upload_asset");

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] JSDoc cast is missing parentheses around the expression — in a @ts-check file, the cast should wrap the full expression.

💡 Fix
// Current — cast may bind only to `result.items`, not the full call chain
const uploadItems = /** `@type` {UploadAssetItem[]} */ result.items.filter(item => item.type === "upload_asset");

// Idiomatic — cast wraps the entire expression
const uploadItems = /** `@type` {UploadAssetItem[]} */ (result.items.filter(item => item.type === "upload_asset"));

Without parentheses, TypeScript's JSDoc parser treats the annotation as targeting result.items rather than the full .filter() return value. The parenthesised form is the canonical cast syntax (TypeScript handbook — JSDoc casts).

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.

Addressed in 5eddc6e by switching uploadItems to a formatter-stable variable-level @type {UploadAssetItem[]} annotation.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
let uploadAssetsScript;
let mockExec;
let tempBase;
let tempFilePath;

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] tempFilePath is a zombie variable — it is assigned inside setAgentOutput and reset to undefined in afterEach, but it is never read anywhere. Since afterEach already recursively removes tempBase (which contains the file), tempFilePath provides no cleanup value and can be dropped.

💡 Suggested removal

Remove the declaration on line 24, the assignment on line 29, and the tempFilePath = undefined; in afterEach. The GH_AW_AGENT_OUTPUT env var is already cleaned up by delete process.env.GH_AW_AGENT_OUTPUT in beforeEach.

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.

Addressed in 5eddc6e by removing the unused tempFilePath state; setAgentOutput() now keeps that path local.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
mockBranchMissing();

await executeScript();
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("missing"));

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] The assertion stringContaining("missing") is too broad — any error message containing "missing" would satisfy it, including unrelated failures like SHA-mismatch messages or file-not-found warnings.

💡 More specific assertion

The actual error from upload_assets.cjs line 178 is:

All X declared assets were missing; no assets published.

A tighter assertion makes the test act as a specification:

expect(mockCore.setFailed).toHaveBeenCalledWith(
  expect.stringContaining("declared assets were missing")
);
// or
expect(mockCore.setFailed).toHaveBeenCalledWith(
  expect.stringContaining("no assets published")
);

This ensures the test fails when the message wording changes and prevents false-passes from unrelated error paths.

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.

Addressed in 848f950 by asserting the all-missing path with a fixture-derived count instead of a broad "missing" substring.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
expect(orphanBranchCreated).toBe(true);
expect(mockCore.setFailed).not.toHaveBeenCalled();
if (fs.existsSync(path.join(process.cwd(), "test.png"))) fs.unlinkSync(path.join(process.cwd(), "test.png"));
if (fs.existsSync(assetPath)) fs.unlinkSync(assetPath);

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] These fs.unlinkSync calls are outside a try/finally block — if any assertion before line 143 fails, the files in process.cwd() will leak between test runs. The assets dir resolution test (line 354) correctly uses try/finally; other tests should follow the same pattern.

💡 Suggested pattern
await executeScript();
try {
  expect(orphanBranchCreated).toBe(true);
  expect(mockCore.setFailed).not.toHaveBeenCalled();
} finally {
  if (fs.existsSync(path.join(process.cwd(), "test.png"))) fs.unlinkSync(path.join(process.cwd(), "test.png"));
  if (fs.existsSync(assetPath)) fs.unlinkSync(assetPath);
}

Alternatively, move all process.cwd() cleanup into afterEach by tracking the produced target filenames there.

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.

Addressed in 5eddc6e by centralizing cwd artifact cleanup in afterEach, so failed assertions no longer leak files.

if (fullCommand.includes("push")) pushCalled = true;
if (fullCommand.includes("rev-parse")) throw new Error("Branch does not exist");
return 0;
});

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] This mockImplementation duplicates the rev-parse throw logic from the newly-introduced mockBranchMissing() helper. Since the test only needs to additionally intercept push, it could compose with the helper instead of rewriting it.

💡 Approach using the helper
let pushCalled = false;
mockBranchMissing(); // sets the rev-parse behaviour
// Override to also track push while preserving rev-parse throwing
mockExec.exec.mockImplementation(async (command, args) => {
  const fullCommand = Array.isArray(args) ? `${command} ${args.join(" ")}` : command;
  if (fullCommand.includes("push")) pushCalled = true;
  if (fullCommand.includes("rev-parse")) throw new Error("Branch does not exist");
  return 0;
});

This is a small duplication but stands out because mockBranchMissing() was introduced specifically to avoid it.

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.

Addressed in 5eddc6e by reusing mockBranchMissing() in the staged-mode case instead of duplicating the rev-parse behavior.

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 88/100 — Excellent

Analyzed 14 test(s): 14 design, 0 implementation, 0 guideline violation(s). Inflation penalty applied due to minified→readable refactor (363 test lines added vs 5 production lines).

📊 Metrics & Test Classification (14 tests analyzed)
Metric Value
New/modified tests analyzed 14
✅ Design tests (behavioral contracts) 14 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 13 (93%)
Duplicate test clusters 0
Test inflation detected YES — 363 test lines added vs 5 production lines (72x ratio). Context: de-minification/cleanup PR; bulk of additions are formatting changes, not new test logic. 7 genuinely new tests added. Binary penalty (−10 pts) applied per rubric.
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
should fail when GH_AW_ASSETS_BRANCH is not set upload_assets.test.cjs:82 ✅ Design
should output upload_count=0 and branch_name when no upload-asset items found upload_assets.test.cjs:90 ✅ Design
should output upload_count=0 when all items are non-upload_asset types upload_assets.test.cjs:99 ✅ Design
should normalize branch names with special characters upload_assets.test.cjs:109 ✅ Design
should allow creating orphaned branch with assets/ prefix when branch does not exist upload_assets.test.cjs:121 ✅ Design
should fail when trying to create orphaned branch without assets/ prefix upload_assets.test.cjs:146 ✅ Design
should allow using existing branch regardless of prefix upload_assets.test.cjs:171 ✅ Design
should fail when asset SHA does not match declared SHA upload_assets.test.cjs:200 ✅ Design
should fail when asset entry is missing required fields upload_assets.test.cjs:218 ✅ Design
should skip missing assets and upload present ones upload_assets.test.cjs:233 ✅ Design
should fail when all declared assets are missing upload_assets.test.cjs:259 ✅ Design
should not push to origin when GH_AW_SAFE_OUTPUTS_STAGED=true upload_assets.test.cjs:276 ✅ Design
should not wrap commit message in extra quotes (injection) upload_assets.test.cjs:303 ✅ Design
should read assets from the GH_AW_ASSETS_DIR directory upload_assets.test.cjs:342 ✅ Design Happy-path only; no error case for invalid/missing dir

Go: 0; JavaScript: 14 (*.test.cjs).

Verdict

Check passed. 0% implementation tests (threshold: 30%). The refactor adds 7 net-new behavioral tests (env validation, SHA mismatch, missing fields, all-assets-missing, staged-mode push suppression, non-upload-type filtering, empty-items output) and converts the existing 7 from minified to readable form with no loss of coverage.

References:

🧪 Test quality analysis by Test Quality Sentinel · 84.9 AIC · ⌖ 23.3 AIC · ⊞ 8.4K ·

@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: 88/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 14 tests verify behavioral contracts 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.

Non-blocking observations on a net-positive cleanup.

🔍 Findings summary

upload_assets.cjs

  • JSDoc cast syntax (line 82): /** @type {T} */ expr without parentheses is a variable annotation, not an inline cast. TypeScript accepts it silently here because result is any-typed, but if loadAgentOutput is ever typed more strictly the intent will be lost. Parenthesise: /** @type {UploadAssetItem[]} */ (result.items.filter(...)).

upload_assets.test.cjs — three new tests have weak assertions or fragile cleanup

  • stringContaining("missing") is too broad (line 271): matches env-var failures and other unrelated paths; use the actual message substring.
  • fullCommand.includes("push") is too broad (line 289): any argument/path containing "push" as a substring satisfies it; check command === "git" && args[0] === "push" instead.
  • cwd file cleanup not in try/finally (line 142, and 6 other sites): if an assertion throws the cleanup is skipped and working-tree files persist across test runs. The "assets dir resolution" test already does this right.

No correctness, security, or CI-breaking issues found in the changed lines.

🔎 Code quality review by PR Code Quality Reviewer · 93.9 AIC · ⌖ 7.49 AIC · ⊞ 5.2K

Comment thread actions/setup/js/upload_assets.cjs Outdated

// Find all upload-asset items
const uploadItems = result.items.filter(/** @param {any} item */ item => item.type === "upload_asset");
const uploadItems = /** @type {UploadAssetItem[]} */ result.items.filter(item => item.type === "upload_asset");

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.

Malformed JSDoc cast — type assertion may not apply where intended.

💡 Details and fix

Per the TypeScript JSDoc spec, an inline cast requires the target expression to be parenthesised:

// ✗ current — `@type` is parsed as a cast of `result` alone
const uploadItems = /** `@type` {UploadAssetItem[]} */ result.items.filter(...);

// ✓ correct — `@type` applies to the full filter expression
const uploadItems = /** `@type` {UploadAssetItem[]} */ (result.items.filter(item => item.type === "upload_asset"));

Without parentheses TypeScript reads the comment as a cast of result to UploadAssetItem[], and .items.filter() is called on that wrongly-typed value. This is silently accepted only because result is any-typed, meaning the annotation provides weaker type-safety than intended and will mislead anyone who later tightens the return type of loadAgentOutput.

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.

Addressed in 5eddc6e by replacing the inline cast with a stable variable-level @type {UploadAssetItem[]} annotation for uploadItems.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
mockBranchMissing();

await executeScript();
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("missing"));

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.

Assertion is too vague to prove the all-assets-missing code path ran.

💡 Details and fix

expect.stringContaining("missing") would also match a missing-env-var failure, a missing-required-fields failure, or any future error message that happens to contain the word. So the test cannot distinguish whether the specific "all declared assets missing" path was triggered.

The production code at line 177 emits a distinctive message:

core.setFailed(`All ${missingAssetCount} declared assets were missing; no assets published.`);

Assert against that:

expect(mockCore.setFailed).toHaveBeenCalledWith(
  expect.stringContaining("All 1 declared assets were missing")
);

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.

Addressed in 848f950 by deriving the expected missing-asset count from the fixture and asserting that specific failure path.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
let pushCalled = false;
mockExec.exec.mockImplementation(async (command, args) => {
const fullCommand = Array.isArray(args) ? `${command} ${args.join(" ")}` : command;
if (fullCommand.includes("push")) pushCalled = true;

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.

Substring match on "push" is too broad for a push-guard assertion.

💡 Details and fix

fullCommand.includes("push") would also fire on any git subcommand argument, file path, or commit message fragment that contains the substring. The test would incorrectly declare pushCalled = true (and then the expect(pushCalled).toBe(false) would fail) for unrelated matches.

Assert on the actual git push command shape instead:

mockExec.exec.mockImplementation(async (command, args) => {
  if (command === "git" && Array.isArray(args) && args[0] === "push") pushCalled = true;
  if (Array.isArray(args) && args.includes("rev-parse")) throw new Error("Branch does not exist");
  return 0;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in 5eddc6e by narrowing the staged-mode check to the actual git push command shape.

Comment thread actions/setup/js/upload_assets.test.cjs Outdated
await executeScript();
expect(orphanBranchCreated).toBe(true);
expect(mockCore.setFailed).not.toHaveBeenCalled();
if (fs.existsSync(path.join(process.cwd(), "test.png"))) fs.unlinkSync(path.join(process.cwd(), "test.png"));

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.

cwd file cleanup runs after assertions — leaked files if a test fails.

💡 Details and fix

This cleanup of process.cwd() artefacts is not guarded by try/finally. If expect(orphanBranchCreated).toBe(true) throws, the unlinkSync calls never run and test.png is left in the working tree. The same pattern repeats at lines 168, 194–195, 255–256, 297–298, and 336–337.

Either wrap the test body:

try {
  await executeScript();
  expect(...);
} finally {
  if (fs.existsSync(path.join(process.cwd(), "test.png"))) fs.unlinkSync(...);
}

or track targetFileName in a shared variable and clean it up in afterEach, mirroring the tempBase pattern.

The "assets dir resolution" test already uses try/finally correctly — apply the same pattern here.

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.

Addressed in 5eddc6e by moving cwd artifact cleanup into shared afterEach handling instead of post-assertion unlinkSync calls.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits June 25, 2026 05:49
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@copilot run pr-finisher skill

Ran the pr-finisher pass. The review-thread fixes are in 5eddc6e and 848f950, local validation (make fmt, make lint, make test-unit, make test, make agent-report-progress) passed, and the earlier CJS run was canceled rather than failed. CI on this head is still unverified until a maintainer re-triggers it.

Copilot AI requested a review from pelikhan June 25, 2026 05:54
@pelikhan pelikhan merged commit dcf4be0 into main Jun 25, 2026
14 checks passed
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.

3 participants