Skip to content

Commit 813bb77

Browse files
authored
ci: split publish to build and publish (#4042)
* ci: split publish to build and publish * npm publish tarballs
1 parent 6689932 commit 813bb77

File tree

6 files changed

+150
-13
lines changed

6 files changed

+150
-13
lines changed

.ado/azure-pipelines.publish.yml

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@ extends:
4545
environmentsEs6: true
4646
environmentsNode: true
4747
stages:
48-
- stage: main
48+
- stage: Build
49+
displayName: Build & Pack
4950
jobs:
50-
- job: NPMPublish
51-
displayName: NPM Publish
51+
- job: BuildAndPack
52+
displayName: Build, Test & Pack
5253
pool:
5354
name: Azure-Pipelines-1ESPT-ExDShared
5455
image: ubuntu-latest
5556
os: linux
5657
templateContext:
5758
outputs:
5859
- output: pipelineArtifact
59-
targetPath: $(System.DefaultWorkingDirectory)
60-
artifactName: dist
60+
targetPath: $(System.DefaultWorkingDirectory)/_packed
61+
artifactName: packed-tarballs
6162
steps:
6263
- task: UseNode@1
6364
inputs:
@@ -70,25 +71,71 @@ extends:
7071
7172
- script: |
7273
yarn buildci
73-
displayName: 'yarn buildci [test]'
74+
displayName: 'yarn buildci [build + test + lint]'
75+
76+
- script: |
77+
yarn lage pack --verbose --grouped
78+
displayName: 'Pack all public packages'
79+
80+
- script: |
81+
echo "Packed tarballs:"
82+
ls -la $(System.DefaultWorkingDirectory)/_packed/
83+
displayName: 'List packed tarballs'
84+
85+
- stage: Publish
86+
displayName: Publish to NPM
87+
dependsOn: Build
88+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), not(${{ parameters.skipNpmPublish }}))
89+
jobs:
90+
- job: PublishPackages
91+
displayName: Publish NPM Packages
92+
pool:
93+
name: Azure-Pipelines-1ESPT-ExDShared
94+
image: ubuntu-latest
95+
os: linux
96+
steps:
97+
- task: UseNode@1
98+
inputs:
99+
version: '22.x'
100+
displayName: 'Use Node.js 22.x'
101+
102+
- task: DownloadPipelineArtifact@2
103+
inputs:
104+
artifactName: packed-tarballs
105+
targetPath: $(System.DefaultWorkingDirectory)/_packed
106+
displayName: 'Download packed tarballs'
107+
108+
- script: |
109+
echo "Downloaded tarballs:"
110+
ls -la $(System.DefaultWorkingDirectory)/_packed/
111+
displayName: 'List downloaded tarballs'
112+
113+
- script: |
114+
yarn --immutable
115+
displayName: 'yarn install --immutable'
74116
75117
- script: |
76118
yarn config set npmPublishAccess public
77119
yarn config set npmPublishRegistry "https://registry.npmjs.org"
78120
yarn config set npmAuthToken $(npmAuth)
79-
displayName: 'Configure yarn for npm publishing'
80-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), not(${{ parameters.skipNpmPublish }}))
121+
npm config set //registry.npmjs.org/:_authToken $(npmAuth)
122+
displayName: 'Configure npm publishing auth'
81123
82124
- script: |
83125
# https://github.com/changesets/changesets/issues/432
84126
# We can't use `changeset publish` because it doesn't support workspaces, so we have to publish each package individually
85127
yarn lage publish --verbose --grouped --reporter azureDevops
86128
displayName: 'Publish NPM Packages'
87-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), not(${{ parameters.skipNpmPublish }}))
88129
89130
- script: |
90131
yarn config unset npmPublishAccess
91132
yarn config unset npmAuthToken
92133
yarn config unset npmPublishRegistry
93-
displayName: 'Cleanup yarn npm config'
134+
npm config delete //registry.npmjs.org/:_authToken
135+
displayName: 'Cleanup npm publishing auth'
136+
condition: always()
137+
138+
- script: |
139+
git clean -dfx
140+
displayName: 'Clean up working directory'
94141
condition: always()

.github/workflows/pr.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ jobs:
320320
- name: Build packages
321321
run: yarn build
322322

323+
- name: Pack packages
324+
run: yarn lage pack --verbose --grouped
325+
323326
- name: Simulate publish
324327
run: yarn lage publish-dry-run --verbose --grouped
325328

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ typings/
7979
# Output of 'npm pack'
8080
*.tgz
8181

82+
# Pack staging directory
83+
_packed/
84+
8285
# Yarn Integrity file
8386
.yarn-integrity
8487

