Skip to content

Commit 26d8a2f

Browse files
authored
feat(web, ci): upload compass-web assets to an s3 bucket during publish COMPASS-10132 (#7631)
* chore(webpack): make sure that images are always inlined in web build * chore(web): add upload script for compass-web * chore(ci): move downloads bucket env to compass-env anchor; move role_arn to project config * feat(ci): add publish-web task to the pipeline * chore(web): switch back to aws sdk2 to avoid too many dependencies being updated * chore(web): add cache-control headers * chore(web): adjust assets cache control policy
1 parent 58057ae commit 26d8a2f

File tree

8 files changed

+173
-20
lines changed

8 files changed

+173
-20
lines changed

.evergreen/buildvariants-and-tasks.in.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ buildvariants:
252252
run_on: ubuntu2204-large
253253
tasks:
254254
- name: publish
255+
- name: publish-web
255256
- name: publish-dev-release-info
256257

257258
- name: static-analysis
@@ -410,6 +411,21 @@ tasks:
410411
- func: get-all-artifacts
411412
- func: publish
412413

414+
- name: publish-web
415+
# Publish both on PRs and on main (if it gets too noisy and won't be used
416+
# much we can probably disable for PRs)
417+
tags: ['run-on-pr']
418+
depends_on:
419+
- name: '.required-for-publish'
420+
variant: '*'
421+
commands:
422+
- func: prepare
423+
- func: install
424+
- func: bootstrap
425+
vars:
426+
scope: "@mongodb-js/compass-web"
427+
- func: publish-web
428+
413429
- name: publish-dev-release-info
414430
tags: []
415431
depends_on:

.evergreen/buildvariants-and-tasks.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ buildvariants:
229229
- name: test-packaged-app-macos-15-x64
230230
display_name: Test Packaged App MacOS x64 15
231231
run_on: macos-15-amd64-gui
232-
patchable: true
232+
patchable: false
233233
depends_on:
234234
- name: package-compass
235235
variant: package-macos-x64
@@ -242,6 +242,7 @@ buildvariants:
242242
run_on: ubuntu2204-large
243243
tasks:
244244
- name: publish
245+
- name: publish-web
245246
- name: publish-dev-release-info
246247
- name: static-analysis
247248
display_name: Create Static Analysis Report
@@ -415,6 +416,19 @@ tasks:
415416
scope: mongodb-compass
416417
- func: get-all-artifacts
417418
- func: publish
419+
- name: publish-web
420+
tags:
421+
- run-on-pr
422+
depends_on:
423+
- name: .required-for-publish
424+
variant: '*'
425+
commands:
426+
- func: prepare
427+
- func: install
428+
- func: bootstrap
429+
vars:
430+
scope: '@mongodb-js/compass-web'
431+
- func: publish-web
418432
- name: publish-dev-release-info
419433
tags: []
420434
depends_on:

.evergreen/functions.yml

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ variables:
4848
GITHUB_TOKEN: ${devtoolsbot_github_token}
4949
DOWNLOAD_CENTER_AWS_ACCESS_KEY_ID: ${aws_key_evergreen_integrations}
5050
DOWNLOAD_CENTER_AWS_SECRET_ACCESS_KEY: ${aws_secret_evergreen_integrations}
51+
DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
52+
DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
53+
DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
5154
EVERGREEN_BUCKET_NAME: mciuploads
5255
EVERGREEN_BUCKET_KEY_PREFIX: ${project}/${revision}_${revision_order_id}
5356
MONGODB_RUNNER_LOG_DIR: ${workdir}/src/.testserver/
@@ -504,23 +507,39 @@ functions:
504507
publish:
505508
- command: ec2.assume_role
506509
params:
507-
role_arn: 'arn:aws:iam::119629040606:role/s3-access.cdn-origin-compass'
510+
role_arn: ${downloads_bucket_role_arn}
508511
- command: shell.exec
509512
params:
510513
working_dir: src
511514
shell: bash
512515
env:
513516
<<: *compass-env
514-
DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
515-
DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
516-
DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
517517
script: |
518518
set -e
519519
# Load environment variables
520520
eval $(.evergreen/print-compass-env.sh)
521521
echo "Uploading release assets to S3 and GitHub if needed..."
522522
npm run --workspace mongodb-compass upload
523523
524+
publish-web:
525+
- command: ec2.assume_role
526+
params:
527+
role_arn: ${downloads_bucket_role_arn}
528+
- command: shell.exec
529+
params:
530+
working_dir: src
531+
shell: bash
532+
env:
533+
<<: *compass-env
534+
script: |
535+
set -e
536+
# Load environment variables
537+
eval $(.evergreen/print-compass-env.sh)
538+
echo "Compiling compass-web"
539+
npm run --workspace @mongodb-js/compass-web compile
540+
echo "Uploading release assets to S3"
541+
npm run --workspace @mongodb-js/compass-web upload
542+
524543
publish-dev-release-info:
525544
- command: shell.exec
526545
params:

configs/webpack-config-compass/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
sourceLoader,
2323
cssLoader,
2424
lessLoader,
25-
assetsLoader,
25+
fontLoader,
26+
imageLoader,
2627
resourceLoader,
2728
sharedObjectLoader,
2829
} from './loaders';
@@ -225,7 +226,8 @@ export function createElectronRendererConfig(
225226
nodeLoader(opts),
226227
cssLoader(opts),
227228
lessLoader(opts),
228-
assetsLoader(opts),
229+
fontLoader(opts),
230+
imageLoader(opts),
229231
sharedObjectLoader(opts),
230232
sourceLoader(opts),
231233
],
@@ -370,7 +372,8 @@ export function createWebConfig(args: Partial<ConfigArgs>): WebpackConfig {
370372
nodeLoader(opts),
371373
cssLoader(opts, true),
372374
lessLoader(opts),
373-
assetsLoader(opts),
375+
fontLoader(opts),
376+
imageLoader(opts),
374377
sourceLoader(opts),
375378
],
376379
parser: {

configs/webpack-config-compass/src/loaders.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,18 @@ export const lessLoader = (args: ConfigArgs) => ({
187187
});
188188

189189
// eslint-disable-next-line @typescript-eslint/no-unused-vars
190-
export const assetsLoader = (_args: ConfigArgs) => ({
191-
test: /\.(jpe?g|png|svg|gif|woff|woff2|ttf|eot|otf)(\?.+?)?$/,
192-
// asset (or asset auto) will either compile as data-uri or to a file path
193-
// based on the size, this is a good strategy for loading assets in the GUI
194-
type: 'asset',
195-
parser: {
196-
dataUrlCondition: {
197-
maxSize: 2 * 1024, // 2kb
198-
},
199-
},
190+
export const fontLoader = (_args: ConfigArgs) => ({
191+
test: /\.(woff|woff2|ttf|eot|otf)(\?.+?)?$/,
192+
// fonts are always big and should be emitted as a separate file
193+
type: 'asset/resource',
194+
});
195+
196+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
197+
export const imageLoader = (_args: ConfigArgs) => ({
198+
test: /\.(jpe?g|png|svg|gif)(\?.+?)?$/,
199+
// it's convenient to inline images as data-urls to make sure that publised
200+
// library artifacts only produce importable javascript assets
201+
type: 'asset/inline',
200202
});
201203

202204
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -223,7 +225,7 @@ export const sourceLoader = (args: ConfigArgs) => ({
223225
nodeLoader(args).test,
224226
cssLoader(args).test,
225227
lessLoader(args).test,
226-
assetsLoader(args).test,
228+
resourceLoader(args).test,
227229
sharedObjectLoader(args).test,
228230
// Produced by html-webpack-plugin and should not be handled
229231
/\.(ejs|html)$/,

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-web/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
"test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test",
6363
"test-watch": "npm run test -- --watch",
6464
"test-ci": "npm run test-cov",
65-
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
65+
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write .",
66+
"upload": "node --experimental-strip-types scripts/upload.mts"
6667
},
6768
"peerDependencies": {
6869
"react": "^17.0.2",
@@ -115,6 +116,7 @@
115116
"@types/react": "^17.0.5",
116117
"@types/react-dom": "^17.0.10",
117118
"@types/sinon-chai": "^3.2.5",
119+
"aws-sdk": "^2.1692.0",
118120
"browser-process-hrtime": "^1.0.0",
119121
"bson": "^6.10.4",
120122
"buffer": "^6.0.3",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import child_process from 'child_process';
4+
import { brotliCompressSync } from 'zlib';
5+
import { promisify } from 'util';
6+
import S3 from 'aws-sdk/clients/s3.js';
7+
8+
// TODO(SRE-4971): replace with a compass-web-only bucket when provisioned
9+
const DOWNLOADS_BUCKET = 'cdn-origin-compass';
10+
11+
const DIST_DIR = path.resolve(import.meta.dirname, '..', 'dist');
12+
13+
const HEAD = child_process
14+
.spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' })
15+
.stdout.trim();
16+
17+
function getCredentials() {
18+
if (
19+
!process.env.DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID ||
20+
!process.env.DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY ||
21+
!process.env.DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN
22+
) {
23+
throw new Error('Missing required env variables');
24+
}
25+
return {
26+
accessKeyId: process.env.DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID,
27+
secretAccessKey: process.env.DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY,
28+
sessionToken: process.env.DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN,
29+
};
30+
}
31+
32+
const artifacts = await fs.promises.readdir(DIST_DIR);
33+
34+
if (!artifacts.length) {
35+
throw new Error('No artifact files found');
36+
}
37+
38+
const contentTypeForExt: Record<string, string> = {
39+
'.mjs': 'text/javascript',
40+
'.txt': 'text/plain', // extracted third party license info
41+
'.ts': 'text/typescript', // type definitions
42+
'.json': 'application/json', // tsdoc meta
43+
};
44+
45+
const ALLOWED_EXTS = Object.keys(contentTypeForExt);
46+
47+
for (const file of artifacts) {
48+
if (!ALLOWED_EXTS.includes(path.extname(file))) {
49+
throw new Error(`Unexpected artifact file extension for ${file}`);
50+
}
51+
}
52+
53+
const s3Client = new S3({
54+
credentials: getCredentials(),
55+
});
56+
57+
const IMMUTABLE_CACHE_MAX_AGE = 1000 * 60 * 60 * 24 * 7; // a week
58+
59+
for (const file of artifacts) {
60+
const filePath = path.join(DIST_DIR, file);
61+
// TODO(SRE-4971): while we're uploading to the downloads bucket, the object
62+
// key always needs to start with `compass/`
63+
const objectKey = `compass/web/${HEAD}/${file}`;
64+
65+
console.log(
66+
'Uploading compass-web/dist/%s to s3://%s/%s ...',
67+
file,
68+
DOWNLOADS_BUCKET,
69+
objectKey
70+
);
71+
72+
const fileContent = fs.readFileSync(filePath, 'utf8');
73+
const compressedFileContent = brotliCompressSync(fileContent);
74+
75+
const asyncPutObject: (
76+
params: S3.Types.PutObjectRequest
77+
) => Promise<S3.Types.PutObjectOutput> = promisify(
78+
s3Client.putObject.bind(s3Client)
79+
);
80+
81+
const res = await asyncPutObject({
82+
ACL: 'private',
83+
Bucket: DOWNLOADS_BUCKET,
84+
Key: objectKey,
85+
Body: compressedFileContent,
86+
ContentType: contentTypeForExt[path.extname(file)],
87+
ContentEncoding: 'br',
88+
ContentLength: compressedFileContent.byteLength,
89+
// Assets stored under the commit hash never change after upload, so the
90+
// cache-control setting for them can be quite generous
91+
CacheControl: `public, max-age=${IMMUTABLE_CACHE_MAX_AGE}, immutable`,
92+
});
93+
94+
console.log('Successfully uploaded %s (ETag: %s)', file, res.ETag);
95+
}

0 commit comments

Comments
 (0)