From 1cf2f9038a864c32fa92dc5e9123f005d2b81231 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 2 Sep 2025 16:27:04 +0100 Subject: [PATCH 01/10] DotSlash utils Summary: Adds utilities for working with DotSlash files in React Native's release scripts. The main ones are: * `processDotSlashFileInPlace` for reading/manipulating the providers (source URLs) and artifact descriptions, while preserving formatting and comments. * `validateAndParseDotSlashFile` for reading the contents of a DotSlash in a reasonably type-safe manner (by spawning `dotslash` itself). * `validateDotSlashArtifactData` for verifying the actual binary data described by a DotSlash file. * `dangerouslyResignGeneratedFile` for fixing up the generated file signature on a DotSlash file after manipulating it. Includes unit tests. --- package.json | 2 + .../__snapshots__/dotslash-utils-test.js.snap | 102 +++++++ .../utils/__tests__/dotslash-utils-test.js | 265 ++++++++++++++++++ scripts/releases/utils/dotslash-utils.js | 215 ++++++++++++++ yarn.lock | 62 +++- 5 files changed, 641 insertions(+), 5 deletions(-) create mode 100644 scripts/releases/utils/__tests__/__snapshots__/dotslash-utils-test.js.snap create mode 100644 scripts/releases/utils/__tests__/dotslash-utils-test.js create mode 100644 scripts/releases/utils/dotslash-utils.js diff --git a/package.json b/package.json index ecaaf19ed11d57..4cd060a7360c88 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "eslint-plugin-react-native": "^4.0.0", "eslint-plugin-redundant-undefined": "^0.4.0", "eslint-plugin-relay": "^1.8.3", + "fb-dotslash": "^0.5.8", "flow-api-translator": "0.32.0", "flow-bin": "^0.280.0", "glob": "^7.1.1", @@ -93,6 +94,7 @@ "jest-diff": "^29.7.0", "jest-junit": "^16.0.0", "jest-snapshot": "^29.7.0", + "jsonc-parser": "^3.3.1", "markdownlint-cli2": "^0.17.2", "markdownlint-rule-relative-links": "^3.0.0", "memfs": "^4.7.7", diff --git a/scripts/releases/utils/__tests__/__snapshots__/dotslash-utils-test.js.snap b/scripts/releases/utils/__tests__/__snapshots__/dotslash-utils-test.js.snap new file mode 100644 index 00000000000000..d090e4b0b44794 --- /dev/null +++ b/scripts/releases/utils/__tests__/__snapshots__/dotslash-utils-test.js.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`processDotSlashFileInPlace comments, multiple platforms, providers + replacement: contents after processing 1`] = ` +"#!/usr/bin/env dotslash +// Top-level comment +{ + \\"name\\": \\"test\\", + \\"platforms\\": { + // Comment on linux-x86_64 + \\"linux-x86_64\\": { + \\"size\\": 0, + \\"hash\\": \\"sha256\\", + \\"digest\\": \\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\\", + \\"providers\\": [ + { + \\"url\\": \\"https://example.com/replaced/test-linux-x86_64.tar.gz\\" + } + ], + \\"format\\": \\"tar.gz\\", + \\"path\\": \\"bar\\" + }, + // Comment on macos-aarch64 + \\"macos-aarch64\\": { + \\"size\\": 0, + \\"hash\\": \\"sha256\\", + \\"digest\\": \\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\\", + \\"providers\\": [ + { + \\"url\\": \\"https://primary.example.com/foo-mac.zip\\", + \\"weight\\": 3 + }, + { + \\"url\\": \\"https://mirror1.example.com/foo-mac.zip\\", + \\"weight\\": 1 + }, + { + \\"url\\": \\"https://example.com/added/test-macos-aarch64.zip\\" + } + ], + \\"format\\": \\"zip\\", + \\"path\\": \\"bar\\", + } + } +}" +`; + +exports[`processDotSlashFileInPlace comments, multiple platforms, providers + replacement: transformProviders calls 1`] = ` +Array [ + Array [ + Array [ + Object { + "url": "https://primary.example.com/foo-linux.tar.gz", + "weight": 3, + }, + Object { + "url": "https://mirror1.example.com/foo-linux.tar.gz", + "weight": 1, + }, + Object { + "url": "https://mirror2.example.com/foo-linux.tar.gz", + "weight": 1, + }, + Object { + "url": "https://mirror3.example.com/foo-linux.tar.gz", + "weight": 1, + }, + ], + "test-linux-x86_64.tar.gz", + Object { + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "hash": "sha256", + "size": 0, + }, + ], + Array [ + Array [ + Object { + "url": "https://primary.example.com/foo-mac.zip", + "weight": 3, + }, + Object { + "url": "https://mirror1.example.com/foo-mac.zip", + "weight": 1, + }, + ], + "test-macos-aarch64.zip", + Object { + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "hash": "sha256", + "size": 0, + }, + ], +] +`; + +exports[`validateDotSlashArtifactData blake3 failure on digest mismatch 1`] = `"blake3 mismatch: expected 2623f14eac39a9cc7b211cda9c52bcb9949ccd63aed4040a6a1a9f5f9b9431fa, got af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"`; + +exports[`validateDotSlashArtifactData blake3 failure on size mismatch 1`] = `"size mismatch: expected 1, got 0"`; + +exports[`validateDotSlashArtifactData sha256 failure on digest mismatch 1`] = `"sha256 mismatch: expected 558b2587b199594ac439b9464e14ea72429bf6998c4fbfa941c1cf89244c0b3e, got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`; + +exports[`validateDotSlashArtifactData sha256 failure on size mismatch 1`] = `"size mismatch: expected 1, got 0"`; diff --git a/scripts/releases/utils/__tests__/dotslash-utils-test.js b/scripts/releases/utils/__tests__/dotslash-utils-test.js new file mode 100644 index 00000000000000..9386e93d2adab4 --- /dev/null +++ b/scripts/releases/utils/__tests__/dotslash-utils-test.js @@ -0,0 +1,265 @@ +/** + * 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 { + dangerouslyResignGeneratedFile, + processDotSlashFileInPlace, + validateAndParseDotSlashFile, + validateDotSlashArtifactData, +} = require('../dotslash-utils'); +const fs = require('fs'); + +jest.useRealTimers(); + +let tmpDir: string; + +beforeEach(() => { + tmpDir = fs.mkdtempSync('dotslash-utils-test-'); +}); + +afterEach(() => { + fs.rmSync(tmpDir, {recursive: true}); +}); + +describe('validateAndParseDotSlashFile', () => { + test('succeeds on a minimal valid DotSlash file', async () => { + const contents = `#!/usr/bin/env dotslash +{ + "name": "test", + "platforms": {} +}`; + await fs.promises.writeFile(`${tmpDir}/entry-point`, contents); + await expect( + validateAndParseDotSlashFile(`${tmpDir}/entry-point`), + ).resolves.toEqual({ + name: 'test', + platforms: {}, + }); + }); +}); + +describe('processDotSlashFileInPlace', () => { + test('succeeds on a minimal valid DotSlash file', async () => { + const transformProviders = jest.fn(); + const contentsBefore = `#!/usr/bin/env dotslash +{ + "name": "test", + "platforms": {} +}`; + await fs.promises.writeFile(`${tmpDir}/entry-point`, contentsBefore); + await processDotSlashFileInPlace( + `${tmpDir}/entry-point`, + transformProviders, + ); + expect(transformProviders).not.toHaveBeenCalled(); + expect(fs.readFileSync(`${tmpDir}/entry-point`, 'utf8')).toBe( + contentsBefore, + ); + }); + + test('comments, multiple platforms, providers + replacement', async () => { + const transformProviders = jest.fn(); + const contentsBefore = `#!/usr/bin/env dotslash +// Top-level comment +{ + "name": "test", + "platforms": { + // Comment on linux-x86_64 + "linux-x86_64": { + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "providers": [ + {"url": "https://primary.example.com/foo-linux.tar.gz", "weight": 3}, + {"url": "https://mirror1.example.com/foo-linux.tar.gz", "weight": 1}, + {"url": "https://mirror2.example.com/foo-linux.tar.gz", "weight": 1}, + {"url": "https://mirror3.example.com/foo-linux.tar.gz", "weight": 1} + ], + "format": "tar.gz", + "path": "bar" + }, + // Comment on macos-aarch64 + "macos-aarch64": { + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "providers": [ + {"url": "https://primary.example.com/foo-mac.zip", "weight": 3}, + {"url": "https://mirror1.example.com/foo-mac.zip", "weight": 1}, + ], + "format": "zip", + "path": "bar", + } + } +}`; + fs.writeFileSync(`${tmpDir}/entry-point`, contentsBefore); + transformProviders.mockImplementationOnce( + (providers, suggestedFilename) => { + return [ + { + url: + 'https://example.com/replaced/' + + encodeURIComponent(suggestedFilename), + }, + ]; + }, + ); + transformProviders.mockImplementationOnce( + (providers, suggestedFilename) => { + return [ + ...providers, + { + url: + 'https://example.com/added/' + + encodeURIComponent(suggestedFilename), + }, + ]; + }, + ); + await processDotSlashFileInPlace( + `${tmpDir}/entry-point`, + transformProviders, + ); + expect(transformProviders.mock.calls).toMatchSnapshot( + 'transformProviders calls', + ); + expect(fs.readFileSync(`${tmpDir}/entry-point`, 'utf8')).toMatchSnapshot( + 'contents after processing', + ); + }); + + test('fails on an invalid DotSlash file (no shebang line)', async () => { + const transformProviders = jest.fn(); + const contentsBefore = `{ + "name": "test", + "platforms": {} +}`; + fs.writeFileSync(`${tmpDir}/entry-point`, contentsBefore); + await expect( + processDotSlashFileInPlace(`${tmpDir}/entry-point`, transformProviders), + ).rejects.toThrow(); + expect(transformProviders).not.toHaveBeenCalled(); + expect(fs.readFileSync(`${tmpDir}/entry-point`, 'utf8')).toBe( + contentsBefore, + ); + }); + + test('fails on an invalid DotSlash file (no platforms)', async () => { + const transformProviders = jest.fn(); + const contentsBefore = `#!/usr/bin/env dotslash +{ + "name": "test" +}`; + fs.writeFileSync(`${tmpDir}/entry-point`, contentsBefore); + await expect( + processDotSlashFileInPlace(`${tmpDir}/entry-point`, transformProviders), + ).rejects.toThrow(); + expect(transformProviders).not.toHaveBeenCalled(); + expect(fs.readFileSync(`${tmpDir}/entry-point`, 'utf8')).toBe( + contentsBefore, + ); + }); +}); + +describe('dangerouslyResignGeneratedFile', () => { + test('successfully re-signs a file', async () => { + const contentsBefore = `#!/usr/bin/env dotslash +// @${'generated SignedSource<<00000000000000000000000000000000' + '>>'} +{ + "name": "test", + "platforms": {} +}`; + fs.writeFileSync(`${tmpDir}/entry-point`, contentsBefore); + await dangerouslyResignGeneratedFile(`${tmpDir}/entry-point`); + expect(fs.readFileSync(`${tmpDir}/entry-point`, 'utf8')) + .toBe(`#!/usr/bin/env dotslash +// @${'generated SignedSource<<5ccb2839bdbd070dffcda52c6aa922a3' + '>>'} +{ + "name": "test", + "platforms": {} +}`); + }); +}); + +describe('validateDotSlashArtifactData', () => { + test('blake3 success', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'blake3', + digest: + 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262', + size: 0, + }), + ).resolves.toBeUndefined(); + }); + + test('blake3 failure on size mismatch', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'blake3', + digest: + 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262', + size: 1, + }), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('blake3 failure on digest mismatch', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'blake3', + digest: + 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262' + .split('') + .reverse() + .join(''), + size: 0, + }), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('sha256 success', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'sha256', + digest: + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + size: 0, + }), + ).resolves.toBeUndefined(); + }); + + test('sha256 failure on size mismatch', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'sha256', + digest: + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + size: 1, + }), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('sha256 failure on digest mismatch', async () => { + await expect( + validateDotSlashArtifactData(Buffer.from([]), { + hash: 'sha256', + digest: + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + .split('') + .reverse() + .join(''), + size: 0, + }), + ).rejects.toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/scripts/releases/utils/dotslash-utils.js b/scripts/releases/utils/dotslash-utils.js new file mode 100644 index 00000000000000..f74a73a5e9bf04 --- /dev/null +++ b/scripts/releases/utils/dotslash-utils.js @@ -0,0 +1,215 @@ +/** + * 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 dotslash = require('fb-dotslash'); +const {promises: fs} = require('fs'); +// $FlowFixMe[untyped-import] TODO: add types for jsonc-parser +const {applyEdits, modify, parse} = require('jsonc-parser'); +const os = require('os'); +const path = require('path'); +const signedsource = require('signedsource'); +const execFile = require('util').promisify(require('child_process').execFile); + +/*:: +export type DotSlashHttpProvider = { + type?: 'http', + url: string, +}; + +export type DotSlashProvider = DotSlashHttpProvider | { + type: 'github-release', + repo: string, + tag: string, + name: string, +}; + +type DotSlashPlatformSpec = { + providers: DotSlashProvider[], + hash: 'blake3' | 'sha256', + digest: string, + size: number, + format?: string, + ... +}; + +export type DotSlashArtifactInfo = $ReadOnly<{ + size: number, + hash: 'blake3' | 'sha256', + digest: string, + ... +}>; + +type JSONCFormattingOptions = { + tabSize?: number, + insertSpaces?: boolean, + eol?: string, +}; + +type DotSlashProvidersTransformFn = ( + providers: $ReadOnlyArray, + suggestedFilename: string, + artifactInfo: DotSlashArtifactInfo, +) => ?$ReadOnlyArray | Promise>; +*/ + +const DEFAULT_FORMATTING_OPTIONS /*: $ReadOnly */ = { + tabSize: 4, + insertSpaces: true, + eol: '\n', +}; + +/** + * Process a DotSlash file and call a callback with the providers for each platform. + * The callback can return a new providers array to update the file. + * The function will preserve formatting and comments in the file (except any comments + * that are within the providers array). + */ +async function processDotSlashFileInPlace( + filename /*: string */, + transformProviders /*: DotSlashProvidersTransformFn */, + formattingOptions /*: $ReadOnly */ = DEFAULT_FORMATTING_OPTIONS, +) /*: Promise */ { + // Validate the file using `dotslash` itself so we can be reasonably sure that it conforms + // to the expected format. + await validateAndParseDotSlashFile(filename); + + const originalContents = await fs.readFile(filename, 'utf-8'); + const [shebang, originalContentsJson] = + splitShebangFromContents(originalContents); + const json = parse(originalContentsJson); + let intermediateContentsJson = originalContentsJson; + for (const [platform, platformSpec] of Object.entries(json.platforms) /*:: + as $ReadOnlyArray<[string, DotSlashPlatformSpec]> + */) { + const providers = platformSpec.providers; + const suggestedFilename = + `${sanitizeFileNameComponent(json.name)}-${platform}` + + (platformSpec.format != null ? `.${platformSpec.format}` : ''); + const {hash, digest, size} = platformSpec; + const newProviders = + (await transformProviders(providers, suggestedFilename, { + hash, + digest, + size, + })) ?? providers; + if (newProviders !== providers) { + const edits = modify( + intermediateContentsJson, + ['platforms', platform, 'providers'], + newProviders, + { + formattingOptions, + }, + ); + intermediateContentsJson = applyEdits(intermediateContentsJson, edits); + } + } + if (originalContentsJson !== intermediateContentsJson) { + await fs.writeFile(filename, shebang + intermediateContentsJson); + // Validate the modified file to make sure we haven't broken it. + await validateAndParseDotSlashFile(filename); + } +} + +function sanitizeFileNameComponent( + fileNameComponent /*: string */, +) /*: string */ { + return fileNameComponent.replace(/[^a-zA-Z0-9.]/g, '.'); +} + +function splitShebangFromContents( + contents /*: string */, +) /*: [string, string] */ { + const shebangMatch = contents.match(/^#!.*\n/); + const shebang = shebangMatch ? shebangMatch[0] : ''; + const contentsWithoutShebang = shebang + ? contents.substring(shebang.length) + : contents; + return [shebang, contentsWithoutShebang]; +} + +/** + * Validate a DotSlash file and return its parsed contents. + * Throws an error if the file is not valid. + * + * See https://dotslash-cli.com/docs/dotslash-file/ + */ +async function validateAndParseDotSlashFile( + filename /*: string */, +) /*: mixed */ { + const {stdout} = await execFile(dotslash, ['--', 'parse', filename]); + return JSON.parse(stdout); +} + +/** + * Re-sign a file previously signed with `signedsource`. Use with caution. + */ +async function dangerouslyResignGeneratedFile( + filename /*: string */, +) /*: Promise */ { + const GENERATED = '@' + 'generated'; + const PATTERN = new RegExp(`${GENERATED} (?:SignedSource<<([a-f0-9]{32})>>)`); + const originalContents = await fs.readFile(filename, 'utf-8'); + + const newContents = signedsource.signFile( + originalContents.replace(PATTERN, signedsource.getSigningToken()), + ); + await fs.writeFile(filename, newContents); +} + +/** + * Checks that the given buffer matches the given hash and size. This is + * equivalent to the validation that DotSlash performs after fetching a blob + * and before extracting/executing it. + */ +async function validateDotSlashArtifactData( + data /*: Buffer */, + artifactInfo /*: DotSlashArtifactInfo */, +) /*: Promise */ { + const {digest: expectedDigest, hash, size} = artifactInfo; + if (data.length !== size) { + throw new Error(`size mismatch: expected ${size}, got ${data.length}`); + } + const hashFunction = hash === 'blake3' ? 'b3sum' : 'sha256'; + + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'validate-artifact-hash-'), + ); + try { + const tempFile = path.join(tempDir, 'data'); + await fs.writeFile(tempFile, data); + const {stdout} = await execFile(dotslash, ['--', hashFunction, tempFile]); + const actualDigest = stdout.trim(); + if (actualDigest !== expectedDigest) { + throw new Error( + `${hash} mismatch: expected ${expectedDigest}, got ${actualDigest}`, + ); + } + } finally { + await fs.rm(tempDir, {recursive: true, force: true}); + } +} + +function isHttpProvider( + provider /*: DotSlashProvider */, +) /*: implies provider is DotSlashHttpProvider */ { + return provider.type === 'http' || provider.type == null; +} + +module.exports = { + DEFAULT_FORMATTING_OPTIONS, + dangerouslyResignGeneratedFile, + isHttpProvider, + processDotSlashFileInPlace, + validateAndParseDotSlashFile, + validateDotSlashArtifactData, +}; diff --git a/yarn.lock b/yarn.lock index b618a2c8bc4ab6..5677596ce7ee76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== @@ -65,6 +65,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.26.9": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.25.9", "@babel/helper-annotate-as-pure@^7.27.1": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -223,6 +234,13 @@ dependencies: "@babel/types" "^7.28.0" +"@babel/parser@^7.26.9", "@babel/parser@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" + integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== + dependencies: + "@babel/types" "^7.28.2" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" @@ -1016,7 +1034,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.25.0", "@babel/template@^7.25.9", "@babel/template@^7.27.2", "@babel/template@^7.3.3": +"@babel/template@^7.25.0", "@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.2", "@babel/template@^7.3.3": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== @@ -1025,7 +1043,33 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9", "@babel/traverse@^7.26.8", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a" + integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.9" + "@babel/parser" "^7.26.9" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9", "@babel/traverse@^7.26.8": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a" + integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.9" + "@babel/parser" "^7.26.9" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b" integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== @@ -1046,6 +1090,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@babel/types@^7.26.9", "@babel/types@^7.28.2": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -4483,7 +4535,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fb-dotslash@0.5.8: +fb-dotslash@0.5.8, fb-dotslash@^0.5.8: version "0.5.8" resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5" integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA== @@ -6061,7 +6113,7 @@ jsonc-eslint-parser@^2.3.0: espree "^9.0.0" semver "^7.3.5" -jsonc-parser@3.3.1: +jsonc-parser@3.3.1, jsonc-parser@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== From d9599232f471942d687c3d2ccd7075c0c54923c0 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 2 Sep 2025 16:45:36 +0100 Subject: [PATCH 02/10] curl utils Summary: Adds the `getWithCurl` utility which uses `curl` to fetch some resource based on its URL. This is useful in cases where the built-in `fetch()` isn't working properly, such as when fetching assets from the Meta CDN in a GitHub Actions worker. Comes with unit tests. --- package.json | 1 + .../utils/__tests__/curl-utils-test.js | 62 +++++++++++++++++++ scripts/releases/utils/curl-utils.js | 56 +++++++++++++++++ yarn.lock | 7 +++ 4 files changed, 126 insertions(+) create mode 100644 scripts/releases/utils/__tests__/curl-utils-test.js create mode 100644 scripts/releases/utils/curl-utils.js diff --git a/package.json b/package.json index 4cd060a7360c88..ecac9c8e10d672 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@babel/preset-env": "^7.25.3", "@babel/preset-flow": "^7.24.7", "@electron/packager": "^18.3.6", + "@expo/spawn-async": "^1.7.2", "@jest/create-cache-key-function": "^29.7.0", "@microsoft/api-extractor": "^7.52.2", "@react-native/metro-babel-transformer": "0.82.0-main", diff --git a/scripts/releases/utils/__tests__/curl-utils-test.js b/scripts/releases/utils/__tests__/curl-utils-test.js new file mode 100644 index 00000000000000..13dbdb248efef0 --- /dev/null +++ b/scripts/releases/utils/__tests__/curl-utils-test.js @@ -0,0 +1,62 @@ +/** + * 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 {getWithCurl} = require('../curl-utils'); +const http = require('http'); + +let server, serverUrl; + +beforeEach(async () => { + await new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + if (req.url !== '/') { + res.writeHead(404); + res.end(); + return; + } + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('Hello World\n'); + }); + server.on('error', reject); + server.listen(0, 'localhost', () => { + const {port} = server.address(); + serverUrl = `http://localhost:${port}`; + resolve(); + }); + }); +}); + +afterEach(async () => { + await new Promise((resolve, reject) => { + server.close(err => { + if (err) { + reject(err); + } + resolve(); + }); + }); +}); + +describe('getWithCurl', () => { + test('success', async () => { + await expect(getWithCurl(serverUrl)).resolves.toEqual({ + data: Buffer.from('Hello World\n'), + headers: expect.objectContaining({ + 'content-type': ['text/plain'], + }), + }); + }); + + test('fails on 404', async () => { + await expect(getWithCurl(serverUrl + '/error')).rejects.toThrowError(); + }); +}); diff --git a/scripts/releases/utils/curl-utils.js b/scripts/releases/utils/curl-utils.js new file mode 100644 index 00000000000000..bbe770855e1c5e --- /dev/null +++ b/scripts/releases/utils/curl-utils.js @@ -0,0 +1,56 @@ +/** + * 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'; + +// $FlowFixMe[untyped-import] TODO: add types for @expo/spawn-async +const spawnAsync = require('@expo/spawn-async'); +const {promises: fs} = require('fs'); +const os = require('os'); +const path = require('path'); + +/*:: +type CurlResult = { + data: Buffer, + headers: {[string]: Array}, +}; +*/ + +async function getWithCurl(url /*: string */) /*: Promise */ { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'get-with-curl-')); + const tempFile = path.join(tempDir, 'data'); + try { + const { + output: [curlStdout], + } = await spawnAsync( + 'curl', + [ + '--silent', + '--location', + '--output', + tempFile, + url, + '--write-out', + '%{header_json}', + '--fail', + ], + {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}, + ); + const data = await fs.readFile(tempFile); + const headers = JSON.parse(curlStdout); + return {data, headers}; + } finally { + await fs.rm(tempDir, {recursive: true, force: true}); + } +} + +module.exports = { + getWithCurl, +}; diff --git a/yarn.lock b/yarn.lock index 5677596ce7ee76..1adc080e5ff18d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,6 +1244,13 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@expo/spawn-async@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58" + integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew== + dependencies: + cross-spawn "^7.0.3" + "@fastify/busboy@^2.0.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" From c49c683a590a11ef8c2668fce53c691bff4cb291 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 2 Sep 2025 16:45:36 +0100 Subject: [PATCH 03/10] Jest snapshot utils Summary: Adds some reusable Jest snapshot serializers used later in this PR. --- package.json | 1 + scripts/releases/__tests__/snapshot-utils.js | 92 ++++++++++++++++++++ scripts/releases/utils/curl-utils.js | 15 ++++ 3 files changed, 108 insertions(+) create mode 100644 scripts/releases/__tests__/snapshot-utils.js diff --git a/package.json b/package.json index ecac9c8e10d672..e5f7d3571b0128 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@tsconfig/node22": "22.0.2", "@types/react": "^19.1.0", "@typescript-eslint/parser": "^8.36.0", + "ansi-regex": "^5.0.1", "ansi-styles": "^4.2.1", "babel-plugin-minify-dead-code-elimination": "^0.5.2", "babel-plugin-syntax-hermes-parser": "0.32.0", diff --git a/scripts/releases/__tests__/snapshot-utils.js b/scripts/releases/__tests__/snapshot-utils.js new file mode 100644 index 00000000000000..5a0103a88c2efa --- /dev/null +++ b/scripts/releases/__tests__/snapshot-utils.js @@ -0,0 +1,92 @@ +/** + * 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'; + +import ansiRegex from 'ansi-regex'; + +const { + getTempDirPatternForTests: getCurlTempDirPattern, +} = require('../utils/curl-utils'); +const invariant = require('invariant'); + +/** + * Returns a Jest snapshot serializer that replaces the given token or pattern + * with the given replacement. + */ +function sanitizeSnapshots( + tokenOrPattern: string | RegExp | (() => string | RegExp), + replacement: string, +): JestPrettyFormatPlugin { + const test = (val: mixed) => { + if (typeof val !== 'string') { + return false; + } + let tokenOrPatternToTest = tokenOrPattern; + if (typeof tokenOrPatternToTest === 'function') { + tokenOrPatternToTest = tokenOrPatternToTest(); + } + if (typeof tokenOrPatternToTest === 'string') { + return val.includes(tokenOrPatternToTest); + } + return tokenOrPatternToTest.test(val); + }; + const serialize = ( + val: mixed, + config: mixed, + indentation: mixed, + depth: mixed, + refs: mixed, + // $FlowFixMe[unclear-type] TODO: add up-to-date and accurate types for Jest snapshot serializers. + printer: any, + ) => { + invariant(typeof val === 'string', 'Received non-string value.'); + let tokenOrPatternToTest = tokenOrPattern; + if (typeof tokenOrPatternToTest === 'function') { + tokenOrPatternToTest = tokenOrPatternToTest(); + } + const replacedVal = val.replaceAll(tokenOrPatternToTest, replacement); + if (test(replacedVal)) { + // Recursion breaker. + throw new Error( + `Failed to sanitize snapshot: ${replacedVal} still contains ${tokenOrPatternToTest.toString()}`, + ); + } + return printer(replacedVal, config, indentation, depth, refs, printer); + }; + return { + serialize, + test, + // $FlowFixMe[unclear-type] expect.addSnapshotSerializer is typed inaccurately + } as any as JestPrettyFormatPlugin; +} + +/** + * A Jest snapshot serializer that removes ANSI color codes from strings. + */ +const removeAnsiColors = sanitizeSnapshots( + ansiRegex(), + '', +) as JestPrettyFormatPlugin; + +/** + * A Jest snapshot serializer that redacts the exact temporary directory path + * used by curl-utils. + */ +const removeCurlPaths = sanitizeSnapshots( + getCurlTempDirPattern(), + '', +) as JestPrettyFormatPlugin; + +module.exports = { + sanitizeSnapshots, + removeAnsiColors, + removeCurlPaths, +}; diff --git a/scripts/releases/utils/curl-utils.js b/scripts/releases/utils/curl-utils.js index bbe770855e1c5e..b8ee609613757d 100644 --- a/scripts/releases/utils/curl-utils.js +++ b/scripts/releases/utils/curl-utils.js @@ -51,6 +51,21 @@ async function getWithCurl(url /*: string */) /*: Promise */ { } } +function getTempDirPatternForTests() /*: RegExp */ { + return new RegExp( + escapeRegex(path.join(os.tmpdir(), 'get-with-curl-')) + + '.[^\\s' + + escapeRegex(path.sep) + + ']+', + 'g', + ); +} + +function escapeRegex(str /*: string */) /*: string */ { + return str.replace(/[-[\]\\/{}()*+?.^$|]/g, '\\$&'); +} + module.exports = { getWithCurl, + getTempDirPatternForTests, }; From 122d782714af1f725c9925d2916c4a911206262d Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 2 Sep 2025 16:45:36 +0100 Subject: [PATCH 04/10] Inject GitHub release asset URLs into first-party DotSlash files Summary: Adds the `write-dotslash-release-asset-urls` script and hooks it up to the `create-release-commit` script. After this script runs, the branch will reference GitHub release assets that haven't been uploaded yet - this is necessary because we publish the branch to npm before we ever create a draft GitHub release we can upload things to. In a separate commit in this PR, we will augment the `create-draft-release` workflow to actually upload the assets, based on the contents of the release commit. Comes with unit tests. --- ...e-dotslash-release-asset-urls-test.js.snap | 128 +++++++++++ .../write-dotslash-release-asset-urls-test.js | 203 ++++++++++++++++++ scripts/releases/create-release-commit.js | 6 + .../write-dotslash-release-asset-urls.js | 171 +++++++++++++++ 4 files changed, 508 insertions(+) create mode 100644 scripts/releases/__tests__/__snapshots__/write-dotslash-release-asset-urls-test.js.snap create mode 100644 scripts/releases/__tests__/write-dotslash-release-asset-urls-test.js create mode 100644 scripts/releases/write-dotslash-release-asset-urls.js diff --git a/scripts/releases/__tests__/__snapshots__/write-dotslash-release-asset-urls-test.js.snap b/scripts/releases/__tests__/__snapshots__/write-dotslash-release-asset-urls-test.js.snap new file mode 100644 index 00000000000000..8639e3c11a7390 --- /dev/null +++ b/scripts/releases/__tests__/__snapshots__/write-dotslash-release-asset-urls-test.js.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`writeReleaseAssetUrlsToDotSlashFile adds a new release asset provider if missing (first release commit in a branch): console.log calls 1`] = ` +Array [ + Array [ + "Updating /entry-point...", + ], + Array [ + "Downloading from for integrity validation...", + ], + Array [ + "Providers: +", + "- Original ++ Updated + + Array [ + Object { ++ \\"url\\": \\"https://github.com/facebook/react-native/releases/download/v1000.0.1/test-linux-x86_64\\", ++ }, ++ Object { + \\"url\\": \\"\\", + }, + ]", + ], +] +`; + +exports[`writeReleaseAssetUrlsToDotSlashFile adds a new release asset provider if missing (first release commit in a branch): updated dotslash file 1`] = ` +"#!/usr/bin/env dotslash +// @generated SignedSource<> +{ + \\"name\\": \\"test\\", + \\"platforms\\": { + \\"linux-x86_64\\": { + \\"providers\\": [ + { + \\"url\\": \\"https://github.com/facebook/react-native/releases/download/v1000.0.1/test-linux-x86_64\\" + }, + { + \\"url\\": \\"\\" + } + ], + \\"size\\": 0, + \\"hash\\": \\"sha256\\", + \\"digest\\": \\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\\", + \\"path\\": \\"bar\\" + } + } +} +" +`; + +exports[`writeReleaseAssetUrlsToDotSlashFile fails if there are no upstream providers 1`] = `"No upstream HTTP providers found for asset: test-linux-x86_64.tar.gz"`; + +exports[`writeReleaseAssetUrlsToDotSlashFile fails if there are no upstream providers: console.log calls 1`] = ` +Array [ + Array [ + "Updating /entry-point...", + ], +] +`; + +exports[`writeReleaseAssetUrlsToDotSlashFile fails if upstream returns an incorrect asset 1`] = `"size mismatch: expected 1, got 0"`; + +exports[`writeReleaseAssetUrlsToDotSlashFile fails if upstream returns an incorrect asset: console.log calls 1`] = ` +Array [ + Array [ + "Updating /entry-point...", + ], + Array [ + "Downloading from for integrity validation...", + ], +] +`; + +exports[`writeReleaseAssetUrlsToDotSlashFile replaces the old release asset provider if exists (Nth release commit in a branch): console.log calls 1`] = ` +Array [ + Array [ + "Updating /entry-point...", + ], + Array [ + "Downloading from for integrity validation...", + ], + Array [ + "Providers: +", + "- Original ++ Updated + + Array [ + Object { +- \\"url\\": \\"\\", ++ \\"url\\": \\"https://github.com/facebook/react-native/releases/download/v1000.0.1/test-linux-x86_64\\", + }, + Object { +- \\"url\\": \\"https://github.com/facebook/react-native/releases/download/v1000.0.0/test.tar.gz\\", ++ \\"url\\": \\"\\", + }, + ]", + ], +] +`; + +exports[`writeReleaseAssetUrlsToDotSlashFile replaces the old release asset provider if exists (Nth release commit in a branch): updated dotslash file 1`] = ` +"#!/usr/bin/env dotslash +// @generated SignedSource<> +{ + \\"name\\": \\"test\\", + \\"platforms\\": { + \\"linux-x86_64\\": { + \\"providers\\": [ + { + \\"url\\": \\"https://github.com/facebook/react-native/releases/download/v1000.0.1/test-linux-x86_64\\" + }, + { + \\"url\\": \\"\\" + } + ], + \\"size\\": 0, + \\"hash\\": \\"sha256\\", + \\"digest\\": \\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\\", + \\"path\\": \\"bar\\" + } + } +} +" +`; diff --git a/scripts/releases/__tests__/write-dotslash-release-asset-urls-test.js b/scripts/releases/__tests__/write-dotslash-release-asset-urls-test.js new file mode 100644 index 00000000000000..ea8acf48720c8f --- /dev/null +++ b/scripts/releases/__tests__/write-dotslash-release-asset-urls-test.js @@ -0,0 +1,203 @@ +/** + * 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 { + writeReleaseAssetUrlsToDotSlashFile, +} = require('../write-dotslash-release-asset-urls'); +const {removeAnsiColors, sanitizeSnapshots} = require('./snapshot-utils'); +const fs = require('fs/promises'); +const http = require('http'); +const os = require('os'); +const path = require('path'); +const signedsource = require('signedsource'); + +let server, serverUrl, tmpDir, consoleLog; + +expect.addSnapshotSerializer(sanitizeSnapshots(() => tmpDir, '')); +expect.addSnapshotSerializer(sanitizeSnapshots(() => serverUrl, '')); +expect.addSnapshotSerializer( + sanitizeSnapshots( + /SignedSource<<[a-f0-9]{32}>>/g, + 'SignedSource<>', + ), +); +expect.addSnapshotSerializer(removeAnsiColors); + +beforeEach(async () => { + consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); + tmpDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'write-dotslash-release-asset-urls-test-'), + ); + await new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + if (req.url !== '/') { + res.writeHead(404); + res.end(); + return; + } + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(''); + }); + server.on('error', reject); + server.listen(0, 'localhost', () => { + const {port} = server.address(); + serverUrl = `http://localhost:${port}`; + resolve(); + }); + }); +}); + +afterEach(async () => { + consoleLog.mockRestore(); + await new Promise((resolve, reject) => { + server.close(err => { + if (err) { + reject(err); + } + resolve(); + }); + }); + await fs.rm(tmpDir, {recursive: true, force: true}); +}); + +describe('writeReleaseAssetUrlsToDotSlashFile', () => { + test('fails if there are no upstream providers', async () => { + const dotslashContents = `#!/usr/bin/env dotslash +{ + "name": "test", + "platforms": { + "linux-x86_64": { + "providers": [ + {"url": "https://github.com/facebook/react-native/releases/download/v1000.0.0/test.tar.gz"}, + ], + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": "tar.gz", + "path": "bar" + } + } +} +`; + await fs.writeFile(`${tmpDir}/entry-point`, dotslashContents); + + await expect( + writeReleaseAssetUrlsToDotSlashFile({ + filename: `${tmpDir}/entry-point`, + releaseTag: 'v1000.0.1', + }), + ).rejects.toThrowErrorMatchingSnapshot(); + + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('adds a new release asset provider if missing (first release commit in a branch)', async () => { + const dotslashContents = `#!/usr/bin/env dotslash +// @${'generated SignedSource<<00000000000000000000000000000000>>'} +{ + "name": "test", + "platforms": { + "linux-x86_64": { + "providers": [ + {"url": "${serverUrl}"}, + ], + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "path": "bar" + } + } +} +`; + await fs.writeFile(`${tmpDir}/entry-point`, dotslashContents); + + await expect( + writeReleaseAssetUrlsToDotSlashFile({ + filename: `${tmpDir}/entry-point`, + releaseTag: 'v1000.0.1', + }), + ).resolves.toBeUndefined(); + + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + + const updatedContents = await fs.readFile(`${tmpDir}/entry-point`, 'utf8'); + + expect(updatedContents).toMatchSnapshot('updated dotslash file'); + expect(signedsource.verifySignature(updatedContents)).toBe(true); + }); + + test('replaces the old release asset provider if exists (Nth release commit in a branch)', async () => { + const dotslashContents = `#!/usr/bin/env dotslash +// @${'generated SignedSource<<00000000000000000000000000000000>>'} +{ + "name": "test", + "platforms": { + "linux-x86_64": { + "providers": [ + {"url": "${serverUrl}"}, + {"url": "https://github.com/facebook/react-native/releases/download/v1000.0.0/test.tar.gz"} + ], + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "path": "bar" + } + } +} +`; + await fs.writeFile(`${tmpDir}/entry-point`, dotslashContents); + + await expect( + writeReleaseAssetUrlsToDotSlashFile({ + filename: `${tmpDir}/entry-point`, + releaseTag: 'v1000.0.1', + }), + ).resolves.toBeUndefined(); + + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + + const updatedContents = await fs.readFile(`${tmpDir}/entry-point`, 'utf8'); + + expect(updatedContents).toMatchSnapshot('updated dotslash file'); + expect(signedsource.verifySignature(updatedContents)).toBe(true); + }); + + test('fails if upstream returns an incorrect asset', async () => { + const dotslashContents = `#!/usr/bin/env dotslash +// @${'generated SignedSource<<00000000000000000000000000000000>>'} +{ + "name": "test", + "platforms": { + "linux-x86_64": { + "providers": [ + {"url": "${serverUrl}"}, + ], + "size": 1, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "path": "bar" + } + } +} +`; + await fs.writeFile(`${tmpDir}/entry-point`, dotslashContents); + + await expect( + writeReleaseAssetUrlsToDotSlashFile({ + filename: `${tmpDir}/entry-point`, + releaseTag: 'v1001.0.0', + }), + ).rejects.toThrowErrorMatchingSnapshot(); + + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); +}); diff --git a/scripts/releases/create-release-commit.js b/scripts/releases/create-release-commit.js index 50d2f8a8acd99d..09d4f7cc888dff 100644 --- a/scripts/releases/create-release-commit.js +++ b/scripts/releases/create-release-commit.js @@ -10,6 +10,9 @@ const {setVersion} = require('../releases/set-version'); const {getBranchName} = require('../releases/utils/scm-utils'); +const { + writeReleaseAssetUrlsToDotSlashFiles, +} = require('../releases/write-dotslash-release-asset-urls'); const {parseVersion} = require('./utils/version-utils'); const {execSync} = require('child_process'); const yargs = require('yargs'); @@ -49,6 +52,9 @@ async function main() { console.info('Setting version for monorepo packages and react-native'); await setVersion(version, false); // version, skip-react-native + console.info('Writing release asset URLs to DotSlash files'); + await writeReleaseAssetUrlsToDotSlashFiles(version); + if (dryRun) { console.info('Running in dry-run mode, skipping git commit'); console.info( diff --git a/scripts/releases/write-dotslash-release-asset-urls.js b/scripts/releases/write-dotslash-release-asset-urls.js new file mode 100644 index 00000000000000..8d09f26e85a9d4 --- /dev/null +++ b/scripts/releases/write-dotslash-release-asset-urls.js @@ -0,0 +1,171 @@ +/** + * 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'; + +/*:: +import type {DotSlashHttpProvider, DotSlashProvider, DotSlashArtifactInfo} from './utils/dotslash-utils'; +*/ + +const {REPO_ROOT} = require('../shared/consts'); +const {getWithCurl} = require('./utils/curl-utils'); +const { + dangerouslyResignGeneratedFile, + isHttpProvider, + processDotSlashFileInPlace, + validateAndParseDotSlashFile, + validateDotSlashArtifactData, +} = require('./utils/dotslash-utils'); +const {diff: jestDiff} = require('jest-diff'); +const path = require('path'); +const {parseArgs} = require('util'); + +const FIRST_PARTY_DOTSLASH_FILES = [ + 'packages/debugger-shell/bin/react-native-devtools', +]; + +async function main() { + const { + positionals: [version], + values: {help}, + } = parseArgs({ + allowPositionals: true, + options: { + help: {type: 'boolean'}, + }, + }); + + if (help) { + console.log(` + Usage: node ./scripts/releases/write-dotslash-release-asset-urls.js + + Inserts references to release assets URLs into first-party DotSlash files in + the repo, in preparation for publishing a new release and uploading the + assets (which happens in a separate step). +`); + return; + } + + if (version == null) { + throw new Error('Missing version argument'); + } + + await writeReleaseAssetUrlsToDotSlashFiles(version); +} + +async function writeReleaseAssetUrlsToDotSlashFiles( + version /*: string */, +) /*: Promise */ { + const releaseTag = version.startsWith('v') ? version : `v${version}`; + for (const filename of FIRST_PARTY_DOTSLASH_FILES) { + await writeReleaseAssetUrlsToDotSlashFile({ + filename, + releaseTag, + }); + } +} + +async function writeReleaseAssetUrlsToDotSlashFile( + {filename, releaseTag} /*: {filename: string, releaseTag: string} */, +) /*: Promise */ { + const fullPath = path.resolve(REPO_ROOT, filename); + console.log(`Updating ${filename}...`); + await processDotSlashFileInPlace( + fullPath, + async (originalProviders, suggestedFilename, artifactInfo) => { + const updatedProviders = await updateAndVerifyProviders({ + providers: originalProviders, + suggestedFilename, + artifactInfo, + releaseTag, + }); + console.log( + 'Providers:\n', + diffProviderArrays(originalProviders, updatedProviders), + ); + return updatedProviders; + }, + ); + await dangerouslyResignGeneratedFile(fullPath); + await validateAndParseDotSlashFile(fullPath); +} + +async function updateAndVerifyProviders( + {providers: providersArg, suggestedFilename, artifactInfo, releaseTag} /*: + {providers: $ReadOnlyArray, + suggestedFilename: string, + artifactInfo: DotSlashArtifactInfo, + releaseTag: string,} +*/, +) { + const providers = providersArg.filter( + provider => !isPreviousReleaseAssetProvider(provider), + ); + const upstreamHttpProviders = providers.filter(isHttpProvider); + if (upstreamHttpProviders.length === 0) { + throw new Error( + 'No upstream HTTP providers found for asset: ' + suggestedFilename, + ); + } + for (const provider of upstreamHttpProviders) { + console.log(`Downloading from ${provider.url} for integrity validation...`); + const {data} = await getWithCurl(provider.url); + await validateDotSlashArtifactData(data, artifactInfo); + } + providers.unshift( + createReleaseAssetProvider({ + releaseTag, + suggestedFilename, + }), + ); + return providers; +} + +function isPreviousReleaseAssetProvider( + provider /*: DotSlashProvider */, +) /*: boolean */ { + return ( + isHttpProvider(provider) && + provider.url.startsWith( + 'https://github.com/facebook/react-native/releases/download/', + ) + ); +} + +function createReleaseAssetProvider( + { + releaseTag, + suggestedFilename, + } /*: {releaseTag: string, suggestedFilename: string} */, +) /*: DotSlashProvider */ { + return { + url: `https://github.com/facebook/react-native/releases/download/${encodeURIComponent(releaseTag)}/${encodeURIComponent(suggestedFilename)}`, + }; +} + +function diffProviderArrays( + original /*: $ReadOnlyArray */, + updated /*: $ReadOnlyArray */, +) { + return jestDiff(original, updated, { + aAnnotation: 'Original', + bAnnotation: 'Updated', + }); +} + +module.exports = { + FIRST_PARTY_DOTSLASH_FILES, + writeReleaseAssetUrlsToDotSlashFiles, + writeReleaseAssetUrlsToDotSlashFile, +}; + +if (require.main === module) { + void main(); +} From 7a2d70713102d8bccd2896fc6b094cda7831454d Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 05/10] Upload first-party DotSlash binary assets to draft releases Summary: Augments the `create-draft-release` workflow to upload GitHub release assets referenced by the first-party DotSlash files in the repo, based on the output of `create-release-commit`. Comes with unit tests. --- .../__tests__/createDraftRelease-test.js | 18 +- .../workflow-scripts/createDraftRelease.js | 9 +- .github/workflows/create-draft-release.yml | 17 +- package.json | 1 + ...d-release-assets-for-dotslash-test.js.snap | 203 +++++++++ ...upload-release-assets-for-dotslash-test.js | 346 +++++++++++++++ .../upload-release-assets-for-dotslash.js | 398 ++++++++++++++++++ yarn.lock | 104 +++++ 8 files changed, 1087 insertions(+), 9 deletions(-) create mode 100644 scripts/releases/__tests__/__snapshots__/upload-release-assets-for-dotslash-test.js.snap create mode 100644 scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js create mode 100644 scripts/releases/upload-release-assets-for-dotslash.js diff --git a/.github/workflow-scripts/__tests__/createDraftRelease-test.js b/.github/workflow-scripts/__tests__/createDraftRelease-test.js index 77901d4df0999e..587e48e6d320bc 100644 --- a/.github/workflow-scripts/__tests__/createDraftRelease-test.js +++ b/.github/workflow-scripts/__tests__/createDraftRelease-test.js @@ -188,6 +188,7 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/ status: 201, json: () => Promise.resolve({ + id: 1, html_url: 'https://github.com/facebook/react-native/releases/tag/v0.77.1', }), @@ -208,9 +209,11 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/ body: fetchBody, }, ); - expect(response).toEqual( - 'https://github.com/facebook/react-native/releases/tag/v0.77.1', - ); + expect(response).toEqual({ + id: 1, + html_url: + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + }); }); 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/ status: 201, json: () => Promise.resolve({ + id: 1, html_url: 'https://github.com/facebook/react-native/releases/tag/v0.77.1', }), @@ -258,9 +262,11 @@ View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/ body: fetchBody, }, ); - expect(response).toEqual( - 'https://github.com/facebook/react-native/releases/tag/v0.77.1', - ); + expect(response).toEqual({ + id: 1, + html_url: + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + }); }); it('throws if the post failes', async () => { diff --git a/.github/workflow-scripts/createDraftRelease.js b/.github/workflow-scripts/createDraftRelease.js index f8737c3cbfda4c..7b7692ce2975d4 100644 --- a/.github/workflow-scripts/createDraftRelease.js +++ b/.github/workflow-scripts/createDraftRelease.js @@ -101,7 +101,11 @@ async function _createDraftReleaseOnGitHub(version, body, latest, token) { } const data = await response.json(); - return data.html_url; + const {html_url, id} = data; + return { + html_url, + id, + }; } function moveToChangelogBranch(version) { @@ -124,7 +128,8 @@ async function createDraftRelease(version, latest, token) { latest, token, ); - log(`Created draft release: ${release}`); + log(`Created draft release: ${release.html_url}, ID ${release.id}`); + return release; } module.exports = { diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml index d3b89dba90d805..5852b8fec7105c 100644 --- a/.github/workflows/create-draft-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -21,9 +21,24 @@ jobs: git config --local user.name "React Native Bot" - name: Create draft release uses: actions/github-script@v6 + id: create-draft-release with: script: | const {createDraftRelease} = require('./.github/workflow-scripts/createDraftRelease.js'); const version = '${{ github.ref_name }}'; const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js'); - await createDraftRelease(version, isLatest(), '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}'); + return (await createDraftRelease(version, isLatest(), '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}')).id; + result-encoding: string + - name: Upload release assets for DotSlash + uses: actions/github-script@v6 + env: + RELEASE_ID: ${{ steps.create-draft-release.outputs.result }} + with: + script: | + const {uploadReleaseAssetsForDotSlashFiles} = require('./scripts/releases/upload-release-assets-for-dotslash.js'); + const version = '${{ github.ref_name }}'; + await uploadReleaseAssetsForDotSlashFiles({ + version, + token: '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}', + releaseId: process.env.RELEASE_ID, + }); diff --git a/package.json b/package.json index e5f7d3571b0128..ca15f887d2b831 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@expo/spawn-async": "^1.7.2", "@jest/create-cache-key-function": "^29.7.0", "@microsoft/api-extractor": "^7.52.2", + "@octokit/rest": "^22.0.0", "@react-native/metro-babel-transformer": "0.82.0-main", "@react-native/metro-config": "0.82.0-main", "@tsconfig/node22": "22.0.2", diff --git a/scripts/releases/__tests__/__snapshots__/upload-release-assets-for-dotslash-test.js.snap b/scripts/releases/__tests__/__snapshots__/upload-release-assets-for-dotslash-test.js.snap new file mode 100644 index 00000000000000..0a6de7623ec1a9 --- /dev/null +++ b/scripts/releases/__tests__/__snapshots__/upload-release-assets-for-dotslash-test.js.snap @@ -0,0 +1,203 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`uploadReleaseAssetsForDotSlashFile deletes and reuploads the asset if force is true: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Deleting existing release asset...", + ], + Array [ + "[test.tar.gz] Downloading from ...", + ], + Array [ + "[test.tar.gz] Validating download...", + ], + Array [ + "[test.tar.gz] Uploading to release...", + ], + Array [ + "[test.tar.gz] Uploaded to https://github.com/facebook/react-native/releases/download/untagged-0b602d8af97c6d3b784c/test.tar.gz", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile deletes and reuploads the asset if force is true: deleteReleaseAsset calls 1`] = ` +Array [ + Array [ + Object { + "asset_id": 1, + "owner": "facebook", + "repo": "react-native", + }, + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile deletes and reuploads the asset if force is true: uploadReleaseAsset calls 1`] = ` +Array [ + Array [ + Object { + "data": Object { + "data": Array [], + "type": "Buffer", + }, + "headers": Object { + "content-type": "text/plain", + }, + "name": "test.tar.gz", + "owner": "facebook", + "release_id": "1", + "repo": "react-native", + }, + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile does not overwrite an existing asset if dryRun is true: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Skipping existing release asset...", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile does not upload the asset if dryRun is true: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Downloading from ...", + ], + Array [ + "[test.tar.gz] Validating download...", + ], + Array [ + "[test.tar.gz] Dry run: Not uploading to release.", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if asset has been renamed by GitHub 1`] = `"Asset name was changed while uploading to the draft release: expected test.tar.gz, got test-renamed.tar.gz. /entry-point has already been published to npm with the following URL, which will not work when the release is published on GitHub: https://github.com/facebook/react-native/releases/download/v1000.0.1/test.tar.gz"`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if asset has been renamed by GitHub: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Downloading from ...", + ], + Array [ + "[test.tar.gz] Validating download...", + ], + Array [ + "[test.tar.gz] Uploading to release...", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if asset has been renamed by GitHub: uploadReleaseAsset calls 1`] = ` +Array [ + Array [ + Object { + "data": Object { + "data": Array [], + "type": "Buffer", + }, + "headers": Object { + "content-type": "text/plain", + }, + "name": "test.tar.gz", + "owner": "facebook", + "release_id": "1", + "repo": "react-native", + }, + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if the upstream asset is corrupt 1`] = `"size mismatch: expected 1, got 0"`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if the upstream asset is corrupt: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Downloading from ...", + ], + Array [ + "[test.tar.gz] Validating download...", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if the upstream asset is unreachable 1`] = `"curl --silent --location --output /data /error --write-out %{header_json} --fail exited with non-zero code: 22"`; + +exports[`uploadReleaseAssetsForDotSlashFile fails loudly if the upstream asset is unreachable: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Downloading from /error...", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile skips uploading the asset if already present: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Skipping existing release asset...", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile uploads the asset if not already present: console.log calls 1`] = ` +Array [ + Array [ + "Uploading assets for /entry-point...", + ], + Array [ + "[test.tar.gz] Downloading from ...", + ], + Array [ + "[test.tar.gz] Validating download...", + ], + Array [ + "[test.tar.gz] Uploading to release...", + ], + Array [ + "[test.tar.gz] Uploaded to https://github.com/facebook/react-native/releases/download/untagged-0b602d8af97c6d3b784c/test.tar.gz", + ], +] +`; + +exports[`uploadReleaseAssetsForDotSlashFile uploads the asset if not already present: uploadReleaseAsset calls 1`] = ` +Array [ + Array [ + Object { + "data": Object { + "data": Array [], + "type": "Buffer", + }, + "headers": Object { + "content-type": "text/plain", + }, + "name": "test.tar.gz", + "owner": "facebook", + "release_id": "1", + "repo": "react-native", + }, + ], +] +`; diff --git a/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js b/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js new file mode 100644 index 00000000000000..4590d83dc2acf8 --- /dev/null +++ b/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js @@ -0,0 +1,346 @@ +/** + * 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 { + getReleaseAssetMap, + uploadReleaseAssetsForDotSlashFile, +} = require('../upload-release-assets-for-dotslash'); +const { + removeAnsiColors, + removeCurlPaths, + sanitizeSnapshots, +} = require('./snapshot-utils'); +const fs = require('fs/promises'); +const http = require('http'); +const os = require('os'); +const path = require('path'); + +let server, serverUrl, tmpDir, consoleLog; + +expect.addSnapshotSerializer(sanitizeSnapshots(() => tmpDir, '')); +expect.addSnapshotSerializer(sanitizeSnapshots(() => serverUrl, '')); +expect.addSnapshotSerializer(removeAnsiColors); +expect.addSnapshotSerializer(removeCurlPaths); + +const mockAssets: Array<{ + id: number, + ... +}> = []; + +const octokit = { + repos: { + listReleaseAssets: jest.fn().mockImplementation(() => { + return { + data: mockAssets, + }; + }), + deleteReleaseAsset: jest.fn().mockImplementation(({asset_id}) => { + const index = mockAssets.findIndex(asset => asset.id === asset_id); + if (index === -1) { + throw new Error('Asset not found'); + } + mockAssets.splice(index, 1); + }), + uploadReleaseAsset: jest.fn().mockImplementation(() => { + const assetId = Math.max(...mockAssets.map(asset => asset.id)) + 1; + mockAssets.push({ + id: assetId, + }); + return { + data: { + id: assetId, + browser_download_url: `https://github.com/facebook/react-native/releases/download/untagged-0b602d8af97c6d3b784c/test.tar.gz`, + }, + }; + }), + }, +}; + +beforeEach(async () => { + mockAssets.length = 0; + octokit.repos.listReleaseAssets.mockClear(); + octokit.repos.deleteReleaseAsset.mockClear(); + octokit.repos.uploadReleaseAsset.mockClear(); + + consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); + tmpDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'upload-release-assets-for-dotslash-test-'), + ); + await new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + if (req.url !== '/') { + res.writeHead(404); + res.end(); + return; + } + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(''); + }); + server.on('error', reject); + server.listen(0, 'localhost', () => { + const {port} = server.address(); + serverUrl = `http://localhost:${port}`; + resolve(); + }); + }); +}); + +afterEach(async () => { + consoleLog.mockRestore(); + await new Promise((resolve, reject) => { + server.close(err => { + if (err) { + reject(err); + } + resolve(); + }); + }); + await fs.rm(tmpDir, {recursive: true, force: true}); +}); + +describe('uploadReleaseAssetsForDotSlashFile', () => { + beforeEach(async () => { + // Simulate the repo in a state where the DotSlash file has been updated + // (by write-release-asset-urls-to-dotslash-file) but the release assets + // have not been uploaded yet. + const dotslashContents = `#!/usr/bin/env dotslash +{ + "name": "test", + "platforms": { + "linux-x86_64": { + "providers": [ + {"url": "https://github.com/facebook/react-native/releases/download/v1000.0.1/test.tar.gz"}, + {"url": "${serverUrl}"} + ], + "size": 0, + "hash": "sha256", + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": "tar.gz", + "path": "bar" + }, + }, +}`; + await fs.writeFile(path.join(tmpDir, 'entry-point'), dotslashContents); + }); + + const releaseId = '1'; + + test('uploads the asset if not already present', async () => { + await uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: false, + }, + octokit, + ); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset.mock.calls).toMatchSnapshot( + 'uploadReleaseAsset calls', + ); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('skips uploading the asset if already present', async () => { + mockAssets.push({ + id: 1, + name: 'test.tar.gz', + }); + await uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: false, + }, + octokit, + ); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset).not.toHaveBeenCalled(); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('deletes and reuploads the asset if force is true', async () => { + mockAssets.push({ + id: 1, + name: 'test.tar.gz', + }); + await uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: true, + dryRun: false, + }, + octokit, + ); + + expect(octokit.repos.deleteReleaseAsset.mock.calls).toMatchSnapshot( + 'deleteReleaseAsset calls', + ); + expect(octokit.repos.uploadReleaseAsset.mock.calls).toMatchSnapshot( + 'uploadReleaseAsset calls', + ); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('does not upload the asset if dryRun is true', async () => { + await uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: true, + }, + octokit, + ); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset).not.toHaveBeenCalled(); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('does not overwrite an existing asset if dryRun is true', async () => { + mockAssets.push({ + id: 1, + name: 'test.tar.gz', + }); + await uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: true, + }, + octokit, + ); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset).not.toHaveBeenCalled(); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('fails loudly if asset has been renamed by GitHub', async () => { + octokit.repos.uploadReleaseAsset.mockImplementationOnce(async () => { + return { + data: { + id: 1, + browser_download_url: `https://github.com/facebook/react-native/releases/download/untagged-0b602d8af97c6d3b784c/test-renamed.tar.gz`, + }, + }; + }); + await expect( + uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: false, + }, + octokit, + ), + ).rejects.toThrowErrorMatchingSnapshot(); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset.mock.calls).toMatchSnapshot( + 'uploadReleaseAsset calls', + ); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('fails loudly if the upstream asset is unreachable', async () => { + const dotslashContents = await fs.readFile( + path.join(tmpDir, 'entry-point'), + 'utf8', + ); + await fs.writeFile( + path.join(tmpDir, 'entry-point'), + dotslashContents.replace(serverUrl, `${serverUrl}/error`), + ); + await expect( + uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: false, + }, + octokit, + ), + ).rejects.toThrowErrorMatchingSnapshot(); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset).not.toHaveBeenCalled(); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); + + test('fails loudly if the upstream asset is corrupt', async () => { + const dotslashContents = await fs.readFile( + path.join(tmpDir, 'entry-point'), + 'utf8', + ); + await fs.writeFile( + path.join(tmpDir, 'entry-point'), + dotslashContents.replace('"size": 0', `"size": 1`), + ); + await expect( + uploadReleaseAssetsForDotSlashFile( + path.join(tmpDir, 'entry-point'), + { + releaseId, + releaseTag: 'v1000.0.1', + existingAssetsByName: await getReleaseAssetMap({releaseId}, octokit), + }, + { + force: false, + dryRun: false, + }, + octokit, + ), + ).rejects.toThrowErrorMatchingSnapshot(); + + expect(octokit.repos.deleteReleaseAsset).not.toHaveBeenCalled(); + expect(octokit.repos.uploadReleaseAsset).not.toHaveBeenCalled(); + expect(consoleLog.mock.calls).toMatchSnapshot('console.log calls'); + }); +}); diff --git a/scripts/releases/upload-release-assets-for-dotslash.js b/scripts/releases/upload-release-assets-for-dotslash.js new file mode 100644 index 00000000000000..401b325f8ee301 --- /dev/null +++ b/scripts/releases/upload-release-assets-for-dotslash.js @@ -0,0 +1,398 @@ +/** + * 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'); +// $FlowFixMe[untyped-import] TODO: add types for @octokit/rest +const {Octokit} = require('@octokit/rest'); +const nullthrows = require('nullthrows'); +const path = require('path'); +const {parseArgs} = require('util'); + +/*:: +import type {DotSlashProvider, DotSlashHttpProvider, DotSlashArtifactInfo} from './utils/dotslash-utils'; + +// $FlowFixMe[unclear-type] TODO: add types for @octokit/rest +type OctokitInstance = any; + +type GitHubReleaseAsset = {id: number, ...}; +type ReleaseAssetMap = $ReadOnlyMap; + +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'}, + }, + }); + + if (help) { + console.log(` + Usage: node ./scripts/releases/upload-release-assets-for-dotslash.js --release_id --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: + 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 */ { + 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 /*: OctokitInstance */, +) /*: Promise */ { + const existingAssets = await octokit.repos.listReleaseAssets({ + owner: 'facebook', + repo: 'react-native', + 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 /*: OctokitInstance */, +) /*: Promise */ { + 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 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, + suggestedFilename: string, + artifactInfo: DotSlashArtifactInfo, + dotslashFilename: string, +} */, + releaseInfo /*: ReleaseInfo */, + executionOptions /*: ExecutionOptions */, + octokit /*: OctokitInstance */, +) { + 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 /*: OctokitInstance */, +) /*: Promise */ { + 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: 'facebook', + repo: 'react-native', + 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, headers} = await getWithCurl(url); + console.log(`[${name}] Validating download...`); + await validateDotSlashArtifactData(data, artifactInfo); + return { + data, + contentType: headers['content-type']?.[0] ?? '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 /*: OctokitInstance */, +) /*: Promise */ { + console.log(`[${name}] Uploading to release...`); + const { + data: {browser_download_url}, + } = await octokit.repos.uploadReleaseAsset({ + owner: 'facebook', + repo: 'react-native', + 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(); +} diff --git a/yarn.lock b/yarn.lock index 1adc080e5ff18d..86f7cce00eafc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1654,6 +1654,11 @@ resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de" integrity sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw== +"@octokit/auth-token@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" + integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== + "@octokit/core@^5.0.2": version "5.2.1" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.1.tgz#58c21a5f689ee81e0b883b5aa77573a7ff1b4ea1" @@ -1680,6 +1685,19 @@ before-after-hook "^3.0.2" universal-user-agent "^7.0.0" +"@octokit/core@^7.0.2": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.3.tgz#0b5288995fed66920128d41cfeea34979d48a360" + integrity sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ== + dependencies: + "@octokit/auth-token" "^6.0.0" + "@octokit/graphql" "^9.0.1" + "@octokit/request" "^10.0.2" + "@octokit/request-error" "^7.0.0" + "@octokit/types" "^14.0.0" + before-after-hook "^4.0.0" + universal-user-agent "^7.0.0" + "@octokit/endpoint@^10.1.4": version "10.1.4" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.4.tgz#8783be38a32b95af8bcb6523af20ab4eed7a2adb" @@ -1688,6 +1706,14 @@ "@octokit/types" "^14.0.0" universal-user-agent "^7.0.2" +"@octokit/endpoint@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.0.tgz#189fcc022721b4c49d0307eea6be3de1cfb53026" + integrity sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ== + dependencies: + "@octokit/types" "^14.0.0" + universal-user-agent "^7.0.2" + "@octokit/endpoint@^9.0.6": version "9.0.6" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.6.tgz#114d912108fe692d8b139cfe7fc0846dfd11b6c0" @@ -1714,6 +1740,15 @@ "@octokit/types" "^14.0.0" universal-user-agent "^7.0.0" +"@octokit/graphql@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.1.tgz#eb258fc9981403d2d751720832652c385b6c1613" + integrity sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg== + dependencies: + "@octokit/request" "^10.0.2" + "@octokit/types" "^14.0.0" + universal-user-agent "^7.0.0" + "@octokit/openapi-types@^24.2.0": version "24.2.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3" @@ -1724,6 +1759,11 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-25.0.0.tgz#adeead36992abf966e89dcd53518d8b0dc910e0d" integrity sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw== +"@octokit/openapi-types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-25.1.0.tgz#5a72a9dfaaba72b5b7db375fd05e90ca90dc9682" + integrity sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA== + "@octokit/plugin-paginate-rest@11.4.4-cjs.2": version "11.4.4-cjs.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz#979a10d577bce7a393e8e65953887e42b0a05000" @@ -1738,6 +1778,13 @@ dependencies: "@octokit/types" "^13.10.0" +"@octokit/plugin-paginate-rest@^13.0.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz#ca5bb1c7b85a583691263c1f788f607e9bcb74b3" + integrity sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw== + dependencies: + "@octokit/types" "^14.1.0" + "@octokit/plugin-request-log@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz#98a3ca96e0b107380664708111864cb96551f958" @@ -1748,6 +1795,11 @@ resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69" integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw== +"@octokit/plugin-request-log@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" + integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== + "@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1": version "13.3.2-cjs.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz#d0a142ff41d8f7892b6ccef45979049f51ecaa8d" @@ -1762,6 +1814,13 @@ dependencies: "@octokit/types" "^13.10.0" +"@octokit/plugin-rest-endpoint-methods@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz#ba30ca387fc2ac8bd93cf9f951174736babebd97" + integrity sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g== + dependencies: + "@octokit/types" "^14.1.0" + "@octokit/request-error@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.1.tgz#b9218f9c1166e68bb4d0c89b638edc62c9334805" @@ -1778,6 +1837,24 @@ dependencies: "@octokit/types" "^14.0.0" +"@octokit/request-error@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.0.0.tgz#48ae2cd79008315605d00e83664891a10a5ddb97" + integrity sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg== + dependencies: + "@octokit/types" "^14.0.0" + +"@octokit/request@^10.0.2": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.3.tgz#2ffdb88105ce20d25dcab8a592a7040ea48306c7" + integrity sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA== + dependencies: + "@octokit/endpoint" "^11.0.0" + "@octokit/request-error" "^7.0.0" + "@octokit/types" "^14.0.0" + fast-content-type-parse "^3.0.0" + universal-user-agent "^7.0.2" + "@octokit/request@^8.4.1": version "8.4.1" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.1.tgz#715a015ccf993087977ea4365c44791fc4572486" @@ -1819,6 +1896,16 @@ "@octokit/plugin-request-log" "^4.0.0" "@octokit/plugin-rest-endpoint-methods" "13.3.2-cjs.1" +"@octokit/rest@^22.0.0": + version "22.0.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.0.tgz#9026f47dacba9c605da3d43cce9432c4c532dc5a" + integrity sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA== + dependencies: + "@octokit/core" "^7.0.2" + "@octokit/plugin-paginate-rest" "^13.0.1" + "@octokit/plugin-request-log" "^6.0.0" + "@octokit/plugin-rest-endpoint-methods" "^16.0.0" + "@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.10.0", "@octokit/types@^13.7.0", "@octokit/types@^13.8.0": version "13.10.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3" @@ -1833,6 +1920,13 @@ dependencies: "@octokit/openapi-types" "^25.0.0" +"@octokit/types@^14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-14.1.0.tgz#3bf9b3a3e3b5270964a57cc9d98592ed44f840f2" + integrity sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g== + dependencies: + "@octokit/openapi-types" "^25.1.0" + "@react-native-community/cli-clean@20.0.0": version "20.0.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-20.0.0.tgz#e685f5404195ded69c81d1394e8c5eb332b780bc" @@ -3066,6 +3160,11 @@ before-after-hook@^3.0.2: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d" integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A== +before-after-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" + integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -4492,6 +4591,11 @@ fast-content-type-parse@^2.0.0: resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b" integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q== +fast-content-type-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb" + integrity sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" From a6f82461b9c6ae19e306cb2e1f088baad209c7a4 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 06/10] Validate first-party DotSlash artifacts continuously and after release Summary: Adds a workflow to ensure the following: * Continuously: `main` contains valid DotSlash files that reference valid artifacts. * When a release is published: the release contains valid DotSlash files that reference valid artifacts. This gives us confidence that branch cuts start in a valid state and that releases go out in a valid state. This is particularly important since there is a span of time, between the release commit and the publishing of the GH release, when the job would fail (as the release assets are not hosted at their final URLs yet). This is a fairly trivial script built on top of unit-tested utilities: `processDotSlashFileInPlace`, `getWithCurl`, `validateDotSlashArtifactData`. --- .../workflows/validate-dotslash-artifacts.yml | 32 ++++++ .../releases/validate-dotslash-artifacts.js | 99 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 .github/workflows/validate-dotslash-artifacts.yml create mode 100644 scripts/releases/validate-dotslash-artifacts.js diff --git a/.github/workflows/validate-dotslash-artifacts.yml b/.github/workflows/validate-dotslash-artifacts.yml new file mode 100644 index 00000000000000..02a3172166af8f --- /dev/null +++ b/.github/workflows/validate-dotslash-artifacts.yml @@ -0,0 +1,32 @@ +name: Validate DotSlash Artifacts + +on: + workflow_dispatch: + release: + types: [published] + push: + branches: + - main + +jobs: + validate-dotslash-artifacts: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Configure Git + shell: bash + run: | + git config --local user.email "bot@reactnative.dev" + git config --local user.name "React Native Bot" + - name: Validate DotSlash artifacts + uses: actions/github-script@v6 + with: + script: | + const {validateDotSlashArtifacts} = require('./scripts/releases/validate-dotslash-artifacts.js'); + await validateDotSlashArtifacts(); diff --git a/scripts/releases/validate-dotslash-artifacts.js b/scripts/releases/validate-dotslash-artifacts.js new file mode 100644 index 00000000000000..7c4841341b6357 --- /dev/null +++ b/scripts/releases/validate-dotslash-artifacts.js @@ -0,0 +1,99 @@ +/** + * 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 path = require('path'); +const {parseArgs, styleText} = require('util'); + +async function main() { + const { + positionals: [], + values: {help}, + } = parseArgs({ + allowPositionals: true, + options: { + help: {type: 'boolean'}, + }, + }); + + if (help) { + console.log(` + Usage: node ./scripts/releases/validate-dotslash-artifacts.js + + Ensures that the first-party DotSlash files in the current commit all point to + valid URLs that return the described artifacts. This script is intended to run + in two key scenarios: + + 1. Continuously on main - this verifies the output of the Meta-internal CI pipeline + that publishes DotSlash files to the repo. + 2. After a release is published - this verifies the behavior of the + write-dotslash-release-asset-urls.js and upload-release-assets-for-dotslash.js + scripts, as well as any commits (e.g. merges, picks) that touched the DotSlash + files in the release branch since the branch was cut. + Release asset URLs are only valid once the release is published, so we can't + run this continuously on commits in the release branch (specifically, it would + fail on the release commit itself). +`); + return; + } + + await validateDotSlashArtifacts(); +} + +async function validateDotSlashArtifacts() /*: Promise */ { + for (const filename of FIRST_PARTY_DOTSLASH_FILES) { + const fullPath = path.join(REPO_ROOT, filename); + console.log(`Validating all HTTP providers for ${filename}...`); + await processDotSlashFileInPlace( + fullPath, + async (providers, suggestedFilename, artifactInfo) => { + for (const provider of providers) { + if (!isHttpProvider(provider)) { + console.log( + styleText( + 'dim', + ` `, + ), + ); + continue; + } + console.log( + styleText( + 'dim', + ` ${provider.url} (expected ${artifactInfo.size} bytes, ${artifactInfo.hash} ${artifactInfo.digest})`, + ), + ); + const {data} = await getWithCurl(provider.url); + await validateDotSlashArtifactData(data, artifactInfo); + } + return providers; + }, + ); + } +} + +module.exports = { + validateDotSlashArtifacts, +}; + +if (require.main === module) { + void main(); +} From 2deeb7e70376ee80b99f27bea4825789f22a89a3 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 07/10] Release 0.82.0-20250903-0830 --- package.json | 4 ++-- packages/assets/package.json | 2 +- packages/babel-plugin-codegen/package.json | 4 ++-- packages/community-cli-plugin/package.json | 4 ++-- packages/core-cli-utils/package.json | 2 +- packages/debugger-frontend/package.json | 2 +- .../debugger-shell/bin/react-native-devtools | 17 ++++++++++++++++- packages/debugger-shell/package.json | 2 +- packages/dev-middleware/package.json | 8 ++++---- .../eslint-config-react-native/package.json | 4 ++-- .../eslint-plugin-react-native/package.json | 2 +- packages/eslint-plugin-specs/package.json | 4 ++-- packages/gradle-plugin/package.json | 2 +- packages/metro-config/package.json | 6 +++--- packages/new-app-screen/package.json | 2 +- packages/normalize-color/package.json | 2 +- packages/polyfills/package.json | 2 +- packages/react-native-babel-preset/package.json | 4 ++-- .../react-native-babel-transformer/package.json | 4 ++-- packages/react-native-codegen/package.json | 2 +- .../package.json | 4 ++-- .../package.json | 4 ++-- .../Libraries/Core/ReactNativeVersion.js | 6 +++--- packages/react-native/React/Base/RCTVersion.m | 6 +++--- .../react-native/ReactAndroid/gradle.properties | 2 +- .../modules/systeminfo/ReactNativeVersion.kt | 13 +++++++++---- .../ReactCommon/cxxreact/ReactNativeVersion.h | 10 +++++----- packages/react-native/package.json | 16 ++++++++-------- .../generate-artifacts-executor-test.js.snap | 10 +++++----- packages/rn-tester/package.json | 4 ++-- packages/typescript-config/package.json | 2 +- packages/virtualized-lists/package.json | 2 +- private/helloworld/package.json | 12 ++++++------ .../package.json | 2 +- 34 files changed, 96 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index ca15f887d2b831..06c252aa0cd659 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "@jest/create-cache-key-function": "^29.7.0", "@microsoft/api-extractor": "^7.52.2", "@octokit/rest": "^22.0.0", - "@react-native/metro-babel-transformer": "0.82.0-main", - "@react-native/metro-config": "0.82.0-main", + "@react-native/metro-babel-transformer": "0.82.0-20250903-0830", + "@react-native/metro-config": "0.82.0-20250903-0830", "@tsconfig/node22": "22.0.2", "@types/react": "^19.1.0", "@typescript-eslint/parser": "^8.36.0", diff --git a/packages/assets/package.json b/packages/assets/package.json index aff74ac9732b96..3a7a8cd83019ff 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/assets-registry", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Asset support code for React Native.", "license": "MIT", "repository": { diff --git a/packages/babel-plugin-codegen/package.json b/packages/babel-plugin-codegen/package.json index 6af39526dbed5f..60eeb310234768 100644 --- a/packages/babel-plugin-codegen/package.json +++ b/packages/babel-plugin-codegen/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/babel-plugin-codegen", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Babel plugin to generate native module and view manager code for React Native.", "license": "MIT", "repository": { @@ -26,7 +26,7 @@ ], "dependencies": { "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.82.0-main" + "@react-native/codegen": "0.82.0-20250903-0830" }, "devDependencies": { "@babel/core": "^7.25.2" diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index f2e912578f8157..0c7a6615cf103a 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/community-cli-plugin", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Core CLI commands for React Native", "keywords": [ "react-native", @@ -22,7 +22,7 @@ "dist" ], "dependencies": { - "@react-native/dev-middleware": "0.82.0-main", + "@react-native/dev-middleware": "0.82.0-20250903-0830", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", diff --git a/packages/core-cli-utils/package.json b/packages/core-cli-utils/package.json index 601eb124a5da8a..2c733e78d2a144 100644 --- a/packages/core-cli-utils/package.json +++ b/packages/core-cli-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/core-cli-utils", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "React Native CLI library for Frameworks to build on", "license": "MIT", "main": "./src/index.flow.js", diff --git a/packages/debugger-frontend/package.json b/packages/debugger-frontend/package.json index 1475dd77753fc3..b9a421164eda12 100644 --- a/packages/debugger-frontend/package.json +++ b/packages/debugger-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/debugger-frontend", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Debugger frontend for React Native based on Chrome DevTools", "keywords": [ "react-native", diff --git a/packages/debugger-shell/bin/react-native-devtools b/packages/debugger-shell/bin/react-native-devtools index b7fd1912e31b5d..a9f5785a198a44 100755 --- a/packages/debugger-shell/bin/react-native-devtools +++ b/packages/debugger-shell/bin/react-native-devtools @@ -1,6 +1,6 @@ #!/usr/bin/env dotslash -// @generated SignedSource<> +// @generated SignedSource<> { @@ -11,6 +11,9 @@ "hash": "sha256", "digest": "4352f1c9848ca919101ec628bd08b87a72a828d1ab55fa43a02098329fa452fa", "providers": [ + { + "url": "https://github.com/facebook/react-native/releases/download/v0.82.0-20250903-0830/React.Native.DevTools-linux-aarch64.tar.gz" + }, { "type": "http", "url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQM24LW0nPZRk0ypuIG9prz_72YjBJNcVlGSIEpO4zdlLXgw4dFNodH7MKg9eKNTnx7wrVDDBNACrnEPt_OfXOjZyZsV9Oaqu0-vNFRdlEyis4YqmpqGLtz3LvD-9R6fzcWxJI9zrhdPvOvlXP-3Syt1UNITaxXDVqIwAAYpCaAh0oLpupAFCc2yvYw" @@ -24,6 +27,9 @@ "hash": "sha256", "digest": "11c7b07942928a6301b07fbf2bc77ce1229b2a52891f23541cdd9858b5250e64", "providers": [ + { + "url": "https://github.com/facebook/react-native/releases/download/v0.82.0-20250903-0830/React.Native.DevTools-linux-x86_64.tar.gz" + }, { "type": "http", "url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQNDwm7HZRhtNxHqMr1FfSb0afHFGrn1OHxH0gOiggrLrht9QRUgJ3GG5jj7huhQzMRogE-LCMsnxh1ioOZks-YYX4KRt6Kj1-whdWsGFc7lBhPOpk1ssbYFGN1NNyuyFRmH-3nCY3lBC4AmbCUkbDTUeCi9DidCtJeyc73CZJEu7M62rIzxR2yV" @@ -37,6 +43,9 @@ "hash": "sha256", "digest": "3cbe8b1b3d17e433347f1601435bb9a6cb758528a5c176a66fc52d9977223175", "providers": [ + { + "url": "https://github.com/facebook/react-native/releases/download/v0.82.0-20250903-0830/React.Native.DevTools-macos-aarch64.tar.gz" + }, { "type": "http", "url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOmC2cqqSv4OrSJKJroYVg_NE8OE4O73AXqY7wXiYqiWQVkDt0Xnyw3ZeUpQT_Qb0-OoT5F8REKoFrB6eqwat8Ovkyina30peYTTwNUzmwnnGQEg7J0fOHNxLF4dkmU1FagXtsoWgex4dKgsK_VpcMsHj3Vp7diomkYvWBVTf_gPVEseYSN9oKq92qa" @@ -50,6 +59,9 @@ "hash": "sha256", "digest": "6fb79bc2ba3008401b4c9c128248657b95b98581ccde60f8fadb622163779775", "providers": [ + { + "url": "https://github.com/facebook/react-native/releases/download/v0.82.0-20250903-0830/React.Native.DevTools-macos-x86_64.tar.gz" + }, { "type": "http", "url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQMMtGn-YGdfLfTVWC8zbQkQx65Asq6iArKt1t__cjZ8UY_s6-sX5XBHr8k1SaexAO21dFZENQVZ1jW_wn_gJ9ENvosQDG1KfWMViKsHli0xRzZ1HVsgPIj_KVXe907QZwwtJf2XhgH0HT8dfH-AQdDcd0_TB5DFUwOsHzhH0nBrHet7YFkbJtPTaA" @@ -63,6 +75,9 @@ "hash": "sha256", "digest": "579a5b0944c51c3b1b541ad5af66c1ffedf93cae2a891ecdf88cb7219fd9b096", "providers": [ + { + "url": "https://github.com/facebook/react-native/releases/download/v0.82.0-20250903-0830/React.Native.DevTools-windows-x86_64.tar.gz" + }, { "type": "http", "url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOQ3E8lBXqdVHbDPyVb5AOQMrDWjFrFV8fnLLBsygQvLWdpu6ixyG9PgWdwpi5jM-XcDdCHkhBdhaq-5dwT_tgRWKCAMsEBoAIUk0Xg77mGyHG2VF7bNfQ2qFBMuObrsTmrKy1nJ-UFDDm29pJD4GkFQW5NesiBwndJj8t3B8Ur8cczh_XR8rF5" diff --git a/packages/debugger-shell/package.json b/packages/debugger-shell/package.json index 3d268cbfbfd853..3e44111b30d49b 100644 --- a/packages/debugger-shell/package.json +++ b/packages/debugger-shell/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/debugger-shell", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Experimental debugger shell for React Native for use with @react-native/debugger-frontend", "keywords": [ "react-native", diff --git a/packages/dev-middleware/package.json b/packages/dev-middleware/package.json index ce6912ae8262e0..5b933017cecf0d 100644 --- a/packages/dev-middleware/package.json +++ b/packages/dev-middleware/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/dev-middleware", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Dev server middleware for React Native", "keywords": [ "react-native", @@ -23,8 +23,8 @@ ], "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.82.0-main", - "@react-native/debugger-shell": "0.82.0-main", + "@react-native/debugger-frontend": "0.82.0-20250903-0830", + "@react-native/debugger-shell": "0.82.0-20250903-0830", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -39,7 +39,7 @@ "node": ">= 20.19.4" }, "devDependencies": { - "@react-native/debugger-shell": "0.82.0-main", + "@react-native/debugger-shell": "0.82.0-20250903-0830", "selfsigned": "^2.4.1", "undici": "^5.29.0", "wait-for-expect": "^3.0.2" diff --git a/packages/eslint-config-react-native/package.json b/packages/eslint-config-react-native/package.json index 9e93303984addf..f15eac13978efc 100644 --- a/packages/eslint-config-react-native/package.json +++ b/packages/eslint-config-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/eslint-config", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "ESLint config for React Native", "license": "MIT", "repository": { @@ -22,7 +22,7 @@ "dependencies": { "@babel/core": "^7.25.2", "@babel/eslint-parser": "^7.25.1", - "@react-native/eslint-plugin": "0.82.0-main", + "@react-native/eslint-plugin": "0.82.0-20250903-0830", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "eslint-config-prettier": "^8.5.0", diff --git a/packages/eslint-plugin-react-native/package.json b/packages/eslint-plugin-react-native/package.json index aba1f4f3673fd6..4613d8cc7c391b 100644 --- a/packages/eslint-plugin-react-native/package.json +++ b/packages/eslint-plugin-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/eslint-plugin", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "ESLint rules for @react-native/eslint-config", "license": "MIT", "repository": { diff --git a/packages/eslint-plugin-specs/package.json b/packages/eslint-plugin-specs/package.json index dc6928e5e962b3..98402bd389e54f 100644 --- a/packages/eslint-plugin-specs/package.json +++ b/packages/eslint-plugin-specs/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/eslint-plugin-specs", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "ESLint rules to validate NativeModule and Component Specs", "license": "MIT", "repository": { @@ -26,7 +26,7 @@ "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@react-native/codegen": "0.82.0-main", + "@react-native/codegen": "0.82.0-20250903-0830", "make-dir": "^2.1.0", "pirates": "^4.0.1", "source-map-support": "0.5.0" diff --git a/packages/gradle-plugin/package.json b/packages/gradle-plugin/package.json index bb4d12c2f2572c..c582db39efcc9b 100644 --- a/packages/gradle-plugin/package.json +++ b/packages/gradle-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/gradle-plugin", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Gradle Plugin for React Native", "license": "MIT", "repository": { diff --git a/packages/metro-config/package.json b/packages/metro-config/package.json index 094d41a7a7a83e..a9be5fc6d4dd58 100644 --- a/packages/metro-config/package.json +++ b/packages/metro-config/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/metro-config", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Metro configuration for React Native.", "license": "MIT", "repository": { @@ -26,8 +26,8 @@ "dist" ], "dependencies": { - "@react-native/js-polyfills": "0.82.0-main", - "@react-native/metro-babel-transformer": "0.82.0-main", + "@react-native/js-polyfills": "0.82.0-20250903-0830", + "@react-native/metro-babel-transformer": "0.82.0-20250903-0830", "metro-config": "^0.83.1", "metro-runtime": "^0.83.1" } diff --git a/packages/new-app-screen/package.json b/packages/new-app-screen/package.json index d1926d11577487..9714409553de5f 100644 --- a/packages/new-app-screen/package.json +++ b/packages/new-app-screen/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/new-app-screen", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "NewAppScreen component for React Native", "keywords": [ "react-native" diff --git a/packages/normalize-color/package.json b/packages/normalize-color/package.json index d6a7de133ca6df..9aa23df9f5fc2c 100644 --- a/packages/normalize-color/package.json +++ b/packages/normalize-color/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/normalize-colors", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Color normalization for React Native.", "license": "MIT", "repository": { diff --git a/packages/polyfills/package.json b/packages/polyfills/package.json index 355da1286ac651..dc81731eed74fa 100644 --- a/packages/polyfills/package.json +++ b/packages/polyfills/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/js-polyfills", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Polyfills for React Native.", "license": "MIT", "repository": { diff --git a/packages/react-native-babel-preset/package.json b/packages/react-native-babel-preset/package.json index a87ff10ba72d26..086aeaeac9cffb 100644 --- a/packages/react-native-babel-preset/package.json +++ b/packages/react-native-babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/babel-preset", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Babel preset for React Native applications", "repository": { "type": "git", @@ -66,7 +66,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.82.0-main", + "@react-native/babel-plugin-codegen": "0.82.0-20250903-0830", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" diff --git a/packages/react-native-babel-transformer/package.json b/packages/react-native-babel-transformer/package.json index a30fb307c87d50..afd4e930e4c2b9 100644 --- a/packages/react-native-babel-transformer/package.json +++ b/packages/react-native-babel-transformer/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/metro-babel-transformer", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Babel transformer for React Native applications.", "repository": { "type": "git", @@ -27,7 +27,7 @@ ], "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.82.0-main", + "@react-native/babel-preset": "0.82.0-20250903-0830", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" }, diff --git a/packages/react-native-codegen/package.json b/packages/react-native-codegen/package.json index 1e9b31cb5204ab..e0a168374ba5ff 100644 --- a/packages/react-native-codegen/package.json +++ b/packages/react-native-codegen/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/codegen", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Code generation tools for React Native", "license": "MIT", "repository": { diff --git a/packages/react-native-compatibility-check/package.json b/packages/react-native-compatibility-check/package.json index 6d8f07fa1c625d..46c15213c401fb 100644 --- a/packages/react-native-compatibility-check/package.json +++ b/packages/react-native-compatibility-check/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/compatibility-check", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Check a React Native app's boundary between JS and Native for incompatibilities", "license": "MIT", "repository": { @@ -29,7 +29,7 @@ "dist" ], "dependencies": { - "@react-native/codegen": "0.82.0-main" + "@react-native/codegen": "0.82.0-20250903-0830" }, "devDependencies": { "flow-remove-types": "^2.237.2", diff --git a/packages/react-native-popup-menu-android/package.json b/packages/react-native-popup-menu-android/package.json index e34b574f4cac96..3425f3deb5e986 100644 --- a/packages/react-native-popup-menu-android/package.json +++ b/packages/react-native-popup-menu-android/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/popup-menu-android", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "PopupMenu for the Android platform", "main": "index.js", "files": [ @@ -21,7 +21,7 @@ }, "license": "MIT", "devDependencies": { - "@react-native/codegen": "0.82.0-main" + "@react-native/codegen": "0.82.0-20250903-0830" }, "peerDependencies": { "@types/react": "^19.1.0", diff --git a/packages/react-native/Libraries/Core/ReactNativeVersion.js b/packages/react-native/Libraries/Core/ReactNativeVersion.js index 1ff12f563e621a..b7e0f9d7ea4c72 100644 --- a/packages/react-native/Libraries/Core/ReactNativeVersion.js +++ b/packages/react-native/Libraries/Core/ReactNativeVersion.js @@ -26,10 +26,10 @@ * ``` */ export default class ReactNativeVersion { - static major: number = 1000; - static minor: number = 0; + static major: number = 0; + static minor: number = 82; static patch: number = 0; - static prerelease: string | null = null; + static prerelease: string | null = '20250903-0830'; static getVersionString(): string { return `${this.major}.${this.minor}.${this.patch}${this.prerelease != null ? `-${this.prerelease}` : ''}`; diff --git a/packages/react-native/React/Base/RCTVersion.m b/packages/react-native/React/Base/RCTVersion.m index 69ce4f75320e63..796e19c305e701 100644 --- a/packages/react-native/React/Base/RCTVersion.m +++ b/packages/react-native/React/Base/RCTVersion.m @@ -21,10 +21,10 @@ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^(void){ __rnVersion = @{ - RCTVersionMajor: @(1000), - RCTVersionMinor: @(0), + RCTVersionMajor: @(0), + RCTVersionMinor: @(82), RCTVersionPatch: @(0), - RCTVersionPrerelease: [NSNull null], + RCTVersionPrerelease: @"20250903-0830", }; }); return __rnVersion; diff --git a/packages/react-native/ReactAndroid/gradle.properties b/packages/react-native/ReactAndroid/gradle.properties index 7c0c467db7a633..fd4be82855f3de 100644 --- a/packages/react-native/ReactAndroid/gradle.properties +++ b/packages/react-native/ReactAndroid/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1000.0.0 +VERSION_NAME=0.82.0-20250903-0830 react.internal.publishingGroup=com.facebook.react android.useAndroidX=true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt index 889fa4d728c424..2b29e6977ec3c3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt @@ -1,15 +1,20 @@ /** * 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. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * * @generated by scripts/releases/set-version.js */ + package com.facebook.react.modules.systeminfo public object ReactNativeVersion { @JvmField - public val VERSION: Map = - mapOf("major" to 1000, "minor" to 0, "patch" to 0, "prerelease" to null) + public val VERSION: Map = mapOf( + "major" to 0, + "minor" to 82, + "patch" to 0, + "prerelease" to "20250903-0830" + ) } diff --git a/packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h b/packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h index 0c876d971a62c4..7b6554db51148d 100644 --- a/packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h +++ b/packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h @@ -12,17 +12,17 @@ #include #include -#define REACT_NATIVE_VERSION_MAJOR 1000 -#define REACT_NATIVE_VERSION_MINOR 0 +#define REACT_NATIVE_VERSION_MAJOR 0 +#define REACT_NATIVE_VERSION_MINOR 82 #define REACT_NATIVE_VERSION_PATCH 0 namespace facebook::react { constexpr struct { - int32_t Major = 1000; - int32_t Minor = 0; + int32_t Major = 0; + int32_t Minor = 82; int32_t Patch = 0; - std::string_view Prerelease = ""; + std::string_view Prerelease = "20250903-0830"; } ReactNativeVersion; } // namespace facebook::react diff --git a/packages/react-native/package.json b/packages/react-native/package.json index b51206a3275316..d510b40110136f 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "1000.0.0", + "version": "0.82.0-20250903-0830", "description": "A framework for building native apps using React", "license": "MIT", "repository": { @@ -162,13 +162,13 @@ }, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.82.0-main", - "@react-native/codegen": "0.82.0-main", - "@react-native/community-cli-plugin": "0.82.0-main", - "@react-native/gradle-plugin": "0.82.0-main", - "@react-native/js-polyfills": "0.82.0-main", - "@react-native/normalize-colors": "0.82.0-main", - "@react-native/virtualized-lists": "0.82.0-main", + "@react-native/assets-registry": "0.82.0-20250903-0830", + "@react-native/codegen": "0.82.0-20250903-0830", + "@react-native/community-cli-plugin": "0.82.0-20250903-0830", + "@react-native/gradle-plugin": "0.82.0-20250903-0830", + "@react-native/js-polyfills": "0.82.0-20250903-0830", + "@react-native/normalize-colors": "0.82.0-20250903-0830", + "@react-native/virtualized-lists": "0.82.0-20250903-0830", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", diff --git a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap index 1f04a84d2400e0..6785144776e259 100644 --- a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap +++ b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap @@ -361,7 +361,7 @@ exports[`execute test-app "ReactAppDependencyProvider.podspec" should match snap # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -version = \\"1000.0.0\\" +version = \\"0.82.0-20250903-0830\\" source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. @@ -399,7 +399,7 @@ exports[`execute test-app "ReactCodegen.podspec" should match snapshot 1`] = ` # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -version = \\"1000.0.0\\" +version = \\"0.82.0-20250903-0830\\" source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. @@ -840,7 +840,7 @@ exports[`execute test-app-legacy "ReactAppDependencyProvider.podspec" should mat # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -version = \\"1000.0.0\\" +version = \\"0.82.0-20250903-0830\\" source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. @@ -878,7 +878,7 @@ exports[`execute test-app-legacy "ReactCodegen.podspec" should match snapshot 1` # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -version = \\"1000.0.0\\" +version = \\"0.82.0-20250903-0830\\" source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. @@ -984,4 +984,4 @@ SCRIPT end " -`; +`; \ No newline at end of file diff --git a/packages/rn-tester/package.json b/packages/rn-tester/package.json index 9961db5e82b5a9..59920abaf7595d 100644 --- a/packages/rn-tester/package.json +++ b/packages/rn-tester/package.json @@ -26,8 +26,8 @@ "e2e-test-ios": "./scripts/maestro-test-ios.sh" }, "dependencies": { - "@react-native/new-app-screen": "0.82.0-main", - "@react-native/popup-menu-android": "0.82.0-main", + "@react-native/new-app-screen": "0.82.0-20250903-0830", + "@react-native/popup-menu-android": "0.82.0-20250903-0830", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "nullthrows": "^1.1.1" diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index a00cd1a2fe912e..3ace0e4efe4fdd 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/typescript-config", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Default TypeScript configuration for React Native apps", "license": "MIT", "repository": { diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json index f5d6eb2d2c82e4..2cd192a00d8e59 100644 --- a/packages/virtualized-lists/package.json +++ b/packages/virtualized-lists/package.json @@ -1,6 +1,6 @@ { "name": "@react-native/virtualized-lists", - "version": "0.82.0-main", + "version": "0.82.0-20250903-0830", "description": "Virtualized lists for React Native.", "license": "MIT", "repository": { diff --git a/private/helloworld/package.json b/private/helloworld/package.json index 757495928cacc0..7054fb58570b0d 100644 --- a/private/helloworld/package.json +++ b/private/helloworld/package.json @@ -13,17 +13,17 @@ }, "dependencies": { "react": "19.1.1", - "react-native": "1000.0.0" + "react-native": "0.82.0-20250903-0830" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native/babel-preset": "0.82.0-main", - "@react-native/core-cli-utils": "0.82.0-main", - "@react-native/eslint-config": "0.82.0-main", - "@react-native/metro-config": "0.82.0-main", - "@react-native/typescript-config": "0.82.0-main", + "@react-native/babel-preset": "0.82.0-20250903-0830", + "@react-native/core-cli-utils": "0.82.0-20250903-0830", + "@react-native/eslint-config": "0.82.0-20250903-0830", + "@react-native/metro-config": "0.82.0-20250903-0830", + "@react-native/typescript-config": "0.82.0-20250903-0830", "@types/jest": "^29.5.14", "commander": "^12.0.0", "eslint": "^8.19.0", diff --git a/private/react-native-codegen-typescript-test/package.json b/private/react-native-codegen-typescript-test/package.json index 1ca5fa84091a37..5ae206132dc3c9 100644 --- a/private/react-native-codegen-typescript-test/package.json +++ b/private/react-native-codegen-typescript-test/package.json @@ -13,7 +13,7 @@ "prepare": "yarn run build" }, "dependencies": { - "@react-native/codegen": "0.82.0-main" + "@react-native/codegen": "0.82.0-20250903-0830" }, "devDependencies": { "@babel/core": "^7.25.2", From 81f334eac5147d4dbf5f6d7d627ddfa52cd197be Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 08/10] [DO NOT LAND] Trigger create-draft-release on PRs --- .github/workflows/create-draft-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml index 5852b8fec7105c..82a91f7e1e28e5 100644 --- a/.github/workflows/create-draft-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -2,6 +2,7 @@ name: Create Draft Release on: workflow_call: + pull_request: jobs: create-draft-release: From 6d8851657629de7e0b710ed8f5dd7d0f7b9847cc Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 09/10] [DO NOT LAND] Disable tag verification in createDraftRelease --- .github/workflow-scripts/createDraftRelease.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflow-scripts/createDraftRelease.js b/.github/workflow-scripts/createDraftRelease.js index 7b7692ce2975d4..03a59349add164 100644 --- a/.github/workflow-scripts/createDraftRelease.js +++ b/.github/workflow-scripts/createDraftRelease.js @@ -118,7 +118,7 @@ async function createDraftRelease(version, latest, token) { version = version.substring(1); } - _verifyTagExists(version); + // _verifyTagExists(version); moveToChangelogBranch(version); const changelog = _extractChangelog(version); const body = _computeBody(version, changelog); From 1428a8da8b9fb29c45fc33d79f311dd1fe273433 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 3 Sep 2025 08:47:15 +0100 Subject: [PATCH 10/10] [DO NOT LAND] Hardcode version number for uploading assets --- .github/workflows/create-draft-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml index 82a91f7e1e28e5..20ad85e9d5feb8 100644 --- a/.github/workflows/create-draft-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -37,7 +37,8 @@ jobs: with: script: | const {uploadReleaseAssetsForDotSlashFiles} = require('./scripts/releases/upload-release-assets-for-dotslash.js'); - const version = '${{ github.ref_name }}'; + // const version = '${{ github.ref_name }}'; + const version = 'v0.82.0-20250903-0830'; await uploadReleaseAssetsForDotSlashFiles({ version, token: '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}',