Skip to content

Feature/GitHub artifacts#42

Merged
SamErde merged 10 commits into
mainfrom
feature/github-artifacts
Jul 3, 2026
Merged

Feature/GitHub artifacts#42
SamErde merged 10 commits into
mainfrom
feature/github-artifacts

Conversation

@svrooij

@svrooij svrooij commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Description

Your checklist for this pull request

🚨Please review the guidelines for contributing to this repository.

  • Make sure you are requesting to pull a topic/feature/bugfix branch (right side). Don't request your main branch!
  • Make sure you are making a pull request against the main branch (left side). Also you should start your branch off maester-action/main.
  • Check the commit's or even all commits' message styles matches our requested structure.
  • Enable the checkbox to allow maintainer edits so the branch can be updated for a merge.
  • If changing anything in the action, make sure it still works and is backward compatible.

Review

We will try to review your pull request as soon as possible.

💖 Thank you!

Summary by CodeRabbit

  • New Features
    • Added an artifact_upload_html option to upload a self-contained HTML report as a separate artifact and link it from the step summary.
    • Added selectable step summary modes: Full, Summary, Table, or false (legacy true maps to Full).
  • Changes
    • Updated the default step summary mode to Table and improved truncation for consistent output sizing.
  • Documentation
    • Refreshed the README and workflow usage example to match the new inputs and defaults.

Copilot AI review requested due to automatic review settings June 18, 2026 20:18
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@SamErde, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e293d6a-e135-4de9-b443-ae47866a959a

📥 Commits

Reviewing files that changed from the base of the PR and between 33136ea and 1a93017.

📒 Files selected for processing (4)
  • README.md
  • action.yml
  • script/Run-MaesterAction.ps1
  • script/Upload-HtmlReportArtifact.js
📝 Walkthrough

Walkthrough

Updates the summary input from a boolean toggle to a mode selector, adds an HTML artifact upload option, rewrites the PowerShell summary output to handle multiple modes, and wires the action to upload and link the HTML report artifact.

Changes

step_summary modes and HTML artifact upload

Layer / File(s) Summary
Input contract and docs
action.yml, README.md
step_summary is documented as Full, Summary, Table, or false, with default Table; artifact_upload_html is added and the README usage example is updated.
PowerShell step summary modes
script/Run-MaesterAction.ps1
GitHubStepSummary becomes a string mode selector, the markdown-report load guard checks for disabled modes, and the step summary writer now emits Full, Summary, or Table output from $results.
HTML artifact upload handler
script/Upload-HtmlReportArtifact.js
The new handler validates the report path and runtime environment, uploads the HTML report through Twirp, finalizes the artifact, and sets the artifact-url output.
Composite action wiring
action.yml
The composite action passes through the raw step_summary value, runs the HTML upload step when enabled, and appends an HTML report link when step summary output is enabled.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant actions/github-script
  participant Upload-HtmlReportArtifact.js
  participant Twirp API
  participant signed_upload_url

  actions/github-script->>Upload-HtmlReportArtifact.js: invoke({ core })
  Upload-HtmlReportArtifact.js->>Upload-HtmlReportArtifact.js: resolve path and validate env
  Upload-HtmlReportArtifact.js->>Twirp API: CreateArtifact(mime_type=text/html)
  Twirp API-->>Upload-HtmlReportArtifact.js: signed_upload_url
  Upload-HtmlReportArtifact.js->>signed_upload_url: PUT HTML bytes
  Upload-HtmlReportArtifact.js->>Twirp API: FinalizeArtifact(hash, size)
  Twirp API-->>Upload-HtmlReportArtifact.js: artifact_id
  Upload-HtmlReportArtifact.js->>actions/github-script: setOutput("artifact-url", url)
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The required Description section is empty, so the PR description does not explain the change or any linked issue. Add a brief summary of the feature, the problem it solves, and any related issue or context under the Description section.
Title check ❓ Inconclusive The title is too generic and doesn’t clearly describe the main change in the pull request. Use a concise, specific title that names the primary change, such as adding HTML artifact upload and step summary modes.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 feature/github-artifacts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codacy-production

