-
Notifications
You must be signed in to change notification settings - Fork 0
A rollout preview: walk the product as any role, in a tenant of its own #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
05ead47
d38a707
3d9c0ae
29de416
9863a53
74491b0
19b255b
e240212
eb44afc
4497442
0ced1ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,6 +142,31 @@ EDGE_HOST_SECRET= | |
| # Skip the migration bootstrap entirely (scripts/db-bootstrap.mjs). | ||
| # SKIP_DB_BOOTSTRAP=true | ||
|
|
||
| # ── The rollout preview ─────────────────────────────────────────────────────── | ||
|
|
||
| # Addresses that may walk the product as any role, to see what the pilot's users | ||
| # will see. Comma-separated, and OFF in every deployment. | ||
| # | ||
| # Unset, empty and whitespace are the same answer: the feature does not exist. | ||
| # /preview 404s for everybody, the eligibility branch is unreachable, and no | ||
| # session carries a preview field — a deployment that never sets this is | ||
| # indistinguishable from one where none of it was built. That is why it is a | ||
| # deployment decision and not a database row; see ADR-0019. | ||
|
Comment on lines
+150
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Determine which ADR records the deployment-decision rationale for the preview.
set -euo pipefail
fd -t f 'ADR-0019*' docs/decisions --exec sed -n '1,40p' {}
fd -t f 'ADR-0020*' docs/decisions --exec sed -n '1,60p' {}
rg -n 'ADR-0019|ADR-0020' apps/web docsRepository: Tenurework/Tenure Length of output: 24248 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ADR-0020 rationale ---'
sed -n '35,75p' docs/decisions/ADR-0020-the-rollout-preview.md
printf '%s\n' '--- affected documentation ---'
sed -n '140,160p' apps/web/.env.example
sed -n '238,252p' docs/RUNBOOK.md
printf '%s\n' '--- exact references ---'
rg -n -C 2 'ADR-0019|ADR-0020' apps/web/.env.example docs/RUNBOOK.md apps/web/scripts/seed-preview-world.mjsRepository: Tenurework/Tenure Length of output: 6573 Update the ADR references. Use ADR-0020 in 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| # | ||
| # The address does NOT need to be on the restricted roster, and is deliberately | ||
| # not put on it: it is admitted through a separate door that never reads the | ||
| # registry and never writes to it. Every admission logs loudly. | ||
| # | ||
| # Which also means this variable is NOT optional for the preview to work. The | ||
| # seeder writes no RestrictedIdentity row, so with this unset the address is | ||
| # refused at sign-in like any other stranger — including by | ||
| # .github/workflows/master-preview-access.yml, whose seed step refuses rather | ||
| # than leaving a password behind for an account the gate will turn away. | ||
| # | ||
| # Build the world it previews with: | ||
| # MASTER_ACCESS_EMAILS=… node scripts/seed-preview-world.mjs | ||
| # MASTER_ACCESS_EMAILS= | ||
|
|
||
| # ── Tenant configuration packs ──────────────────────────────────────────────── | ||
|
|
||
| # Where to read this tenant's configuration packs from — the institutional | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { expect, test } from "@playwright/test" | ||
|
|
||
| /** | ||
| * The deployment where MASTER_ACCESS_EMAILS is not set. | ||
| * | ||
| * This is the pilot's configuration, and the requirement is stronger than "the | ||
| * preview is off": it must be indistinguishable from a build where none of this | ||
| * was written. The unit tests prove the branch is unreachable; this proves the | ||
| * SERVER agrees, which is the only version of the claim that survives somebody | ||
| * typing the URL. | ||
| * | ||
| * Run against a server started WITHOUT the variable: | ||
| * | ||
| * PREVIEW_EXPECT_DISABLED=1 npx playwright test e2e/preview-disabled.spec.ts | ||
| */ | ||
|
|
||
| test.skip( | ||
| process.env.PREVIEW_EXPECT_DISABLED !== "1", | ||
| "Only meaningful against a server started with MASTER_ACCESS_EMAILS unset.", | ||
| ) | ||
|
|
||
| const PREVIEW_ADDRESS = "satvik@tenurework.com" | ||
|
|
||
| test.describe("with the allowlist unset", () => { | ||
| test("the chooser route does not exist for anybody", async ({ page }) => { | ||
| // Signed in as the real tenant's Director — the widest-privileged account | ||
| // there is. Even they get a 404, because the route is not gated on a role; | ||
| // it is gated on a deployment decision that was not taken. | ||
| await page.context().clearCookies() | ||
| await page.goto("/signin") | ||
| const form = page.getByRole("region", { name: "Pilot access" }) | ||
| await form.getByLabel("Email address").fill("director@tenure.demo") | ||
| const passphrase = process.env.DEV_LOGIN_PASSPHRASE | ||
| if (passphrase) await form.getByLabel("Access passphrase").fill(passphrase) | ||
| await form.getByRole("button", { name: "Sign in" }).click() | ||
| // Wherever their WORKSPACE puts them, which for this account is `/admin`. | ||
| // This waited for `/dashboard` and hung for the full timeout: ADR-0019 makes | ||
| // the landing path a function of role, and an OSE Director's is the console | ||
| // — `/signin` sends them to `/workspace`, which redirects. `preview.spec.ts` | ||
| // in this same change already encodes that (`"OSE Director": /\/admin/`), | ||
| // so the two specs disagreed about the product and only one of them ran. | ||
| // | ||
| // Left as an alternation rather than pinned to `/admin`, because the landing | ||
| // is not what is being tested here: the assertion is the 404 below, and this | ||
| // line only has to establish that the widest-privileged account is signed in | ||
| // before it asks for the route. | ||
| await page.waitForURL(/\/admin|\/dashboard|\/orgs/) | ||
|
|
||
| const response = await page.goto("/preview") | ||
| expect(response?.status()).toBe(404) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }) | ||
|
|
||
| test("the preview address has no standing of its own", async ({ page }) => { | ||
| await page.context().clearCookies() | ||
| await page.goto("/signin") | ||
| const form = page.getByRole("region", { name: "Pilot access" }) | ||
| await form.getByLabel("Email address").fill(PREVIEW_ADDRESS) | ||
| const passphrase = process.env.DEV_LOGIN_PASSPHRASE | ||
| if (passphrase) await form.getByLabel("Access passphrase").fill(passphrase) | ||
| await form.getByRole("button", { name: "Sign in" }).click() | ||
|
|
||
| // It is an ordinary account holding no membership and no seat, so the | ||
| // entitlement gate in `(app)/layout.tsx` sends it where it sends anybody | ||
| // else in that state. No chooser, no persona, no badge. | ||
| await page.waitForURL(/\/access-pending|\/signin/) | ||
| expect(page.url()).not.toContain("/preview") | ||
| expect(page.url()).not.toContain("/dashboard") | ||
| await page.setViewportSize({ width: 1280, height: 800 }) | ||
| await page.screenshot({ path: "test-results/preview-18-disabled-no-standing.png" }) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Revoke existing access before seeding.
This ordering protects only a newly created Cognito user. On the existing-user path,
AdminGetUserselectsACTION=resetbut does not invalidate the current permanent password. If the seeder refuses the email or the seed task fails, that old password remains usable while the failure summary says no usable account was left behind. Disable or delete the existing user before seeding, then enable it only after the new password and postconditions succeed.🤖 Prompt for AI Agents