Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion .ado/azure-pipelines.publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ extends:
# https://github.com/changesets/changesets/issues/432
# We can't use `changeset publish` because it doesn't support workspaces, so we have to publish each package individually
yarn workspaces foreach --all --topological --no-private \
exec zx $(Build.SourcesDirectory)/.github/scripts/publish-package-if-needed.mts
exec node $(Build.SourcesDirectory)/.github/scripts/publish-package-if-needed.mts
displayName: 'Publish NPM Packages'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), not(${{ parameters.skipNpmPublish }}))

Expand Down
59 changes: 30 additions & 29 deletions .github/scripts/publish-package-if-needed.mts
Original file line number Diff line number Diff line change
@@ -1,63 +1,64 @@
#!/usr/bin/env zx
import 'zx/globals';
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';

// Type declaration for process (zx doesn't include Node.js types)
declare const process: {
env: Record<string, string | undefined>;
cwd: () => string;
argv: string[];
exit: (code: number) => never;
};
interface PackageJson {
name: string;
version: string;
private?: boolean;
}

/**
* Publish a single package to npm if needed
*
* This script:
* - Operates on the current workspace directory
* - Checks npm registry before publishing
* - Checks npm registry before publishing (idempotent - safe to retry)
* - Requires yarn npm authentication to be configured (npmAuthToken)
* - Skips private packages automatically
*
* Usage:
* # In a workspace directory:
* npx zx publish-package-if-needed.mts # Publish for real
* npx zx publish-package-if-needed.mts --dry-run # Simulate publishing
* node publish-package-if-needed.mts # Publish for real
* node publish-package-if-needed.mts --dry-run # Simulate publishing
*
* # For all workspaces in topological order:
* yarn workspaces foreach --all --topological --no-private \
* exec npx zx .github/scripts/publish-package-if-needed.mts
* exec node .github/scripts/publish-package-if-needed.mts
*/

// Parse command line arguments
const isDryRun = process.argv.includes('--dry-run');

// Read package.json from current directory
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const packageJson: PackageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
const { name, version, private: isPrivate } = packageJson;

// Skip private packages
if (isPrivate) {
echo(`⊘ Skipping private package: ${name}`);
console.log(`⊘ Skipping private package: ${name}`);
process.exit(0);
}

// Check if package@version already exists on npm
const result = await $`npm view ${name}@${version} version 2>&1`.nothrow().quiet();
const shouldPublish = result.exitCode !== 0 || result.stdout.trim() !== version;
const checkResult = spawnSync('npm', ['view', `${name}@${version}`, 'version'], {
encoding: 'utf-8',
stdio: 'pipe',
});
const alreadyPublished = checkResult.status === 0 && checkResult.stdout.trim() === version;

if (!shouldPublish) {
echo(`✓ Already published: ${name}@${version}`);
if (alreadyPublished) {
console.log(`✓ Already published: ${name}@${version}`);
process.exit(0);
}

// Publish the package
const startMsg = isDryRun ? 'Simulating publish' : 'Publishing';
const endMsg = isDryRun ? 'Dry-run successful for' : 'Successfully published';

echo(`→ ${startMsg}: ${name}@${version}`);
console.log(`→ ${startMsg}: ${name}@${version}`);

const publishCmd = isDryRun
? $`yarn npm publish --access public --dry-run`
: $`yarn npm publish --access public`;
const publishArgs = ['npm', 'publish', '--access', 'public'];
if (isDryRun) publishArgs.push('--dry-run');

await publishCmd;
echo(`✓ ${endMsg}: ${name}@${version}`);
const publishResult = spawnSync('yarn', publishArgs, { stdio: 'inherit' });
if (publishResult.status !== 0) {
process.exit(publishResult.status ?? 1);
}
console.log(`✓ ${endMsg}: ${name}@${version}`);
5 changes: 2 additions & 3 deletions .github/workflows/changesets-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ jobs:
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permissions: |
contents: write
pull-requests: write
permission-contents: write # for GH releases and Git tags (Changesets)
permission-pull-requests: write # version PRs (Changesets)

- name: Create Version Bump PR
id: changesets
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,30 @@ jobs:
- name: Validate changesets
run: yarn changeset:validate

publish-dry-run:
name: NPM Publish Dry Run
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up toolchain
uses: microsoft/react-native-test-app/.github/actions/setup-toolchain@5.0.14
with:
node-version: 22

- name: Install dependencies
run: yarn

- name: Build packages
run: yarn build

- name: Simulate publish
run: |
yarn workspaces foreach --all --topological --no-private \
exec node $(pwd)/.github/scripts/publish-package-if-needed.mts --dry-run

test-links:
name: Test repo links
runs-on: ubuntu-latest
Expand Down Expand Up @@ -336,6 +360,7 @@ jobs:
- windows
- win32
- check-changesets
- publish-dry-run
- test-links
steps:
- name: All required jobs passed
Expand Down
Loading