This comment was marked as low quality.

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 extends the Maester composite GitHub Action to (1) support uploading the HTML report as a standalone artifact rendered inline by GitHub, and (2) make the GitHub step summary output configurable by detail level.

Changes:

  • Add a Node script that uploads the self-contained HTML report via GitHub’s Twirp Artifact API (non-zip, text/html).
  • Change step_summary from a boolean to a multi-mode option (Full / Summary / Table / false) and generate compact summary formats.
  • Update action inputs and README to document the new options and HTML artifact upload input.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
script/Upload-HtmlReportArtifact.js New helper to upload HTML as a standalone artifact using the runtime token/results API.
script/Run-MaesterAction.ps1 Adds step summary “modes” and compact summary generation paths.
action.yml Updates step_summary input contract, adds artifact_upload_html, and adds steps to upload/link the HTML report.
README.md Documents new step_summary behavior and the new HTML artifact upload input.

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

Comment thread action.yml
Comment thread action.yml
Comment thread script/Run-MaesterAction.ps1 Outdated
Comment thread script/Upload-HtmlReportArtifact.js
Comment thread script/Upload-HtmlReportArtifact.js Outdated
Comment thread README.md
Comment thread script/Upload-HtmlReportArtifact.js Outdated
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

merill
merill previously requested changes Jun 19, 2026

@merill merill 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.

@svrooij can you review the GH Copilot comments? Tx!

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread action.yml
Comment thread script/Run-MaesterAction.ps1
Comment thread script/Run-MaesterAction.ps1 Outdated

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
script/Upload-HtmlReportArtifact.js (2)

93-143: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail soft on unexpected upload exceptions.

The script warns and returns on non-OK responses, but network failures, JSON parse failures, and blob upload exceptions still throw and fail the workflow after Maester has completed. Wrap the optional upload sequence and convert unexpected errors to core.warning.

Proposed fix shape
-  const createResp = await fetch(`${origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact`, {
+  try {
+    const createResp = await fetch(`${origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact`, {
       method: 'POST',
       headers: authHeaders,
       body: JSON.stringify({
         workflow_run_backend_id: runBackendId,
         workflow_job_run_backend_id: jobBackendId,
         name: artifactName,
         version: 7,
         mime_type: 'text/html'
       })
-  });
+    });
+    // existing create/upload/finalize handling...
+  } catch (err) {
+    core.warning(`HTML artifact upload failed: ${err.message}`);
+    return;
+  }
🤖 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 `@script/Upload-HtmlReportArtifact.js` around lines 93 - 143, Wrap the optional
artifact upload flow in Upload-HtmlReportArtifact.js so unexpected exceptions
are converted to core.warning instead of failing the workflow after Maester
completes. In the sequence that calls CreateArtifact, reads the file, PUTs to
signedUploadUrl, and FinalizeArtifact, catch network/JSON/blob upload errors
around the existing createResp/blobResp/finalizeResp handling and log the
failure with context, then return early just like the current non-OK paths.

108-142: 🎯 Functional Correctness | 🟠 Major

Use the camelCase response fields here.

CreateArtifactResponse exposes signedUploadUrl, and FinalizeArtifactResponse exposes artifactId; the current signed_upload_url / artifact_id destructuring leaves both undefined and breaks the upload flow. Add a missing-field warning before using either value.

