Skip to content

feat: upload HTML artifact on report truncation#38

Open
eriksw wants to merge 15 commits intomainfrom
feature/artifact-on-truncation
Open

feat: upload HTML artifact on report truncation#38
eriksw wants to merge 15 commits intomainfrom
feature/artifact-on-truncation

Conversation

@eriksw
Copy link
Copy Markdown
Collaborator

@eriksw eriksw commented Mar 30, 2026

User-facing changes

When a report is too large for a GitHub comment, the action now uploads the full
un-truncated report as an HTML artifact and links to it from the truncation
notice. Previously, truncated reports only linked to the workflow run logs.

The artifact is an HTML page styled with GitHub Primer CSS (loaded from CDN),
rendered via the GitHub /markdown API for full GFM fidelity (task lists,
emoji, syntax highlighting).

Additionally, when a step fails without any captured stdout/stderr output, a
new "logs notice" is appended directing users to the workflow run logs — this
appears independently of truncation and addresses the case where error details
are only visible in the raw logs.

always-upload-report input

A new always-upload-report input (default "false") allows opting in to
unconditional artifact upload — even when the report is not truncated. When
enabled and the upload succeeds, a compact 📎 link to the artifact is appended
to the comment. This is useful for archival or sharing reports outside of GitHub.

- uses: retailnext/tf-report-action@main
  with:
    steps: ${{ toJSON(steps) }}
    github-token: ${{ github.token }}
    always-upload-report: "true"

This PR's CI workflow has this enabled to demonstrate the feature.

Artifact upload details

  • Protocol: Actions Results Service v7 (Twirp RPC + Azure Blob Storage)
  • GHES: Artifact upload is automatically skipped on GHES (only github.com
    and *.ghe.com are supported). Falls back to linking the workflow run logs.
  • Failures: Upload failures are non-fatal — logged as ::warning:: and the
    truncation notice falls back to the logs URL.
  • Naming: Artifact filenames include workspace and operation for clarity
    (e.g., cluster-plan.html, apply.html).
  • Zero new runtime deps: The artifact upload protocol is implemented from
    scratch using node:http/node:https (reusing the existing HTTP transport).

Internal changes

New modules

Module Layer Responsibility
src/artifact/jwt.ts 6 Decode ACTIONS_RUNTIME_TOKEN JWT for backend IDs
src/artifact/twirp.ts 6 CreateArtifact/FinalizeArtifact Twirp RPC with retry
src/artifact/blob-upload.ts 6 Azure Blob Storage PUT with retry
src/artifact/upload.ts 6 3-step upload orchestrator with GHES guard
src/html/page.ts 4 Primer CSS HTML page wrapper
src/action/artifact-upload.ts 6 Never-throws upload orchestrator with full DI

Modified modules

  • src/action/main.tsrun() refactored from positional params to RunDeps
    object for dependency injection. Upload triggered when truncated OR when
    always-upload-report is enabled.
  • src/action/inputs.ts — New alwaysUploadReport boolean parsed from
    always-upload-report input.
  • src/index.tsReportFromStepsResult gains operation and
    hasUnresolvedFailures fields; exports buildLogsNotice and
    buildArtifactNotice.
  • src/model/report.tshasUnresolvedFailures flag on Report.
  • src/compositor/truncation.tsbuildLogsNotice(), buildArtifactNotice()
    alongside existing buildTruncationNotice().
  • src/model/status-icons.tsINFO_ICON, ARTIFACT_ICON.

Tests

93 new tests across 7 test files (all passing, coverage thresholds met).

Fix: CreateArtifact mime_type field

The Actions Results Service v7 requires a mime_type field in the
CreateArtifact Twirp request. The initial implementation omitted it,
causing an HTTP 400 rejection. Now mime_type is derived from the filename
extension and passed through to the Twirp call.

Dependency-injected logging

All ::warning:: and ::error:: annotation output is now routed through
a Logger interface (src/action/logger.ts). Tests inject nullLogger()
or a capturing implementation, preventing annotations from leaking into the
CI test runner output (which GitHub Actions would interpret as real workflow
commands). This eliminates spurious warnings like Artifact upload is not supported on github.mycompany.com that appeared when tests exercised error
paths.

New enforcement:

  • ESLint rule: no-restricted-syntax bans process.stderr,
    process.stdout, and console.* in src/ (except logger.ts)
  • Governance test: no-direct-io.test.ts scans source files for
    forbidden I/O globals

