Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions .github/workflows/e2e-cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand Down
81 changes: 81 additions & 0 deletions e2e/cli/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 <domain>`) 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/<repoName> 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 — <url>` 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.
},
);
});
9 changes: 9 additions & 0 deletions e2e/cli/fixtures/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-<runId>` 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.
Expand Down
116 changes: 116 additions & 0 deletions e2e/cli/helpers/moddable-setup.ts
Original file line number Diff line number Diff line change
@@ -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/<repoName>`, 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 <returned>`
* 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 <name> --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;
}
Loading