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
43 changes: 38 additions & 5 deletions .depot/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: Deploy

# DOCS AT MAIN: every main push that changes what the Worker ships deploys `preview`
# (docs.iterate-dev-preview.workers.dev) and then, once that answers, `prd` (docs.iterate.workers.dev,
# which the iterate project's `docs` slug and docs.iterate.com route to), both from envs.ts. A red run
# pages.
# (docs.iterate-dev-preview.workers.dev), runs the browser specs against it (test/playwright/docs, on
# main on dev's platform, installing this commit's @iterate-com/docs from pkg.pr.new), and then, once
# they pass, deploys `prd` (docs.iterate.workers.dev, which the iterate project's `docs` slug and
# docs.iterate.com route to), both from envs.ts. A red run pages.
permissions:
contents: read

Expand Down Expand Up @@ -40,10 +41,41 @@ jobs:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
run: pnpm --dir packages/docs-app run-script deploy --env preview

# only after preview answered: a deploy that fails there never reaches production
specs:
name: Browser specs against preview
needs: preview
# six browsers at once (test/playwright.config.ts `workers`)
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{ github.sha }}
- name: Setup
uses: ./.depot/actions/setup
- name: Browser
run: pnpm --dir test exec playwright install --only-shell chromium
- name: Specs
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
PUBLISHED_PACKAGE_COMMIT: ${{ github.sha }}
WORKER_BASE_URL: https://os.iterate-dev-preview.workers.dev
DOCS_BASE_URL: https://docs.iterate-dev-preview.workers.dev
run: doppler run --project os --config preview -- pnpm spec
- name: Upload the report
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: test-results/
retention-days: 14

# only after preview passed its specs: a deploy that fails there never reaches production
prd:
name: Deploy prd
needs: preview
needs: specs
runs-on: depot-ubuntu-24.04
timeout-minutes: 20
steps:
Expand All @@ -62,6 +94,7 @@ jobs:
page:
needs:
- preview
- specs
- prd
if: always() && github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure')
runs-on: depot-ubuntu-24.04
Expand Down
38 changes: 38 additions & 0 deletions .depot/workflows/pull-request-closed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Pull request closed

# A CLOSED PULL REQUEST'S DOCS WORKER, `docs-pr<N>` (specs.yml deployed it), deleted. Its own
# workflow, so no push to an open pull request carries it as a skipped job. Depot registers the
# `closed` trigger once this file is on the default branch. By hand:
# pnpm --dir packages/docs-app delete-pull-request --pr <N>
permissions:
contents: read

on:
pull_request:
types:
- closed

concurrency:
# waits for the pull request's specs run to settle instead of deleting its Worker from under it
group: specs-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
delete:
name: Delete the pull request's Docs
runs-on: depot-ubuntu-24.04
timeout-minutes: 10
steps:
# main's code, not the pull request's: a head whose install broke still gets its Worker
# deleted, and none of its code runs with the Doppler token
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
- name: Setup
uses: ./.depot/actions/setup
- name: Delete
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
run: pnpm --dir packages/docs-app delete-pull-request --pr ${{ github.event.pull_request.number }}
Comment thread
mmkal marked this conversation as resolved.
Resolved
54 changes: 54 additions & 0 deletions .depot/workflows/specs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Specs

# A PULL REQUEST'S DOCS UNDER ITS BROWSER SPECS (test/playwright/docs): its head deployed as a Worker
# of its own on the dev/preview account (`docs-pr<N>`, envs.ts `pullRequestEnv`), served through
# fixture projects on main on dev's platform (os.iterate-dev-preview.workers.dev), each installing
# the head's @iterate-com/docs from pkg.pr.new (.github/workflows/pkg-pr-new.yml, which the specs
# wait for). The Worker is deleted when the pull request closes (pull-request-closed.yml). Main's
# specs run in deploy.yml, against `preview`, before prd.
permissions:
contents: read

on:
pull_request:

