From 4e59c8cd7b488170340e92563605dae53d800029 Mon Sep 17 00:00:00 2001 From: ottovlotto <142217647+ottovlotto@users.noreply.github.com> Date: Thu, 21 May 2026 22:28:59 +0200 Subject: [PATCH] test(e2e): add nightly-deploy-moddable cell + cleanup sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-creates a fresh paritytech/e2e-cli-moddable- public GH repo each nightly, pushes the frontend-only fixture into it, then runs dot deploy --moddable against that working dir. Asserts exit 0, the "Deploy complete" summary, the "Moddable: yes — " line, and the registry has an entry for e2emoddab00.dot. Mirrors the cold-start sequence a Summit attendee takes (gh repo create → push → dot deploy --moddable) — the only test that exercises the per-run-fresh-repo path. mod.test.ts continues to cover the import half of the round-trip. Adds E2E_GH_PAT secret reference for paritytech/ repo create + delete. Replaces e2e-cleanup.yml's stub with a real 7-day sweep by topic e2e-test-fixture. Known limitation documented in-workflow: registry domain entry at e2emoddab00.dot accumulates re-publishes (one fixed domain, same owner — bounded + benign). Closes #107 --- .github/workflows/e2e-cleanup.yml | 62 ++++++++++++---- .github/workflows/e2e.yml | 13 ++++ e2e/cli/deploy.test.ts | 81 +++++++++++++++++++++ e2e/cli/fixtures/accounts.ts | 9 +++ e2e/cli/helpers/moddable-setup.ts | 116 ++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 e2e/cli/helpers/moddable-setup.ts diff --git a/.github/workflows/e2e-cleanup.yml b/.github/workflows/e2e-cleanup.yml index 5b97102..0921566 100644 --- a/.github/workflows/e2e-cleanup.yml +++ b/.github/workflows/e2e-cleanup.yml @@ -17,21 +17,57 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Sweep rotating moddable repos and domains + - name: Sweep rotating moddable repos shell: bash env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Same PAT as the nightly-deploy-moddable cell — it has + # delete_repo scope. Rotate "Playground CLI cold start test" + # before 2026-08-20. + GH_TOKEN: ${{ secrets.E2E_GH_PAT }} run: | - echo "::group::What this workflow sweeps" - echo "Per spec §9c, this cron sweeps rotating per-run E2E state:" - echo " - GH repos matching 'e2e-cli-moddable-*' older than 14 days" - echo " - Registry domains matching 'e2e-cli-moddable-*' older than 14 days" - echo "" - echo "Phase 5e (moddable) hasn't shipped yet, so there's nothing to sweep today." - echo "When Phase 5e lands, this step gets the actual sweep logic:" - echo " gh repo list --topic e2e-test-fixture --limit 100 ..." - echo " bun tools/sweep-moddable-domains.ts (would be added then)" + set -euo pipefail + + # 7-day retention (spec called for 14; tightening surfaces + # cleanup-cron regressions sooner while Summit is close). + # `date -u -d` only works on GNU coreutils (Linux); we never + # run this on macOS, so no need for portable date arithmetic. + cutoff=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) + + echo "::group::Sweep plan" + echo "Cutoff (delete repos created before this):" + echo " ${cutoff}" + echo "Filter: paritytech/* repos tagged 'e2e-test-fixture'" + echo " (set by tests at e2e/cli/helpers/moddable-setup.ts)" + echo "::endgroup::" + + # `gh repo list` paginates implicitly via --limit; 200 is + # generous for the daily-nightly + 7-day cadence (max ~7 + # repos in flight before cleanup runs). Bump if we ever + # add more per-run rotating repos. + candidates=$(gh repo list paritytech \ + --topic e2e-test-fixture \ + --limit 200 \ + --json name,createdAt \ + --jq ".[] | select(.createdAt < \"${cutoff}\") | .name") + + if [ -z "${candidates}" ]; then + echo "no repos to sweep" + exit 0 + fi + + echo "::group::Deleting" + while read -r name; do + [ -n "${name}" ] || continue + echo " paritytech/${name}" + gh repo delete "paritytech/${name}" --yes + done <<< "${candidates}" echo "::endgroup::" - # Stub — exit 0. Replace with real sweep when Phase 5e adds rotating state. - echo "no rotating state to sweep — Phase 5e not yet shipped" + # Known limitation: this sweep only handles GH repos. The + # on-chain registry entry at e2emoddab00.dot accumulates + # cumulative re-publishes (same domain, same owner, so the + # registry-side keeps growing CIDs over time) — that's + # bounded (one fixed domain) and benign. A future + # tools/sweep-moddable-domains.ts could prune historical + # CIDs, but we don't have a public DotNS-side delete API + # today and adding one is out of scope for Phase 5e. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c0428cd..387a4bb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -189,6 +189,12 @@ jobs: - cell: nightly-deploy-multi # source: e2e/cli/deploy.test.ts → describe("dot deploy — multi …") pattern: "deploy — multi" + - cell: nightly-deploy-moddable + # source: e2e/cli/deploy.test.ts → describe("dot deploy — moddable …") + # Needs `gh` (pre-installed on parity-default + ubuntu-latest runners) + # and GH_TOKEN with repo:create on paritytech/ — set below via the + # E2E_GH_PAT secret. Other cells ignore the env var harmlessly. + pattern: "deploy — moddable" - cell: nightly-chaos-rpc # source: e2e/cli/chaos.test.ts → describe("dot deploy — chaos RPC failover") pattern: "chaos RPC failover" @@ -212,6 +218,13 @@ jobs: DOT_DEPLOY_VERBOSE: "1" DOT_TAG: ${{ steps.setup.outputs.tag }} DOT_TELEMETRY: "1" + # nightly-deploy-moddable creates a fresh public repo in + # paritytech/ each run + pushes the fixture into it. Other + # cells in this matrix don't consume GH_TOKEN so setting it + # here is harmless. Rotate the underlying PAT (labelled + # "Playground CLI cold start test" on Rebecca's account) + # before 2026-08-20 — 90-day expiry from 2026-05-21. + GH_TOKEN: ${{ secrets.E2E_GH_PAT }} - name: Surface failure detail if: failure() diff --git a/e2e/cli/deploy.test.ts b/e2e/cli/deploy.test.ts index ee26f00..6a62cb8 100644 --- a/e2e/cli/deploy.test.ts +++ b/e2e/cli/deploy.test.ts @@ -27,6 +27,7 @@ import { describe, test, expect } from "vitest"; import { resolve } from "node:path"; import { dot } from "./helpers/dot.js"; +import { setupModdableFixture } from "./helpers/moddable-setup.js"; import { SIGNER, BOB, E2E_DOMAINS } from "./fixtures/accounts.js"; import { fixturePath } from "./fixtures/templates.js"; import { getApp } from "./fixtures/registry.js"; @@ -251,3 +252,83 @@ describe("dot deploy --playground — full pipeline (requires Paseo + IPFS)", () expect(output.toLowerCase()).toMatch(/revert|taken|registered|owned|unavailable|already/); }); }); + +// `dot deploy --moddable` — the tagging half of the moddable round-trip. +// Pre-creates a public GH repo in test setup, runs `--moddable`, and +// asserts the deploy succeeds + the registry entry exists. The import +// half (`dot mod `) is covered separately in mod.test.ts. +// +// Coverage focus: this is the only cell that exercises the +// `gh repo create → push → dot deploy --moddable` cold-start sequence +// — the exact path a Summit attendee takes. The deploy reads +// `git remote get-url origin` from the test's temp working dir, HEADs +// the GH URL, stamps it into bulletin metadata, and writes the registry +// entry. Requires `GH_TOKEN` on the runner (see e2e.yml — moddable cell +// only). +describe("dot deploy — moddable (requires Paseo + IPFS + GH)", () => { + test( + "--moddable deploy stamps origin URL into registry metadata", + { timeout: 600_000 }, + async () => { + const runId = process.env.GITHUB_RUN_ID ?? `local-${Date.now()}`; + const repoName = `e2e-cli-moddable-${runId}`; + const repoUrl = `https://github.com/paritytech/${repoName}`; + const domain = E2E_DOMAINS.moddable; + + // Sets up a temp working dir with the frontend-only fixture + + // freshly-created paritytech/ with `origin` pointing + // to it. Throws (loudly) on `gh repo create` / push failures so + // the cell fails with the actual gh error rather than a + // downstream "no origin configured" red herring. + const workDir = setupModdableFixture(repoName, frontendOnly); + + const result = await dot( + [ + "deploy", + "--signer", "dev", + "--domain", domain, + "--buildDir", absBuildDir(workDir), + "--moddable", + "--playground", + "--private", + "--suri", SIGNER.suri, + "--dir", workDir, + ], + { timeout: 500_000, cwd: workDir }, + ); + + expect( + result.exitCode, + `moddable deploy failed: ${result.stdout}\n${result.stderr}`, + ).toBe(0); + expect(result.stdout).toContain("Deploy complete"); + expect(result.stdout).toContain(domain); + // The post-deploy summary at src/commands/deploy/summary.ts:76 + // prints `Moddable: yes — ` only when --moddable resolved + // successfully. Catches regressions where --moddable is + // silently downgraded (e.g. origin read but URL stamping + // skipped). The literal `yes — ${url}` reproduces the summary + // line shape; loosening to `/yes/` would match the boolean + // "yes" in unrelated rows (e.g. private=yes). + expect(result.stdout).toContain(`yes — ${repoUrl}`); + + // Belt-and-braces: the registry entry must exist on-chain. + // `metadata.repository` lives inside the bulletin-stored JSON + // — the `getApp` helper today only returns the CID, not the + // JSON contents. The stdout assertion above + the entry + // existence here give sufficient coverage; a future + // enhancement can fetch the metadata JSON from bulletin and + // assert `metadata.repository === repoUrl` directly. + const entry = await getApp(`${domain}.dot`); + expect(entry, `registry has no entry for ${domain}.dot`).not.toBeNull(); + + // Deliberately no test-side `gh repo delete` — the weekly + // cleanup cron (e2e-cleanup.yml) sweeps repos older than 7 + // days by topic filter. A `finally` cleanup here would race + // the bulletin upload serialisation: deleting the GH repo + // before bulletin commits the metadata could in theory + // invalidate the URL the CLI just stamped on-chain. Crashed + // runs still get cleaned up by the cron. + }, + ); +}); diff --git a/e2e/cli/fixtures/accounts.ts b/e2e/cli/fixtures/accounts.ts index 67bd419..5fdb50e 100644 --- a/e2e/cli/fixtures/accounts.ts +++ b/e2e/cli/fixtures/accounts.ts @@ -123,6 +123,15 @@ export const E2E_DOMAINS = { cdm: "e2ecdmapp00", hardhat: "e2ehardhat00", multi: "e2emultip00", + /** + * Phase 5e — `nightly-deploy-moddable` cell. Same-owner re-publishes + * across nightlies; each run pre-creates a fresh `paritytech/e2e-cli- + * moddable-` GH repo and the test points the deploy's `origin` + * at it. The on-chain registry entry's `metadata.repository` changes + * per run; the domain itself is fixed (NoStatus-compatible label so + * the NoStatus deployer can re-publish without PoP). + */ + moddable: "e2emoddab00", /** * Used by the nightly-chaos-sigint cell only. The deploy is interrupted by * SIGINT before it completes, so this domain is never actually registered. diff --git a/e2e/cli/helpers/moddable-setup.ts b/e2e/cli/helpers/moddable-setup.ts new file mode 100644 index 0000000..8844d48 --- /dev/null +++ b/e2e/cli/helpers/moddable-setup.ts @@ -0,0 +1,116 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Setup helper for the `nightly-deploy-moddable` cell. + * + * Builds a temp working dir containing the source fixture, initialises a + * fresh git repo, pre-creates a public GH repo at + * `paritytech/`, and pushes the fixture into it so the + * subsequent `dot deploy --moddable` call has a `origin` URL that + * `assertPublicGitHubRepo()` will accept. + * + * Failures bubble up cleanly so the CI cell fails loudly with the `gh` + * error message — per the locked design decision (no retries, no + * auto-renaming). The weekly cleanup cron sweeps repos by topic, so + * test crashes still get tidied up later. + */ + +import { execFileSync } from "node:child_process"; +import { cpSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const TOPIC = "e2e-test-fixture"; +const ORG = "paritytech"; + +function sh(cmd: string, args: string[], cwd?: string): string { + return execFileSync(cmd, args, { + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + cwd, + }).trim(); +} + +/** + * Create a fresh moddable test fixture: temp dir, git-init'd source, + * paired public GH repo with the fixture pushed to `main`. Returns the + * temp dir path; the caller runs `dot deploy --moddable --dir ` + * from there. + * + * Requires `gh` on PATH and `GH_TOKEN` set in the environment (the CI cell + * sets it via `${{ secrets.E2E_GH_PAT }}`). + */ +export function setupModdableFixture( + repoName: string, + sourceFixture: string, +): string { + const workDir = mkdtempSync(join(tmpdir(), "dot-e2e-moddable-")); + // Copy fixture contents into workDir as the new repo's initial state. + // `recursive: true` walks dirs; not following symlinks (default). + cpSync(sourceFixture, workDir, { recursive: true }); + + // Local git setup. Pass user.email + user.name as -c overrides so we + // don't depend on the runner's global git config (CI runners often + // have neither set, and a missing identity makes `git commit` fail). + sh("git", ["init", "-b", "main"], workDir); + sh("git", ["add", "-A"], workDir); + sh( + "git", + [ + "-c", + "user.email=e2e@playground-cli.invalid", + "-c", + "user.name=playground-cli e2e", + "commit", + "-m", + "e2e fixture", + ], + workDir, + ); + + // `gh repo create --source --push` does the origin-add + initial push in + // one round-trip, which is the same flow a Summit user would use + // (`gh repo create --public --source . --push`). Description + // makes the auto-cleanup origin obvious to anyone who stumbles on the + // repo before the cron sweeps it. + sh("gh", [ + "repo", + "create", + `${ORG}/${repoName}`, + "--public", + "--description", + "playground-cli E2E moddable test fixture (auto-cleaned)", + "--source", + workDir, + "--push", + ]); + + // Topic-tag separately. `gh repo create` doesn't accept a `--topic` + // flag today, and the cleanup cron filters by `--topic e2e-test-fixture` + // — so without this edit, the repo would never get swept and would + // accumulate indefinitely. Belt-and-braces: if `gh repo edit` fails + // (rate limit, transient), the cleanup falls back to name-prefix + // matching is NOT implemented today, so this throw is load-bearing. + sh("gh", [ + "repo", + "edit", + `${ORG}/${repoName}`, + "--add-topic", + TOPIC, + ]); + + return workDir; +}