Skip to content

Commit 16ec82e

Browse files
committed
Auto-upload RNDT binaries as GH release assets
1 parent a3a2b7e commit 16ec82e

File tree

6 files changed

+351
-9
lines changed

6 files changed

+351
-9
lines changed

.github/workflow-scripts/__tests__/createDraftRelease-test.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/
188188
status: 201,
189189
json: () =>
190190
Promise.resolve({
191+
id: 1,
191192
html_url:
192193
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
193194
}),
@@ -208,9 +209,11 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/
208209
body: fetchBody,
209210
},
210211
);
211-
expect(response).toEqual(
212-
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
213-
);
212+
expect(response).toEqual({
213+
id: 1,
214+
html_url:
215+
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
216+
});
214217
});
215218

216219
it('creates a draft release for prerelease on GitHub', async () => {
@@ -238,6 +241,7 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/
238241
status: 201,
239242
json: () =>
240243
Promise.resolve({
244+
id: 1,
241245
html_url:
242246
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
243247
}),
@@ -258,9 +262,11 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/
258262
body: fetchBody,
259263
},
260264
);
261-
expect(response).toEqual(
262-
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
263-
);
265+
expect(response).toEqual({
266+
id: 1,
267+
html_url:
268+
'https://github.com/facebook/react-native/releases/tag/v0.77.1',
269+
});
264270
});
265271