🤖 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 `@script/Upload-HtmlReportArtifact.js` around lines 108 - 142, The upload flow
is destructuring the wrong response fields, so `createResp.json()` and
`finalizeResp.json()` are leaving `signedUploadUrl` and `artifactId` undefined.
Update `Upload-HtmlReportArtifact.js` to use the camelCase properties from
`CreateArtifactResponse` and `FinalizeArtifactResponse`, and add a warning check
before using either value so missing fields are reported early. Keep the fix
localized around the artifact creation/finalization logic and the existing
`signedUploadUrl` / `artifactId` references.
🤖 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 `@action.yml`:
- Around line 199-200: The step-summary link gate is too loose and does not
match the normalization in Run-MaesterAction.ps1. Update the condition on the
“Add HTML report link to step summary” step so it only runs when the summary
mode is actually enabled using the same allowed values as the PowerShell logic
(true, Full, Summary, Table), and not for other inputs like Disabled. Use the
existing inputs.step_summary check in action.yml to align the GitHub Actions
gate with the mode handling in Run-MaesterAction.ps1.
- Around line 196-197: The `require()` call in the upload step is inlining
`github.action_path`, which can break on Windows because backslashes may be
treated as escape sequences before the module loads. Update the step that calls
`upload({ core })` to pass the action path through `env`, then build the script
path inside the `require()` logic using `path.join()` so the module resolves
safely across platforms.

---

Outside diff comments:
In `@script/Upload-HtmlReportArtifact.js`:
- Around line 93-143: Wrap the optional artifact upload flow in
Upload-HtmlReportArtifact.js so unexpected exceptions are converted to
core.warning instead of failing the workflow after Maester completes. In the
sequence that calls CreateArtifact, reads the file, PUTs to signedUploadUrl, and
FinalizeArtifact, catch network/JSON/blob upload errors around the existing
createResp/blobResp/finalizeResp handling and log the failure with context, then
return early just like the current non-OK paths.
- Around line 108-142: The upload flow is destructuring the wrong response
fields, so `createResp.json()` and `finalizeResp.json()` are leaving
`signedUploadUrl` and `artifactId` undefined. Update
`Upload-HtmlReportArtifact.js` to use the camelCase properties from
`CreateArtifactResponse` and `FinalizeArtifactResponse`, and add a warning check
before using either value so missing fields are reported early. Keep the fix
localized around the artifact creation/finalization logic and the existing
`signedUploadUrl` / `artifactId` references.
🪄 Autofix (Beta)

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 Plus

Run ID: f3a956ac-cf54-47f6-a7ad-03d485004495

📥 Commits

Reviewing files that changed from the base of the PR and between 3207a70 and becea33.

📒 Files selected for processing (4)
  • README.md
  • action.yml
  • script/Run-MaesterAction.ps1
  • script/Upload-HtmlReportArtifact.js
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • script/Run-MaesterAction.ps1

Comment thread action.yml Outdated
Comment thread action.yml Outdated
svrooij and others added 2 commits June 25, 2026 15:44
…false positives

- Pass github.action_path via env + path.join so backslashes on Windows
  runners are not mangled as JS escape sequences in the inline script
- Gate the HTML-report summary link on the same enabled modes that
  Run-MaesterAction.ps1 normalizes (true/Full/Summary/Table)
- Add justified eslint/semgrep suppressions for path-traversal and XSS
  findings that are false positives (path is confined to GITHUB_WORKSPACE
  by resolveReportPath; MAESTER_HTML_PATH is a file path, not HTML)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 3, 2026 11:11
@SamErde

SamErde commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review follow-up: findings, decisions, and fixes (pushed in 33136ea)

A full pass was done over all changes, commit history, and every review conversation (including resolved ones). Summary of each open question, the evidence, and the confidence level of the decision:

1. Windows require() bug (CodeRabbit, action.yml inline script) — Fixed · Confidence: High

Evidence: github.action_path on Windows runners is e.g. D:\a\_actions\...; interpolated into a JS string literal inside actions/github-script, sequences like \a are treated as string escapes and the backslashes are silently dropped, breaking require(). The action already uses Windows-style paths elsewhere (\script\Run-MaesterAction.ps1), so Windows support is in scope. Forward slashes in the YAML would not help - the backslashes come from the value of the context variable, not the separators we write.
Decision: Implemented CodeRabbit''s suggested pattern: pass the path via env: MAESTER_ACTION_PATH and build it with path.join(). Env vars are never parsed as JS source, so this is correct on every OS.

