Skip to content

Build pipeline for agent SDK tarballs#320853

Closed
TylerLeonhardt wants to merge 3 commits into
mainfrom
tyler/nutty-tarantula
Closed

Build pipeline for agent SDK tarballs#320853
TylerLeonhardt wants to merge 3 commits into
mainfrom
tyler/nutty-tarantula

Conversation

@TylerLeonhardt

Copy link
Copy Markdown
Member

Build pipeline that produces the SDK tarballs the runtime side (#320709) downloads from main.vscode-cdn.net.

What this adds

build/agent-sdk/

Four self-contained scripts:

  • package.ts — builds one (sdk, target) tarball. npm install with npm_config_libc/os/cpu set to fetch the foreign-platform binary, chmod platform executables, normalize mtimes (lutimes so symlinks like .bin/codex get their stamp too), tar+gzip with reproducible flags (--format=gnu --sort=name --owner=0 ... --mtime=@0 -n -9). Writes <sdk>-<version>-<target>.tgz + a .tgz.json sidecar carrying {sdk, sdkVersion, sdkTarget, sha256}.
  • upload.ts — uploads one tarball to the vscodeweb storage account's $web container at agent-sdk/<sdk>/<version>/<target>.tgz. HEAD-then-decide: absent → upload; matching metadata.sha256 → skip (idempotent re-runs are free); different sha → fail loud, refusing to overwrite content-addressed history. Reuses the established ClientAssertionCredential auth pattern from build/azure-pipelines/upload-cdn.ts.
  • aggregate.ts — scans all per-job sidecars, validates every expected (sdk, target) pair is present (reads each SDK's own optionalDependencies for the expected list — no parallel list to keep in sync), and prints the product.agentSdks JSON fragment for a human to paste into vscode-distro's product.json.
  • common.ts — shared types and helpers. The two interesting bits:
    • getSdkVersion(sdk) reads the pinned exact version from the repo-root package.json devDeps. Rejects ranged versions (^, ~) — they'd let npm resolve different versions across days, breaking determinism.
    • getTargets(sdk) reads the supported platform list from the SDK package's own optionalDependencies. The SDK declares which platforms it ships; we ask it directly instead of duplicating the list.

build/azure-pipelines/agent-sdk/ + product-build.yml

A new AgentSDK stage in the existing scheduled product build, fanning out 14 jobs:

  • Per-target jobs (product-build-agent-sdk-target.yml) — one per (sdk, target) pair, each invoked from an each loop over claudeTargets / codexTargets parameter arrays. Builds the tarball, publishes it as a pipeline artifact, and (when VSCODE_PUBLISH=true) uploads to CDN.
  • Aggregate job (product-build-agent-sdk.yml) — downloads every per-target artifact and runs aggregate.ts. Uses condition: succeededOrFailed() deliberately: when one or more per-target jobs fail, the operator still sees a loud "Missing sidecars for N expected pairs" message naming each missing target, instead of having to click through 14 sibling jobs.

How a SDK bump now works

  1. Edit the corresponding devDep in the repo-root package.json (@anthropic-ai/claude-agent-sdk or @openai/codex) to the new exact version.
  2. npm install, commit lockfile.
  3. Next scheduled (or manual) pipeline run produces new tarballs at new content-addressed CDN paths.
  4. Copy the printed JSON fragment from the aggregate job's log.
  5. Paste into vscode-distro's product.json under agentSdks.

No lockfiles to regenerate, no parallel "supported targets" list to update, no SDK_VERSIONS constant to bump.

Determinism

The build is byte-reproducible for a given Linux runner image + Node version through:

  • Pinned exact SDK version (and the SDK itself pins its optionalDependencies to exact versions, so the platform binary fetched is deterministic).
  • GNU tar + gzip with deterministic flags + sorted entries + zero mtimes.
  • lutimes (not utimes) for symlinks so things like node_modules/.bin/codex get normalized too.

Cross-run drift (agent image bump changes gzip output, registry-side transitive resolution shifts, etc.) is caught at upload time by the upload.ts HEAD-then-fail check — that's the actual defense, and it catches across-day drift that a "build twice in a row" check never could. We deliberately don't have a verify-determinism.ts script anymore for that reason.

Decisions worth knowing

  • Reuses vscodeweb storage account. Same account as the existing web bundle uploads, just under the agent-sdk/ prefix. No new infra to provision, no new credentials.
  • No lockfiles committed for the per-target builds. Pinning the SDK to an exact version is enough; transitive dep drift in Claude's tree (98 packages) surfaces at upload time as a sha mismatch where a human investigates.
  • One source of truth per concept. Version → repo-root package.json devDeps. Targets → SDK's optionalDependencies. SDK package name → PACKAGE_NAME constant (unavoidable — defines what to look up).
  • Drafted as Draft PR because the AzDO pipeline doesn't run on PRs; first verification will be a manual pipeline queue against this branch.

Risks / things to verify

  • replace() template expression in YAML — used to convert darwin-arm64darwin_arm64 for Azure Pipelines job names (which disallow hyphens). Documented in Azure's template-expression reference but no other prior art in this repo. If it doesn't expand, the fallback is one extra layer: pass target arrays as {slug, target} objects.
  • Blob metadata permissionsBlobServiceClient.uploadFile with metadata may require Storage Blob Data Owner rather than Contributor role on the storage account. We'll find out on first upload attempt.
  • First pipeline run cost — 14 parallel jobs × ~3 min each = roughly 42 agent-minutes per pipeline run when this stage fires.

Test plan

  • Trigger a manual pipeline run against this branch with VSCODE_PUBLISH=false. Expect 14 per-target jobs green + aggregate job prints the fragment to its log (without uploading anything).
  • Manual run with VSCODE_PUBLISH=true once the dry run is green. Verify blobs land at https://main.vscode-cdn.net/agent-sdk/<sdk>/<version>/<target>.tgz and HEAD-then-fail idempotency works on a re-run.
  • Verify aggregate's printed fragment is paste-ready JSON that matches the IProductConfiguration.agentSdks shape from src/vs/base/common/product.ts.

🤖 Generated with Claude Code

Adds the build-side tooling that packages, verifies, and uploads the
Claude and Codex SDK tarballs that the runtime (PR #320709) downloads
from main.vscode-cdn.net.

build/agent-sdk/:
- common.ts — shared helpers. getSdkVersion(sdk) reads the pinned
  exact version from the repo-root package.json devDeps; getTargets(sdk)
  reads the supported platform list from the SDK's own
  optionalDependencies — both are live reads, no parallel constants
  to keep in sync.
- package.ts — builds one (sdk, target) tarball: npm install with
  npm_config_libc/os/cpu set, chmod platform binaries, normalize mtimes
  (lutimes for symlinks), tar+gzip with reproducible flags. Writes a
  JSON sidecar carrying {sdk, sdkVersion, sdkTarget, sha256}.
- upload.ts — uploads tarball to vscodeweb's $web container at
  agent-sdk/<sdk>/<version>/<target>.tgz. HEAD-then-decide:
  absent → upload; matching sha → skip (idempotent); different sha →
  fail loud, refusing to overwrite content-addressed history.
- aggregate.ts — scans all per-job sidecars, validates every expected
  (sdk, target) is present, prints the product.agentSdks JSON fragment
  for a human to paste into vscode-distro's product.json.

build/azure-pipelines/agent-sdk/ + product-build.yml:
- New AgentSDK stage that fans out 14 jobs (8 Claude + 6 Codex
  targets) via each-loop over claudeTargets/codexTargets parameter
  arrays. Aggregate job uses condition: succeededOrFailed() so the
  operator sees what's missing even when a per-target job fails.

Also pins @openai/codex devDep to an exact version (no ^ range) —
getSdkVersion rejects ranged specifiers because they'd let npm
resolve a different version every day, breaking determinism.
Copilot AI review requested due to automatic review settings June 10, 2026 22:24

Copilot AI left a comment

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.

Pull request overview

Adds a new build-side pipeline + tooling to produce and (optionally) upload per-platform agent SDK tarballs for Claude and Codex, intended to feed the runtime-side SDK downloader via main.vscode-cdn.net and a product.agentSdks fragment.

Changes:

  • Pin @openai/codex to an exact devDependency version to support deterministic/content-addressed publishing.
  • Introduce build/agent-sdk/* scripts to package, upload, and aggregate SDK tarballs + sha256 sidecars.
  • Add a new AgentSDK stage to the scheduled product build that fans out per-target jobs and runs an aggregate job.
Show a summary per file
File Description
src/vs/platform/agentHost/node/claude/roadmap.md Updates roadmap text to describe upload-time determinism enforcement.
package.json Pins @openai/codex devDependency to an exact version.
package-lock.json Aligns lockfile devDependency entry for @openai/codex to the exact version.
build/azure-pipelines/product-build.yml Adds the new AgentSDK stage to the product build pipeline.
build/azure-pipelines/agent-sdk/product-build-agent-sdk.yml Defines the fan-out matrix + aggregate job for SDK builds.
build/azure-pipelines/agent-sdk/product-build-agent-sdk-target.yml Defines the per-(sdk,target) job that builds and conditionally uploads one tarball.
build/agent-sdk/README.md Documents the new build/agent-sdk tooling and workflow.
build/agent-sdk/common.ts Shared helpers (version/target discovery, sidecar I/O, hashing).
build/agent-sdk/package.ts Produces a reproducible per-target SDK tarball + .tgz.json sidecar.
build/agent-sdk/upload.ts Uploads tarball to Azure $web with HEAD-then-skip/fail idempotency guard.
build/agent-sdk/aggregate.ts Validates completeness and prints a product.agentSdks JSON fragment.

Copilot's findings

  • Files reviewed: 10/11 changed files
  • Comments generated: 11

Comment thread build/agent-sdk/README.md Outdated
Comment on lines +11 to +15
Stages a minimal `package.json` pinning `SDK_VERSIONS[sdk]` (from
`common.ts`), runs `npm install --no-package-lock --ignore-scripts` with
`npm_config_libc/os/cpu` set to the target's triple to fetch the foreign
platform binary, normalizes mtimes (including symlinks via `lutimes`), and
tars with reproducible flags. Emits `<sdk>-<version>-<target>.tgz` + a
Comment thread build/agent-sdk/README.md Outdated
`npm_config_libc/os/cpu` set to the target's triple to fetch the foreign
platform binary, normalizes mtimes (including symlinks via `lutimes`), and
tars with reproducible flags. Emits `<sdk>-<version>-<target>.tgz` + a
`.tgz.json` sidecar carrying the sha256 and size.
Comment thread build/agent-sdk/README.md
Comment on lines +26 to +27
sidecars (one per per-target job's artifact) and prints a markdown table
with the JSON fragment a human pastes into `vscode-distro`'s `product.json`.
Comment on lines +12 to +16
* Where `<sdkTarget>` matches the npm `optionalDependencies` suffix the SDK
* ships its platform package under (e.g. `darwin-arm64`, `linux-x64-musl`,
* `win32-x64`). The supported set per SDK is `TARGETS` in `common.ts`; the
* pinned version is read from the repo-root `package.json` devDeps (via
* `getSdkVersion` in `common.ts`).
Comment on lines +24 to +27
* - SDK pinned to an exact version by `SDK_VERSIONS`. npm install fetches
* transitive deps fresh each run; day-to-day drift in those resolutions
* surfaces at upload time as a sha mismatch against the existing blob,
* where a human investigates whether it's an intentional SDK bump.
Comment thread build/agent-sdk/common.ts
Comment on lines +11 to +15
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

Comment thread build/agent-sdk/common.ts Outdated
Comment on lines +77 to +94
export function getTargets(sdk: Sdk): readonly string[] {
const thisDir = path.dirname(fileURLToPath(import.meta.url));
const sdkPackageJson = path.resolve(thisDir, '..', '..', 'node_modules', PACKAGE_NAME[sdk], 'package.json');
const json = JSON.parse(fs.readFileSync(sdkPackageJson, 'utf8')) as {
optionalDependencies?: Record<string, string>;
};
const prefix = `${PACKAGE_NAME[sdk]}-`;
const targets: string[] = [];
for (const name of Object.keys(json.optionalDependencies ?? {})) {
if (name.startsWith(prefix)) {
targets.push(name.slice(prefix.length));
}
}
if (targets.length === 0) {
throw new Error(`No platform packages found in ${sdkPackageJson}'s optionalDependencies (expected entries starting with '${prefix}')`);
}
return targets.sort();
}
Comment thread build/agent-sdk/common.ts Outdated
Comment on lines +73 to +75
* Requires the repo-root `npm install` to have run (the SDKs are devDeps
* in `package.json`). All pipeline jobs run after the standard install
* step so `node_modules/<sdk-package>/` exists for them to read.
Comment on lines +1 to +4
# Single (sdk, target) build + upload job. Invoked per matrix entry from
# product-build-agent-sdk.yml. Builds twice (determinism check), keeps the
# second build's tarball+sidecar as the publishable artifact, then uploads
# (gated on VSCODE_PUBLISH).
Comment on lines +56 to +58
# FAIL LOUD listing the missing targets (aggregate.ts validates completeness
# against the TARGETS constant in common.ts). Without this condition, Azure's
# default `succeeded()` silently skips aggregate on any failure — leaving the
The pipeline jobs don't run a full repo npm install, so reading
@anthropic-ai/claude-agent-sdk/package.json from node_modules/ fails
with ENOENT. Switch getTargets() to call 'npm view <pkg>@<version>
optionalDependencies --json' instead — works against the registry
without needing the package installed locally. The private npm proxy
the pipeline already uses handles the lookup transparently.

Also drop the getTargets() call from parseTarget() — the pipeline
matrix controls what targets get invoked in production, and an
invalid target surfaces downstream when npm install can't resolve
the platform package. Avoids a network round-trip per CLI parse.
The pipeline's claudeTargets/codexTargets arrays now drive both the
matrix fan-out AND aggregate's completeness check (passed in as
--claude-targets/--codex-targets CLI flags). One source of truth for
which platforms we build; aggregate.ts has no parallel list to drift
from.

Add warn-only drift detection: aggregate queries each SDK's registry
optionalDependencies and emits ##vso[task.logissue type=warning] when
upstream declares a platform we don't build (or, separately, when we
build one upstream has dropped). Failing here would block routine
pipeline runs every time an SDK publishes a new platform overnight,
so just shout — the operator sees the warning in the build UI.

Renames getTargets() → getRegistryTargets() to make its purpose clear.
@TylerLeonhardt

Copy link
Copy Markdown
Member Author

Superseded by the runtime simplification in #320883. The replacement build PR inlines SDK production into the per-platform packageTask in the existing gulpfiles (no separate AgentSDK stage, no aggregate). New PR coming shortly.

@TylerLeonhardt TylerLeonhardt deleted the tyler/nutty-tarantula branch June 11, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants