From 255f2a3983179b713551a2754dbe60db8a3eb1ed Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 17 Jan 2023 09:56:51 +0000 Subject: [PATCH] Migrate from CircleCI to GitHub actions. --- .circleci/config.yml | 204 ------------------------------------ .github/workflows/build.yml | 64 +++++++++++ bin/print-ci-env.js | 21 ++++ bin/public-url.js | 10 -- package.json | 3 +- src/e2e/app.ts | 14 ++- 6 files changed, 98 insertions(+), 218 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/build.yml create mode 100644 bin/print-ci-env.js delete mode 100644 bin/public-url.js diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index b870060a8..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,204 +0,0 @@ -version: 2.1 -orbs: - queue: eddiewebb/queue@1.6.2 - -_default_workflow: &default_workflow - context: - - "GitHub Packages Read" - - "CircleCI API" - - "AWS Web" -_defaults: &defaults - resource_class: large - working_directory: ~/repo -_docker_defaults: &docker_defaults - image: cimg/node:16.14 -_steps: - queue_until_front_of_line: &queue_until_front_of_line - # Ensures we don't deploy concurrently - # See https://github.com/eddiewebb/circleci-queue - queue/until_front_of_line: - time: "60" - restore_npm_cache: &restore_npm_cache - restore_cache: - keys: - - npm-v1-{{ .Branch }}-{{ checksum "package-lock.json" }} - - npm-v1-{{ .Branch }}- - - npm-v1- - save_npm_cache: &save_npm_cache - save_cache: - paths: - - .npm-cache - key: npm-v1-{{ .Branch }}-{{ checksum "package-lock.json" }} - set_public_url: - &set_public_url # Set PUBLIC_URL based on the deployment prefix. PUBLIC_URL is equivalent to CRA's package.json homepage setting https://create-react-app.dev/docs/advanced-configuration/ - run: - name: "Set PUBLIC_URL" - environment: - NODE_PATH: /usr/local/lib/node_modules - # Two env vars as PUBLIC_URL seems to be blank when running jest even if we set it. - command: URL=$(npm run --silent public-url); echo "export PUBLIC_URL=$URL && export E2E_PUBLIC_URL=$URL" >> $BASH_ENV - - install_aws_cli: &install_aws_cli - run: sudo apt-get update && sudo apt-get install awscli - install_dependencies: &install_dependencies - run: npm ci --cache .npm-cache && sudo npm config set @microbit-foundation:registry https://npm.pkg.github.com/microbit-foundation && sudo npm i -g @microbit-foundation/website-deploy-aws@0.3.0 @microbit-foundation/website-deploy-aws-config@0.5.0-dev.94 @microbit-foundation/circleci-npm-package-versioner@1 - install_theme: &install_theme - run: npm config set @microbit-foundation:registry https://npm.pkg.github.com/microbit-foundation && npm install --no-save @microbit-foundation/python-editor-v3-microbit@0.1.0-dev.198 - update_version: &update_version - run: npm run ci:update-version - build: &build - run: - name: Build - command: npm run ci - # https://circleci.com/orbs/registry/orb/threetreeslight/puppeteer - chrome_deps: &chrome_deps - run: - name: Install Headless Chrome dependencies - command: | - sudo apt-get install -yq \ - ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 \ - libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 \ - libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \ - libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils - serve: &serve - run: - name: Serve files for e2e tests - command: mkdir -p /tmp/app${PUBLIC_URL} && cp -r build/* /tmp/app${PUBLIC_URL} && npx serve --no-clipboard -l 3000 /tmp/app - background: true - serve_wait: &serve_wait - run: - name: Wait for e2e server - command: "curl --insecure -4 --retry 7 --retry-connrefused http://localhost:3000 1>/dev/null" - e2e: &e2e - run: - name: e2e tests - command: npm run test:e2e:headless - store_reports: &store_reports - store_artifacts: - path: reports/ - destination: reports - deploy: &deploy - run: - name: Deploy - environment: - NODE_PATH: /usr/local/lib/node_modules - command: npm run deploy - invalidate: &invalidate - run: - name: Invalidate CloudFront distribution - command: "npm run invalidate" - configure_registry_auth: &configure_registry_auth - run: - name: Configure registry auth - # One for each user as we do global and local installs. - command: echo "//npm.pkg.github.com/:_authToken=$GITHUB_TOKEN" >> ~/repo/.npmrc && sudo echo "//npm.pkg.github.com/:_authToken=$GITHUB_TOKEN" | sudo tee -a /root/.npmrc > /dev/null - -jobs: - review: - <<: *defaults - docker: - - <<: *docker_defaults - environment: - STAGE: REVIEW - REACT_APP_STAGE: REVIEW - REVIEW_CLOUDFRONT_DISTRIBUTION_ID: "E3267W09ZJHQG9" - steps: - - checkout - - *restore_npm_cache - - *configure_registry_auth - - *install_aws_cli - - *install_dependencies - - *install_theme - - *update_version - - *save_npm_cache - - *set_public_url - - *build - # Unlike other stages deploy, first so we still get deployments when e2e fails. - - *queue_until_front_of_line - - *deploy - - *invalidate - - *chrome_deps - - *serve - - *serve_wait - - *e2e - - *store_reports - - staging: - <<: *defaults - docker: - - <<: *docker_defaults - environment: - STAGE: STAGING - REACT_APP_STAGE: STAGING - # Ideally we'd do stage-python and python.microbit.org. For now just the latter. - #STAGING_CLOUDFRONT_DISTRIBUTION_ID: "E1IW78R9PB0PD1" - STAGING_CLOUDFRONT_DISTRIBUTION_ID: "E2ELTBTA2OFPY2" - steps: - - checkout - - *restore_npm_cache - - *configure_registry_auth - - *install_aws_cli - - *install_dependencies - - *install_theme - - *update_version - - *save_npm_cache - - *set_public_url - - *build - - *chrome_deps - - *serve - - *serve_wait - - *e2e - - *store_reports - - *queue_until_front_of_line - - *deploy - - *invalidate - - production: - <<: *defaults - docker: - - <<: *docker_defaults - environment: - STAGE: PRODUCTION - REACT_APP_STAGE: PRODUCTION - PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID: "E2ELTBTA2OFPY2" - steps: - - checkout - - *restore_npm_cache - - *configure_registry_auth - - *install_aws_cli - - *install_dependencies - - *install_theme - - *update_version - - *save_npm_cache - - *set_public_url - - *build - # This doesn't work for tags. Don't release more than one at once! - # - *queue_until_front_of_line - - *deploy - - *invalidate - -workflows: - version: 2 - review: - jobs: - - review: - <<: *default_workflow - filters: - branches: - ignore: main - staging: - jobs: - - staging: - <<: *default_workflow - filters: - branches: - only: main - production: - jobs: - - production: - <<: *default_workflow - filters: - tags: - only: /^v.*/ - branches: - ignore: /.*/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..99b0bd09b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,64 @@ +name: build + +on: + release: + types: [created] + push: + branches: + - "**" + +# This is conservative: ideally we'd include branch and stage in this key +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency +concurrency: deploy-python-editor-v3 + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + env: + AWS_DEFAULT_REGION: eu-west-1 + PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 + STAGING_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 + REVIEW_CLOUDFRONT_DISTRIBUTION_ID: E3267W09ZJHQG9 + + steps: + # Note: This workflow will not run on forks without modification; we're open to making steps + # that reply on our deployment infrastructure conditional. Please open an issue. + - uses: actions/checkout@v3 + - name: Configure node + uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: "npm" + registry-url: "https://npm.pkg.github.com" + - run: npm ci + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: npm install --no-save @microbit-foundation/python-editor-v3-microbit@0.1.0-dev.198 @microbit-foundation/website-deploy-aws@0.3.0 @microbit-foundation/website-deploy-aws-config@0.7.0 @microbit-foundation/circleci-npm-package-versioner@1 + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: node ./bin/print-ci-env.js >> $GITHUB_ENV + - run: npm run ci:update-version + - run: npm run ci + - run: mkdir -p /tmp/app${PUBLIC_URL} && cp -r build/* /tmp/app${PUBLIC_URL} && npx serve --no-clipboard -l 3000 /tmp/app & + if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + - run: curl --insecure -4 --retry 7 --retry-connrefused http://localhost:3000 1>/dev/null + if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + - run: npm run test:e2e:headless + if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + - name: Store reports + if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + uses: actions/upload-artifact@v3 + with: + name: reports + path: reports/ + - run: npm run deploy + env: + AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} + - run: npm run invalidate + env: + AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} diff --git a/bin/print-ci-env.js b/bin/print-ci-env.js new file mode 100644 index 000000000..aaa11876e --- /dev/null +++ b/bin/print-ci-env.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +const { bucketPrefix } = require("../deployment"); +const ref = process.env.GITHUB_REF; +const eventName = process.env.GITHUB_EVENT_NAME; + +let stage = ""; +if (ref === "refs/heads/main") { + if (eventName === "release") { + stage = "PRODUCTION"; + } else { + stage = "STAGING"; + } +} else { + stage = "REVIEW"; +} + +console.log(`STAGE=${stage}`); +console.log(`REACT_APP_STAGE=${stage}`); +// Two env vars as PUBLIC_URL seems to be blank when running jest even if we set it. +console.log(`PUBLIC_URL=/${bucketPrefix}/`); +console.log(`E2E_PUBLIC_URL=/${bucketPrefix}/`); diff --git a/bin/public-url.js b/bin/public-url.js deleted file mode 100644 index 6f913bff2..000000000 --- a/bin/public-url.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Used by the CI build to output the bucket prefix to use as the - * PUBLIC_URL environement variable. - * - * (c) 2022, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -const { bucketPrefix } = require("../deployment"); -console.log(`/${bucketPrefix}/`); diff --git a/package.json b/package.json index 10ce24b7e..c1d4cc772 100644 --- a/package.json +++ b/package.json @@ -91,8 +91,7 @@ "invalidate": "aws cloudfront create-invalidation --distribution-id $(printenv ${STAGE}_CLOUDFRONT_DISTRIBUTION_ID) --paths \"/*\"", "i18n:compile": "node bin/tidy-lang.js && formatjs compile-folder --ast lang src/messages", "i18n:convert": "node bin/tidy-lang.js && node bin/crowdin-convert.js", - "fix-licensing-headers": "node bin/fix-licensing-headers.js", - "public-url": "node bin/public-url.js" + "fix-licensing-headers": "node bin/fix-licensing-headers.js" }, "eslintConfig": { "extends": [ diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 32a2c35e6..cba967069 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -144,7 +144,7 @@ export class App { await dialog.accept(); }); - const logsPath = reportsPath + expect.getState().currentTestName + ".txt"; + const logsPath = this.reportFilename("txt"); // Clears previous output from local file. fs.writeFile(logsPath, "", (err) => { if (err) { @@ -1052,10 +1052,20 @@ export class App { async screenshot() { const page = await this.page; return page.screenshot({ - path: reportsPath + expect.getState().currentTestName + ".png", + path: this.reportFilename("png"), }); } + private reportFilename(extension: string): string { + return ( + reportsPath + + // GH actions has character restrictions + expect.getState().currentTestName.replace(/[^0-9a-zA-Z]+/g, "-") + + "." + + extension + ); + } + private async focusEditorContent(): Promise { const document = await this.document(); const content = await document.$("[data-testid='editor'] .cm-content");