eriksw and others added 8 commits March 30, 2026 13:35
Extract workflowRunBackendId and workflowJobRunBackendId from the
ACTIONS_RUNTIME_TOKEN JWT scp claim. The token is decoded without
signature verification (consumed by the same process that received it).

Includes ArtifactTransport, BackendIds, UploadArtifactResult, and
ArtifactUploaderDeps type definitions in types.ts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CreateArtifact and FinalizeArtifact Twirp calls with protocol version 7.
Both are wrapped in exponential-backoff retry for 429/5xx errors via
withRetry from src/http/retry.ts. Injectable transport and sleep for
testability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Single PUT upload to Azure Blob Storage signed URL with BlockBlob
header, correct UTF-8 byte length, and retry for 5xx errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three-step upload flow: CreateArtifact → blob PUT → FinalizeArtifact.
Includes GHES guard (github.com and *.ghe.com only), SHA-256 hash
computation, MIME type detection, and barrel exports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wrap a GitHub-rendered HTML fragment in a complete HTML5 page that loads
Primer CSS from unpkg CDN. The markdown-body class matches GitHub's
rendered Markdown container for correct styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tryUploadFullReport() renders full markdown to HTML via the GitHub API,
wraps it in a Primer CSS page, uploads as an artifact, and returns the
artifact URL. Never throws — all errors become ::warning:: annotations
and the function returns undefined.

All I/O dependencies are injected via parameters for testability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add hasUnresolvedFailures flag to the Report model, computed when any
step fails without captured stdout/stderr. Add buildLogsNotice() to the
compositor alongside buildTruncationNotice(), directing users to
workflow run logs when error details are not in the report.

Add INFO_ICON to status-icons for the logs notice blockquote.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire artifact upload into the action entry point. When a report is
truncated, the full markdown is rendered to HTML, uploaded as an
artifact, and the truncation notice links to the artifact. Falls back
to logs URL if upload is unavailable (GHES) or fails.

Key changes:
- Add operation and hasUnresolvedFailures to ReportFromStepsResult
- Export buildLogsNotice from the public API
- Refactor run() from positional params to RunDeps object for DI
- Artifact name includes workspace and operation (e.g. cluster-plan)
- Logs notice appended independently when steps fail without output
- Add integration coverage exclusions for artifact/, html/, action/

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an artifact-upload fallback path so that when the generated report exceeds GitHub comment limits, the action can still provide access to the full report by uploading an HTML artifact and linking it from the truncation notice. Also adds a separate “logs notice” for failure cases where step output wasn’t captured, directing users to workflow run logs for the missing error details.

Changes:

  • Implement Actions Results Service (Twirp v7 + Azure Blob Storage) artifact upload pipeline and action-layer “never-throws” upload wrapper.
  • Add HTML page wrapper for GitHub-rendered Markdown fragments and integrate upload attempt into truncation handling in src/action/main.ts.
  • Track/report hasUnresolvedFailures and append a logs notice when failures have no captured stdout/stderr.

Reviewed changes

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