concurrency:
group: specs-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
specs:
name: Browser specs
# six browsers at once (test/playwright.config.ts `workers`)
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 30
env:
# the head pkg.pr.new published, not the merge commit a pull request's run checks out
PUBLISHED_PACKAGE_COMMIT: ${{ github.event.pull_request.head.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup
uses: ./.depot/actions/setup
- name: Browser
run: pnpm --dir test exec playwright install --only-shell chromium
- name: Deploy the pull request's Docs
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
run: pnpm --dir packages/docs-app run-script deploy --env pr${{ github.event.pull_request.number }}
- name: Specs
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
WORKER_BASE_URL: https://os.iterate-dev-preview.workers.dev
DOCS_BASE_URL: https://docs-pr${{ github.event.pull_request.number }}.iterate-dev-preview.workers.dev
run: doppler run --project os --config preview -- pnpm spec
- name: Upload the report
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: test-results/
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ dist

__pycache__/

# what a browser spec run leaves (test/playwright.config.ts)
test-results/

# ad-hoc ignored files/paths
*ignoreme*
*.log
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
[README.md](README.md) first, then the package's own README. `packages/docs/AGENTS.md` is for agents
working on a project's docs through the platform, not on this repo.

- Before pushing: `pnpm typecheck && pnpm lint && pnpm format && pnpm test`.
- Before pushing: `pnpm typecheck && pnpm lint && pnpm format && pnpm test`. The browser specs
(`pnpm spec`, `test/playwright/docs`) need a deployed Docs: `test/playwright.config.ts` says how.
- `test/helpers/` is iterate/private's spec harness, copied: keep it close to the original.
- Never hand-edit `packages/docs-app/src/routeTree.gen.ts`: `pnpm --dir packages/docs-app routes:generate`.
- `envs.ts` owns the deployments; Doppler project `_shared` holds the deploy credentials. Main
deploys itself (`.depot/workflows/deploy.yml`). Workers are never deleted.
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pnpm typecheck
pnpm lint
pnpm format
pnpm test
pnpm spec # the browser specs (test/playwright.config.ts says against what)
```

## Releases and deploys
Expand All @@ -22,9 +23,11 @@ pnpm test
- **npm**: every main commit whose packed package changed publishes
`<version>-main.<commit date>-<sha>` under the `main` and `latest` dist-tags
(`.github/workflows/npm-publish.yml`, `scripts/ci/npm-publish.ts`).
- **The app**: main deploys `preview` (docs.iterate-dev-preview.workers.dev), then `prd`
(docs.iterate.workers.dev), both in `envs.ts` (`.depot/workflows/deploy.yml`). A red run pages
#error-pulse.
- **The app**: main deploys `preview` (docs.iterate-dev-preview.workers.dev), runs the browser
specs against it, then deploys `prd` (docs.iterate.workers.dev), all in `envs.ts`
(`.depot/workflows/deploy.yml`). A red run pages #error-pulse.
- **A pull request** gets its own Worker, `docs-pr<N>`, and the browser specs against it, on main on
dev's platform (`.depot/workflows/specs.yml`); it's deleted when the pull request closes.

CI runs on [Depot](https://depot.dev) (`.depot/workflows/`); its one secret is the org's
`DOPPLER_TOKEN`. pkg.pr.new and npm run on GitHub Actions, which their GitHub App and trusted
Expand Down
20 changes: 18 additions & 2 deletions envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,27 @@ export const docsEnvs = {
},
} satisfies Record<string, DocsEnv>;

/** A PULL REQUEST'S OWN DOCS, `pr<N>`: `docs-pr<N>` on the dev/preview account, linked as `preview`
* is, which its CI deploys and runs the browser specs against (.depot/workflows/specs.yml) and
* deletes once it closes (.depot/workflows/pull-request-closed.yml). It installs the pull
* request's own @iterate-com/docs build (vite.config.ts `pkgPrNewRef`). Undefined for any other
* name. */
export function pullRequestEnv(name: string): DocsEnv | undefined {
const number = /^pr(\d+)$/.exec(name)?.[1];
if (!number) return undefined;
const workerName = `docs-pr${number}`;
return {
...docsEnvs.preview,
workerName,
baseUrl: `https://${workerName}.iterate-dev-preview.workers.dev`,
};
}

