Skip to content

Commit 1c9d87b

Browse files
committed
feat(web, ci): add publish script; automatically publish for dev on every merge to main
1 parent 26d8a2f commit 1c9d87b

File tree

9 files changed

+230
-46
lines changed

9 files changed

+230
-46
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ tasks:
424424
- func: bootstrap
425425
vars:
426426
scope: "@mongodb-js/compass-web"
427+
- func: upload-web
427428
- func: publish-web
428429

429430
- name: publish-dev-release-info

.evergreen/buildvariants-and-tasks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ tasks:
428428
- func: bootstrap
429429
vars:
430430
scope: '@mongodb-js/compass-web'
431+
- func: upload-web
431432
- func: publish-web
432433
- name: publish-dev-release-info
433434
tags: []

.evergreen/functions.yml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# The variables are with the functions because they are only used by the
2-
# functions and also because you can't use variables across includes.
1+
# The variables and paratemers are with the functions because they are only used
2+
# by the functions and also because you can't use variables across includes.
33
variables:
44
- &save-artifact-params-private
55
aws_key: ${aws_key}
@@ -77,6 +77,14 @@ variables:
7777
E2E_TESTS_ATLAS_CS_WITHOUT_SEARCH: ${e2e_tests_atlas_cs_without_search}
7878
E2E_TESTS_ATLAS_CS_WITH_SEARCH: ${e2e_tests_atlas_cs_with_search}
7979

80+
parameters:
81+
- key: compass_web_publish_environment
82+
value: dev
83+
description: Atlas Cloud environment that compass-web will be deployed for during a CI run
84+
- key: compass_web_release_commit
85+
value: '${github_commit}'
86+
description: Exact commit that will be deployed as a latest release for provided compass_web_publish_environment
87+
8088
# This is here with the variables because anchors aren't supported across includes
8189
post:
8290
- command: archive.targz_pack
@@ -521,7 +529,7 @@ functions:
521529
echo "Uploading release assets to S3 and GitHub if needed..."
522530
npm run --workspace mongodb-compass upload
523531
524-
publish-web:
532+
upload-web:
525533
- command: ec2.assume_role
526534
params:
527535
role_arn: ${downloads_bucket_role_arn}
@@ -540,6 +548,27 @@ functions:
540548
echo "Uploading release assets to S3"
541549
npm run --workspace @mongodb-js/compass-web upload
542550
551+
publish-web:
552+
- command: ec2.assume_role
553+
params:
554+
role_arn: ${downloads_bucket_role_arn}
555+
- command: shell.exec
556+
params:
557+
working_dir: src
558+
shell: bash
559+
env:
560+
<<: *compass-env
561+
COMPASS_WEB_PUBLISH_ENVIRONMENT: '${compass_web_publish_environment}'
562+
COMPASS_WEB_RELEASE_COMMIT: '${compass_web_release_commit}'
563+
script: |
564+
set -e
565+
# Load environment variables
566+
eval $(.evergreen/print-compass-env.sh)
567+
# Deploy to dev on every commit to main, do nothing for release branches
568+
# TODO: gate and only run for 10gen-compass-main project and not patches
569+
echo "Publishing compass-web@$COMPASS_WEB_RELEASE_COMMIT to $COMPASS_WEB_PUBLISH_ENVIRONMENT"
570+
npm run --workspace @mongodb-js/compass-web publish
571+
543572
publish-dev-release-info:
544573
- command: shell.exec
545574
params:

