Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4d93597
fix(ci): forward-port release integration fixes from 0.83
Saadnajmi Sep 15, 2026
a976521
fix(codegen): match package-relative source paths
Saadnajmi Sep 15, 2026
87adb52
fix(types): canonicalize checkout-relative API imports
Saadnajmi Sep 15, 2026
2b97335
fix(pods): resolve dependency package fallback
Saadnajmi Sep 15, 2026
07921f8
fix(prebuild): fail invalid platform requests
Saadnajmi Sep 15, 2026
584cfd3
fix(text): preserve explicit custom font weights
Saadnajmi Sep 16, 2026
581aa73
fix(release): publish prepared Changesets versions with one tag
Saadnajmi Sep 16, 2026
ac2c766
fix(release): validate prepared version PRs against their base
Saadnajmi Sep 16, 2026
809fa80
test(release): isolate invalid publication graph fixtures
Saadnajmi Sep 16, 2026
75926e2
fix(packaging): complete fork package dependencies and notices
Saadnajmi Sep 17, 2026
ef79dc1
fix(release): preserve fork workspace version relationships
Saadnajmi Sep 17, 2026
35a72da
fix(macos): initialize and dismiss RedBox through its controller
Saadnajmi Sep 17, 2026
3c658c0
fix(pods): resolve platform-specific framework dependency headers
Saadnajmi Sep 17, 2026
b18a9d3
fix(graphics): use canonical HostPlatformColor header import
Saadnajmi Sep 17, 2026
5a7c77b
fix(macos): decouple core views from the optional text framework
Saadnajmi Sep 17, 2026
d7d9fc0
fix(pods): declare direct RCTUIKit framework dependencies
Saadnajmi Sep 17, 2026
ea7972a
fix(macos): link UniformTypeIdentifiers from Fabric
Saadnajmi Sep 17, 2026
0b1306d
fix(pods): link CoreGraphics from its graphics consumer
Saadnajmi Sep 17, 2026
a9e5b9e
fix(pods): link CoreGraphics from the sample module
Saadnajmi Sep 17, 2026
29191b6
fix(macos): size RedBox rows from native table content
Saadnajmi Sep 17, 2026
ff90a9b
fix(fabric): preserve canonical platform headers in frameworks
Saadnajmi Sep 17, 2026
52c69fc
fix(hermes): resolve default artifacts from branch metadata
Saadnajmi Sep 16, 2026
2dd03ee
test(hermes): validate each branch metadata without fixed versions
Saadnajmi Sep 17, 2026
685d05a
fix(packaging): include the Hermes V1 source tag
Saadnajmi Sep 17, 2026
f2467af
test(release): validate graph policy across private and public branches
Saadnajmi Sep 17, 2026
05d941b
fix(release): honor local Changesets bases and repository remotes
Saadnajmi Sep 17, 2026
c440e99
fix(release): generate exported package types before publication
Saadnajmi Sep 17, 2026
4157235
fix(release): preserve private upstream workspace versions
Saadnajmi Sep 17, 2026
0145de8
fix(types): preserve canonical fork event declarations
Saadnajmi Sep 17, 2026
e8a68d0
fix(hermes): preserve macOS slices and symbols during recomposition
Saadnajmi Sep 17, 2026
febd7d6
fix(flow): type Hermes framework validation metadata
Saadnajmi Sep 17, 2026
4de3e75
fix(flow): normalize Hermes metadata command output
Saadnajmi Sep 20, 2026
f741b50
fix(types): resolve private workspace dependencies during generation
Saadnajmi Sep 21, 2026
226a7ab
fix(ci): apply reviewed release validation repairs
Saadnajmi Sep 22, 2026
9b59f96
fix(ci): align lock metadata with the public registry
Saadnajmi Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .ado/jobs/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
jobs:
- job: NPMPublish
displayName: NPM Publish
# Also disable direct template consumers until ADO publication is re-enabled.
condition: false
pool:
name: cxeiss-ubuntu-20-04-large
image: cxe-ubuntu-20-04-1es-pt
Expand Down
3 changes: 3 additions & 0 deletions .ado/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,8 @@ extends:
stages:
- stage: NPM
dependsOn: []
# GitHub Trusted Publishing owns releases. Retain the ADO publication
# path for explicit re-enablement after its contract/auth are reviewed.
condition: false
jobs:
- template: /.ado/jobs/npm-publish.yml@self
274 changes: 41 additions & 233 deletions .ado/scripts/configure-publish.mts
Original file line number Diff line number Diff line change
@@ -1,237 +1,45 @@
#!/usr/bin/env node
import { $, argv, echo, fs } from 'zx';
import { resolve } from 'node:path';