export function getEnv(name: string): DocsEnv {
const env = (docsEnvs as Record<string, DocsEnv>)[name];
const env = (docsEnvs as Record<string, DocsEnv>)[name] || pullRequestEnv(name);
if (!env)
throw new Error(
`unknown env ${JSON.stringify(name)}; known envs: ${Object.keys(docsEnvs).join(", ")}`,
`unknown env ${JSON.stringify(name)}; known envs: ${Object.keys(docsEnvs).join(", ")}, pr<N>`,
);
return env;
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"lint": "oxlint . --deny-warnings --report-unused-disable-directives-severity error",
"format": "oxfmt",
"format:check": "oxfmt --check",
"test": "pnpm -r --parallel test && vitest run --root scripts"
"test": "pnpm -r --parallel test && vitest run --root scripts",
"spec": "pnpm --dir test exec playwright test"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
10 changes: 9 additions & 1 deletion packages/docs-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ Under subdomains the tunnel's host is an origin of its own, and plain `pnpm dev`
dev signs in against prd's platform; a gitignored `.dev.vars` names another
(`ITERATE_APP__URLS__OS=http://localhost:8788`).

Deploy: main deploys `preview`, then `prd` (`.depot/workflows/deploy.yml`). By hand,
The browser specs are the root's `test/playwright/docs`: CI runs them against each pull request's
own Worker, and against `preview` before main deploys prd. By hand, against any Docs Worker:

```sh
doppler run --project os --config preview -- env WORKER_BASE_URL=https://os.iterate-dev-preview.workers.dev \
DOCS_BASE_URL=https://docs-pr12.iterate-dev-preview.workers.dev pnpm spec
```

Deploy: main deploys `preview`, then `prd` once the specs pass (`.depot/workflows/deploy.yml`). By hand,
`pnpm --dir packages/docs-app run deploy --env prd`. Docs has no secrets, so it deploys with the
Cloudflare token in Doppler's `_shared` project ([scripts/app.ts](scripts/app.ts)).
3 changes: 2 additions & 1 deletion packages/docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"typecheck": "pnpm routes:check && tsc --noEmit",
"deploy": "node scripts/app.ts deploy",
"routes:generate": "node scripts/app.ts generate-route-tree",
"routes:check": "node scripts/app.ts generate-route-tree --check"
"routes:check": "node scripts/app.ts generate-route-tree --check",
"delete-pull-request": "node scripts/app.ts delete-pull-request"
},
"dependencies": {
"@atomic-editor/editor": "0.6.2",
Expand Down
44 changes: 39 additions & 5 deletions packages/docs-app/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* The Docs app's command line, behind its package scripts:
*
* deploy --env <name> vite build for that env → wrangler deploy → /healthz smoke
* delete-pull-request --pr <n>
* delete a closed pull request's own Worker, `docs-pr<n>` (envs.ts
* `pullRequestEnv`); no other Worker is ever deleted
* generate-route-tree regenerate src/routeTree.gen.ts outside `vite dev`/`vite build`;
* `--check` fails (and restores the file) when the checked-in tree is stale
*
Expand All @@ -16,7 +19,7 @@ import { fileURLToPath } from "node:url";
import { Generator, getConfig } from "@tanstack/router-generator";
import { createCli, t } from "trpc-cli";
import { z } from "zod";
import { getEnv } from "../../../envs.ts";
import { getEnv, pullRequestEnv } from "../../../envs.ts";

const root = fileURLToPath(new URL("..", import.meta.url));