2. Summary-link gate !contains('false', inputs.step_summary) (CodeRabbit) — Fixed · Confidence: High

Evidence: The reversed-argument trick meant values like Disabled (which Run-MaesterAction.ps1 normalizes to no summary) still appended an HTML-link section, while quirky values like F or als suppressed it.
Decision: Replaced with an explicit allowlist mirroring the PowerShell normalization: contains(fromJSON('["true","Full","Summary","Table"]'), inputs.step_summary). GitHub expression string comparisons are case-insensitive, so FALSE/False remain handled.

3. Codacy security findings (3 critical / 3 high) — All six assessed as false positives; suppressed with justification · Confidence: High

Pulled the actual findings from Codacy''s API. All are in script/Upload-HtmlReportArtifact.js:

Rule Lines Assessment
Semgrep + ESLint non-literal-fs-filename (x4) 59, 111 filePath is not user input (set to a literal in action.yml) and is confined to GITHUB_WORKSPACE by resolveReportPath() before use - the canonical CWE-22 mitigation
Semgrep path-join-resolve-traversal 25 Fires on the sanitizer itself - the resolve + relative containment check is the mitigation these rules ask for
ESLint xss/no-mixed-html 54 Naming heuristic only: a variable containing "Html" passed to a function. It is a file path; no HTML is ever rendered

Codacy''s own metadata marks each of these patterns with an 80% false-positive threshold. Inline eslint-disable-next-line / nosemgrep suppressions with written justifications were added so the rationale lives in the code and survives re-analysis. If Codacy still gates on them, a repo admin should mark the six issues as ignored in the Codacy dashboard.

4. Undocumented Twirp Artifact API — Kept, no change · Confidence: Medium

Evidence: No publicly documented API provides inline-rendered single-file artifacts: the official @actions/artifact / actions/upload-artifact always ZIP-wrap (losing inline rendering), and GitHub Pages would publish security test results. The script already degrades gracefully (warnings only; the link step is skipped when no URL is produced).
Decision: Accept the fragility consciously - the file header already documents that this calls the internal Twirp API. Maintainers should be aware it may break without notice if GitHub changes internals.

Remaining before merge

  • Codacy gate re-check on the new commit (suppressions may need dashboard dismissal as well)
  • Re-review/approval from @merill (review is currently CHANGES_REQUESTED)

All other checks pass (CodeQL, script analysis, live Maester runs on latest + preview).

@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

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
script/Upload-HtmlReportArtifact.js (2)

103-118: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the artifact upload best-effort on thrown failures.

The non-OK paths degrade gracefully, but fetch(), response.json(), a missing signed_upload_url, or a missing artifact_id can still throw or emit an undefined artifact link. Catch those paths and warn/return.