lage.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,21 @@ const config = {
4646
inputs: [],
4747
outputs: [],
4848
},
49+
pack: {
50+
dependsOn: ['build-all', '^pack'],
51+
type: 'worker',
52+
options: {
53+
worker: 'scripts/src/worker/pack.mts',
54+
outputDir: '_packed',
55+
},
56+
cache: false,
57+
},
4958
publish: {
5059
dependsOn: ['^publish'],
5160
type: 'worker',
5261
options: {
5362
worker: 'scripts/src/worker/publish.mts',
63+
outputDir: '_packed',
5464
},
5565
cache: false,
5666
},
@@ -59,6 +69,7 @@ const config = {
5969
type: 'worker',
6070
options: {
6171
worker: 'scripts/src/worker/publish.mts',
72+
outputDir: '_packed',
6273
dryRun: true,
6374
},
6475
cache: false,

scripts/src/worker/pack.mts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { WorkerRunnerFunction } from 'lage';
2+
3+
import { $, fs } from 'zx';
4+
import { join, resolve } from 'node:path';
5+
6+
/**
7+
* Lage worker that runs `yarn npm pack` for each public package,
8+
* collecting the resulting .tgz files into a flat staging directory.
9+
*
10+
* The output directory is passed via `target.options.outputDir` in lage.config.js.
11+
* The tgz filename is derived from the package name and version so it is
12+
* unique and easy to correlate back to the package.
13+
*/
14+
export const run: WorkerRunnerFunction = async ({ target }) => {
15+
const pkg = await fs.readJson(join(target.cwd, 'package.json'));
16+
17+
if (pkg.private) {
18+
return;
19+
}
20+
21+
const outputDir = target.options?.outputDir as string | undefined;
22+
if (!outputDir) {
23+
throw new Error('pack worker requires options.outputDir to be set in lage.config.js');
24+
}
25+
26+
// Resolve relative to cwd (lage runs from repo root, so this resolves correctly)
27+
const stagingDir = resolve(outputDir);
28+
await fs.mkdirp(stagingDir);
29+
30+
// Build a safe filename: @fluentui-react-native/button@1.0.0 -> fluentui-react-native-button-1.0.0.tgz
31+
const safeName = (pkg.name as string).replace(/@/g, '').replace(/\//g, '-');
32+
const tgzFilename = `${safeName}-${pkg.version}.tgz`;
33+
const outPath = join(stagingDir, tgzFilename);
34+
35+
await $({ cwd: target.cwd, verbose: true })`yarn pack --out ${outPath}`;
36+
};

scripts/src/worker/publish.mts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
import type { WorkerRunnerFunction } from 'lage';
22

33
import { $, fs } from 'zx';
4-
import { join } from 'node:path';
4+
import { join, resolve } from 'node:path';
5+
6+
/**
7+
* Find the tarball for a package in the output directory.
8+
* Returns the full path if found, undefined otherwise.
9+
*/
10+
async function findTarball(pkg: { name: string; version: string }, outputDir: string): Promise<string | undefined> {
11+
const stagingDir = resolve(outputDir);
12+
const safeName = (pkg.name as string).replace(/@/g, '').replace(/\//g, '-');
13+
const tgzFilename = `${safeName}-${pkg.version}.tgz`;
14+
const tgzPath = join(stagingDir, tgzFilename);
15+
16+
if (await fs.pathExists(tgzPath)) {
17+
return tgzPath;
18+
}
19+
20+
// Fallback: match by package name prefix in case version format differs
21+
if (await fs.pathExists(stagingDir)) {
22+
const files = await fs.readdir(stagingDir);
23+
const match = files.find((f: string) => f.startsWith(`${safeName}-`) && f.endsWith('.tgz'));
24+
if (match) {
25+
return join(stagingDir, match);
26+
}
27+
}
28+
29+
return undefined;
30+
}
531

632
export const run: WorkerRunnerFunction = async ({ target }) => {
733
const pkg = await fs.readJson(join(target.cwd, 'package.json'));
@@ -11,7 +37,18 @@ export const run: WorkerRunnerFunction = async ({ target }) => {
1137
}
1238

1339
const dryRun = target.options?.dryRun ?? false;
14-
const args = ['--tolerate-republish', ...(dryRun ? ['--dry-run'] : [])];
40+
const outputDir = target.options?.outputDir as string | undefined;
1541

16-
await $({ cwd: target.cwd, verbose: true })`yarn npm publish ${args}`;
42+
// If an outputDir is configured, look for a pre-packed tarball
43+
const tarball = outputDir ? await findTarball(pkg, outputDir) : undefined;
44+
45+
if (tarball) {
46+
// yarn npm publish doesn't support tarballs, so use npm directly
47+
const args = ['publish', tarball, '--access', 'public', ...(dryRun ? ['--dry-run'] : [])];
48+
await $({ cwd: target.cwd, verbose: true })`npm ${args}`;
49+
} else {
50+
// No tarball found — publish from source (local dev / dry-run)
51+
const args = ['--tolerate-republish', ...(dryRun ? ['--dry-run'] : [])];
52+
await $({ cwd: target.cwd, verbose: true })`yarn npm publish ${args}`;
53+
}
1754
};

0 commit comments

Comments
 (0)