feat(npm): publish APM to npm via platform-specific optional packages#1017
feat(npm): publish APM to npm via platform-specific optional packages#1017anrouxel wants to merge 18 commits intomicrosoft:mainfrom
Conversation
- Introduced the main APM package with version 0.10.0 and its metadata. - Added scripts for generating platform-specific packages and updating versions. - Created package.json files for CLI binaries targeting darwin (arm64 and x64), linux (arm64 and x64), and win32 (x64). - Implemented workspace configuration for pnpm to manage packages efficiently. Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
|
@microsoft-github-policy-service agree |
…te .gitignore files Co-authored-by: Copilot <copilot@github.com>
…onfig Co-authored-by: Copilot <copilot@github.com>
APM Review Panel VerdictDisposition: REQUEST_CHANGES (four required fixes before merge) Per-persona findingsPython Architect: This PR contains no Python code changes. The analysis covers the JavaScript launcher, build scripts, and CI workflow. 1. OO / Class DiagramThe PR is purely procedural -- no classes. Module-boundary view: classDiagram
direction LR
class NpmLauncher {
<<IOBoundary>>
+PLATFORMS map
+detectPackageManager() string
+isMusl() bool
+spawnSync(binary, args, opts)
}
class GeneratePackages {
<<Pure>>
+getArchiveName(platform, arch) string
+copyBinaryToNativePackage(platform, arch)
+updateVersionInJsPackage(name)
+updateVersionInDependencies(deps, version)
}
class VersionScripts {
<<IOBoundary>>
+update-beta-version.mjs
+update-preview-version.mjs
}
class PublishNpmJob {
<<IOBoundary>>
+NPM_TOKEN secret
+npm-publish environment
}
class NpmLauncher:::touched
class GeneratePackages:::touched
class VersionScripts:::touched
class PublishNpmJob:::touched
PublishNpmJob ..> GeneratePackages : executes at release
PublishNpmJob ..> VersionScripts : optionally executes
NpmLauncher ..> GeneratePackages : binary resolved at install time
classDef touched fill:#fff3b0,stroke:#d47600
2. Execution Flowflowchart TD
A["[EXEC] npm install -g apm-wrapper"] --> B["[FS] npm resolves optionalDeps by os/cpu"]
B --> C["[FS] platform binary package downloaded"]
C --> D["[EXEC] apm CLI invoked -> packages/apm/bin/apm"]
D --> E{"env.APM_BINARY set?"}
E -->|Yes| F["[EXEC] spawnSync(env.APM_BINARY, args)"]
E -->|No| G{"PLATFORMS[platform][arch] exists?"}
G -->|Yes| H["[FS] require.resolve(platform-binary-pkg/apm)"]
H --> I["[EXEC] spawnSync(binary, args, shell:false, stdio:inherit)"]
G -->|No| J["console.error: platform not supported, exit 1"]
I --> K["exit with result.status"]
L["[EXEC] git tag push -> build-release.yml trigger"] --> M["build jobs for all platforms"]
M --> N["[FS] download-artifact -> ./artifacts/"]
N --> O["[EXEC] node generate-packages.mjs"]
O --> P{"binary in dist/ or artifacts/?"}
P -->|No| Q["console.error + process.exit(1)"]
P -->|Yes| R["[FS] copyFileSync binary -> packages/cli-platform/"]
R --> S["[FS] chmodSync 0o755"]
S --> T["[FS] cpSync _internal dir"]
T --> U["[FS] writeFileSync package.json with synced version"]
U --> V["[NET] npm publish --tag latest for each of 6 packages"]
3. Design patternsDesign patterns
Structural bugs found:
CLI Logging Expert: No changes to Python CLI output paths, The launcher's user-facing error message is clean, non-technical, and provides a concrete next action. The DevX UX Expert: The Gaps that matter:
Supply Chain Security Expert: The core choices are sound: npm provenance (
Auth Expert: Not activated -- PR modifies only npm packaging infrastructure ( OSS Growth Hacker: This PR is a major conversion surface unlock for TypeScript and Node.js developers -- removing the single biggest friction point for teams that want to consume APM as a project dependency.
Critical gap flagged to CEO: The README does not mention npm installation. The npm package will exist after this PR merges but no user will discover it through the README or docs. The launch story is incomplete without a doc pass. This must be a merge requirement per Rule 4, not a follow-up. Growth note: verifying CEO arbitrationThis is a solid, well-structured contribution from an external contributor that directly addresses a long-standing community request (#732). The approach -- esbuild/bun optional-dependency pattern, npm provenance, pnpm workspace -- is industry-standard and the right call. Four issues require resolution before merge, all straightforward without architectural rethinking. The On security: The Growth Hacker and DevX UX findings are aligned: the npm install path must be discoverable through docs and README before or concurrent with merge. Rule 4 makes this a required action. Shipping the infrastructure without updating the funnel wastes the launch beat. Once the four required actions below are addressed, this PR has CEO approval. This is exactly the kind of community contribution APM needs to encourage. Required actions before merge
Optional follow-ups
|
…ntation Co-authored-by: Copilot <copilot@github.com>
|
Hi @danielmeppiel , I wanted to clarify a point regarding the naming pattern that will be used for the packages published to npm. What will the final scope (namespace) be? Currently, according to the PR, it seems we are going with the However, since this is a Microsoft project, shouldn't we use the official Has the availability of the Best Regards |
|
@anrouxel we should use microsoft org |
…eration - Implemented the APM CLI launcher in Node.js to handle platform-specific binaries. - Created Jinja2 templates for generating package.json files for the main CLI and platform-specific packages. - Developed a Python script to automate the generation and publishing of npm packages for different platforms. - Included logging for better traceability during package generation. - Ensured compatibility with Node.js version 20 and above. Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
|
I've significantly refactored our entire NPM packaging strategy to simplify the publish loop, eliminate static file repetition, and make the generation of platform-specific CLI wrappers much more robust. Key Changes
Architecture SimplificationPrevious Approachgraph TD
A[Node scripts: update-beta-version.mjs, etc.] --> B[Static duplicate packages]
C[Hardcoded apm bin script] --> D[Static PLATFORMS map for CLI wrapper]
New Approachgraph TD
G[scripts/npm_publish.py] -->|toml lib| H[pyproject.toml]
G -->|Delegated helpers| I[Jinja2 Engine]
I --> J[platform-package.json.jinja2]
I --> K[apm-cli-package.json.jinja2]
I --> L[apm-cli-bin.js.jinja2]
L --> M[Dynamic & accurate PLATFORMS map for CLI wrapper]
|
Closes #732
TL;DR
Issue #732 reported that APM has no npm distribution channel, blocking TypeScript projects from consuming it as a dependency without installing a separate CLI tool manually on each dev machine. This PR implements the full npm publishing infrastructure: a pnpm monorepo, five platform-specific binary packages (
@apm/cli-*), a Node.js launcher wrapper (@apm/apm), assembly scripts, and a newpublish-npmCI job. After merge, users can runnpm install -g @apm/apmor pin"@apm/apm": "^0.10.0"indevDependencies.Problem (WHY)
Note
The platform-as-
optionalDependencypattern is the established approach for shipping native binaries via npm (esbuild, Bun, Vite all use it). Only the package for the current OS/arch is downloaded; the other four are skipped.Approach (WHAT)
package.json+pnpm-workspace.yamlcovering allpackages/@apm/*packages/@apm/apm/bin/apmNode.js launcher resolves correct binary viaPLATFORMS[process.platform][process.arch]packages/@apm/cli-*stubs; binaries injected at publish time bygenerate-packages.mjsgenerate-packages.mjspropagates@apm/apmversion into all platformpackage.jsonspublish-npmjob inbuild-release.yml, gated on tag + non-private + non-prereleaseImplementation (HOW)
pnpm-workspace.yamlpackages/@apm/*package.json(root)pnpm@10.33.2packages/@apm/apm/package.json@apm/apm@0.10.0; lists all five platform packages asoptionalDependencies; enables npm provenancepackages/@apm/apm/bin/apmprocess.platform/arch, resolves the native binary viarequire.resolve(), andspawnSyncwith inherited stdio; honoursAPM_BINARYenv overridepackages/@apm/cli-*/package.jsonos/cpufields so npm only downloads the matching packagepackages/@apm/apm/scripts/generate-packages.mjsdist/or CIartifacts/, updatespackage.jsonversion in all packages, setschmod 0o755packages/@apm/apm/scripts/update-beta-version.mjsINPUT_VERSIONenv into@apm/apm/package.jsonpackages/@apm/apm/scripts/update-preview-version.mjs.github/workflows/build-release.ymlpublish-npmjob: downloads artifacts, installs Node 24, runsgenerate-packages.mjs, then publishes all six packages in dependency orderDiagrams
Package resolution at install time
npm downloads only the platform package matching the current OS/arch.
graph LR A["npm install -g @apm/apm"] --> B["@apm/apm (wrapper)"] B -- "optionalDependency (linux/x64 only)" --> C["@apm/cli-linux-x64"] B -- "optionalDependency (darwin/arm64 only)" --> D["@apm/cli-darwin-arm64"] B -- "optionalDependency (darwin/x64 only)" --> E["@apm/cli-darwin-x64"] B -- "optionalDependency (linux/arm64 only)" --> F["@apm/cli-linux-arm64"] B -- "optionalDependency (win32/x64 only)" --> G["@apm/cli-win32-x64"]CI publish flow
publish-npmruns after all build/test/release jobs succeed.sequenceDiagram participant CI as build-release.yml participant S as generate-packages.mjs participant npm CI->>CI: download-artifact (all platforms) CI->>S: node generate-packages.mjs S->>S: copy binary + chmod 0o755 S->>S: sync version in package.json loop each of 6 packages CI->>npm: npm publish --tag latest endTrade-offs
pnpm-workspace.yaml; pnpm is only required to manage npm packages and is not needed to build the Python CLI.configuration_schema.jsonbundled in@apm/apm— the schema (~16 000 lines) adds weight to the npm package. Acceptable for v0.10.0; can be moved to a CDN fetch in a future iteration.is_prerelease != 'true'— beta/preview builds do NOT auto-publish tolatest; they require a separate workflow step (version scripts provided). This is intentional to protect thelatesttag.Benefits
npm install -g @apm/apmand"devDependencies": { "@apm/apm": "^0.10.0" }become supported install paths, directly addressing #732.publishConfig.provenance: true) is enabled out of the box, satisfying supply-chain security requirements.generate-packages.mjseliminates manual edits across sixpackage.jsonfiles on every release.APM_BINARYenv override in the launcher allows CI and local development to point to any binary without reinstalling.How to test
npm install -g pnpmpnpm install— verify workspace packages resolve without errornode packages/@apm/apm/scripts/generate-packages.mjsafter placing a local build binary indist/apm-linux-x86_64/apm— confirm version is synced and binary is copiedINPUT_VERSION=0.10.0-beta.1 GITHUB_SHA=abc123 node packages/@apm/apm/scripts/update-beta-version.mjs— verifypackages/@apm/apm/package.jsonis updatedpublish-npmCI job runs and all six packages appear on https://www.npmjs.com/package/@apm/apm