packages/compass-web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
"test-watch": "npm run test -- --watch",
6464
"test-ci": "npm run test-cov",
6565
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write .",
66-
"upload": "node --experimental-strip-types scripts/upload.mts"
66+
"upload": "node --experimental-strip-types scripts/release/upload.mts",
67+
"publish": "node --experimental-strip-types scripts/release/publish.mts"
6768
},
6869
"peerDependencies": {
6970
"react": "^17.0.2",
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import path from 'path';
2+
import { brotliCompressSync } from 'zlib';
3+
import { inspect } from 'util';
4+
import {
5+
DOWNLOADS_BUCKET,
6+
DOWNLOADS_BUCKET_PUBLIC_HOST,
7+
ENTRYPOINT_FILENAME,
8+
MANIFEST_FILENAME,
9+
RELEASE_COMMIT,
10+
asyncPutObject,
11+
getObjectKey,
12+
} from './utils.mts';
13+
14+
const publicManifestUrl = new URL(
15+
getObjectKey(MANIFEST_FILENAME),
16+
DOWNLOADS_BUCKET_PUBLIC_HOST
17+
);
18+
19+
const publicEntrypointUrl = new URL(
20+
getObjectKey(ENTRYPOINT_FILENAME),
21+
DOWNLOADS_BUCKET_PUBLIC_HOST
22+
);
23+
24+
let assets: URL[];
25+
26+
function assertResponseIsOk(res: Response) {
27+
if (res.status !== 200) {
28+
throw new Error(
29+
`Request returned a non-OK response: ${res.status} ${res.statusText}`
30+
);
31+
}
32+
}
33+
34+
try {
35+
const res = await fetch(publicManifestUrl);
36+
assertResponseIsOk(res);
37+
const manifest = await res.json();
38+
39+
if (
40+
!(
41+
Array.isArray(manifest) &&
42+
manifest.every((asset) => {
43+
return typeof asset === 'string';
44+
})
45+
)
46+
) {
47+
throw new Error(
48+
`Manifest schema is not matching: expected string[], got ${inspect(
49+
manifest
50+
)}`
51+
);
52+
}
53+
54+
assets = manifest.map((asset) => {
55+
return new URL(getObjectKey(asset), DOWNLOADS_BUCKET_PUBLIC_HOST);
56+
});
57+
58+
await Promise.all(
59+
assets.map(async (assetUrl) => {
60+
const res = await fetch(assetUrl, { method: 'HEAD' });
61+
assertResponseIsOk(res);
62+
})
63+
);
64+
} catch (err) {
65+
throw new AggregateError(
66+
[err],
67+
`Aborting publish, failed to resolve manifest ${publicManifestUrl}`
68+
);
69+
}
70+
71+
const ALLOWED_PUBLISH_ENVIRONMENTS = ['dev', 'qa', 'stage', 'prod'];
72+
73+
const PUBLISH_ENVIRONMENT = process.env.COMPASS_WEB_PUBLISH_ENVIRONMENT;
74+
75+
if (!ALLOWED_PUBLISH_ENVIRONMENTS.includes(PUBLISH_ENVIRONMENT ?? '')) {
76+
throw new Error(
77+
`Unknown publish environment: expected ${inspect(
78+
ALLOWED_PUBLISH_ENVIRONMENTS
79+
)}, got ${inspect(PUBLISH_ENVIRONMENT)}`
80+
);
81+
}
82+
83+
function buildProxyEntrypointFile() {
84+
return (
85+
assets
86+
.map((asset) => {
87+
return `import ${JSON.stringify(asset)};`;
88+
})
89+
.concat(
90+
`export * from ${JSON.stringify(publicEntrypointUrl)};`,
91+
`/** Compass version: https://github.com/mongodb-js/compass/tree/${RELEASE_COMMIT} */`
92+
)
93+
.join('\n') + '\n'
94+
);
95+
}
96+
97+
const fileKey = getObjectKey('index.mjs', PUBLISH_ENVIRONMENT);
98+
const fileContent = buildProxyEntrypointFile();
99+
const compressedFileContent = brotliCompressSync(fileContent);
100+
101+
console.log(
102+
'Uploading entrypoint to s3://%s/%s ...',
103+
DOWNLOADS_BUCKET,
104+
fileKey
105+
);
106+
107+
const ENTRYPOINT_CACHE_MAX_AGE = 1000 * 60 * 3; // 3mins
108+
109+
const res = await asyncPutObject({
110+
ACL: 'private',
111+
Bucket: DOWNLOADS_BUCKET,
112+
Key: fileKey,
113+
Body: compressedFileContent,
114+
ContentType: 'text/javascript',
115+
ContentEncoding: 'br',
116+
ContentLength: compressedFileContent.byteLength,
117+
// "Latest" entrypoint files can change quite often, so max-age is quite
118+
// short and browser should always revalidate on stale
119+
CacheControl: `public, max-age=${ENTRYPOINT_CACHE_MAX_AGE}, must-revalidate`,
120+
});
121+
122+
console.log(
123+
'Successfully uploaded %s (ETag: %s)',
124+
path.basename(fileKey),
125+
res.ETag
126+
);

packages/compass-web/scripts/upload.mts renamed to packages/compass-web/scripts/release/upload.mts

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,12 @@
11
import fs from 'fs';
22
import path from 'path';
3-
import child_process from 'child_process';
43
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-
}
4+
import {
5+
DIST_DIR,
6+
DOWNLOADS_BUCKET,
7+
asyncPutObject,
8+
getObjectKey,
9+
} from './utils.mts';
3110

