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 .github/workflows/composer_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'pnpm'
cache-dependency-path: 'tools/composer/pnpm-lock.yaml'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/editor_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install web_core deps
working-directory: ./renderers/web_core
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/inspector_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install web_core deps
working-directory: ./renderers/web_core
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lit_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install web_core dependencies
working-directory: ./renderers/web_core
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lit_samples_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Build lit renderer and its dependencies
working-directory: ./samples/client/lit
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ng_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install top-level deps
working-directory: ./samples/client/angular
Expand All @@ -43,6 +43,10 @@ jobs:
working-directory: ./samples/client/angular
run: npm run build:renderer

- name: Test Angular renderer
working-directory: ./renderers/angular
run: npm run test:ci

- name: Build restaurant sample
working-directory: ./samples/client/angular
run: npm run build restaurant
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/react_renderer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Build web_core dependency
working-directory: ./renderers/web_core
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Build web_core dependency
working-directory: ./renderers/web_core
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Build web_core dependency
working-directory: ./renderers/web_core
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate_specifications.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Set up Python
uses: actions/setup-python@v6
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/web_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install web_core dependencies
working-directory: ./renderers/web_core
Expand All @@ -60,7 +60,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Run publish script integration test
run: node --test renderers/scripts/publish_npm.test.mjs
23 changes: 21 additions & 2 deletions renderers/docs/web_publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ This script will:
- `--yes`: Bypasses the manual user confirmation prompt (useful for CI).
- `--dry-run`: Simulates the process, printing the commands it *would* execute without actually running them.
- `--skip-tests`: Skips the `npm run test` phase before publishing.
- `--test-only`: Runs the full build and test suite in topological order, but skips the final `npm run publish:package` step. Useful for verifying that packages build and tests pass before performing a real release.

### 3. Upload Manifest

