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
52 changes: 29 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down Expand Up @@ -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())
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 2 additions & 8 deletions bin/print-ci-env-public-url.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "/";
}
Expand Down
17 changes: 7 additions & 10 deletions bin/print-ci-env-stage.cjs
Original file line number Diff line number Diff line change
@@ -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}`);
26 changes: 26 additions & 0 deletions bin/print-ci-stages.cjs
Original file line number Diff line number Diff line change
@@ -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 })}`);
37 changes: 19 additions & 18 deletions deployment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("/", "")
Expand Down
Loading