diff --git a/.github/workflows/install-e2e-macos-run.yml b/.github/workflows/install-e2e-macos-run.yml index e2d1895105..de90b863b2 100644 --- a/.github/workflows/install-e2e-macos-run.yml +++ b/.github/workflows/install-e2e-macos-run.yml @@ -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: diff --git a/.github/workflows/install-e2e-run.yml b/.github/workflows/install-e2e-run.yml index f47aea2c8d..29cc5a8c51 100644 --- a/.github/workflows/install-e2e-run.yml +++ b/.github/workflows/install-e2e-run.yml @@ -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 diff --git a/.github/workflows/install-e2e-windows-run.yml b/.github/workflows/install-e2e-windows-run.yml index ea67fafb7a..c90831a772 100644 --- a/.github/workflows/install-e2e-windows-run.yml +++ b/.github/workflows/install-e2e-windows-run.yml @@ -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 diff --git a/.github/workflows/install-e2e.yml b/.github/workflows/install-e2e.yml index 746d540c91..93d3b3ae5d 100644 --- a/.github/workflows/install-e2e.yml +++ b/.github/workflows/install-e2e.yml @@ -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: diff --git a/scripts/sandbox/generate-e2e-matrix.mjs b/scripts/sandbox/generate-e2e-matrix.mjs index 70a9f28acc..35053d8a65 100644 --- a/scripts/sandbox/generate-e2e-matrix.mjs +++ b/scripts/sandbox/generate-e2e-matrix.mjs @@ -283,14 +283,14 @@ export function renderMarkdownPlan(envs, tags) { * when the pair is declared but no driver arm runs it yet. * @param {Map} [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-` (windows) or `install-e2e-logs--` - * (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-` (windows) or + * `install-e2e-logs--` (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-` (windows) or with a // trailing `-` (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}`; diff --git a/tests/install/e2e-assets/playback.html b/tests/install/e2e-assets/playback.html index 4bece90d7b..315d002b64 100644 --- a/tests/install/e2e-assets/playback.html +++ b/tests/install/e2e-assets/playback.html @@ -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).'; diff --git a/tests/install/e2e-assets/ts-prefix.ps1 b/tests/install/e2e-assets/ts-prefix.ps1 index 86398ca328..cc9acc8780 100644 --- a/tests/install/e2e-assets/ts-prefix.ps1 +++ b/tests/install/e2e-assets/ts-prefix.ps1 @@ -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), $_ } }