const isGitHubActions = process.env['GITHUB_ACTIONS'] === 'true';

const NPM_TAG_NEXT = 'next';

export type ReleaseState = 'STABLE_IS_LATEST' | 'STABLE_IS_NEW' | 'STABLE_IS_OLD';

export interface ReleaseStateInfo {
state: ReleaseState;
currentVersion: number;
latestVersion: number;
nextVersion: number;
}

export interface TagInfo {
npmTags: string[];
prerelease?: string;
}

interface Options {
'mock-branch'?: string;
tag?: string;
verbose?: boolean;
}

function enablePublishingOnAzurePipelines() {
echo(`##vso[task.setvariable variable=publish_react_native_macos]1`);
}

function enablePublishingOnGitHubActions() {
if (process.env['GITHUB_OUTPUT']) {
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `publish_react_native_macos=1\n`);
}
}

export function isMainBranch(branch: string): boolean {
return branch === 'main';
}

export function isStableBranch(branch: string): boolean {
return /^\d+\.\d+-stable$/.test(branch);
}

export function versionToNumber(version: string): number {
const [major, minor] = version.split('-')[0].split('.');
return Number(major) * 1000 + Number(minor);
}

function getTargetBranch(): string | undefined {
// Azure Pipelines
if (process.env['TF_BUILD'] === 'True') {
const targetBranch = process.env['SYSTEM_PULLREQUEST_TARGETBRANCH'];
return targetBranch?.replace(/^refs\/heads\//, '');
}

// GitHub Actions
if (process.env['GITHUB_ACTIONS'] === 'true') {
return process.env['GITHUB_BASE_REF'];
}

return undefined;
}

async function getCurrentBranch(options: Options): Promise<string> {
const targetBranch = getTargetBranch();
if (targetBranch) {
return targetBranch;
}

// Azure DevOps Pipelines
if (process.env['TF_BUILD'] === 'True') {
const sourceBranch = process.env['BUILD_SOURCEBRANCHNAME'];
if (sourceBranch) {
return sourceBranch.replace(/^refs\/heads\//, '');
}
}

// GitHub Actions
if (process.env['GITHUB_ACTIONS'] === 'true') {
const headRef = process.env['GITHUB_HEAD_REF'];
if (headRef) return headRef;

const ref = process.env['GITHUB_REF'];
if (ref) return ref.replace(/^refs\/heads\//, '');
}

if (options['mock-branch']) {
return options['mock-branch'];
}

const result = await $`git rev-parse --abbrev-ref HEAD`;
return result.stdout.trim();
}

function getPublishedVersionSync(tag: 'latest' | 'next'): number {
const result = $.sync`npm view react-native-macos@${tag} version`;
return versionToNumber(result.stdout.trim());
}

export function getReleaseState(
branch: string,
getVersion: (tag: 'latest' | 'next') => number = getPublishedVersionSync,
): ReleaseStateInfo {
if (!isStableBranch(branch)) {
throw new Error('Expected a stable branch');
}

const latestVersion = getVersion('latest');
const nextVersion = getVersion('next');
const currentVersion = versionToNumber(branch);

let state: ReleaseState;
if (currentVersion === latestVersion) {
state = 'STABLE_IS_LATEST';
} else if (currentVersion < latestVersion) {
state = 'STABLE_IS_OLD';
import {execFileSync} from 'node:child_process';
import {appendFileSync} from 'node:fs';
import {parseArgs} from 'node:util';
import {
createPublishPlan,
isStableBranch,
publishPrepared,
readChangesetStatus,
readWorkspaces,
} from '../../.github/scripts/publishing-contract.mjs';

const {values: options} = parseArgs({options: {
'mock-branch': {type: 'string'},
verbose: {type: 'boolean'},
publish: {type: 'boolean'},
}});
const branch = process.env.GITHUB_REF_NAME ?? process.env.BUILD_SOURCEBRANCHNAME ??
options['mock-branch'] ?? execFileSync('git', ['branch', '--show-current'], {encoding: 'utf8'}).trim();
const isPullRequest = Boolean(process.env.GITHUB_BASE_REF ||
process.env.SYSTEM_PULLREQUEST_TARGETBRANCH || process.env.BUILD_REASON === 'PullRequest');

function output(name: string, value: string) {
if (process.env.TF_BUILD === 'True') console.log(`##vso[task.setvariable variable=${name}]${value}`);
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}

output('publish_react_native_macos', '0');
if (!isStableBranch(branch) || isPullRequest) {
console.log(`Publication disabled for ${isPullRequest ? 'pull requests' : branch}`);
} else {
const plan = await createPublishPlan({
branch,
status: await readChangesetStatus(),
workspaces: readWorkspaces(),
});
if (options.verbose) console.log(JSON.stringify(plan, null, 2));
output('publishTag', plan.tag ?? '');
if (plan.packages.length) {
output('publish_react_native_macos', '1');
if (options.publish) publishPrepared(plan);
} else {
state = 'STABLE_IS_NEW';
}

return { state, currentVersion, latestVersion, nextVersion };
}

export function getPublishTags(
stateInfo: ReleaseStateInfo,
branch: string,
tag: string = NPM_TAG_NEXT,
): TagInfo {
const { state, currentVersion, nextVersion } = stateInfo;

switch (state) {
case 'STABLE_IS_LATEST':
// Patching the current latest version
return { npmTags: ['latest', branch] };

case 'STABLE_IS_OLD':
// Patching an older stable version
return { npmTags: [branch] };

case 'STABLE_IS_NEW': {
if (tag === 'latest') {
// Promoting this branch to latest
const npmTags = ['latest', branch];
if (currentVersion > nextVersion) {
npmTags.push(NPM_TAG_NEXT);
}
return { npmTags };
}

// Publishing a release candidate
if (currentVersion < nextVersion) {
throw new Error(
`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`,
);
}

return { npmTags: [NPM_TAG_NEXT], prerelease: 'rc' };
}
}
}

async function enablePublishing(tagInfo: TagInfo, options: Options) {
const [primaryTag, ...additionalTags] = tagInfo.npmTags;

// Output publishTag for subsequent pipeline steps
echo(`##vso[task.setvariable variable=publishTag]${primaryTag}`);
if (process.env['GITHUB_OUTPUT']) {
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `publishTag=${primaryTag}\n`);
}

// Output additional tags
if (additionalTags.length > 0) {
const tagsValue = additionalTags.join(',');
echo(`##vso[task.setvariable variable=additionalTags]${tagsValue}`);
if (process.env['GITHUB_OUTPUT']) {
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `additionalTags=${tagsValue}\n`);
}
}

// Don't enable publishing in PRs
if (!getTargetBranch()) {
if (isGitHubActions) {
enablePublishingOnGitHubActions();
} else if (process.env['TF_BUILD'] === 'True') {
enablePublishingOnAzurePipelines();
} else {
echo('ℹ️ Local run — publishing not enabled');
}
}
}

const isDirectRun =
process.argv[1] != null &&
resolve(process.argv[1]) === new URL(import.meta.url).pathname;

if (isDirectRun) {
// Parse CLI args using zx's argv (minimist)
const options: Options = {
'mock-branch': argv['mock-branch'] as string | undefined,
tag: typeof argv['tag'] === 'string' ? argv['tag'] : NPM_TAG_NEXT,
verbose: Boolean(argv['verbose']),
};

const branch = await getCurrentBranch(options);
if (!branch) {
echo('❌ Could not get current branch');
process.exit(1);
}

const log = options.verbose ? (msg: string) => echo(`ℹ️ ${msg}`) : () => {};

try {
if (isMainBranch(branch)) {
// Nightlies are currently disabled — skip publishing from main
echo('ℹ️ On main branch — nightly publishing is currently disabled');
} else if (isStableBranch(branch)) {
const stateInfo = getReleaseState(branch);
log(`react-native-macos@latest: ${stateInfo.latestVersion}`);
log(`react-native-macos@next: ${stateInfo.nextVersion}`);
log(`Current version: ${stateInfo.currentVersion}`);
log(`Release state: ${stateInfo.state}`);

const tagInfo = getPublishTags(stateInfo, branch, options.tag);
log(`Expected npm tags: ${tagInfo.npmTags.join(', ')}`);

await enablePublishing(tagInfo, options);
} else {
echo(`ℹ️ Branch '${branch}' is not main or a stable branch — skipping`);
}
} catch (e) {
echo(`❌ ${(e as Error).message}`);
process.exit(1);
console.log(plan.reason ?? 'All prepared versions are already published');
}
}
8 changes: 7 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"access": "public",
"baseBranch": "origin/main",
"bumpVersionsWithWorkspaceProtocolOnly": true,
"changelog": "@changesets/cli/changelog",
"commit": false,
"ignore": [],
"fixed": [["react-native-macos", "@react-native-macos/virtualized-lists"]],
"ignore": ["react-native-macos", "@react-native/tester"],
"privatePackages": {
"version": false,
"tag": false
},
"linked": []
}
55 changes: 55 additions & 0 deletions .github/scripts/__tests__/__fixtures__/resolve-hermes.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright (c) Microsoft Corporation.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

// Preload in the CLI subprocess. Exercise the real ESM entry point and URL
// helper without network access or changes to checked-in metadata.
const fs = require('node:fs');
const path = require('node:path');

const propertiesPath = path.resolve(
__dirname,
'../../../../packages/react-native/sdks/hermes-engine/version.properties',
);
const readFileSync = fs.readFileSync;
fs.readFileSync = function (file, ...args) {
if (file === propertiesPath && process.env.HERMES_TEST_PROPERTIES != null) {
if (process.env.HERMES_TEST_PROPERTIES === 'MISSING') {
throw Object.assign(new Error(`ENOENT: ${propertiesPath}`), {
code: 'ENOENT',
});
}
if (process.env.HERMES_TEST_PROPERTIES === 'UNREADABLE') {
throw Object.assign(new Error(`EACCES: ${propertiesPath}`), {
code: 'EACCES',
});
}
return process.env.HERMES_TEST_PROPERTIES;
}
return readFileSync.call(this, file, ...args);
};

const urls = [];
global.fetch = async url => {
urls.push(url);
const mode = process.env.HERMES_TEST_DOWNLOAD;
if (url.endsWith('/maven-metadata.xml')) {
return {
ok: mode === 'snapshot',
text: async () =>
'<metadata><snapshot><timestamp>20260101.010203</timestamp><buildNumber>4</buildNumber></snapshot></metadata>',
};
}
return {
ok: mode === 'release' || (mode === 'snapshot' && url.includes('SNAPSHOT')),
status: 404,
statusText: 'Not Found',
arrayBuffer: async () => Buffer.from('mock Hermes archive'),
};
};
process.on('exit', () => console.log(`HERMES_TEST_URLS=${JSON.stringify(urls)}`));
Loading
Loading