fix(install-e2e): windows transcripts were empty; player gets #zip= hash + one player per run
Windows transcripts were ZERO bytes: ts-prefix.ps1 formatted with
{0:D2}, but Floor() returns a double and the D specifier is
integer-only - it threw per line, and under the driver's relaxed EAP
every line errored into the void. {0:00} fixes it (custom numeric
format works on doubles). Reproduced the exact pipeline locally
(empty file + Format specifier invalid), verified the fix produces
prefixed merged stdout+stderr with exit code intact. That is also
why the log timeline never auto-synced: there was nothing in the
files to sync.
The GitHub artifact URL 307s to /suites/... server-side and strips
the ?zip= query param. The player now reads the zip URL from a
#zip= HASH param (client-side, survives the redirect) with ?zip=
as fallback; the hash path was verified in a real browser against a
real leg zip (auto-fetch + boot).
Per ethie's design, one player artifact for the whole run: new
leg-player job uploads playback.html (archive:false) before the
matrix legs, the report job needs it, and each ran cell gets TWO
links - 📼 to the player with #zip=<that leg's logs zip> and ⬇️ to
the raw zip. Per-leg player uploads removed from all three run
workflows.
This commit is contained in:
10
.github/workflows/install-e2e-macos-run.yml
vendored
10
.github/workflows/install-e2e-macos-run.yml
vendored
@@ -93,16 +93,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Upload leg player
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: install-e2e-player-${{ inputs.leg-id }}
|
||||
path: tests/install/e2e-assets/playback.html
|
||||
archive: false
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
|
||||
21
.github/workflows/install-e2e-run.yml
vendored
21
.github/workflows/install-e2e-run.yml
vendored
@@ -134,23 +134,10 @@ jobs:
|
||||
-c copy "${{ runner.temp }}/e2e-logs/recording.mp4"
|
||||
fi
|
||||
|
||||
# The leg player: one static HTML, uploaded BEFORE the driver runs
|
||||
# (if: always() covers failures too). Single file, no zip, so the
|
||||
# artifacts URL serves playback.html directly.
|
||||
- name: Upload leg player
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: install-e2e-player-${{ inputs.leg-id }}
|
||||
path: tests/install/e2e-assets/playback.html
|
||||
archive: false
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
# Artifact names cannot contain '/', and install-ref may be a full ref
|
||||
# like refs/heads/main. GitHub Actions expressions have no string-replace
|
||||
# function, so build the safe name here. Runs even on failure -- that is
|
||||
# exactly when the logs are wanted.
|
||||
# The leg player: ONE static HTML per run, uploaded up front by the
|
||||
# leg-player job in install-e2e.yml (archive: false, so GitHub names
|
||||
# the artifact after the file: playback.html). The report job links
|
||||
# every ran leg to it with the leg's zip as a #zip= hash param.
|
||||
- name: Build artifact name
|
||||
if: always()
|
||||
id: artifact
|
||||
|
||||
10
.github/workflows/install-e2e-windows-run.yml
vendored
10
.github/workflows/install-e2e-windows-run.yml
vendored
@@ -119,16 +119,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Upload leg player
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: install-e2e-player-${{ inputs.leg-id }}
|
||||
path: tests/install/e2e-assets/playback.html
|
||||
archive: false
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
# One recording mechanism on every OS: the composite action installs
|
||||
# ffmpeg (cached - winget's download is the slow part), starts the
|
||||
# capture, and record-stop fails on a zero-frame file so a silently
|
||||
|
||||
26
.github/workflows/install-e2e.yml
vendored
26
.github/workflows/install-e2e.yml
vendored
@@ -194,6 +194,30 @@ jobs:
|
||||
tag-has-desktop: ${{ matrix.tag_has_desktop }}
|
||||
leg-id: ${{ matrix.leg_id }}
|
||||
|
||||
# The leg player: one static HTML for the whole run. Uploaded BEFORE the
|
||||
# matrix legs so it exists even when every leg dies; the report job links
|
||||
# every ran leg to it with that leg's logs zip as a #zip= hash param
|
||||
# (hash survives the artifact URL's server-side redirect, the query does
|
||||
# not). archive: false makes GitHub name the artifact after the FILE
|
||||
# (playback.html), ignoring the name: input -- harmless, the renderer
|
||||
# looks it up by that name.
|
||||
leg-player:
|
||||
name: Upload leg player
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: tests/install/e2e-assets/playback.html
|
||||
sparse-checkout-cone-mode: false
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: install-e2e-player
|
||||
path: tests/install/e2e-assets/playback.html
|
||||
archive: false
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
# The outcome, human-readable: the plan chart again, with each cell
|
||||
# replaced by how that leg actually concluded. Per-leg conclusions are
|
||||
# NOT reachable through `needs` (a matrix job's result collapses to one
|
||||
@@ -203,7 +227,7 @@ jobs:
|
||||
report:
|
||||
name: Result chart
|
||||
if: always()
|
||||
needs: [pick-releases, linux, windows, macos]
|
||||
needs: [leg-player, pick-releases, linux, windows, macos]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
||||
@@ -283,14 +283,14 @@ export function renderMarkdownPlan(envs, tags) {
|
||||
* when the pair is declared but no driver arm runs it yet.
|
||||
* @param {Map<string, number>} [artifactById] Artifact name -> id for this
|
||||
* run (the report job's --artifacts). Legs that RAN (success or failure)
|
||||
* get a 📼 link: the leg's player artifact (playback.html, archive:false
|
||||
* — GitHub names those after the FILE, so they all collide on
|
||||
* `playback.html` and any one of them works, they are the same blob)
|
||||
* with ?zip= pointing at the leg's logs artifact. Logs artifacts are
|
||||
* `install-e2e-logs-<leg_id>` (windows) or `install-e2e-logs-<leg_id>-<sha>`
|
||||
* (posix arms append the sha at upload) — matched by prefix. leg_id is
|
||||
* rebuilt from the parsed job name with the same formula the generator
|
||||
* mints (legId).
|
||||
* get TWO links: 📼 to the run's single player artifact (playback.html,
|
||||
* archive:false names artifacts after the FILE) with the leg's logs zip
|
||||
* as a #zip= HASH param — the artifact URL 307s server-side and strips
|
||||
* the query, the hash survives — and ⬇️ to the raw logs zip. Logs
|
||||
* artifacts are `install-e2e-logs-<leg_id>` (windows) or
|
||||
* `install-e2e-logs-<leg_id>-<sha>` (posix arms append the sha at
|
||||
* upload) — matched by prefix. leg_id is rebuilt from the parsed job
|
||||
* name with the same formula the generator mints (legId).
|
||||
* @returns {string}
|
||||
*/
|
||||
export function renderMarkdownResults(jobs, tagAnnotations = [], artifactById = new Map()) {
|
||||
@@ -333,15 +333,17 @@ export function renderMarkdownResults(jobs, tagAnnotations = [], artifactById =
|
||||
// Logs artifacts: `install-e2e-logs-<leg_id>` (windows) or with a
|
||||
// trailing `-<sha>` (posix arms append it at upload). Match by prefix.
|
||||
const logsName = [...artifactById.keys()].find((n) => n.startsWith(`install-e2e-logs-${legId2}`));
|
||||
// Player artifacts: upload-artifact `archive: false` names the
|
||||
// artifact after the FILE (playback.html), ignoring `name:` — so
|
||||
// every leg's player artifact is called `playback.html`, and they
|
||||
// are all the same blob. Any one of them works.
|
||||
// Player artifacts: the single per-run leg-player upload; archive:
|
||||
// false names it after the FILE (playback.html), ignoring `name:`.
|
||||
const playerName = [...artifactById.keys()].find((n) => n === 'playback.html');
|
||||
const playerId = playerName !== undefined ? artifactById.get(playerName) : undefined;
|
||||
const logsId = logsName !== undefined ? artifactById.get(logsName) : undefined;
|
||||
// Two links per ran leg: the player with the zip as a HASH param
|
||||
// (GitHub's artifact URL 307s to /suites/... and strips the query —
|
||||
// the hash survives client-side), and the raw zip download.
|
||||
const runBase = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
const reel = (playerId !== undefined && logsId !== undefined)
|
||||
? ` [📼](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${playerId}?zip=${encodeURIComponent(`https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts/${logsId}`)})`
|
||||
? ` [📼](${runBase}/artifacts/${playerId}#zip=${encodeURIComponent(`${runBase}/artifacts/${logsId}`)}) [⬇️](${runBase}/artifacts/${logsId})`
|
||||
: '';
|
||||
switch (job.conclusion) {
|
||||
case 'success': return `✅${reel}`;
|
||||
|
||||
@@ -269,6 +269,19 @@ async function loadZip(url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the zip URL: hash param wins (?zip= gets stripped by GitHub's
|
||||
* artifact redirect, the hash survives), then the query param.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function resolveZipUrl() {
|
||||
const m = location.hash.match(/(?:^|[#&])zip=([^&]+)/);
|
||||
if (m) {
|
||||
try { return decodeURIComponent(m[1]); } catch { return m[1]; }
|
||||
}
|
||||
return new URLSearchParams(location.search).get('zip');
|
||||
}
|
||||
|
||||
/* drag-drop + file picker: the universal transport for auth-gated zips */
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -339,13 +352,15 @@ function boot() {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const zipUrl = params.get('zip');
|
||||
const zipUrl = resolveZipUrl();
|
||||
if (zipUrl) {
|
||||
// Attempt the direct fetch (works when the page is served on
|
||||
// github.com itself — same-origin, session cookies flow). On failure
|
||||
// the status line becomes a download link + drop target.
|
||||
if (await loadZip(zipUrl)) boot();
|
||||
} else {
|
||||
// No ?zip=: this is the manual path — the leg player artifact page or
|
||||
// a local copy. Drop or pick a leg zip.
|
||||
// No zip param: this is the manual path — the leg player artifact
|
||||
// page or a local copy. Drop or pick a leg zip.
|
||||
statusEl.className = 'error';
|
||||
statusEl.innerHTML =
|
||||
'Drop a leg artifact zip anywhere on this page (or click here to pick the file).';
|
||||
|
||||
@@ -13,6 +13,9 @@ $script:TsPrefixStart = Get-Date
|
||||
function Add-TsPrefix {
|
||||
process {
|
||||
$t = (Get-Date) - $script:TsPrefixStart
|
||||
"[+{0:D2}:{1:D2}] {2}" -f [math]::Floor($t.TotalMinutes), [math]::Floor($t.TotalSeconds % 60), $_
|
||||
# {0:00} not {0:D2}: Floor() returns a double and the D specifier
|
||||
# is integer-only - it throws per line, and under the driver's
|
||||
# relaxed EAP every line errors into the void (empty transcripts).
|
||||
"[+{0:00}:{1:00}] {2}" -f [math]::Floor($t.TotalMinutes), [math]::Floor($t.TotalSeconds % 60), $_
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user