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
62 changes: 6 additions & 56 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
token: ${{ secrets.PAT_TOKEN }}

- name: Setup registry
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
Expand All @@ -46,69 +46,19 @@ jobs:
echo "is_main=false" >> $GITHUB_OUTPUT
fi

- name: Update npm for trusted publishing
run: npm install -g npm@latest

- name: Build
run: rush build

- name: Get npm token via OIDC
run: |
echo "Requesting OIDC token from GitHub..."
OIDC_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https://registry.npmjs.org" | jq -r '.value')

if [ -z "$OIDC_TOKEN" ] || [ "$OIDC_TOKEN" = "null" ]; then
echo "::error::Failed to get OIDC token from GitHub"
exit 1
fi
echo "OIDC token obtained successfully"

echo "Exchanging OIDC token for npm token..."
RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "https://registry.npmjs.org/-/npm/v1/security/oidc/token" \
-H "Content-Type: application/json" \
-d "{\"oidcToken\": \"${OIDC_TOKEN}\"}")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

echo "npm OIDC exchange HTTP status: $HTTP_CODE"

if [ "$HTTP_CODE" != "200" ]; then
echo "::error::npm OIDC exchange failed with status $HTTP_CODE"
echo "Response: $BODY"
exit 1
fi

NPM_TOKEN=$(echo "$BODY" | jq -r '.token')
if [ -z "$NPM_TOKEN" ] || [ "$NPM_TOKEN" = "null" ]; then
echo "::error::npm returned empty token"
echo "Response: $BODY"
exit 1
fi

echo "::add-mask::$NPM_TOKEN"
echo "NPM_AUTH_TOKEN=$NPM_TOKEN" >> $GITHUB_ENV
echo "npm token obtained successfully"

- name: Verify npm auth
run: |
echo "=== .npmrc content (masked) ==="
cat ~/.npmrc 2>/dev/null | sed 's/_authToken=.*/_authToken=***/' || echo "No ~/.npmrc"
echo ""
echo "=== NPM_AUTH_TOKEN set? ==="
if [ -n "$NPM_AUTH_TOKEN" ]; then echo "YES (length: ${#NPM_AUTH_TOKEN})"; else echo "NO"; fi
echo ""
echo "=== npm whoami ==="
npm whoami 2>&1 || echo "npm whoami failed - NOT authenticated"
echo ""
echo "=== publish .npmrc ==="
cat common/temp/publish-home/.npmrc 2>/dev/null | sed 's/_authToken=.*/_authToken=***/' || echo "No publish .npmrc yet"

- name: Publish (main)
if: steps.branch.outputs.is_main == 'true'
run: rush publish --publish --target-branch main --include-all --set-access-level=public
run: node common/scripts/npm-publish.js

- name: Publish (prerelease)
if: steps.branch.outputs.is_main == 'false'
run: rush publish --publish --tag ${{ steps.branch.outputs.name }} --include-all --set-access-level=public --apply
run: node common/scripts/npm-publish.js --tag ${{ steps.branch.outputs.name }}

- name: Commit version bumps
run: |
Expand Down
185 changes: 185 additions & 0 deletions common/scripts/npm-publish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Custom publish script that uses `npm publish` instead of `pnpm publish`.
* This enables npm's built-in OIDC Trusted Publishing support.
*
* Usage:
* node common/scripts/npm-publish.js [--tag <tag>] [--dry-run]
*
* Reads rush.json to find all packages with shouldPublish: true,
* checks if the local version differs from the published version,
* and publishes using `npm publish --provenance --access public`.
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..', '..');

function exec(cmd, opts = {}) {
return execSync(cmd, { encoding: 'utf-8', cwd: ROOT, ...opts }).trim();
}

function getPublishedVersion(packageName) {
try {
return exec(`npm view ${packageName} version 2>/dev/null`);
} catch {
return null;
}
}

function getLocalVersion(packageJsonPath) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
return pkg.version;
}

function parseArgs() {
const args = process.argv.slice(2);
const result = { tag: null, dryRun: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--tag' && args[i + 1]) {
result.tag = args[++i];
}
if (args[i] === '--dry-run') {
result.dryRun = true;
}
}
return result;
}