Show a summary per file
File Description
vitest.integration.config.ts Excludes src/artifact/** and src/html/** from integration-only coverage with rationale comments.
src/model/status-icons.ts Adds INFO_ICON for informational notices.
src/model/report.ts Adds hasUnresolvedFailures flag to the Report model.
src/index.ts Extends ReportFromStepsResult with operation and hasUnresolvedFailures; re-exports buildLogsNotice.
src/html/page.ts New HTML page wrapper used for artifact rendering.
src/html/index.ts Public barrel export for HTML utilities.
src/compositor/truncation.ts Adds buildLogsNotice() alongside truncation notice helper.
src/builder/report-from-steps.ts Computes report.hasUnresolvedFailures from step issues.
src/artifact/upload.ts Artifact upload orchestrator (guard, hash, create → blob PUT → finalize).
src/artifact/types.ts Shared artifact types and DI surface (transport/hash/sleep).
src/artifact/twirp.ts Twirp RPC implementations with retry logic.
src/artifact/jwt.ts Extracts backend IDs from ACTIONS_RUNTIME_TOKEN JWT scope.
src/artifact/index.ts Artifact module barrel export.
src/artifact/blob-upload.ts Single-PUT Azure blob upload with retry.
src/action/main.ts Refactors run() deps to an object; attempts artifact upload on truncation; appends logs notice on unresolved failures.
src/action/artifact-upload.ts “Never-throws” action-layer orchestrator: render Markdown via GitHub API → wrap HTML → upload artifact → return URL.
tests/unit/html/page.test.ts Unit coverage for HTML wrapper behavior.
tests/unit/compositor/truncation.test.ts Adds tests for logs notice rendering.
tests/unit/artifact/upload.test.ts Unit coverage for upload orchestrator wiring (sequence, hash/size, MIME detection, GHES guard).
tests/unit/artifact/twirp.test.ts Unit coverage for Twirp calls and retry behavior.
tests/unit/artifact/jwt.test.ts Unit coverage for JWT scope parsing and failure modes.
tests/unit/artifact/blob-upload.test.ts Unit coverage for blob PUT + retry policy.
tests/unit/action/main.test.ts Updates for new run() signature and adds new tests around logs notice/artifact upload wiring.
tests/unit/action/artifact-upload.test.ts Unit coverage for tryUploadFullReport() behavior and error handling.
dist/index.js Re-bundled distribution output reflecting new/changed source modules.

eriksw and others added 6 commits March 30, 2026 14:57
- Remove "self-contained" wording from HTML page builder JSDoc since the
  page loads Primer CSS from CDN (requires internet access)
- Remove redundant test that claimed to test truncation wiring but did
  not actually force truncation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an off-by-default `always-upload-report` input that unconditionally
uploads the full report as an HTML artifact, even when the report is not
truncated. When the report fits in the comment, a compact artifact link
(📎) is appended. When truncated, the existing truncation notice still
links to the artifact.

Includes:
- New ARTIFACT_ICON constant in status-icons
- buildArtifactNotice() in compositor/truncation.ts
- alwaysUploadReport boolean in ActionInputs
- Wiring in main.ts: upload when flag is set OR truncated
- Enabled in ci.yml to demonstrate on this PR
- Documented in README inputs table, features, and size limits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…me_type

- Add Logger interface (warning/error/info) in src/action/logger.ts
  with actionsLogger() for production and nullLogger() for tests
- Thread Logger through RunDeps and TryUploadParams so tests never
  emit ::warning:: or ::error:: annotations into CI output
- Add exit function DI in RunDeps (defaults to process.exit)
- Fix CreateArtifact Twirp request: include mime_type field required
  by Actions Results Service v7
- Pass detected MIME type from upload orchestrator to createArtifact()
- Rewrite main.test.ts: replace vi.spyOn(process/console) hacks with
  capturingLogger/nullLogger and throwingExit
- Rewrite artifact-upload.test.ts: verify warning messages via
  capturing logger on error paths
- Add ESLint no-restricted-syntax rule banning direct process.stderr,
  process.stdout, and console.* access in src/ (except logger.ts)
- Add governance test (no-direct-io.test.ts) scanning source for
  forbidden I/O globals

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Actions Results Service returns snake_case JSON field names
(signed_upload_url, artifact_id), not camelCase. This caused
CreateArtifact to succeed but the response parsing to fail with
'missing signedUploadUrl'. Updated all response field access and
test mock responses to use snake_case matching the real API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Actions Results Service expects protobuf JSON format:
- Request field names use proto (snake_case) names, matching the toolkit's
  useProtoFieldName: true serialization option
- StringValue wrapper types serialize as plain strings, not {value: ...}
  objects — fixes the FinalizeArtifact 'malformed' error caused by sending
  hash as a nested object

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Primer CSS CDN link (which requires data-color-mode attributes
to activate) with fully embedded inline CSS and JavaScript. The artifact
page is now self-contained with no external dependencies:

- GitHub-like .markdown-body styles: headings, tables, code blocks,
  blockquotes, details/summary, ins/del diff styling, etc.
- Copy-to-clipboard button on every <pre> code block (appears on hover,
  shows 'Copied!' feedback)
- markdown-accessiblity-table custom element styled as display: block

The render script (npm run render / npm run gallery) now imports the same
shared CSS from src/html/ for consistent local preview.

Also changes the artifact link text from 'Full report artifact' to
'View/Download Report'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

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

- Support both markdown-accessiblity-table (GitHub's actual misspelling)
  and markdown-accessibility-table (correct spelling) in CSS selector
- Fix copy button text pollution: read from <code> child element instead
  of pre.textContent to avoid including the button label in copied text

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

ci Succeeded

Steps

Step Outcome
checkout success
setup-node success
install success
ci success
check-dist success

📎 View/Download Report


View logs

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