mirror of
https://github.com/react/react-native.git
synced 2026-09-29 16:58:04 +08:00
Summary: Fixes the three post-release failures from the [0.87.0-rc.2 publish run](https://github.com/react/react-native/actions/runs/29775529785/): - Keep Flow annotations in `verifyArtifactsAreOnMaven.js` as comments so `actions/github-script` can load the file with plain Node. - Pin the Podfile lock workflow to `macos-15`, which provides the configured Xcode 16.4 version. - Use the canonical `react/react-native` owner for release asset API operations so upload POST requests are not redirected from the former owner. ## Changelog: [INTERNAL] [FIXED] - Fix post-release Maven verification, Podfile lock, and release asset jobs Pull Request resolved: https://github.com/react/react-native/pull/57627 Test Plan: ```sh node --check .github/workflow-scripts/verifyArtifactsAreOnMaven.js node --check scripts/releases/upload-release-assets-for-dotslash.js yarn jest .github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js --runInBand ``` 13 tests and 15 snapshots pass. Reviewed By: zeyap Differential Revision: D113032768 Pulled By: cipolleschi fbshipit-source-id: df5f493603e6c25157fd7660b54aa409dcb742e7
400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const {REPO_ROOT} = require('../shared/consts');
|
|
const {getWithCurl} = require('./utils/curl-utils');
|
|
const {
|
|
isHttpProvider,
|
|
processDotSlashFileInPlace,
|
|
validateDotSlashArtifactData,
|
|
} = require('./utils/dotslash-utils');
|
|
const {
|
|
FIRST_PARTY_DOTSLASH_FILES,
|
|
} = require('./write-dotslash-release-asset-urls');
|
|
const {Octokit} = require('@octokit/rest');
|
|
const path = require('node:path');
|
|
const {parseArgs} = require('node:util');
|
|
const nullthrows = require('nullthrows');
|
|
|
|
// Upload POSTs are not preserved when GitHub redirects from the former owner.
|
|
const GITHUB_OWNER = 'react';
|
|
const GITHUB_REPO = 'react-native';
|
|
|
|
/*::
|
|
import type {DotSlashProvider, DotSlashHttpProvider, DotSlashArtifactInfo} from './utils/dotslash-utils';
|
|
import type {IOctokit} from './utils/octokit-utils';
|
|
|
|
type GitHubReleaseAsset = {id: string, ...};
|
|
type ReleaseAssetMap = ReadonlyMap<string, GitHubReleaseAsset>;
|
|
|
|
type ReleaseInfo = Readonly<{
|
|
releaseId: string,
|
|
releaseTag: string,
|
|
existingAssetsByName: ReleaseAssetMap,
|
|
}>;
|
|
|
|
type ExecutionOptions = Readonly<{
|
|
force: boolean,
|
|
dryRun: boolean,
|
|
}>;
|
|
*/
|
|
|
|
async function main() {
|
|
const {
|
|
positionals: [version],
|
|
values: {help, token, releaseId, force, dryRun},
|
|
} = parseArgs({
|
|
allowPositionals: true,
|
|
options: {
|
|
token: {type: 'string'},
|
|
releaseId: {type: 'string'},
|
|
force: {type: 'boolean', default: false},
|
|
dryRun: {type: 'boolean', default: false},
|
|
help: {type: 'boolean'},
|
|
} /*:: as {[string]: util$ParseArgsOption} */,
|
|
});
|
|
|
|
if (help) {
|
|
console.log(`
|
|
Usage: node ./scripts/releases/upload-release-assets-for-dotslash.js <version> --release_id <id> --token <github-token> [--force] [--dry-run]
|
|
|
|
Scans first-party DotSlash files in the repo for URLs referencing assets of
|
|
an upcoming release, and uploads the actual assets to the GitHub release
|
|
identified by the given release ID.
|
|
|
|
Options:
|
|
<version> The version of the release to upload assets for, with or
|
|
without the 'v' prefix.
|
|
--dry-run Do not upload release assets.
|
|
--force Overwrite existing release assets.
|
|
--release_id The ID of the GitHub release to upload assets to.
|
|
--token A GitHub token with write access to the release.
|
|
`);
|
|
return;
|
|
}
|
|
|
|
if (version == null) {
|
|
throw new Error('Missing version argument');
|
|
}
|
|
|
|
await uploadReleaseAssetsForDotSlashFiles({
|
|
version,
|
|
token,
|
|
releaseId,
|
|
force,
|
|
dryRun,
|
|
});
|
|
}
|
|
|
|
async function uploadReleaseAssetsForDotSlashFiles(
|
|
{version, token, releaseId, force = false, dryRun = false} /*: {
|
|
version: string,
|
|
token: string,
|
|
releaseId: string,
|
|
force?: boolean,
|
|
dryRun?: boolean,
|
|
} */,
|
|
) /*: Promise<void> */ {
|
|
const releaseTag = version.startsWith('v') ? version : `v${version}`;
|
|
const octokit = new Octokit({auth: token});
|
|
const existingAssetsByName = await getReleaseAssetMap(
|
|
{
|
|
releaseId,
|
|
},
|
|
octokit,
|
|
);
|
|
const releaseInfo = {
|
|
releaseId,
|
|
releaseTag,
|
|
existingAssetsByName,
|
|
};
|
|
const executionOptions = {
|
|
force,
|
|
dryRun,
|
|
};
|
|
for (const filename of FIRST_PARTY_DOTSLASH_FILES) {
|
|
await uploadReleaseAssetsForDotSlashFile(
|
|
filename,
|
|
releaseInfo,
|
|
executionOptions,
|
|
octokit,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all release assets for a particular GitHub release ID, and return them
|
|
* as a map keyed by asset names.
|
|
*/
|
|
async function getReleaseAssetMap(
|
|
{releaseId} /*: {
|
|
releaseId: string,
|
|
} */,
|
|
octokit /*: IOctokit */,
|
|
) /*: Promise<ReleaseAssetMap> */ {
|
|
const existingAssets = await octokit.repos.listReleaseAssets({
|
|
owner: GITHUB_OWNER,
|
|
repo: GITHUB_REPO,
|
|
release_id: releaseId,
|
|
});
|
|
return new Map(existingAssets.data.map(asset => [asset.name, asset]));
|
|
}
|
|
|
|
/**
|
|
* Given a first-party DotSlash file path in the repo, reupload the referenced
|
|
* binaries from the upstream provider (typically: Meta CDN) to the draft
|
|
* release (hosted on GitHub).
|
|
*/
|
|
async function uploadReleaseAssetsForDotSlashFile(
|
|
filename /*: string */,
|
|
releaseInfo /*: ReleaseInfo */,
|
|
executionOptions /*: ExecutionOptions */,
|
|
octokit /*: IOctokit */,
|
|
) /*: Promise<void> */ {
|
|
const fullPath = path.resolve(REPO_ROOT, filename);
|
|
console.log(`Uploading assets for ${filename}...`);
|
|
await processDotSlashFileInPlace(
|
|
fullPath,
|
|
async (providers, suggestedFilename, artifactInfo) => {
|
|
await fetchUpstreamAssetAndUploadToRelease(
|
|
{
|
|
providers,
|
|
suggestedFilename,
|
|
artifactInfo,
|
|
dotslashFilename: filename,
|
|
},
|
|
releaseInfo,
|
|
executionOptions,
|
|
octokit,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Given a description of a DotSlash artifact for a particular platform,
|
|
* infers the upstream URL ( = where the binary is currently available) and
|
|
* release asset URL ( = where the binary will be hosted after the release),
|
|
* then downloads the asset from the upstream URL and uploads it to GitHub
|
|
* at the desired URL.
|
|
*/
|
|
async function fetchUpstreamAssetAndUploadToRelease(
|
|
{
|
|
providers,
|
|
// NOTE: We mostly ignore suggestedFilename in favour of reading the actual asset URLs
|
|
suggestedFilename,
|
|
artifactInfo,
|
|
dotslashFilename,
|
|
} /*: {
|
|
providers: ReadonlyArray<DotSlashProvider>,
|
|
suggestedFilename: string,
|
|
artifactInfo: DotSlashArtifactInfo,
|
|
dotslashFilename: string,
|
|
} */,
|
|
releaseInfo /*: ReleaseInfo */,
|
|
executionOptions /*: ExecutionOptions */,
|
|
octokit /*: IOctokit */,
|
|
) {
|
|
const targetReleaseAssetInfo = providers
|
|
.map(provider => parseReleaseAssetInfo(provider, releaseInfo.releaseTag))
|
|
.find(Boolean);
|
|
if (targetReleaseAssetInfo == null) {
|
|
console.log(
|
|
`[${suggestedFilename} (suggested)] DotSlash file does not reference any release URLs for this asset - ignoring.`,
|
|
);
|
|
return;
|
|
}
|
|
const upstreamProvider /*: ?DotSlashHttpProvider */ = providers
|
|
.filter(isHttpProvider)
|
|
.find(provider => !parseReleaseAssetInfo(provider, releaseInfo.releaseTag));
|
|
if (upstreamProvider == null) {
|
|
throw new Error(
|
|
`No upstream URL found for release asset ${targetReleaseAssetInfo.name}`,
|
|
);
|
|
}
|
|
const existingAsset = releaseInfo.existingAssetsByName.get(
|
|
targetReleaseAssetInfo.name,
|
|
);
|
|
if (existingAsset && !executionOptions.force) {
|
|
console.log(
|
|
`[${targetReleaseAssetInfo.name}] Skipping existing release asset...`,
|
|
);
|
|
return;
|
|
}
|
|
await maybeDeleteExistingReleaseAsset(
|
|
{
|
|
name: targetReleaseAssetInfo.name,
|
|
existingAsset,
|
|
},
|
|
executionOptions,
|
|
octokit,
|
|
);
|
|
const {data, contentType} = await fetchAndValidateUpstreamAsset({
|
|
name: targetReleaseAssetInfo.name,
|
|
url: upstreamProvider.url,
|
|
artifactInfo,
|
|
});
|
|
if (executionOptions.dryRun) {
|
|
console.log(
|
|
`[${targetReleaseAssetInfo.name}] Dry run: Not uploading to release.`,
|
|
);
|
|
return;
|
|
}
|
|
await uploadAndVerifyReleaseAsset(
|
|
{
|
|
name: targetReleaseAssetInfo.name,
|
|
url: targetReleaseAssetInfo.url,
|
|
data,
|
|
contentType,
|
|
releaseId: releaseInfo.releaseId,
|
|
dotslashFilename,
|
|
},
|
|
octokit,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the given DotSlash artifact provider refers to an asset URL
|
|
* that is part of the current release. Returns the asset name as well as the
|
|
* full URL if that is the case. Returns null otherwise.
|
|
*/
|
|
function parseReleaseAssetInfo(
|
|
provider /*: DotSlashProvider */,
|
|
releaseTag /*: string */,
|
|
) /*:
|
|
?{
|
|
name: string,
|
|
url: string,
|
|
}
|
|
*/ {
|
|
const releaseAssetPrefix = `https://github.com/facebook/react-native/releases/download/${encodeURIComponent(releaseTag)}/`;
|
|
|
|
if (isHttpProvider(provider) && provider.url.startsWith(releaseAssetPrefix)) {
|
|
return {
|
|
name: decodeURIComponent(provider.url.slice(releaseAssetPrefix.length)),
|
|
url: provider.url,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Deletes the specified release asset if it exists, unless we are in dry run
|
|
* mode (in which case this is a noop).
|
|
*/
|
|
async function maybeDeleteExistingReleaseAsset(
|
|
{name, existingAsset} /*: {
|
|
name: string,
|
|
existingAsset: ?GitHubReleaseAsset,
|
|
}
|
|
*/,
|
|
{dryRun} /*: ExecutionOptions */,
|
|
octokit /*: IOctokit */,
|
|
) /*: Promise<void> */ {
|
|
if (!existingAsset) {
|
|
return;
|
|
}
|
|
if (dryRun) {
|
|
console.log(`[${name}] Dry run: Not deleting existing release asset.`);
|
|
return;
|
|
}
|
|
console.log(`[${name}] Deleting existing release asset...`);
|
|
await octokit.repos.deleteReleaseAsset({
|
|
owner: GITHUB_OWNER,
|
|
repo: GITHUB_REPO,
|
|
asset_id: existingAsset.id,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Given a description of a DotSlash artifact, downloads it and verifies its
|
|
* size and hash (similarly to how DotSlash itself would do it after release).
|
|
*/
|
|
async function fetchAndValidateUpstreamAsset(
|
|
{name, url, artifactInfo} /*: {
|
|
name: string,
|
|
url: string,
|
|
artifactInfo: DotSlashArtifactInfo,
|
|
} */,
|
|
) /*: Promise<{
|
|
data: Buffer,
|
|
contentType: string,
|
|
}> */ {
|
|
console.log(`[${name}] Downloading from ${url}...`);
|
|
// NOTE: Using curl because we have seen issues with fetch() on GHA
|
|
// and the Meta CDN. ¯\_(ツ)_/¯
|
|
const {data, contentType} = await getWithCurl(url);
|
|
console.log(`[${name}] Validating download...`);
|
|
await validateDotSlashArtifactData(data, artifactInfo);
|
|
return {
|
|
data,
|
|
contentType: contentType ?? 'application/octet-stream',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Uploads the specified asset to a GitHub release.
|
|
*
|
|
* By the time we call this function, we have already commited (and published!)
|
|
* a reference to the asset's eventual URL, so we also verify that the URL path
|
|
* hasn't changed in the process.
|
|
*/
|
|
async function uploadAndVerifyReleaseAsset(
|
|
{name, data, contentType, url, releaseId, dotslashFilename} /*: {
|
|
name: string,
|
|
data: Buffer,
|
|
contentType: string,
|
|
url: string,
|
|
releaseId: string,
|
|
dotslashFilename: string,
|
|
}
|
|
*/,
|
|
octokit /*: IOctokit */,
|
|
) /*: Promise<void> */ {
|
|
console.log(`[${name}] Uploading to release...`);
|
|
const {
|
|
data: {browser_download_url},
|
|
} = await octokit.repos.uploadReleaseAsset({
|
|
owner: GITHUB_OWNER,
|
|
repo: GITHUB_REPO,
|
|
release_id: releaseId,
|
|
name,
|
|
data,
|
|
headers: {
|
|
'content-type': contentType,
|
|
},
|
|
});
|
|
|
|
// Once uploaded, check that the name didn't get mangled.
|
|
const actualUrlPathname = new URL(browser_download_url).pathname;
|
|
const actualAssetName = decodeURIComponent(
|
|
nullthrows(/[^/]*$/.exec(actualUrlPathname))[0],
|
|
);
|
|
if (actualAssetName !== name) {
|
|
throw new Error(
|
|
`Asset name was changed while uploading to the draft release: expected ${name}, got ${actualAssetName}. ` +
|
|
`${dotslashFilename} has already been published to npm with the following URL, which will not work when the release is published on GitHub: ${url}`,
|
|
);
|
|
}
|
|
console.log(`[${name}] Uploaded to ${browser_download_url}`);
|
|
}
|
|
|
|
module.exports = {
|
|
uploadReleaseAssetsForDotSlashFiles,
|
|
getReleaseAssetMap,
|
|
uploadReleaseAssetsForDotSlashFile,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
void main();
|
|
}
|