Expand All @@ -53,7 +54,25 @@ Finally, trigger the public release to npmjs.com by uploading a manifest file:
./renderers/scripts/upload_manifest.mjs
```

This generates a `manifest.json` with the current versions of all renderer packages and uploads it to GCS to trigger the internal release infrastructure.
This generates a `manifest.json` with the current versions of all renderer packages and uploads it to GCS to trigger the internal release infrastructure. You should receive an email from exit-gate noting that publishing has commenced.

#### Manual alternative

You can also do this step manually, if you are authenticated with `gcloud` with a corporate Google account in the correct groups:

1. Create a new manifest.json file with these contents:
```json
{
"publish_all": true
}
```

2. Upload the file

```sh
gcloud storage cp manifest.json gs://oss-exit-gate-prod-projects-bucket/a2ui/npm/manifests/manifest.json
```

---

## Internal Release Process
Expand Down Expand Up @@ -83,7 +102,7 @@ npm run publish:package
**What happens during `npm run publish:package`?**
Before publishing, the script runs the necessary `build` command which processes the code. Then, a preparation script (usually `prepare-publish.mjs`) runs, which:
1. Copies `package.json`, `README.md`, and `LICENSE` to the `dist/` folder.
2. If it's a renderer, it reads the `version` from `@a2ui/web_core` and updates the `file:` dependency in the `dist/package.json` to the actual core version (e.g., `^0.9.0`).
2. It scans all dependencies and peerDependencies for internal `@a2ui/` packages (those using `file:` links) and updates them to the actual current versions in the mono-repo (e.g., `^0.9.0`).
3. Adjusts exports and paths (removing the `./dist/` prefix) so they are correct when consumed from the package root.
4. Removes any build scripts (`prepublishOnly`, `scripts`, `wireit`) so they don't interfere with the publish process.

Expand Down
27 changes: 15 additions & 12 deletions renderers/scripts/prepare-publish.mjs
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are relying on trusted people to execute this, and allowing to publish potentially skipping a PR review. Until we have CD set up I think this is fine.

A nice to have to prevent human error: to early exit this script if there are any uncommitted changes in the current workspace.

Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';

import { fileURLToPath } from 'node:url';
import { getPackageGraph } from './lib/workspace.mjs';

// This script prepares a package for publishing.
// Arguments:
Expand Down Expand Up @@ -46,19 +46,22 @@ if (!existsSync(resolvedDistDir)) {
mkdirSync(resolvedDistDir, { recursive: true });
}

// 1. Get current @a2ui/web_core version
const corePkgPath = join(rootDir, 'renderers/web_core/package.json');
const coreVersion = JSON.parse(readFileSync(corePkgPath, 'utf8')).version;

const graph = getPackageGraph();
const pkg = JSON.parse(readFileSync(resolvedSourcePkg, 'utf8'));

// 2. Update @a2ui/web_core dependency
if (pkg.dependencies && pkg.dependencies['@a2ui/web_core']) {
pkg.dependencies['@a2ui/web_core'] = '^' + coreVersion;
}
if (pkg.peerDependencies && pkg.peerDependencies['@a2ui/web_core']) {
pkg.peerDependencies['@a2ui/web_core'] = '^' + coreVersion;
}
// 2. Update internal @a2ui dependencies
const updateInternalDeps = (deps) => {
if (!deps) return;
for (const name in deps) {
const version = deps[name];
if (version.startsWith('file:') && graph[name]) {
deps[name] = '^' + graph[name].version;
}
}
};
Comment on lines +53 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Refactoring updateInternalDeps to use Object.entries is cleaner and more idiomatic in modern JavaScript. Additionally, throwing an error when a file: dependency cannot be resolved in the workspace graph prevents the accidental publication of broken packages that contain unresolved local links.

Suggested change
const updateInternalDeps = (deps) => {
if (!deps) return;
for (const name in deps) {
const version = deps[name];
if (version.startsWith('file:') && graph[name]) {
deps[name] = '^' + graph[name].version;
}
}
};
const updateInternalDeps = (deps) => {
if (!deps) return;
for (const [name, version] of Object.entries(deps)) {
if (typeof version === 'string' && version.startsWith('file:')) {
if (graph[name]) {
deps[name] = '^' + graph[name].version;
} else {
throw new Error(`Internal dependency ${name} not found in workspace.`);
}
}
}
};


updateInternalDeps(pkg.dependencies);
updateInternalDeps(pkg.peerDependencies);
Comment on lines +63 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The script should also update internal dependencies in optionalDependencies to ensure they are correctly resolved when the package is published to npm. Currently, only dependencies and peerDependencies are handled.

Suggested change
updateInternalDeps(pkg.dependencies);
updateInternalDeps(pkg.peerDependencies);
updateInternalDeps(pkg.dependencies);
updateInternalDeps(pkg.peerDependencies);
updateInternalDeps(pkg.optionalDependencies);


// 3. Adjust paths
if (!skipPathAdjustment) {
Expand Down
29 changes: 22 additions & 7 deletions renderers/scripts/publish_npm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function runPublish(args, customRunCommand, customExecSync, customR
let autoYes = false;
let dryRun = false;
let skipTests = false;
let testOnly = false;

for (const arg of args) {
if (arg.startsWith('--packages=')) {
Expand All @@ -43,11 +44,13 @@ export async function runPublish(args, customRunCommand, customExecSync, customR
dryRun = true;
} else if (arg === '--skip-tests') {
skipTests = true;
} else if (arg === '--test-only') {
testOnly = true;
}
}

if (packagesToPublish.length === 0) {
throw new Error('Usage: publish_npm --packages=pkg1,pkg2 [--force] [--yes] [--dry-run] [--skip-tests]');
throw new Error('Usage: publish_npm --packages=pkg1,pkg2 [--force] [--yes] [--dry-run] [--skip-tests] [--test-only]');
}

const graph = getPackageGraph();
Expand All @@ -62,16 +65,23 @@ export async function runPublish(args, customRunCommand, customExecSync, customR
return pkg.name;
});

// Validation: web_core check
// Validation: core dependencies check
const webCoreName = '@a2ui/web_core';
const markdownItName = '@a2ui/markdown-it';
const renderers = ['@a2ui/lit', '@a2ui/angular', '@a2ui/react'];
const requestedRenderers = resolvedPackages.filter(p => renderers.includes(p));
Comment on lines 71 to 72
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of hardcoding the list of renderers, you can dynamically identify packages that depend on core libraries (like @a2ui/web_core or @a2ui/markdown-it) using the package graph. This makes the safety check more robust as new packages are added to the mono-repo.

  const corePackages = [webCoreName, markdownItName];
  const requestedRenderers = resolvedPackages.filter(name => {
    const pkg = graph[name];
    return pkg && pkg.internalDependencies.some(dep => corePackages.includes(dep));
  });


if (requestedRenderers.length > 0 && !resolvedPackages.includes(webCoreName) && !force) {
console.warn('WARNING: You are publishing renderers but NOT @a2ui/web_core.');
console.warn('This can lead to broken versions if web_core has changed.');
console.warn('Use --force to override this check.');
throw new Error('Safety check failed: web_core missing from publish list.');
if (requestedRenderers.length > 0 && !force) {
const missingCores = [];
if (!resolvedPackages.includes(webCoreName)) missingCores.push(webCoreName);
if (!resolvedPackages.includes(markdownItName)) missingCores.push(markdownItName);

if (missingCores.length > 0) {
console.warn(`WARNING: You are publishing renderers but NOT ${missingCores.join(' and ')}.`);
console.warn('This can lead to broken versions if shared dependencies have changed.');
console.warn('Use --force to override this check.');
throw new Error(`Safety check failed: ${missingCores.join(' and ')} missing from publish list.`);
}
}

// Topological Sort
Expand Down Expand Up @@ -229,6 +239,11 @@ export async function runPublish(args, customRunCommand, customExecSync, customR
}
}

if (testOnly) {
console.log('\n[TEST ONLY] Build and tests completed successfully. Skipping publish phase.');
return;
}

console.log('\n--- Proceeding to publish ---');

for (const pkgName of sortedPackages) {
Expand Down
39 changes: 37 additions & 2 deletions renderers/scripts/publish_npm.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,69 @@ describe('publish_npm script integration test', () => {
if (cmd.includes('npm view')) {
// Return older versions so pre-flight check passes
if (cmd.includes('@a2ui/web_core')) return '0.0.1\n';
if (cmd.includes('@a2ui/markdown-it')) return '0.0.1\n';
if (cmd.includes('@a2ui/lit')) return '0.0.1\n';
}
return '';
}

// Run the script with --yes, --skip-tests (no --dry-run so we can record commands)
// We target web_core and lit. lit depends on web_core, so web_core MUST be processed first.
// We target web_core, markdown-it, and lit. lit depends on them, so they MUST be processed first.
await runPublish(
['--packages=lit,web_core', '--yes', '--skip-tests'],
['--packages=lit,web_core,markdown-it', '--yes', '--skip-tests'],
mockRunCommand,
mockExecSync,
null // readline not needed with --yes
);

// Verify topological order in preparation phase
const webCoreInstallIndex = executedCommands.findIndex(cmd => cmd.includes('install') && cmd.includes('web_core'));
const markdownItInstallIndex = executedCommands.findIndex(cmd => cmd.includes('install') && cmd.includes('markdown-it'));
const litInstallIndex = executedCommands.findIndex(cmd => cmd.includes('install') && cmd.includes('lit'));

assert.ok(webCoreInstallIndex > -1, 'Should install web_core');
assert.ok(markdownItInstallIndex > -1, 'Should install markdown-it');
assert.ok(litInstallIndex > -1, 'Should install lit');
assert.ok(webCoreInstallIndex < litInstallIndex, 'web_core must be prepared before lit (topological sort)');
assert.ok(markdownItInstallIndex < litInstallIndex, 'markdown-it must be prepared before lit');

// Verify topological order in publish phase
const webCorePublishIndex = executedCommands.findIndex(cmd => cmd.includes('publish:package') && cmd.includes('web_core'));
const markdownItPublishIndex = executedCommands.findIndex(cmd => cmd.includes('publish:package') && cmd.includes('markdown-it'));
const litPublishIndex = executedCommands.findIndex(cmd => cmd.includes('publish:package') && cmd.includes('lit'));

assert.ok(webCorePublishIndex > -1, 'Should publish web_core');
assert.ok(markdownItPublishIndex > -1, 'Should publish markdown-it');
assert.ok(litPublishIndex > -1, 'Should publish lit');
assert.ok(webCorePublishIndex < litPublishIndex, 'web_core must be published before lit');
assert.ok(markdownItPublishIndex < litPublishIndex, 'markdown-it must be published before lit');
});

it('should skip publishing when --test-only is provided', async () => {
const executedCommands = [];

function mockRunCommand(cmd, args, options) {
executedCommands.push(`${cmd} ${args.join(' ')}`);
}

function mockExecSync(cmd) {
if (cmd.includes('npm view')) return '0.0.1\n';
return '';
}

await runPublish(
['--packages=web_core', '--yes', '--test-only'],
mockRunCommand,
mockExecSync,
null
);

const hasInstall = executedCommands.some(cmd => cmd.includes('npm install'));
const hasTest = executedCommands.some(cmd => cmd.includes('npm run test'));
const hasPublish = executedCommands.some(cmd => cmd.includes('publish:package'));

assert.ok(hasInstall, 'Should run npm install');
assert.ok(hasTest, 'Should run npm test');
assert.strictEqual(hasPublish, false, 'Should NOT run publish:package');
});
});
Loading
Loading