Skip to content

Commit 95e02b5

Browse files
authored
actions: promote release action (#142)
Signed-off-by: flakey5 <[email protected]>
1 parent 09a0891 commit 95e02b5

File tree

4 files changed

+183
-2
lines changed

4 files changed

+183
-2
lines changed

.github/workflows/promote.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Promote Release
2+
3+
on:
4+
# Triggered by the dist server
5+
workflow_dispatch:
6+
inputs:
7+
path:
8+
description: 'path to promote'
9+
required: true
10+
recursive:
11+
description: 'is the path a directory'
12+
type: boolean
13+
required: true
14+
15+
jobs:
16+
promote-release:
17+
name: Promote Release
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Git Checkout
22+
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744
23+
24+
- name: Setup Node
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: lts/*
28+
cache: 'npm'
29+
30+
- name: Promote Files
31+
env:
32+
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
33+
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
34+
run: |
35+
node scripts/promote-release.mjs ${{ inputs.path }} ${{ inputs.recursive == true && '--recursive' || '' }}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"type": "module",
66
"scripts": {
77
"start": "wrangler dev --remote",
8-
"format": "prettier --check --write \"**/*.{ts,js,json,md}\"",
9-
"prettier": "prettier --check \"**/*.{ts,js,json,md}\"",
8+
"format": "prettier --check --write \"**/*.{ts,js,mjs,json,md}\"",
9+
"prettier": "prettier --check \"**/*.{ts,js,mjs,json,md}\"",
1010
"lint": "eslint ./src",
1111
"test": "npm run test:unit && npm run test:e2e",
1212
"test:unit": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/unit/index.test.ts",

scripts/constants.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
export const ENDPOINT =
4+
process.env.ENDPOINT ??
5+
'https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com';
6+
7+
export const PROD_BUCKET = process.env.PROD_BUCKET ?? 'dist-prod';
8+
9+
export const STAGING_BUCKET = process.env.STAGING_BUCKET ?? 'dist-staging';
10+
11+
export const R2_RETRY_COUNT = 3;

scripts/promote-release.mjs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Usage: `promote-release <prefix in dist-staging to promote> [--recursive]`
5+
* ex/ `promote-release nodejs/release/v20.0.0/ --recursive`
6+
*/
7+
8+
import {
9+
S3Client,
10+
ListObjectsV2Command,
11+
CopyObjectCommand,
12+
} from '@aws-sdk/client-s3';
13+
import {
14+
ENDPOINT,
15+
PROD_BUCKET,
16+
STAGING_BUCKET,
17+
R2_RETRY_COUNT,
18+
} from './constants.mjs';
19+
20+
if (process.argv.length !== 3 && process.argv.length !== 4) {
21+
console.error(
22+
`usage: promote-release <prefix in dist-staging to promote> [--recursive]`
23+
);
24+
process.exit(1);
25+
}
26+
27+
if (!process.env.CF_ACCESS_KEY_ID) {
28+
console.error('CF_ACCESS_KEY_ID missing');
29+
process.exit(1);
30+
}
31+
32+
if (!process.env.CF_SECRET_ACCESS_KEY) {
33+
console.error('CF_SECRET_ACCESS_KEY missing');
34+
process.exit(1);
35+
}
36+
37+
const client = new S3Client({
38+
endpoint: ENDPOINT,
39+
region: 'auto',
40+
credentials: {
41+
accessKeyId: process.env.CF_ACCESS_KEY_ID,
42+
secretAccessKey: process.env.CF_SECRET_ACCESS_KEY,
43+
},
44+
});
45+
46+
const path = process.argv[2];
47+
const recursive =
48+
process.argv.length === 4 && process.argv[3] === '--recursive';
49+
50+
if (recursive) {
51+
const files = await getFilesToPromote(path);
52+
53+
for (const file of files) {
54+
await promoteFile(file);
55+
}
56+
} else {
57+
await promoteFile(file);
58+
}
59+
60+
/**
61+
* @param {string} path
62+
* @returns {string[]}
63+
*/
64+
async function getFilesToPromote(path) {
65+
let paths = [];
66+
67+
let truncated = true;
68+
let continuationToken;
69+
while (truncated) {
70+
const data = await retryWrapper(async () => {
71+
return await client.send(
72+
new ListObjectsV2Command({
73+
Bucket: STAGING_BUCKET,
74+
Delimiter: '/',
75+
Prefix: path,
76+
ContinuationToken: continuationToken,
77+
})
78+
);
79+
});
80+
81+
if (data.CommonPrefixes) {
82+
for (const directory of data.CommonPrefixes) {
83+
paths.push(...(await getFilesToPromote(directory.Prefix)));
84+
}
85+
}
86+
87+
if (data.Contents) {
88+
for (const object of data.Contents) {
89+
paths.push(object.Key);
90+
}
91+
}
92+
93+
truncated = data.IsTruncated ?? false;
94+
continuationToken = data.NextContinuationToken;
95+
}
96+
97+
return paths;
98+
}
99+
100+
/**
101+
* @param {string} file
102+
*/
103+
async function promoteFile(file) {
104+
console.log(`Promoting ${file}`);
105+
106+
await retryWrapper(async () => {
107+
return await client.send(
108+
new CopyObjectCommand({
109+
Bucket: PROD_BUCKET,
110+
CopySource: `${STAGING_BUCKET}/${file}`,
111+
Key: file,
112+
})
113+
);
114+
}, R2_RETRY_COUNT);
115+
}
116+
117+
/**
118+
* @param {() => Promise<T>} request
119+
* @returns {Promise<T>}
120+
*/
121+
async function retryWrapper(request, retryLimit) {
122+
let r2Error;
123+
124+
for (let i = 0; i < R2_RETRY_COUNT; i++) {
125+
try {
126+
const result = await request();
127+
return result;
128+
} catch (err) {
129+
r2Error = err;
130+
process.emitWarning(`error when contacting r2: ${err}`);
131+
}
132+
}
133+
134+
throw r2Error;
135+
}

0 commit comments

Comments
 (0)