Proposed guard pattern
+async function fetchOrWarn(core, label, url, options) {
+  try {
+    return await fetch(url, options);
+  } catch (error) {
+    core.warning(`${label} request failed: ${error.message}`);
+    return null;
+  }
+}
+
+async function jsonOrWarn(core, label, response) {
+  try {
+    return await response.json();
+  } catch (error) {
+    core.warning(`${label} returned invalid JSON: ${error.message}`);
+    return null;
+  }
+}
+
-  const createResp = await fetch(`${origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact`, {
+  const createResp = await fetchOrWarn(core, 'CreateArtifact', `${origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact`, {
     method: 'POST',
     headers: authHeaders,
     body: JSON.stringify({
       workflow_run_backend_id: runBackendId,
       workflow_job_run_backend_id: jobBackendId,
@@
       mime_type: 'text/html'
     })
   });
+  if (!createResp) {
+    return;
+  }
   if (!createResp.ok) {
     core.warning(`CreateArtifact failed (${createResp.status}): ${await createResp.text()}`);
     return;
   }
-  const { signed_upload_url: signedUploadUrl } = await createResp.json();
+  const createBody = await jsonOrWarn(core, 'CreateArtifact', createResp);
+  const signedUploadUrl = createBody?.signed_upload_url;
+  if (!signedUploadUrl) {
+    core.warning('CreateArtifact did not return a signed upload URL.');
+    return;
+  }
@@
-  const blobResp = await fetch(signedUploadUrl, {
+  const blobResp = await fetchOrWarn(core, 'Blob upload', signedUploadUrl, {
     method: 'PUT',
     headers: {
       'Content-Type': 'text/html',
       'x-ms-blob-type': 'BlockBlob'
     },
     body: fileBytes
   });
+  if (!blobResp) {
+    return;
+  }
@@
-  const finalizeResp = await fetch(`${origin}/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact`, {
+  const finalizeResp = await fetchOrWarn(core, 'FinalizeArtifact', `${origin}/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact`, {
     method: 'POST',
     headers: authHeaders,
     body: JSON.stringify({
       workflow_run_backend_id: runBackendId,
       workflow_job_run_backend_id: jobBackendId,
@@
       hash: `sha256:${sha256}`
     })
   });
+  if (!finalizeResp) {
+    return;
+  }
   if (!finalizeResp.ok) {
     core.warning(`FinalizeArtifact failed (${finalizeResp.status}): ${await finalizeResp.text()}`);
     return;
   }
-  const { artifact_id: artifactId } = await finalizeResp.json();
+  const finalizeBody = await jsonOrWarn(core, 'FinalizeArtifact', finalizeResp);
+  const artifactId = finalizeBody?.artifact_id;
+  if (!artifactId) {
+    core.warning('FinalizeArtifact did not return an artifact ID.');
+    return;
+  }

Also applies to: 125-154

🤖 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 `@script/Upload-HtmlReportArtifact.js` around lines 103 - 118, The
Upload-HtmlReportArtifact flow is still failing hard on thrown paths even though
non-OK responses are handled gracefully. In the artifact creation/upload logic
around the fetch/response handling in Upload-HtmlReportArtifact, wrap the
response parsing and subsequent signed URL/artifact ID usage in best-effort
guards so fetch(), createResp.json(), missing signed_upload_url, and missing
artifact_id all emit a core.warning and return early instead of throwing or
producing an undefined link. Reference the existing createResp, signedUploadUrl,
and artifactId handling and keep the upload path resilient in the same style as
the current non-OK checks.

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

Close the symlink escape in workspace confinement.

The path.resolve/relative check blocks lexical traversal, but readFileSync will still follow a symlink under GITHUB_WORKSPACE to a target outside the workspace. Resolve the real file path, or reject symlinks, before reading.

Proposed hardening
+function isInsideBase(baseDir, targetPath) {
+  const relative = path.relative(baseDir, targetPath);
+  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
+}
+
 function resolveReportPath(rawPath) {
-  const baseDir = path.resolve(process.env.GITHUB_WORKSPACE || process.cwd());
+  const baseDir = fs.realpathSync.native(path.resolve(process.env.GITHUB_WORKSPACE || process.cwd()));
   // This resolve+relative check IS the CWE-22 mitigation: anything escaping
   // baseDir is rejected below, so the taint warning is a false positive.
   // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
   const resolved = path.resolve(baseDir, rawPath);
-  const relative = path.relative(baseDir, resolved);
-  if (relative.startsWith('..') || path.isAbsolute(relative)) {
+  if (!isInsideBase(baseDir, resolved)) {
     return null;
   }
-  return resolved;
+  return { baseDir, filePath: resolved };
 }
 
 module.exports = async ({ core }) => {
-  const filePath = resolveReportPath(process.env.MAESTER_HTML_PATH || 'test-results/test-results.html');
-  if (!filePath) {
+  const reportPath = resolveReportPath(process.env.MAESTER_HTML_PATH || 'test-results/test-results.html');
+  if (!reportPath) {
     core.warning('HTML report path is outside the workspace; refusing to upload.');
     return;
   }
+  const { baseDir, filePath } = reportPath;
+
+  let realFilePath;
+  try {
+    realFilePath = fs.realpathSync.native(filePath);
+  } catch {
+    core.warning(`HTML report not found: ${filePath}`);
+    return;
+  }
+  if (!isInsideBase(baseDir, realFilePath)) {
+    core.warning('HTML report path resolves outside the workspace; refusing to upload.');
+    return;
+  }
-  if (!fs.existsSync(filePath)) {
-    core.warning(`HTML report not found: ${filePath}`);
-    return;
-  }
 
-  const fileBytes = fs.readFileSync(filePath);
+  const fileBytes = fs.readFileSync(realFilePath);

Also applies to: 62-69, 121-123

🤖 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 `@script/Upload-HtmlReportArtifact.js` around lines 25 - 35, The workspace
confinement check in resolveReportPath only prevents lexical path traversal and
still allows symlink escapes when the file is later read. Harden the file lookup
by resolving the real filesystem path or rejecting symlinks before any
readFileSync call, and apply the same fix to the other report-path usages noted
in the diff so all access stays inside GITHUB_WORKSPACE.

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.

Outside diff comments:
In `@script/Upload-HtmlReportArtifact.js`:
- Around line 103-118: The Upload-HtmlReportArtifact flow is still failing hard
on thrown paths even though non-OK responses are handled gracefully. In the
artifact creation/upload logic around the fetch/response handling in
Upload-HtmlReportArtifact, wrap the response parsing and subsequent signed
URL/artifact ID usage in best-effort guards so fetch(), createResp.json(),
missing signed_upload_url, and missing artifact_id all emit a core.warning and
return early instead of throwing or producing an undefined link. Reference the
existing createResp, signedUploadUrl, and artifactId handling and keep the
upload path resilient in the same style as the current non-OK checks.
- Around line 25-35: The workspace confinement check in resolveReportPath only
prevents lexical path traversal and still allows symlink escapes when the file
is later read. Harden the file lookup by resolving the real filesystem path or
rejecting symlinks before any readFileSync call, and apply the same fix to the
other report-path usages noted in the diff so all access stays inside
GITHUB_WORKSPACE.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d1b928-e39f-4f1a-8645-767e3ad56c68

📥 Commits

Reviewing files that changed from the base of the PR and between 31f5792 and 33136ea.

📒 Files selected for processing (2)
  • action.yml
  • script/Upload-HtmlReportArtifact.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • action.yml

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread script/Upload-HtmlReportArtifact.js Outdated
Comment thread action.yml Outdated
Comment thread README.md Outdated
Comment thread action.yml Outdated
SamErde and others added 2 commits July 3, 2026 07:18
The output URL is the artifact URL on the workflow run (rendered inline
because the artifact is uploaded with mime_type=text/html), not a direct
file URL. Align the JS header, input description, README, and step
summary link text with what the link actually does.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 3, 2026 11:23

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread script/Upload-HtmlReportArtifact.js Outdated
Comment thread script/Run-MaesterAction.ps1 Outdated
Comment thread action.yml Outdated
…rd, quote summary path

- Normalize step_summary input to a mode once (before the markdown
  writer import gate) so empty/unknown values consistently disable the
  summary, matching the later write logic
- Replace existsSync with statSync + isFile so a directory path warns
  instead of readFileSync throwing EISDIR and failing the step
- Quote \ redirects in bash

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@SamErde
SamErde self-requested a review July 3, 2026 11:33
@SamErde
SamErde dismissed merill’s stale review July 3, 2026 11:38

Changes have been resolved.

@SamErde
SamErde merged commit 910da5a into main Jul 3, 2026
7 checks passed
@SamErde
SamErde deleted the feature/github-artifacts branch July 3, 2026 11:38
@svrooij svrooij linked an issue Jul 3, 2026 that may be closed by this pull request
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.

Nice integration with artifacts

4 participants