diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bb004e8a..379a32c9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,18 +18,34 @@ env: THEME_VERSION: 0.4.0 jobs: + stages: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + matrix: ${{ steps.stages.outputs.matrix }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - id: stages + run: node ./bin/print-ci-stages.cjs >> "$GITHUB_OUTPUT" + build: + needs: stages + name: build (${{ matrix.stage }}) + strategy: + # Each stage deploys only if its own checks pass. + fail-fast: false + matrix: ${{ fromJSON(needs.stages.outputs.matrix) }} timeout-minutes: 20 runs-on: ubuntu-latest permissions: contents: read packages: read - # Review deploys assume an AWS role through GitHub OIDC. + # Deploys assume an AWS role through GitHub OIDC. id-token: write env: + STAGE: ${{ matrix.stage }} AWS_DEFAULT_REGION: eu-west-1 - PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 - STAGING_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 VITE_FOUNDATION_BUILD: ${{ github.repository_owner == 'microbit-foundation' }} # This feature is also controlled by a client-side feature flag that is disabled by default for the moment FEATURE_PWA: ${{ github.repository_owner == 'microbit-foundation' }} @@ -59,43 +75,33 @@ jobs: VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} # Uploads source maps to Sentry. Review builds don't report to Sentry. SENTRY_AUTH_TOKEN: ${{ env.STAGE != 'REVIEW' && secrets.SENTRY_AUTH_TOKEN || '' }} + # One stage per ref runs Playwright (bin/print-ci-stages.cjs). - name: Run Playwright tests - if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + if: matrix.playwright timeout-minutes: 10 uses: docker://mcr.microsoft.com/playwright:v1.64.0-noble with: args: npx playwright test - name: Store reports - if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure() + if: matrix.playwright && failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: playwright-report path: playwright-report/ retention-days: 3 - # Review deploys even when tests fail. - # Its bucket and distribution are in microbit-foundation/microbit-org's - # apps/python, which also defines this role. + # Review deploys even when tests fail. The buckets, distributions and + # roles are in microbit-foundation/microbit-org's apps/python. Only main + # and release tags can assume the role for staging, beta and production. - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 - if: ${{ !cancelled() && github.repository_owner == 'microbit-foundation' && env.STAGE == 'REVIEW' }} + if: github.repository_owner == 'microbit-foundation' && (env.STAGE == 'REVIEW' && !cancelled() || success()) with: - role-to-assume: arn:aws:iam::700731169244:role/github-actions-deploy-python-content + role-to-assume: arn:aws:iam::700731169244:role/github-actions-deploy-python-content${{ env.STAGE != 'REVIEW' && '-release' || '' }} aws-region: eu-west-1 - run: npm run deploy - if: ${{ !cancelled() && github.repository_owner == 'microbit-foundation' && env.STAGE == 'REVIEW' }} + if: github.repository_owner == 'microbit-foundation' && (env.STAGE == 'REVIEW' && !cancelled() || success()) env: # Set to 1 for one deploy after changing params in deployment.cjs, # as unchanged files are otherwise skipped. WEBSITE_DEPLOY_FORCE: 0 - run: npm run invalidate - if: ${{ !cancelled() && github.repository_owner == 'microbit-foundation' && env.STAGE == 'REVIEW' }} - - run: npm run deploy - if: github.repository_owner == 'microbit-foundation' && env.STAGE != 'REVIEW' && success() - env: - AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} - WEBSITE_DEPLOY_FORCE: 0 - - run: npm run invalidate - if: github.repository_owner == 'microbit-foundation' && env.STAGE != 'REVIEW' && success() - env: - AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} + if: github.repository_owner == 'microbit-foundation' && (env.STAGE == 'REVIEW' && !cancelled() || success()) diff --git a/README.md b/README.md index bf742c4bd..74d30a24e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Most users should use the supported Foundation deployment at https://python.micr The editor is deployed by [GitHub actions](https://github.com/microbit-foundation/python-editor-v3/actions). -The `main` branch is deployed to https://python.microbit.org/v/beta on each push. +Releases (`v3.x.y` tags) are deployed to https://python.microbit.org/v/3. The `main` branch is deployed to https://python.microbit.org/v/beta on each push, and to https://stage-python.microbit.org/v/3 for internal testing. Other branches (e.g. for PRs) are deployed to https://review-python.microbit.org/{branch}. Special characters in the branch name are replaced by hyphens. Deployments will not run in forks. diff --git a/bin/print-ci-env-public-url.cjs b/bin/print-ci-env-public-url.cjs index 8540d4e79..4b513b96c 100644 --- a/bin/print-ci-env-public-url.cjs +++ b/bin/print-ci-env-public-url.cjs @@ -2,16 +2,10 @@ let baseUrl; if (process.env.GITHUB_REPOSITORY_OWNER === "microbit-foundation") { // STAGE must be defined before this is imported - const { bucketPrefix } = require("../deployment.cjs"); + const { bucketPrefix, hostname } = require("../deployment.cjs"); baseUrl = `/${bucketPrefix}/`; - - const host = - process.env.STAGE === "REVIEW" - ? "review-python.microbit.org" - : "python.microbit.org"; - const fullUrl = `https://${host}${baseUrl}`; // Used by index.html for og:image and the Crowdin in-context translation exit URL. - console.log(`VITE_FULL_URL=${fullUrl}`); + console.log(`VITE_FULL_URL=https://${hostname}${baseUrl}`); } else { baseUrl = "/"; } diff --git a/bin/print-ci-env-stage.cjs b/bin/print-ci-env-stage.cjs index 8afa27508..f9efdb0bc 100644 --- a/bin/print-ci-env-stage.cjs +++ b/bin/print-ci-env-stage.cjs @@ -1,13 +1,10 @@ #!/usr/bin/env node -const ref = process.env.GITHUB_REF; -let stage; -if (ref === "refs/heads/main") { - stage = "STAGING"; -} else if (ref.startsWith("refs/tags/v")) { - stage = "PRODUCTION"; -} else { - stage = "REVIEW"; +// STAGE is the deployment stage, set by the build matrix +// (bin/print-ci-stages.cjs). Beta is main's build for public use, so it +// behaves as staging: same simulator, analytics and flags. +const stage = process.env.STAGE; +if (!stage) { + throw new Error("STAGE must be set by the build matrix"); } -console.log(`STAGE=${stage}`); -console.log(`VITE_STAGE=${stage}`); +console.log(`VITE_STAGE=${stage === "BETA" ? "STAGING" : stage}`); diff --git a/bin/print-ci-stages.cjs b/bin/print-ci-stages.cjs new file mode 100644 index 000000000..3277a9d63 --- /dev/null +++ b/bin/print-ci-stages.cjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ + +// Prints the build matrix of deployment stages for GITHUB_OUTPUT. Each stage +// is its own build, as the base path differs. main builds staging +// (stage-python.microbit.org/v/3/) and beta (python.microbit.org/v/beta/). +const ref = process.env.GITHUB_REF; +let stages; +if (ref === "refs/heads/main") { + stages = ["STAGING", "BETA"]; +} else if (ref.startsWith("refs/tags/v")) { + stages = ["PRODUCTION"]; +} else { + stages = ["REVIEW"]; +} + +// Playwright runs once per ref, before release. +const include = stages.map((stage) => ({ + stage, + playwright: stage === "REVIEW" || stage === "STAGING", +})); +console.log(`matrix=${JSON.stringify({ include })}`); diff --git a/deployment.cjs b/deployment.cjs index 7d2f3adbb..10593d65a 100644 --- a/deployment.cjs +++ b/deployment.cjs @@ -7,40 +7,41 @@ const { createDeploymentDetailsFromOptions, } = require("@microbit/website-deploy-s3"); -const review = process.env.STAGE === "REVIEW"; - +// The buckets and their distributions are defined in +// microbit-foundation/microbit-org (apps/python), which deploys the +// versioner at the root of the staging and production buckets. const { s3Config } = createDeploymentDetailsFromOptions({ production: { - bucket: "python-editor-v3.microbit.org", + bucket: "python-microbit-org-production", + hostname: "python.microbit.org", mode: "major", allowPrerelease: true, }, - staging: { - bucket: "stage-python-editor-v3.microbit.org", + beta: { + bucket: "python-microbit-org-production", + hostname: "python.microbit.org", prefix: "v/beta", }, + staging: { + bucket: "python-microbit-org-staging", + hostname: "stage-python.microbit.org", + prefix: "v/3", + }, review: { bucket: "python-microbit-org-review", + hostname: "review-python.microbit.org", mode: "branch-prefix", }, }); module.exports = { ...s3Config, region: "eu-west-1", + // The buckets are private (BucketOwnerEnforced), so objects take no ACL. + acl: null, + findDistribution: true, removeNonexistentObjects: true, - redirects: [], - // Review is a private bucket behind CloudFront, defined in - // microbit-foundation/microbit-org's apps/python. Staging and production - // are still public website buckets. - ...(review - ? { - acl: null, - findDistribution: true, - // Open or cached pages may still ask for the previous build's - // hashed files. - staleObjectsPatterns: ["assets/**", "workers/**"], - } - : {}), + // Open or cached pages may still ask for the previous build's hashed files. + staleObjectsPatterns: ["assets/**", "workers/**"], params: { "**/**.html": { CacheControl: "public, max-age=0, must-revalidate", diff --git a/vite.config.ts b/vite.config.ts index 4e1166ea7..00bfd95e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,6 +40,7 @@ const pwaCacheId = // v3 vs beta should have distinct caches // for the moment we do this for all review stages too but likely that's too much over time process.env.STAGE === "PRODUCTION" || + process.env.STAGE === "BETA" || process.env.STAGE === "STAGING" || process.env.STAGE === "REVIEW" ? process.env.BASE_URL.replaceAll("/", "")