async function main() {
const { tag, dryRun } = parseArgs();
const rushJson = JSON.parse(fs.readFileSync(path.join(ROOT, 'rush.json'), 'utf-8'));

const publishable = rushJson.projects.filter(p => p.shouldPublish === true);

// Debug info
console.log('='.repeat(60));
console.log(' npm-publish.js - Debug Info');
console.log('='.repeat(60));
console.log(`npm version: ${exec('npm --version')}`);
console.log(`node version: ${exec('node --version')}`);
console.log(`tag: ${tag || 'latest'}`);
console.log(`dry-run: ${dryRun}`);
console.log(`publishable packages: ${publishable.length}`);
console.log('');

// Check npm auth
console.log('--- npm auth check ---');
try {
const whoami = exec('npm whoami 2>&1');
console.log(`npm whoami: ${whoami}`);
} catch (err) {
console.log(`npm whoami: FAILED - ${err.stderr || err.message}`);
console.log('WARNING: npm is not authenticated. Trusted Publishing OIDC will be used during publish.');
}
console.log('');

// Check .npmrc
console.log('--- .npmrc files ---');
const homeNpmrc = path.join(process.env.HOME || '~', '.npmrc');
if (fs.existsSync(homeNpmrc)) {
const content = fs.readFileSync(homeNpmrc, 'utf-8');
console.log(`~/.npmrc exists (${content.split('\n').length} lines)`);
content.split('\n').forEach(line => {
if (line.includes('authToken') || line.includes('_auth')) {
console.log(` ${line.replace(/=.*/, '=***')}`);
} else if (line.trim()) {
console.log(` ${line}`);
}
});
} else {
console.log('~/.npmrc: not found');
}
console.log('');

// Check env
console.log('--- Environment ---');
console.log(`ACTIONS_ID_TOKEN_REQUEST_URL: ${process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? 'SET' : 'NOT SET'}`);
console.log(`ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? 'SET' : 'NOT SET'}`);
console.log(`NPM_AUTH_TOKEN: ${process.env.NPM_AUTH_TOKEN ? 'SET' : 'NOT SET'}`);
console.log(`NODE_AUTH_TOKEN: ${process.env.NODE_AUTH_TOKEN ? 'SET' : 'NOT SET'}`);
console.log(`NPM_CONFIG_PROVENANCE: ${process.env.NPM_CONFIG_PROVENANCE || 'NOT SET'}`);
console.log('');
console.log('='.repeat(60));
console.log(' Starting publish');
console.log('='.repeat(60));
console.log('');

let published = 0;
let skipped = 0;
let failed = 0;

for (const project of publishable) {
const pkgJsonPath = path.join(ROOT, project.projectFolder, 'package.json');
const localVersion = getLocalVersion(pkgJsonPath);
const publishedVersion = getPublishedVersion(project.packageName);

if (localVersion === publishedVersion) {
console.log(`SKIP ${project.packageName}@${localVersion} (already published)`);
skipped++;
continue;
}

console.log(`PUBLISH ${project.packageName}@${localVersion} (npm: ${publishedVersion || 'not found'})`);

const distPath = path.join(ROOT, project.projectFolder, 'dist');
if (!fs.existsSync(distPath)) {
console.log(` WARN: dist/ folder not found at ${distPath}`);
} else {
const files = fs.readdirSync(distPath);
console.log(` dist/ contains: ${files.join(', ')}`);
}

const publishCmd = [
'npm publish',
'--provenance',
'--access public',
tag ? `--tag ${tag}` : '',
dryRun ? '--dry-run' : '',
].filter(Boolean).join(' ');

console.log(` CMD: ${publishCmd}`);
console.log(` CWD: ${project.projectFolder}`);

try {
const output = exec(publishCmd, {
cwd: path.join(ROOT, project.projectFolder),
});
console.log(` OUTPUT: ${output}`);
console.log(` OK ${project.packageName}@${localVersion}`);
published++;
} catch (err) {
console.error(` FAIL ${project.packageName}@${localVersion}`);
if (err.stdout) console.error(` STDOUT: ${err.stdout}`);
if (err.stderr) console.error(` STDERR: ${err.stderr}`);
if (!err.stdout && !err.stderr) console.error(` ERROR: ${err.message}`);
failed++;
}

console.log('');
}

console.log('='.repeat(60));
console.log(` Results`);
console.log('='.repeat(60));
console.log(`Published: ${published}`);
console.log(`Skipped: ${skipped}`);
console.log(`Failed: ${failed}`);

if (failed > 0) {
process.exit(1);
}
}

function getPublishedVersionWithTag(packageName, tag) {
try {
return exec(`npm view ${packageName}@${tag} version 2>/dev/null`);
} catch {
return null;
}
}

main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
Loading