Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy with riff-raff #1693

Merged
merged 9 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
39 changes: 38 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dist
.changeset
README.md
CHANGELOG.md
riff-raff.yaml
.vscode/settings.json
playwright-report/
pnpm-lock.yaml
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment on lines +20 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, the build:prod script sets process.env.RIFFRAFF_DEPLOY to false to exclude Riff-Raff-specific code from the production build.
The build:riff-raff script sets process.env.RIFFRAFF_DEPLOY to true and includes additional steps for Riff-Raff deployment. Could you please clarify if this understanding is correct? which specific scenarios where each build would be used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env.RIFFRAFF_DEPLOY is set by the webpack config used by each build task. The comment below on DefinePlugin might help.

"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",
Expand Down
21 changes: 21 additions & 0 deletions riff-raff.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion src/commercial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Comment on lines +15 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, this code sets __webpack_public_path__ at runtime to determine the base path for assets. If process.env.RIFFRAFF_DEPLOY is false, the public path is set to the CDN (https://assets.guim.co.uk/javascripts/commercial/). If process.env.RIFFRAFF_DEPLOY is true, the public path is managed by the RiffRaff deployment process, and the frontend application reads the parameter store key to include the appropriate <script> tag.

Could you confirm if this understanding is correct and provide any additional context?

Copy link
Member Author

@Jakeii Jakeii Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both frontendAssetsFullURL and page.assetsPath gets set to /assets instead of assets(-code).guim.co.uk when running locally to serve frontends static assets locally. But as commercial would no longer be there that's no use.

By setting to auto here, chunks will use whatever the entrypoint uses.

Often we are overriding the bundle location completely locally so not a huge issue, but it's nice to have ads working even if not overriding the bundle location.


/**
* Choose whether to launch Googletag or Opt Out tag (ootag) based on consent state
Expand Down
1 change: 1 addition & 0 deletions webpack.config.prod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we setting the global RIFFRAFF_DEPLOY to false to isolate the 2 build processes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use DefinePlugin to replace instances in our code of the strings on the left (the key) with the values on the right. It allows us to selectively override these values in the webpack config.

As an example you see how we configure process.env.OVERRIDE_BUNDLE_PATH in our dev vs prod webpack build here:

new DefinePlugin({
'process.env.OVERRIDE_BUNDLE_PATH':
JSON.stringify(overrideBundlePath),
}),

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(),
}),

...gitCommitSHA(),
}),
],
Expand Down
119 changes: 119 additions & 0 deletions webpack.config.riff-raff.mjs
Original file line number Diff line number Diff line change
@@ -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()],
},
});
Loading