Expand All @@ -39,10 +42,7 @@ async function deploy(options: { env: string }) {
console.log(
`Deploying Docs to ${options.env} (worker ${env.workerName}, account ${env.cloudflareAccountId})`,
);
const token = execFileSync("doppler", ["secrets", "get", "CLOUDFLARE_API_TOKEN", "--plain"], {
encoding: "utf8",
env: { ...process.env, DOPPLER_PROJECT: "_shared", DOPPLER_CONFIG: env.dopplerConfig },
}).trim();
const token = cloudflareToken(env.dopplerConfig);
rmSync(path.join(root, "dist"), { recursive: true, force: true });
run("pnpm", ["exec", "vite", "build"], { CLOUDFLARE_ENV: options.env });
const credentials = {
Expand Down Expand Up @@ -73,6 +73,37 @@ async function deploy(options: { env: string }) {
console.log(`✅ ${options.env} deployed and serving at ${env.baseUrl}`);
}

/** Deletes pull request `pr`'s own Worker, which its CI deployed (envs.ts `pullRequestEnv`). One
* already gone (a pull request whose CI never deployed) is no error. `force=true` because the
* Worker owns a Durable Object class (BrowserSession), and Cloudflare refuses to delete such a
* script without it; the class's storage goes with the Worker, as it should here. */
async function deletePullRequest(options: { pr: number }) {
const env = pullRequestEnv(`pr${options.pr}`)!;
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.cloudflareAccountId}/workers/scripts/${env.workerName}?force=true`,
{
method: "DELETE",
headers: { authorization: `Bearer ${cloudflareToken(env.dopplerConfig)}` },
signal: AbortSignal.timeout(30_000),
},
);
if (response.status === 404) return console.log(`${env.workerName} is already gone`);
if (!response.ok)
throw new Error(
`deleting ${env.workerName} answered ${response.status}: ${await response.text()}`,
);
console.log(`deleted ${env.workerName}`);
}
Comment thread
mmkal marked this conversation as resolved.
Resolved

/** The Cloudflare token every iterate app deploys with, Doppler `_shared`'s, for one of its configs.
* Doppler's CLI authenticates with DOPPLER_TOKEN in CI and the developer's login elsewhere. */
function cloudflareToken(dopplerConfig: string) {
return execFileSync("doppler", ["secrets", "get", "CLOUDFLARE_API_TOKEN", "--plain"], {
encoding: "utf8",
env: { ...process.env, DOPPLER_PROJECT: "_shared", DOPPLER_CONFIG: dopplerConfig },
}).trim();
}

/** Probes `url` until it answers 200: a new version takes a few seconds to reach every colo. */
async function smoke(url: string) {
for (let attempt = 1; attempt <= 18; attempt++) {
Expand Down Expand Up @@ -167,6 +198,9 @@ if (import.meta.main)
deploy: t.procedure
.input(z.object({ env: z.string().describe("Target environment name from envs.ts") }))
.handler(({ input }) => deploy(input)),
deletePullRequest: t.procedure
.input(z.object({ pr: z.number().int().positive().describe("The pull request's number") }))
.handler(({ input }) => deletePullRequest(input)),
generateRouteTree: t.procedure
.input(
z.object({
Expand Down
15 changes: 13 additions & 2 deletions packages/docs-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { COMPATIBILITY_DATE } from "iterate/compatibility-date";
import { defineConfig } from "vite";
import type { z } from "zod";
import type { StartAppConfig } from "./src/app/start-app-config.ts";
import { DENY_ZONES, docsEnvs, getEnv } from "../../envs.ts";
import { DENY_ZONES, docsEnvs, getEnv, pullRequestEnv } from "../../envs.ts";
import { routeTreeStyle } from "./scripts/app.ts";

export default defineConfig({
Expand Down Expand Up @@ -52,7 +52,7 @@ function workerConfig(envName: string | undefined) {
urls: { ...linked.urls, docs: linked.baseUrl },
denyZones: DENY_ZONES,
...(env?.posthogProjectKey && { posthogProjectKey: env.posthogProjectKey }),
pkgPrNewRef: "main",
pkgPrNewRef: envName && pullRequestEnv(envName) ? pullRequestCommit() : "main",
} satisfies z.input<typeof StartAppConfig>;
return {
name: env?.workerName || "docs",
Expand Down Expand Up @@ -89,3 +89,14 @@ function workerConfig(envName: string | undefined) {
...(env && { account_id: env.cloudflareAccountId, workers_dev: true }),
};
}

/** The commit whose @iterate-com/docs a pull request's Docs installs (`PUBLISHED_PACKAGE_COMMIT`,
* its head, which pkg.pr.new publishes; .depot/workflows/specs.yml sets it). */
function pullRequestCommit() {
const commit = process.env.PUBLISHED_PACKAGE_COMMIT;
if (!commit || !/^[0-9a-f]{40}$/.test(commit))
throw new Error(
`a pull request's Docs installs its head's build: PUBLISHED_PACKAGE_COMMIT is ${JSON.stringify(commit)}, not a commit's 40 hex digits`,
);
return commit;
}
Loading
Loading