3211
const artifacts = await fs.promises.readdir(DIST_DIR);
3312

@@ -39,7 +18,7 @@ const contentTypeForExt: Record<string, string> = {
3918
'.mjs': 'text/javascript',
4019
'.txt': 'text/plain', // extracted third party license info
4120
'.ts': 'text/typescript', // type definitions
42-
'.json': 'application/json', // tsdoc meta
21+
'.json': 'application/json', // tsdoc / assets meta
4322
};
4423

4524
const ALLOWED_EXTS = Object.keys(contentTypeForExt);
@@ -50,17 +29,11 @@ for (const file of artifacts) {
5029
}
5130
}
5231

53-
const s3Client = new S3({
54-
credentials: getCredentials(),
55-
});
56-
5732
const IMMUTABLE_CACHE_MAX_AGE = 1000 * 60 * 60 * 24 * 7; // a week
5833

5934
for (const file of artifacts) {
6035
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}`;
36+
const objectKey = getObjectKey(file);
6437

6538
console.log(
6639
'Uploading compass-web/dist/%s to s3://%s/%s ...',
@@ -72,12 +45,6 @@ for (const file of artifacts) {
7245
const fileContent = fs.readFileSync(filePath, 'utf8');
7346
const compressedFileContent = brotliCompressSync(fileContent);
7447

75-
const asyncPutObject: (
76-
params: S3.Types.PutObjectRequest
77-
) => Promise<S3.Types.PutObjectOutput> = promisify(
78-
s3Client.putObject.bind(s3Client)
79-
);
80-
8148
const res = await asyncPutObject({
8249
ACL: 'private',
8350
Bucket: DOWNLOADS_BUCKET,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import S3 from 'aws-sdk/clients/s3.js';
2+
import child_process from 'child_process';
3+
import path from 'path';
4+
import { promisify } from 'util';
5+
6+
// TODO(SRE-4971): replace with a compass-web-only bucket when provisioned
7+
export const DOWNLOADS_BUCKET = 'cdn-origin-compass';
8+
9+
export const DOWNLOADS_BUCKET_PUBLIC_HOST = 'https://downloads.mongodb.com';
10+
11+
export const ENTRYPOINT_FILENAME = 'compass-web.mjs';
12+
13+
export const MANIFEST_FILENAME = 'assets-manifest.json';
14+
15+
export const DIST_DIR = path.resolve(import.meta.dirname, '..', '..', 'dist');
16+
17+
export const RELEASE_COMMIT =
18+
process.env.COMPASS_WEB_RELEASE_COMMIT ||
19+
child_process
20+
.spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' })
21+
.stdout.trim();
22+
23+
function getAWSCredentials() {
24+
if (
25+
!process.env.DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID ||
26+
!process.env.DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY ||
27+
!process.env.DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN
28+
) {
29+
throw new Error('Missing required env variables');
30+
}
31+
return {
32+
accessKeyId: process.env.DOWNLOAD_CENTER_NEW_AWS_ACCESS_KEY_ID,
33+
secretAccessKey: process.env.DOWNLOAD_CENTER_NEW_AWS_SECRET_ACCESS_KEY,
34+
sessionToken: process.env.DOWNLOAD_CENTER_NEW_AWS_SESSION_TOKEN,
35+
};
36+
}
37+
const s3Client = new S3({
38+
credentials: getAWSCredentials(),
39+
});
40+
41+
export const asyncPutObject: (
42+
params: S3.Types.PutObjectRequest
43+
) => Promise<S3.Types.PutObjectOutput> = promisify(
44+
s3Client.putObject.bind(s3Client)
45+
);
46+
47+
export function getObjectKey(filename: string, release = RELEASE_COMMIT) {
48+
// TODO(SRE-4971): while we're uploading to the downloads bucket, the object
49+
// key always needs to start with `compass/`
50+
return `compass/web/${release}/${filename}`;
51+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{
22
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
// reversing options that are not relevant for the build
5+
"allowImportingTsExtensions": false,
6+
"noEmit": false
7+
},
38
"include": ["src/**/*"],
49
"exclude": ["./src/**/*.spec.*"]
510
}

packages/compass-web/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{
22
"extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json",
33
"compilerOptions": {
4-
"outDir": "dist"
4+
"outDir": "dist",
5+
// for scripts that we run with Node.js strip-types option
6+
"allowImportingTsExtensions": true,
7+
"noEmit": true
58
},
69
"include": ["**/*"],
710
"exclude": ["node_modules", "dist", "test/types"]

0 commit comments

Comments
 (0)