266272
it('throws if the post failes', async () => {

.github/workflow-scripts/createDraftRelease.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ async function _createDraftReleaseOnGitHub(version, body, latest, token) {
101101
}
102102

103103
const data = await response.json();
104-
return data.html_url;
104+
const {html_url, id} = data;
105+
return {
106+
html_url,
107+
id,
108+
};
105109
}
106110

107111
function moveToChangelogBranch(version) {
@@ -124,7 +128,8 @@ async function createDraftRelease(version, latest, token) {
124128
latest,
125129
token,
126130
);
127-
log(`Created draft release: ${release}`);
131+
log(`Created draft release: ${release.html_url}`);
132+
return release;
128133
}
129134

130135
module.exports = {

.github/workflows/create-draft-release.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,24 @@ jobs:
2121
git config --local user.name "React Native Bot"
2222
- name: Create draft release
2323
uses: actions/github-script@v6
24+
id: create-draft-release
2425
with:
2526
script: |
2627
const {createDraftRelease} = require('./.github/workflow-scripts/createDraftRelease.js');
2728
const version = '${{ github.ref_name }}';
2829
const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js');
29-
await createDraftRelease(version, isLatest(), '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}');
30+
return (await createDraftRelease(version, isLatest(), '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}')).id;
31+
result-encoding: string
32+
- name: Upload release assets for DotSlash
33+
uses: actions/github-script@v6
34+
env:
35+
RELEASE_ID: ${{ steps.create-draft-release.outputs.result }}
36+
with:
37+
script: |
38+
const {uploadReleaseAssetsForDotSlash} = require('./scripts/releases/upload-release-assets-for-dotslash.js');
39+
const version = '${{ github.ref_name }}';
40+
await uploadReleaseAssetsForDotSlash({
41+
version,
42+
token: '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}',
43+
releaseId: process.env.RELEASE_ID,
44+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@jest/create-cache-key-function": "^29.7.0",
5858
"@microsoft/api-extractor": "^7.52.2",
5959
"@motizilberman/dotslash": "0.5.7-1754587239403",
60+
"@octokit/rest": "^22.0.0",
6061
"@react-native/metro-babel-transformer": "0.82.0-main",
6162
"@react-native/metro-config": "0.82.0-main",
6263
"@tsconfig/node22": "22.0.2",
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const {
14+
validateAndParseDotSlashFile,
15+
processDotSlashFileInPlace,
16+
} = require('./utils/dotslash-utils');
17+
const {REPO_ROOT} = require('../shared/consts');
18+
const {parseArgs} = require('util');
19+
const path = require('path');
20+
const {Octokit} = require('@octokit/rest');
21+
const {
22+
FIRST_PARTY_DOTSLASH_FILES,
23+
} = require('./write-dotslash-release-asset-urls');
24+
25+
const config = {
26+
allowPositionals: true,
27+
options: {
28+
token: {type: 'string'},
29+
releaseId: {type: 'string'},
30+
force: {type: 'boolean', default: false},
31+
dryRun: {type: 'boolean', default: false},
32+
help: {type: 'boolean'},
33+
},
34+
};
35+
36+
async function main() {
37+
const {
38+
positionals: [version],
39+
values: {help, token, releaseId, force, dryRun},
40+
/* $FlowFixMe[incompatible-call] Natural Inference rollout. See
41+
* https://fburl.com/workplace/6291gfvu */
42+
} = parseArgs(config);
43+
44+
if (help) {
45+
console.log(`
46+
Usage: node ./scripts/releases/upload-release-assets-for-dotslash.js <version> --release_id <id> --token <github-token> [--force]
47+
48+
Scans first-party DotSlash files in the repo for URLs referencing assets of
49+
an upcoming release, and uploads the actual assets to the GitHub release
50+
identified by the given release ID.
51+
52+
If run with --force, the script will overwrite any assets that happen to
53+
already exist at the given URLs. This is useful for retrying failed or
54+
corrupted uploads.
55+
`);
56+
return;
57+
}
58+
59+
if (version == null) {
60+
throw new Error('Missing version argument');
61+
}
62+
63+
await uploadReleaseAssetsForDotSlash({
64+
version,
65+
token,
66+
releaseId,
67+
force,
68+
dryRun,
69+
});
70+
}
71+
72+
async function uploadReleaseAssetsForDotSlash(
73+
{version, token, releaseId, force = false, dryRun = false} /*: {
74+
version: string,
75+
token: string,
76+
releaseId: string,
77+
force?: boolean,
78+
dryRun?: boolean,
79+
} */,
80+
) /*: Promise<void> */ {
81+
if (version.startsWith('v')) {
82+
version = version.substring(1);
83+
}
84+
85+
const releaseTag = `v${version}`;
86+
const releaseAssetPrefix = `https://github.com/facebook/react-native/releases/download/${encodeURIComponent(releaseTag)}/`;
87+
const octokit = new Octokit({auth: token});
88+
const existingAssets = await octokit.repos.listReleaseAssets({
89+
owner: 'facebook',
90+
repo: 'react-native',
91+
release_id: releaseId,
92+
});
93+
const existingAssetsByName = new Map(
94+
existingAssets.data.map(asset => [asset.name, asset]),
95+
);
96+
for (const filename of FIRST_PARTY_DOTSLASH_FILES) {
97+
const fullPath = path.join(REPO_ROOT, filename);
98+
console.log(`Uploading assets for ${filename}...`);
99+
const uploadPromises = [];
100+
await processDotSlashFileInPlace(
101+
fullPath,
102+
(
103+
providers,
104+
// NOTE: We mostly ignore suggestedFilename in favour of reading the actual asset URLs
105+
suggestedFilename,
106+
) => {
107+
let upstreamUrl, targetReleaseAssetInfo;
108+
for (const provider of providers) {
109+
if (provider.type != null && provider.type !== 'http') {
110+
console.log(
111+
'Skipping non-HTTP provider: ' + JSON.stringify(provider),
112+
);
113+
continue;
114+
}
115+
const url = provider.url;
116+
if (url.startsWith(releaseAssetPrefix)) {
117+
const name = decodeURIComponent(
118+
url.slice(releaseAssetPrefix.length),
119+
);
120+
targetReleaseAssetInfo = {name, url};
121+
} else {
122+
upstreamUrl = url;
123+
}
124+
if (upstreamUrl != null && targetReleaseAssetInfo != null) {
125+
break;
126+
}
127+
}
128+
if (targetReleaseAssetInfo == null) {
129+
// This DotSlash providers array does not reference any relevant release asset URLs, so we can ignore it.
130+
console.log(
131+
`[${suggestedFilename} (suggested)] No provider URLs matched release asset prefix: ${releaseAssetPrefix}`,
132+
);
133+
return;
134+
}
135+
if (upstreamUrl == null) {
136+
throw new Error(
137+
`No upstream URL found for release asset ${targetReleaseAssetInfo.name}`,
138+
);
139+
}
140+
uploadPromises.push(
141+
(async () => {
142+
if (existingAssetsByName.has(targetReleaseAssetInfo.name)) {
143+
if (!force) {
144+
console.log(
145+
`[${targetReleaseAssetInfo.name}] Skipping existing release asset...`,
146+
);
147+
return;
148+
}
149+
if (dryRun) {
150+
console.log(
151+
`[${targetReleaseAssetInfo.name}] Dry run: Not deleting existing release asset.`,
152+
);
153+
} else {
154+
console.log(
155+
`[${targetReleaseAssetInfo.name}] Deleting existing release asset...`,
156+
);
157+
await octokit.repos.deleteReleaseAsset({
158+
owner: 'facebook',
159+
repo: 'react-native',
160+
asset_id: existingAssetsByName.get(
161+
targetReleaseAssetInfo.name,
162+
).id,
163+
});
164+
}
165+
}
166+
console.log(
167+
`[${targetReleaseAssetInfo.name}] Downloading from ${upstreamUrl}...`,
168+
);
169+
const response = await fetch(upstreamUrl);
170+
if (!response.ok) {
171+
throw new Error(
172+
`Failed to download ${upstreamUrl}: ${response.status} ${response.statusText}`,
173+
);
174+
}
175+
const data = await response.arrayBuffer();
176+
const contentType = response.headers.get('content-type');
177+
if (dryRun) {
178+
console.log(
179+
`[${targetReleaseAssetInfo.name}] Dry run: Not uploading to release.`,
180+
);
181+
return;
182+
} else {
183+
console.log(
184+
`[${targetReleaseAssetInfo.name}] Uploading to release...`,
185+
);
186+
await octokit.repos.uploadReleaseAsset({
187+
owner: 'facebook',
188+
repo: 'react-native',
189+
release_id: releaseId,
190+
name: targetReleaseAssetInfo.name,
191+
data,
192+
headers: {
193+
'content-type': contentType,
194+
},
195+
});
196+
}
197+
})(),
198+
);
199+
},
200+
);
201+
await Promise.all(uploadPromises);
202+
}
203+
}
204+
205+
module.exports = {
206+
uploadReleaseAssetsForDotSlash,
207+
};
208+
209+
if (require.main === module) {
210+
void main();
211+
}

0 commit comments

Comments
 (0)