diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48cd24c6a..fe042c7c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,6 @@ jobs: run: pnpm build - name: Save build - if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: dist @@ -116,3 +115,41 @@ jobs: env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + riff-raff: + name: Upload Riff Raff Artifacts + needs: [build, test, lint, types] + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Node + uses: ./.github/actions/setup-node-env + + - name: Fetch build + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Riff-Raff Upload + uses: guardian/actions-riff-raff@v4 + with: + roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + projectName: dotcom::commercial-bundle + configPath: ./riff-raff.yaml + contentDirectories: | + frontend-static/commercial: + - dist/riff-raff/js/commercial + commercial-bundle-path: + - dist/riff-raff/cloudformation diff --git a/.prettierignore b/.prettierignore index 38270338b..bcd094fcc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ dist .changeset README.md CHANGELOG.md +riff-raff.yaml .vscode/settings.json playwright-report/ pnpm-lock.yaml diff --git a/README.md b/README.md index 1322bee47..ddc863f74 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,11 @@ In order to do this, first run: `pnpm changeset add`, again, This will create a **Note**: Once the beta version is released, the label will be removed from the PR, so you will need to add it again if you want to release subsequent new versions. On a branch on frontend you can update the version of the bundle to the beta version and deploy to CODE to test. + +### Deploying to PROD + +Ensure your PR has a chageset and has been merged to main. This will trigger a changesets release PR, which will bump the version of the package, once this is merged the package will be published to NPM. + +To get your changes live on the site, you will need to update the `@guardian/commercial` version in the [Frontend](https://github.com/guardian/frontend) repository. You can do this by running the [bump_commercial.sh](./scripts/bump_commercial.sh) script. + +We're experimenting with direct deployments via riff-raff, when you merge to main a a riff-raff build will be created but you will need to manually deploy it in the riff-raff GUI, these deployments are currently only available behind a server side test that you'll need to opt in to. diff --git a/package.json b/package.json index 0e9fccff9..e7df3b240 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ "main": "dist/cjs/core/index.js", "module": "dist/esm/core/index.js", "scripts": { - "build": "npm-run-all clean --parallel compile:core:* build:prod build:dev", + "build": "npm-run-all clean --parallel compile:core:* build:prod build:dev build:riff-raff", "build:dev": "webpack -c webpack.config.dev.mjs", "build:prod": "webpack -c webpack.config.prod.mjs", + "build:riff-raff": "webpack -c webpack.config.riff-raff.mjs", "clean": "rm -rf dist", "compile:core:common": "tsc --project ./tsconfig.core.json --outDir ./dist/cjs --module CommonJS", "compile:core:esm": "tsc --project ./tsconfig.core.json --outDir ./dist/esm", diff --git a/riff-raff.yaml b/riff-raff.yaml new file mode 100644 index 000000000..e7be84766 --- /dev/null +++ b/riff-raff.yaml @@ -0,0 +1,21 @@ +regions: [eu-west-1] +stacks: [frontend] +allowedStages: + - CODE + - PROD +deployments: + frontend-static/commercial: + type: aws-s3 + parameters: + bucketSsmKey: /account/services/dotcom-static.bucket + cacheControl: public, max-age=315360000, immutable + prefixStack: false + publicReadAcl: false + commercial-bundle-path: + type: cloud-formation + parameters: + templateStagePaths: + CODE: code.json + PROD: prod.json + dependencies: + - frontend-static/commercial diff --git a/src/commercial.ts b/src/commercial.ts index efc6fa86c..a7a4d149a 100644 --- a/src/commercial.ts +++ b/src/commercial.ts @@ -7,11 +7,14 @@ const decideAssetsPath = () => { if (process.env.OVERRIDE_BUNDLE_PATH) { return process.env.OVERRIDE_BUNDLE_PATH; } + const assetsPath = frontendAssetsFullURL ?? page.assetsPath; return `${assetsPath}javascripts/commercial/`; }; -__webpack_public_path__ = decideAssetsPath(); +if (!process.env.RIFFRAFF_DEPLOY) { + __webpack_public_path__ = decideAssetsPath(); +} /** * Choose whether to launch Googletag or Opt Out tag (ootag) based on consent state diff --git a/webpack.config.prod.mjs b/webpack.config.prod.mjs index 4f9a801d6..f39491d59 100644 --- a/webpack.config.prod.mjs +++ b/webpack.config.prod.mjs @@ -39,6 +39,7 @@ export default merge(config, { new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.OVERRIDE_BUNDLE_PATH': JSON.stringify(false), + 'process.env.RIFFRAFF_DEPLOY': JSON.stringify(false), ...gitCommitSHA(), }), ], diff --git a/webpack.config.riff-raff.mjs b/webpack.config.riff-raff.mjs new file mode 100644 index 000000000..8b89aa550 --- /dev/null +++ b/webpack.config.riff-raff.mjs @@ -0,0 +1,119 @@ +import { execSync } from 'child_process'; +import { join } from 'path'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +import { merge } from 'webpack-merge'; +import config from './webpack.config.mjs'; + +const { DefinePlugin } = webpack; + +class GenerateCloudformation { + apply = (compiler) => { + compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => { + const entry = compilation.entrypoints.get('commercial-standalone'); + + const hashedFilePath = entry?.getFiles()[0]; + + if (!hashedFilePath) { + throw new Error( + 'Could not find hashed file for commercial-standalone', + ); + } + + const stages = ['code', 'prod']; + stages.forEach((stage) => { + /** + * This small bit of cloudformation will update the SSM parameter that frontend + * reads to get the bundle path. + */ + const cloudformation = { + Resources: { + BundlePath: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: `/frontend/${stage}/commercial.bundlePath`, + Type: 'String', + Value: hashedFilePath, + }, + }, + }, + }; + + // If we're in prod, we also want to update the dev bundle path + if (stage === 'prod') { + cloudformation.Resources.DevBundlePath = { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/frontend/dev/commercial.bundlePath', + Type: 'String', + Value: hashedFilePath, + }, + }; + } + + const output = JSON.stringify(cloudformation, null, 2); + const outputPath = join( + import.meta.dirname, + 'dist', + 'riff-raff', + 'cloudformation', + `${stage}.json`, + ); + compiler.outputFileSystem.mkdirSync( + join( + import.meta.dirname, + 'dist', + 'riff-raff', + 'cloudformation', + ), + { recursive: true }, + ); + compiler.outputFileSystem.writeFileSync(outputPath, output); + }); + }); + }; +} + +const gitCommitSHA = () => { + try { + const commitSHA = execSync('git rev-parse HEAD').toString().trim(); + return { 'process.env.COMMIT_SHA': JSON.stringify(commitSHA) }; + } catch (_) { + return {}; + } +}; + +const prefix = process.env.BUNDLE_PREFIX ?? '[chunkhash]/'; + +// eslint-disable-next-line import/no-default-export -- webpack config +export default merge(config, { + mode: 'production', + output: { + filename: `commercial/${prefix}graun.standalone.commercial.js`, + chunkFilename: `commercial/${prefix}graun.[name].commercial.js`, + path: join(import.meta.dirname, 'dist', 'riff-raff', 'js'), + publicPath: 'auto', + clean: true, + }, + devtool: 'source-map', + plugins: [ + // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- circular-dependency-plugin is not typed + new BundleAnalyzerPlugin({ + reportFilename: './commercial-bundle-analyzer-report.html', + analyzerMode: 'static', + openAnalyzer: false, + }), + new DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + 'process.env.OVERRIDE_BUNDLE_PATH': JSON.stringify(false), + 'process.env.RIFFRAFF_DEPLOY': JSON.stringify(true), + ...gitCommitSHA(), + }), + new GenerateCloudformation(), + ], + optimization: { + minimize: true, + minimizer: [new TerserPlugin()], + }, +});