diff --git a/.gitattributes b/.gitattributes index b17d448d5..a455f6f5c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ *.res linguist-language=ReScript *.resi linguist-language=ReScript -/public/playground-bundles/** binary linguist-vendored +/apps/docs/public/playground-bundles/** binary linguist-vendored diff --git a/.github/scripts/deployment-environment.sh b/.github/scripts/deployment-environment.sh index 41c3f1715..79d173324 100644 --- a/.github/scripts/deployment-environment.sh +++ b/.github/scripts/deployment-environment.sh @@ -1,10 +1,13 @@ #!/usr/bin/env bash set -euo pipefail -# Export the canonical production URL or the branch-specific Pages preview URL -# for later GitHub Actions steps. -if [[ "$RAW_BRANCH" == "master" ]]; then +# Pages selects production by branch. Manual previews of master must use a +# preview alias, while a manual production deploy must explicitly target master. +if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "$RAW_BRANCH" == "master" ]] || + [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" && "${DEPLOY_ENVIRONMENT:-}" == "production" ]]; then { + echo "SAFE_BRANCH=master" + echo "DOCS_DEPLOYMENT_ENVIRONMENT=production" echo "VITE_DEPLOYMENT_URL=" echo "DOCS_DEPLOYMENT_URL=https://rescript-lang.org" } >> "$GITHUB_ENV" @@ -19,6 +22,7 @@ else SAFE_BRANCH="${BRANCH_PREFIX}-${BRANCH_DIGEST}" { echo "SAFE_BRANCH=$SAFE_BRANCH" + echo "DOCS_DEPLOYMENT_ENVIRONMENT=preview" echo "VITE_DEPLOYMENT_URL=https://${SAFE_BRANCH}.rescript-lang.pages.dev" echo "DOCS_DEPLOYMENT_URL=https://${SAFE_BRANCH}.rescript-lang.pages.dev" } >> "$GITHUB_ENV" diff --git a/.github/scripts/lighthouse-environment.sh b/.github/scripts/lighthouse-environment.sh index 3bf5320f7..701c1d9c2 100644 --- a/.github/scripts/lighthouse-environment.sh +++ b/.github/scripts/lighthouse-environment.sh @@ -7,12 +7,14 @@ REPOSITORY_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then RAW_TARGET_BRANCH=$(gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.base.ref') + TARGET_ENVIRONMENT="" else RAW_TARGET_BRANCH="$RAW_BRANCH" + TARGET_ENVIRONMENT="${DOCS_DEPLOYMENT_ENVIRONMENT:-}" fi -ARTIFACT_NAME=$(node "$REPOSITORY_ROOT/apps/docs/scripts/lighthouse-report.mjs" artifact-name "$RAW_BRANCH") -TARGET_ARTIFACT_NAME=$(node "$REPOSITORY_ROOT/apps/docs/scripts/lighthouse-report.mjs" artifact-name "$RAW_TARGET_BRANCH") +ARTIFACT_NAME=$(node "$REPOSITORY_ROOT/apps/docs/scripts/lighthouse-report.mjs" artifact-name "$RAW_BRANCH" "${DOCS_DEPLOYMENT_ENVIRONMENT:-}") +TARGET_ARTIFACT_NAME=$(node "$REPOSITORY_ROOT/apps/docs/scripts/lighthouse-report.mjs" artifact-name "$RAW_TARGET_BRANCH" "$TARGET_ENVIRONMENT") { echo "LIGHTHOUSE_ARTIFACT_NAME=$ARTIFACT_NAME" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9bd8bc6e2..7bbe3ff2c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,11 @@ on: workflow_dispatch: inputs: environment: - description: "Choose an environment to deploy to: " + description: "Deployment environment for docs and guide" + type: choice + options: + - preview + - production required: true default: "preview" pull_request: @@ -41,6 +45,7 @@ jobs: - name: Set VITE_DEPLOYMENT_URL env: RAW_BRANCH: ${{ github.head_ref || github.ref_name }} + DEPLOY_ENVIRONMENT: ${{ inputs.environment }} shell: bash run: bash .github/scripts/deployment-environment.sh - name: Build @@ -93,6 +98,7 @@ jobs: - name: Set deployment URL env: RAW_BRANCH: ${{ github.head_ref || github.ref_name }} + DEPLOY_ENVIRONMENT: ${{ inputs.environment }} shell: bash run: bash .github/scripts/deployment-environment.sh - name: Set Lighthouse environment @@ -250,6 +256,7 @@ jobs: - name: Set deployment URL env: RAW_BRANCH: ${{ github.head_ref || github.ref_name }} + DEPLOY_ENVIRONMENT: ${{ inputs.environment }} shell: bash run: bash .github/scripts/deployment-environment.sh - name: Cypress E2E tests diff --git a/.gitignore b/.gitignore index eed8655e7..86bfbc802 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ apps/docs/public/blog/feed.xml node_modules/ -.next/ out/ apps/docs/out/ index_data/*.json @@ -28,15 +27,8 @@ apps/docs/lib/ apps/guide/lib/ packages/*/lib/ -.vercel - -apps/docs/src/**/*.mjs apps/docs/src/**/*.jsx -apps/guide/src/**/*.mjs -apps/guide/src/**/*.jsx -packages/*/src/**/*.mjs packages/*/src/**/*.jsx -apps/docs/scripts/generate_*.mjs apps/docs/scripts/generate_*.jsx apps/docs/scripts/LogAlgoliaEnvStatus.jsx apps/docs/scripts/__tests__/**/*.jsx @@ -48,38 +40,23 @@ apps/docs/public/llms/react/**/llm*.txt apps/docs/markdown-pages/docs/**/**/llms.mdx apps/docs/public/playground-bundles/ -!apps/docs/public/_redirects -dist build -apps/docs/dist apps/docs/build -apps/guide/dist apps/guide/build .react-router apps/docs/.react-router apps/guide/.react-router -mdx-manifest.json -apps/docs/mdx-manifest.json -apps/docs/app/**/*.mjs apps/docs/app/**/*.jsx -apps/guide/app/**/*.mjs apps/guide/app/**/*.jsx !apps/guide/app/root.jsx !apps/guide/app/routes.jsx -apps/docs/functions/**/*.mjs apps/docs/functions/**/*.jsx -apps/docs/__tests__/**/*.mjs apps/docs/__tests__/**/*.jsx -apps/docs/test-utils/**/*.mjs apps/docs/test-utils/**/*.jsx -apps/guide/__tests__/**/*.mjs apps/guide/__tests__/**/*.jsx -apps/docs/e2e/**/*.mjs apps/docs/e2e/**/*.jsx -!_shims.mjs -!_shims.jsx # Yarn .yarn/* @@ -101,9 +78,6 @@ apps/docs/_scripts .env.local apps/docs/.env.local -# Local git worktrees -.worktrees - # Vitest screenshots !apps/docs/__tests__/**/__screenshots__/**/* .vitest-attachments diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 85a79c701..aade3da38 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,15 +3,11 @@ "printWidth": 80, "sortPackageJson": false, "ignorePatterns": [ - "!_shims.mjs", - "!apps/docs/public/_redirects", ".DS_Store", ".worktrees/", - ".next/", ".react-router", "*.swo", "*.swp", - "apps/docs/app/**/*.mjs", "apps/guide/app/**/*.jsx", "build", "data/api/**/*.json", @@ -21,22 +17,15 @@ "apps/docs/out/", "apps/docs/public/playground-bundles/", "apps/docs/public/blog/feed.xml", - "_tempFile.cmi", - "_tempFile.cmj", - "_tempFile.cmt", - "_tempFile.res", "temp", ".bsb.lock", ".merlin", "lib/", "apps/docs/lib/", - ".vercel", - "apps/docs/src/**/*.mjs", - "apps/docs/scripts/generate_*.mjs", "apps/docs/public/llms/manual/**/llm*.txt", "apps/docs/public/llms/react/**/llm*.txt", "apps/docs/markdown-pages/docs/**/**/llms.mdx", "apps/docs/markdown-pages/docs/manual/installation.mdx", - ".yarn/releases/yarn-4.12.0.cjs" + ".yarn/releases/*.cjs" ] } diff --git a/Makefile b/Makefile index 11380d35a..7079af3a3 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,4 @@ clean: .DEFAULT_GOAL := build -.PHONY: clean test +.PHONY: build dev clean test diff --git a/apps/docs/scripts/__tests__/LighthouseFixtures.res b/apps/docs/scripts/__tests__/LighthouseFixtures.res index e0e171fad..4c2f194f4 100644 --- a/apps/docs/scripts/__tests__/LighthouseFixtures.res +++ b/apps/docs/scripts/__tests__/LighthouseFixtures.res @@ -27,7 +27,7 @@ type commentInput = { } @module("../lighthouse-report.mjs") -external artifactName: string => string = "lighthouseArtifactName" +external artifactName: (string, ~environment: string=?) => string = "lighthouseArtifactName" @module("../lighthouse-report.mjs") external median: array => float = "median" @module("../lighthouse-report.mjs") external createBaseline: baselineInput => baseline = "createBaseline" diff --git a/apps/docs/scripts/__tests__/LighthouseReport_.test.res b/apps/docs/scripts/__tests__/LighthouseReport_.test.res index 5f4a7ab43..1ef51a79b 100644 --- a/apps/docs/scripts/__tests__/LighthouseReport_.test.res +++ b/apps/docs/scripts/__tests__/LighthouseReport_.test.res @@ -25,6 +25,25 @@ test("artifact-name CLI prints the same key used by the report helper", async () expect(result.stdout)->toBe(`${artifactName("perf/fonts")}\n`) }) +for_([ + ("master", "preview"), + ("perf/homepage", "production"), +])("artifact-name CLI separates %s baselines in %s", async ((branch, deploymentEnvironment)) => { + let state = await workspace() + let result = run(state, ["artifact-name", branch, deploymentEnvironment], ()) + expectSuccess(result) + let artifact = artifactName(branch, ~environment=deploymentEnvironment) + expect(result.stdout)->toBe(`${artifact}\n`) + expect(artifact != artifactName(branch))->toBe(true) +}) + +test("artifact-name CLI rejects an invalid environment", async () => { + let state = await workspace() + let result = run(state, ["artifact-name", "master", "staging"], ()) + expectStatus(result, 1) + expect(result.stderr)->toContain("Invalid Lighthouse deployment environment: staging") +}) + for_([[], [""]])("artifact-name CLI rejects missing or empty branch %j", async arguments => { let state = await workspace() let result = run(state, ["artifact-name", ...arguments], ()) diff --git a/apps/docs/scripts/__tests__/LighthouseWorkflowFixtures.res b/apps/docs/scripts/__tests__/LighthouseWorkflowFixtures.res index 4853409d8..50dd575e0 100644 --- a/apps/docs/scripts/__tests__/LighthouseWorkflowFixtures.res +++ b/apps/docs/scripts/__tests__/LighthouseWorkflowFixtures.res @@ -62,6 +62,8 @@ let fixture = async (~overrides=[], ()) => { ("CALLS", join([directory, "calls.jsonl"])), ("GITHUB_ENV", join([directory, "environment"])), ("GITHUB_EVENT_NAME", "pull_request"), + ("DEPLOY_ENVIRONMENT", ""), + ("DOCS_DEPLOYMENT_ENVIRONMENT", ""), ("GITHUB_REPOSITORY", "owner/site"), ("PR_NUMBER", "1355"), ("RAW_BRANCH", "Perf/Homepage"), diff --git a/apps/docs/scripts/__tests__/LighthouseWorkflow_.test.res b/apps/docs/scripts/__tests__/LighthouseWorkflow_.test.res index 749da9848..7c8ec1bbf 100644 --- a/apps/docs/scripts/__tests__/LighthouseWorkflow_.test.res +++ b/apps/docs/scripts/__tests__/LighthouseWorkflow_.test.res @@ -28,23 +28,74 @@ test("production uses its own baseline without a PR lookup", async () => { expect(exists(join([state.directory, "calls.jsonl"])))->toBe(false) }) +test("preview PR audits compare against the target branch's default environment", async () => { + let state = await Workflow.fixture( + ~overrides=[("GH_RESPONSE", "master\n"), ("DOCS_DEPLOYMENT_ENVIRONMENT", "preview")], + (), + ) + expectSuccess(Workflow.run("lighthouse-environment.sh", state)) + let environment = await Workflow.environmentContents(state) + expect(environment)->toContain( + `LIGHTHOUSE_TARGET_ARTIFACT_NAME=${LighthouseFixtures.artifactName("master")}\n`, + ) + expect(environment)->toContain("LIGHTHOUSE_TARGET_BRANCH=master\n") +}) + +let productionEnvironment = "SAFE_BRANCH=master\nDOCS_DEPLOYMENT_ENVIRONMENT=production\nVITE_DEPLOYMENT_URL=\nDOCS_DEPLOYMENT_URL=https://rescript-lang.org\n" +let homepagePreviewEnvironment = "SAFE_BRANCH=perf-homepage-19ab1c0e\nDOCS_DEPLOYMENT_ENVIRONMENT=preview\nVITE_DEPLOYMENT_URL=https://perf-homepage-19ab1c0e.rescript-lang.pages.dev\nDOCS_DEPLOYMENT_URL=https://perf-homepage-19ab1c0e.rescript-lang.pages.dev\n" +let masterPreviewEnvironment = "SAFE_BRANCH=master-fc613b4d\nDOCS_DEPLOYMENT_ENVIRONMENT=preview\nVITE_DEPLOYMENT_URL=https://master-fc613b4d.rescript-lang.pages.dev\nDOCS_DEPLOYMENT_URL=https://master-fc613b4d.rescript-lang.pages.dev\n" + for_([ - ("master", "VITE_DEPLOYMENT_URL=\n", "DOCS_DEPLOYMENT_URL=https://rescript-lang.org\n"), - ( - "Perf/Homepage", - "VITE_DEPLOYMENT_URL=https://perf-homepage-19ab1c0e.rescript-lang.pages.dev\n", - "DOCS_DEPLOYMENT_URL=https://perf-homepage-19ab1c0e.rescript-lang.pages.dev\n", - ), -])("deployment environment for %s does not depend on GitHub artifact access", async (( + ("master", "preview"), + ("Perf/Homepage", "production"), +])("manual Lighthouse baselines for %s in %s stay separate from the default environment", async (( branch, - expectedViteUrl, - expectedDocsUrl, + deploymentEnvironment, )) => { - let state = await Workflow.fixture(~overrides=[("RAW_BRANCH", branch), ("GH_FAIL", "1")], ()) - expectSuccess(Workflow.run("deployment-environment.sh", state)) + let state = await Workflow.fixture( + ~overrides=[ + ("GITHUB_EVENT_NAME", "workflow_dispatch"), + ("RAW_BRANCH", branch), + ("DOCS_DEPLOYMENT_ENVIRONMENT", deploymentEnvironment), + ("GH_FAIL", "1"), + ], + (), + ) + expectSuccess(Workflow.run("lighthouse-environment.sh", state)) let environment = await Workflow.environmentContents(state) - expect(environment)->toContain(expectedViteUrl) - expect(environment)->toContain(expectedDocsUrl) + let artifact = LighthouseFixtures.artifactName(branch, ~environment=deploymentEnvironment) + expect(artifact != LighthouseFixtures.artifactName(branch))->toBe(true) + expect(environment)->toContain(`LIGHTHOUSE_ARTIFACT_NAME=${artifact}\n`) + expect(environment)->toContain(`LIGHTHOUSE_TARGET_ARTIFACT_NAME=${artifact}\n`) + expect(environment)->toContain(`LIGHTHOUSE_BRANCH=${branch}\n`) + expect(exists(join([state.directory, "calls.jsonl"])))->toBe(false) +}) + +for_([ + ("push", "master", "", productionEnvironment), + ("pull_request", "Perf/Homepage", "", homepagePreviewEnvironment), + ("pull_request", "master", "", masterPreviewEnvironment), + ("workflow_dispatch", "master", "preview", masterPreviewEnvironment), + ("workflow_dispatch", "Perf/Homepage", "preview", homepagePreviewEnvironment), + ("workflow_dispatch", "master", "production", productionEnvironment), + ("workflow_dispatch", "Perf/Homepage", "production", productionEnvironment), +])("deployment environment for %s on %s with selection '%s'", async (( + event, + branch, + selection, + expectedEnvironment, +)) => { + let state = await Workflow.fixture( + ~overrides=[ + ("GITHUB_EVENT_NAME", event), + ("RAW_BRANCH", branch), + ("DEPLOY_ENVIRONMENT", selection), + ("GH_FAIL", "1"), + ], + (), + ) + expectSuccess(Workflow.run("deployment-environment.sh", state)) + expect(await Workflow.environmentContents(state))->toBe(expectedEnvironment) expect(exists(join([state.directory, "calls.jsonl"])))->toBe(false) }) diff --git a/apps/docs/scripts/lighthouse-report.mjs b/apps/docs/scripts/lighthouse-report.mjs index fea5129cb..15d5e2150 100644 --- a/apps/docs/scripts/lighthouse-report.mjs +++ b/apps/docs/scripts/lighthouse-report.mjs @@ -17,16 +17,32 @@ const reportCategoryKeys = { seo: "seo", }; -export function lighthouseArtifactName(branch) { +export function lighthouseArtifactName(branch, environment) { + const defaultEnvironment = branch === "master" ? "production" : "preview"; + const effectiveEnvironment = environment || defaultEnvironment; + if (!["production", "preview"].includes(effectiveEnvironment)) { + throw new Error( + `Invalid Lighthouse deployment environment: ${effectiveEnvironment}`, + ); + } + // Preserve existing production-master and branch-preview keys. Alternative + // environments use a separate identity so they cannot overwrite those keys. + const isDefaultEnvironment = effectiveEnvironment === defaultEnvironment; + const identity = isDefaultEnvironment + ? branch + : `${effectiveEnvironment}\0${branch}`; + const environmentPrefix = isDefaultEnvironment + ? "" + : `${effectiveEnvironment}-`; const slug = branch .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/^-+|-+$/g, "") .replace(/-+/g, "-") .slice(0, 80); - const digest = createHash("sha256").update(branch, "utf8").digest("hex"); + const digest = createHash("sha256").update(identity, "utf8").digest("hex"); - return `homepage-lighthouse-${slug || "branch"}-${digest}`; + return `homepage-lighthouse-${environmentPrefix}${slug || "branch"}-${digest}`; } function getFiniteNumber(value, description) { @@ -224,13 +240,13 @@ async function writeComment() { ); } -async function main(command, branch) { +async function main(command, branch, environment) { if (command === "artifact-name") { if (!branch) { throw new Error("A branch name is required for artifact-name"); } - console.log(lighthouseArtifactName(branch)); + console.log(lighthouseArtifactName(branch, environment)); return; } @@ -252,5 +268,5 @@ const entryPath = process.argv[1] : ""; if (import.meta.url === entryPath) { - await main(process.argv[2], process.argv[3]); + await main(process.argv[2], process.argv[3], process.argv[4]); }