From 2dff5627ae79fa0d09a82b50081d094cd7f83f3c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 12:29:18 +0000 Subject: [PATCH 1/7] Attachments: the served Content-Type, and a page with its total From gh-v1's C4: - attachments.get reports the Content-Type the server sent the bytes with (falling back to the Attachment's recorded type, which stays as recordedContentType). A request can now take onResponse to see a successful response's status and headers; wire/attachment's getFile uses it, and also reads the filename from Content-Disposition. - attachments.page returns { attachments, total, more }. It rests on a new assetApi.queryPage, which sends needTotal and reads the total api/asset reports in resultMetaData (query.v1 ignores needTotal). list is unchanged. Live (v1sdktesting): text/plain served, total 2 with more after a page of 1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- README.md | 2 +- src/client.ts | 1 + src/core/fetchTransport.ts | 5 +++- src/core/transport.ts | 6 +++++ src/services/attachments.ts | 21 ++++++++++++---- src/wire/assetApi.ts | 16 +++++++++++++ src/wire/attachment.ts | 18 ++++++++++++++ test/integration/attachments.test.ts | 5 ++++ test/unit/attachment.test.ts | 36 ++++++++++++++++++++++++++-- 9 files changed, 101 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 541480c..39e1b9e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ npm test - `queryV1.query` / `queryV1.queries` - reads through query.v1: the same query bodies as `assets.query`, rows translated the same way, but the endpoint is read-only and answers with rows alone, so it is the safe place to explore a query - `resolve.numbers` / `resolve.ref` - asset numbers and OID tokens to assets, in one request - `assets.sendGuarded` - a batch as a compare-and-set on each asset's ChangeDate; changed assets come back `skipped` - - `attachments.list` / `info` / `get` / `add` / `put` / `remove` / `url` - files attached to an asset: metadata on the Attachment asset, bytes through attachment.img; `add` sends a small file inline (Base64, one request) and a large one in two steps (create, then upload), and a failed upload throws `AttachmentUploadError` naming the empty attachment it left + - `attachments.list` / `page` / `info` / `get` / `add` / `put` / `remove` / `url` - files attached to an asset: metadata on the Attachment asset, bytes through attachment.img; `page` adds the total and whether more pages follow; `get`'s `contentType` is the type the server sent the bytes with; `add` sends a small file inline (Base64, one request) and a large one in two steps (create, then upload), and a failed upload throws `AttachmentUploadError` naming the empty attachment it left - `embeddedImages.upload` / `url` / `normalize` - images in rich text: upload returns the `` URL; `normalize` rewrites `embedded.img` references to this instance's URL. They cannot be deleted - `batch.run` / `batch.reversible` - preview, snapshot, (confirm,) send guarded, and a receipt with a reverse batch. Undo (`batch.reversible`) reactivates a closed asset before deleting it, and a second command on one asset is sent as a second guarded pass. `confirm` sees the touched assets' names; aliased updates/executes are sent as written with `guard: false`; a failure after the send throws `BatchRecordError` carrying the receipt - `TypeCache` - one instance's loaded type definitions, shared by many clients (`new AgilityApiClient(config, { types })`): a server that builds a client per user fetches each type once diff --git a/src/client.ts b/src/client.ts index 519d94a..c0b510a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -123,6 +123,7 @@ export class AgilityApiClient { */ readonly attachments = { list: (asset: string, options?: { page?: { size: number; start: number } }) => attachments.list(this.transport, asset, options), + page: (asset: string, options?: { page?: { size: number; start: number } }) => attachments.page(this.transport, asset, options), info: (id: string | number) => attachments.info(this.transport, id), get: (id: string | number) => attachments.get(this.transport, id), add: (asset: string, data: attachment.Bytes, options: attachments.AddOptions) => attachments.add(this.transport, asset, data, options), diff --git a/src/core/fetchTransport.ts b/src/core/fetchTransport.ts index d020ae4..95ddc13 100644 --- a/src/core/fetchTransport.ts +++ b/src/core/fetchTransport.ts @@ -184,7 +184,10 @@ export function createFetchTransport(config: FetchTransportConfig): Transport { } try { - if (response.ok) return (await readBody(response, responseType)) as T; + if (response.ok) { + options.onResponse?.({ status: response.status, headers: response.headers }); + return (await readBody(response, responseType)) as T; + } // An answer, not a failure to get one. 5xx may be retried when // idempotent; anything else is final. diff --git a/src/core/transport.ts b/src/core/transport.ts index aa68361..e982a2f 100644 --- a/src/core/transport.ts +++ b/src/core/transport.ts @@ -29,6 +29,12 @@ export interface RequestOptions { * still to come) with an ApiError coded ABORTED. Never retried. */ signal?: AbortSignal; + /** + * Called with a successful response's status and headers before its body + * is read — for what the body alone does not say (an attachment's + * Content-Type, say). A transport that cannot see headers may skip it. + */ + onResponse?: (response: { status: number; headers: { get(name: string): string | null } }) => void; } export interface Transport { diff --git a/src/services/attachments.ts b/src/services/attachments.ts index 3cd7b0e..1645d02 100644 --- a/src/services/attachments.ts +++ b/src/services/attachments.ts @@ -20,7 +20,7 @@ import type { Transport } from "../core/transport"; import type { Oid } from "../types/asset"; import { momentless } from "../types/oid"; import * as assetApi from "../wire/assetApi"; -import { getBytes, numericId, putBytes, toBase64, toUint8Array, type Bytes } from "../wire/attachment"; +import { getFile, numericId, putBytes, toBase64, toUint8Array, type Bytes } from "../wire/attachment"; /** Files up to this size go inline by default; larger ones in two steps. */ export const INLINE_LIMIT = 1_000_000; @@ -44,7 +44,13 @@ const infoOf = (r: Row): AttachmentInfo => { return { oid, id: numericId(oid), name: r.Name ?? null, filename: r.Filename ?? null, contentType: r.ContentType ?? null, description: r.Description ?? null, createDate: r.CreateDate ?? null, createdBy: r["CreatedBy.Name"] ?? null }; }; -/** The files attached to an asset, newest first, metadata only. */ +/** One page of the files attached to an asset, newest first, metadata only, with the total and whether more pages follow. */ +export async function page(transport: Transport, asset: Oid, options: { page?: { size: number; start: number } } = {}): Promise<{ attachments: AttachmentInfo[]; total: number | null; more: boolean }> { + const { rows, total, more } = await assetApi.queryPage(transport, { from: "Attachment", select: SELECT, filter: ["Asset=$asset"], with: { $asset: momentless(asset) }, sort: ["-CreateDate"], page: options.page ?? { size: 50, start: 0 } }); + return { attachments: rows.map((r) => infoOf(r as unknown as Row)), total, more }; +} + +/** The files attached to an asset, newest first, metadata only (one page; `page` also gives the total). */ export async function list(transport: Transport, asset: Oid, options: { page?: { size: number; start: number } } = {}): Promise { const rows = await assetApi.query(transport, { from: "Attachment", select: SELECT, filter: ["Asset=$asset"], with: { $asset: momentless(asset) }, sort: ["-CreateDate"], page: options.page ?? { size: 50, start: 0 } }); return rows.map((r) => infoOf(r as unknown as Row)); @@ -56,11 +62,16 @@ export async function info(transport: Transport, id: Oid | string | number): Pro return row ? infoOf(row as unknown as Row) : null; } -/** One attachment: its metadata and its bytes. */ -export async function get(transport: Transport, id: Oid | string | number): Promise { +/** + * One attachment: its metadata and its bytes. `contentType` is the type the + * server sent the bytes with, falling back to the Attachment's recorded + * one; `recordedContentType` is always the latter. + */ +export async function get(transport: Transport, id: Oid | string | number): Promise { const meta = await info(transport, id); if (!meta) throw new ApiError(`attachment ${numericId(id)}: not found`, 404, "NOT_FOUND"); - return { ...meta, bytes: await getBytes(transport, meta.id) }; + const file = await getFile(transport, meta.id); + return { ...meta, bytes: file.bytes, contentType: file.contentType ?? meta.contentType, recordedContentType: meta.contentType }; } export interface AddOptions { diff --git a/src/wire/assetApi.ts b/src/wire/assetApi.ts index 07db990..1f834e9 100644 --- a/src/wire/assetApi.ts +++ b/src/wire/assetApi.ts @@ -50,6 +50,8 @@ export interface QueryBody { find?: string; findin?: string[]; asof?: string; + /** Ask api/asset for the total number of matches (query.v1 ignores it); read it with queryPage. */ + needTotal?: boolean; /** Group the rows by a relation: each result row is then a group, its rows under `_children` (translated like any row of `from`). */ group?: (string | { from: string; [key: string]: unknown })[]; } @@ -408,6 +410,20 @@ export async function query(transport: Transport, q: return rows ?? []; } +/** + * One page of a query, with the total number of matches (api/asset reports + * it when asked with needTotal; `total` is null when it does not) and + * whether more pages follow. + */ +export async function queryPage(transport: Transport, q: QueryBody, options: ReadOptions = {}): Promise<{ rows: Row[]; total: number | null; more: boolean }> { + if (!q.page) throw new Error(`query of ${q.from} without a page: every api/asset read must be bounded`); + const response = await send(transport, [{ ...q, needTotal: true }], { ...options, throwOnFailure: true }); + const rows = response.queryResult.results[0] ?? []; + const reported = (response.queryResult as { resultMetaData?: { total?: unknown }[] }).resultMetaData?.[0]?.total; + const total = typeof reported === "number" ? reported : null; + return { rows, total, more: total === null ? rows.length === q.page.size : q.page.start + rows.length < total }; +} + /** * Several queries in one request; one array of rows per query, in order, [] * where a query matched nothing. A query the server rejected (a bad select diff --git a/src/wire/attachment.ts b/src/wire/attachment.ts index 60de955..1c94c1c 100644 --- a/src/wire/attachment.ts +++ b/src/wire/attachment.ts @@ -92,6 +92,24 @@ export async function getBytes(transport: Transport, id: Oid | string | number): } } +/** An attachment's bytes with the type and filename the server sent them with (Content-Type, Content-Disposition), when it did. */ +export async function getFile(transport: Transport, id: Oid | string | number): Promise<{ bytes: Uint8Array; contentType: string | null; filename: string | null }> { + const n = numericId(id); + let contentType: string | null = null, filename: string | null = null; + const onResponse = ({ headers }: { headers: { get(name: string): string | null } }) => { + contentType = headers.get("content-type"); + const disposition = headers.get("content-disposition") ?? ""; + const encoded = /filename\*=UTF-8''([^;]+)/i.exec(disposition)?.[1]; + filename = encoded ? decodeURIComponent(encoded) : /filename="?([^";]+)"?/i.exec(disposition)?.[1] ?? null; + }; + try { + const bytes = await transport.request({ method: "GET", path: `/attachment.img/${n}`, responseType: "bytes", onResponse }); + return { bytes, contentType, filename }; + } catch (error) { + throw named(error, `attachment ${n}`); + } +} + /** Replace an attachment's bytes. The Attachment asset (name, filename, content type) is unchanged. */ export async function putBytes(transport: Transport, id: Oid | string | number, data: Bytes, contentType = "application/octet-stream"): Promise { const n = numericId(id); diff --git a/test/integration/attachments.test.ts b/test/integration/attachments.test.ts index 8873ef6..93f74ef 100644 --- a/test/integration/attachments.test.ts +++ b/test/integration/attachments.test.ts @@ -39,6 +39,11 @@ describeInstance("attachments (live)", () => { expect(got).toMatchObject({ filename: "sdk-integration large.txt", contentType: "text/plain", description: "two-step" }); const listed = await client.attachments.list(story); expect(listed.map((a) => a.oid).sort()).toEqual([small.oid, large.oid].sort()); + // The type the server served the bytes with, and a page with its total. + expect((await client.attachments.get(small.id)).contentType).toMatch(/^text\/plain/); + const first = await client.attachments.page(story, { page: { size: 1, start: 0 } }); + expect(first).toMatchObject({ total: 2, more: true }); + expect(first.attachments).toHaveLength(1); }); it("put replaces the bytes; remove deletes the attachment; a missing one is NOT_FOUND", async () => { diff --git a/test/unit/attachment.test.ts b/test/unit/attachment.test.ts index 8a81937..a8698f5 100644 --- a/test/unit/attachment.test.ts +++ b/test/unit/attachment.test.ts @@ -3,8 +3,9 @@ import { AgilityApiClient } from "../../src/client"; import { ApiError } from "../../src/core/errors"; import { createFetchTransport } from "../../src/core/fetchTransport"; import type { RequestOptions, Transport } from "../../src/core/transport"; -import { AttachmentUploadError, add, list } from "../../src/services/attachments"; -import { fromBase64, getBytes, normalizeEmbeddedImageUrls, numericId, toBase64, uploadEmbeddedImage } from "../../src/wire/attachment"; +import { AttachmentUploadError, add, list, page } from "../../src/services/attachments"; +import { queryPage } from "../../src/wire/assetApi"; +import { fromBase64, getBytes, getFile, normalizeEmbeddedImageUrls, numericId, toBase64, uploadEmbeddedImage } from "../../src/wire/attachment"; const envelope = (over: Record = {}) => ({ requestId: "r", assetsCreated: { oidTokens: [], count: 0 }, assetsModified: { oidTokens: [], count: 0 }, assetsOperatedOn: { oidTokens: [], count: 0 }, @@ -103,3 +104,34 @@ describe("attachments service", () => { expect(client.embeddedImages.url(12)).toBe("https://www14.v1host.com/v1sdktesting/embedded.img/12"); }); }); + +describe("attachment gaps from gh-v1's C4", () => { + it("the fetch transport hands onResponse the headers of a successful response", async () => { + const fetchImpl = vi.fn().mockResolvedValue(new Response(new Uint8Array([1]), { status: 200, headers: { "content-type": "image/png", "content-disposition": 'inline; filename="a b.png"' } })); + const seen: (string | null)[] = []; + await createFetchTransport({ baseUrl: "https://x", fetchImpl }).request({ method: "GET", path: "/attachment.img/5", responseType: "bytes", onResponse: ({ headers }) => seen.push(headers.get("content-type")) }); + expect(seen).toEqual(["image/png"]); + }); + + it("getFile returns the bytes with the served Content-Type and the filename from Content-Disposition", async () => { + const fetchImpl = vi.fn() + .mockResolvedValueOnce(new Response(new Uint8Array([1, 2]), { status: 200, headers: { "content-type": "text/plain", "content-disposition": 'inline; filename="inline.txt"' } })) + .mockResolvedValueOnce(new Response(new Uint8Array([3]), { status: 200, headers: { "content-type": "image/png", "content-disposition": "attachment; filename*=UTF-8''caf%C3%A9.png" } })); + const t = createFetchTransport({ baseUrl: "https://x", fetchImpl }); + expect(await getFile(t, "Attachment:5")).toEqual({ bytes: new Uint8Array([1, 2]), contentType: "text/plain", filename: "inline.txt" }); + expect((await getFile(t, 6)).filename).toBe("café.png"); + }); + + it("queryPage asks for the total and says whether more pages follow; attachments.page uses it", async () => { + // resultMetaData as api/asset sends it with needTotal (v1sdktesting, 2026-11-07). + const withTotal = (rows: unknown[], total: number | null) => envelope({ queryResult: { results: [rows], resultMetaData: [{ page: { start: 0, size: 2 }, total }], count: 1 } }); + const t = fake(withTotal([{ _oid: "Story:1" }, { _oid: "Story:2" }], 17093)); + const p = await queryPage(t, { from: "Story", select: ["Name"], page: { size: 2, start: 0 } }); + expect((t.calls[0]!.body as Record[])[0]!.needTotal).toBe(true); + expect(p).toMatchObject({ total: 17093, more: true }); + const last = await queryPage(fake(withTotal([{ _oid: "Story:3" }], 3)), { from: "Story", select: ["Name"], page: { size: 2, start: 2 } }); + expect(last).toMatchObject({ total: 3, more: false }); + const a = await page(fake(withTotal([{ _oid: "Attachment:5", Name: "n" }], 1)), "Story:1"); + expect(a).toMatchObject({ total: 1, more: false, attachments: [{ oid: "Attachment:5", id: "5" }] }); + }); +}); From 3ce916b0bc2095424af693341b82851fda804132 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 12:32:32 +0000 Subject: [PATCH 2/7] Plan 001: tick C4 (gh-v1#38) Track C is done: gh-v1 talks to Agility only through the vendored SDK, apart from the gh v1 api passthrough. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- plans/001-sdk-adoption-campaign.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plans/001-sdk-adoption-campaign.md b/plans/001-sdk-adoption-campaign.md index 8bdf4de..7b2a925 100644 --- a/plans/001-sdk-adoption-campaign.md +++ b/plans/001-sdk-adoption-campaign.md @@ -484,5 +484,5 @@ ESM" here means finishing the edges: - [x] C1 gh-v1 seam swap — versionone/gh-v1#33, with the vendored SDK's hashes, CI and Node 22 floor in #35 - [x] C2 gh-v1 rows translated through the schema; `isTrue`/`oidOf` gone — versionone/gh-v1#36 - [x] C3 gh-v1 resolve and batch on `client.resolve`/`client.batch` — versionone/gh-v1#37 (with the SDK fixes from #43) -- [ ] C4 gh-v1 attachments on `client.attachments` (unblocked: A7 done); `raw` stays CLI-local +- [x] C4 gh-v1 `gh v1 attach` on `client.attachments`, `blob` removed — versionone/gh-v1#38 (re-vendored at `d51fb16`; small files now upload inline); `raw` stays CLI-local - [ ] D1–D4 agility-rooms From 1697282c4abd36a2ec4319159ef4f3798470740d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 12:40:41 +0000 Subject: [PATCH 3/7] Rename the package to @versionone/agility-sdk Before the first publish. The @versionone scope already has another SDK (ctm-pipeline-builder-sdk), and the product is Agility. package.json gains a description, keywords, repository, homepage and bugs for npm; the README leads with what the package is, how to install the alpha, and that it is not a compatible successor to v1sdk 2.x. Plans, handoffs and the CommonJS install test use the new name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- README.md | 23 +++++++++++++++++--- package-lock.json | 4 ++-- package.json | 20 +++++++++++++---- plans/001-sdk-adoption-campaign.md | 12 +++++----- plans/002-adopt-sdk-gh-v1.md | 6 ++--- plans/003-adopt-sdk-k6i-agility-slack-bot.md | 8 +++---- plans/handoffs/gh-v1.md | 2 +- plans/handoffs/k6i-agility-slack-bot.md | 4 ++-- test/unit/cjs.test.ts | 10 ++++----- 9 files changed, 60 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 39e1b9e..41ef522 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@ -# VersionOne SDK (TypeScript starter) +# @versionone/agility-sdk -Minimal TypeScript SDK starter for building JavaScript bindings to external APIs. +TypeScript SDK for the Digital.ai Agility (formerly VersionOne) API. It works in Node (ESM or CommonJS) and the browser, ships its own types, and has no runtime dependencies. -## Quick start +Not to be confused with `v1sdk`, the 2.x JavaScript SDK: this is a new API, not a compatible successor. + +## Install + +Alpha releases are for feedback; the API may change between them. + +```bash +npm install @versionone/agility-sdk@alpha +``` + +```ts +import { AgilityApiClient } from "@versionone/agility-sdk"; + +const client = new AgilityApiClient({ baseUrl: "https://www14.v1host.com/v1sdktesting", accessToken: process.env.V1_ACCESS_TOKEN }); +const stories = await client.queryV1.query({ from: "Story", select: ["Name", "Estimate"], page: { size: 10, start: 0 } }); +``` + +## Working on the SDK ```bash npm install diff --git a/package-lock.json b/package-lock.json index 340f87a..6000665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@versionone/sdk", + "name": "@versionone/agility-sdk", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@versionone/sdk", + "name": "@versionone/agility-sdk", "version": "0.1.0", "license": "MIT", "devDependencies": { diff --git a/package.json b/package.json index 1d55ce8..f723857 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { - "name": "@versionone/sdk", + "name": "@versionone/agility-sdk", "version": "0.1.0", - "description": "Minimal TypeScript SDK starter for digital.ai Agility APIs", + "description": "TypeScript SDK for the Digital.ai Agility (VersionOne) API: queries, batches, schema-translated rows, guarded writes, attachments", "keywords": [ + "agility", + "digital.ai", + "versionone", + "v1", "sdk", - "typescript", - "api-client" + "api-client", + "typescript" ], "license": "MIT", "type": "module", @@ -52,5 +56,13 @@ }, "volta": { "node": "24.14.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/versionone/VersionOne.SDK.JavaScript.git" + }, + "homepage": "https://github.com/versionone/VersionOne.SDK.JavaScript#readme", + "bugs": { + "url": "https://github.com/versionone/VersionOne.SDK.JavaScript/issues" } } diff --git a/plans/001-sdk-adoption-campaign.md b/plans/001-sdk-adoption-campaign.md index 7b2a925..4e2dd29 100644 --- a/plans/001-sdk-adoption-campaign.md +++ b/plans/001-sdk-adoption-campaign.md @@ -1,7 +1,7 @@ # Plan 001: TypeScript SDK campaign and adoption A parallel campaign, run from a cloud agent session on this repo, alongside -daily work. Goal: take `@versionone/sdk` from a verified starter (layers, +daily work. Goal: take `@versionone/agility-sdk` from a verified starter (layers, api/asset + rest-1 + meta.v1 wire modules, schema-driven translation, 49 unit tests green) to a package that three surfaces depend on: @@ -83,7 +83,7 @@ Findings from the testing instance worth carrying into later milestones: the `Attributes` envelope via `lib/transformDataToAsset.js`; `findAssets` reads with `find`/`findin`/`page`), and `loc-2.v1`. Its `AGENTS.md` already plans for this: the Bolt migration's Phase 2 rewrites the seam as -`services/v1.js` on native `fetch` "so it can migrate into `@versionone/sdk`". +`services/v1.js` on native `fetch` "so it can migrate into `@versionone/agility-sdk`". It hand-decodes string scalars in 3 places (`"Team.HasProcess" === "True"`, `IsKanban.toLowerCase() === "true"`) — exactly what spec 000 §2a removes. @@ -196,7 +196,7 @@ Found by reading the code above against `src/`: 8. **Distribution.** No CI in this repo, no publish target, version 0.1.0. Source is browser-safe already (no `node:` imports, no `Buffer`). 9. **CommonJS consumers — done.** `test/unit/cjs.test.ts` builds the CJS - bundle, installs it as `node_modules/@versionone/sdk`, and has a plain + bundle, installs it as `node_modules/@versionone/agility-sdk`, and has a plain `.cjs` script `require` and use it. The exports map now gives each condition its own types (`.d.cts` for require), which a CJS caller type-checking with NodeNext (gh-v1 after G2) needs. @@ -449,7 +449,9 @@ ESM" here means finishing the edges: ## Decisions needed - ~~Publish target~~ — decided 2026-11-07: public npmjs, as - `@versionone/sdk` (the `@versionone` org already publishes there), starting + `@versionone/agility-sdk` (renamed from `@versionone/sdk` on 2026-11-07, + before any publish: the scope already has `ctm-pipeline-builder-sdk`, and + the product is Agility; the `@versionone` org already publishes there), starting at `1.0.0-alpha.0` on the `alpha` dist-tag so `latest` is never a prerelease. Not `v1sdk` 3.x: the 2.x package (`v1sdk`, last published 2022) stays as it is, because a new major of the same name would imply a @@ -473,7 +475,7 @@ ESM" here means finishing the edges: - [x] A4 slack-shaped reads — `queryV1` wire module, `client.queryV1.query`/`queries` (read-only, idempotent, same row translation as api/asset) - [x] A5 services port from gh-v1 — `resolve` (`client.resolve.numbers`/`.ref`), `guard` (`client.assets.sendGuarded`), `batch` (`client.batch.run`/`.reversible`) - [x] A7 attachments and embedded images — `attachment` wire module (`attachment.img` bytes, `EmbeddedImage.mvc/Upload`, URL helpers, browser-safe Base64), `client.attachments` (list/info/get/add/put/remove/url; inline up to `INLINE_LIMIT`, two-step above) and `client.embeddedImages` (upload/url/normalize) -- [ ] A6 CI + prerelease publish — CI (`.github/workflows/ci.yml`: verify on Node 20/22/24, `npm pack` artifact), git-SHA installs (`prepare`) and the dependency audit done; publish `@versionone/sdk@1.0.0-alpha.0` to npmjs with `--tag alpha` next (needs an org owner's token or trusted publisher) +- [ ] A6 CI + prerelease publish — CI (`.github/workflows/ci.yml`: verify on Node 20/22/24, `npm pack` artifact), git-SHA installs (`prepare`) and the dependency audit done; publish `@versionone/agility-sdk@1.0.0-alpha.0` to npmjs with `--tag alpha` next (needs an org owner's token or trusted publisher) - [x] G0–G3 gh-v1 migrations — versionone/gh-v1#29–#32 - [x] S1–S4 slack-bot migrations — digital-ai/k6i-agility-slack-bot#21–#24 (on `bolt-migration`) - [x] B1 slack bot client swap — digital-ai/k6i-agility-slack-bot#29 diff --git a/plans/002-adopt-sdk-gh-v1.md b/plans/002-adopt-sdk-gh-v1.md index ee299c5..f1d0b70 100644 --- a/plans/002-adopt-sdk-gh-v1.md +++ b/plans/002-adopt-sdk-gh-v1.md @@ -1,4 +1,4 @@ -# Plan 002: gh-v1 adopts `@versionone/sdk` (Track C) +# Plan 002: gh-v1 adopts `@versionone/agility-sdk` (Track C) Brief for a session on `versionone/gh-v1`. Start once the SDK PR that adds this file has merged into `proposed_type_script_api_version`. Source plan: @@ -6,7 +6,7 @@ this file has merged into `proposed_type_script_api_version`. Source plan: ## Goal -gh-v1 talks to Agility only through `@versionone/sdk`. CLI behaviour and +gh-v1 talks to Agility only through `@versionone/agility-sdk`. CLI behaviour and output stay the same; the code that duplicates the SDK goes. ## Where things stand (checked 2026-11-06, gh-v1 `2c58c7e`) @@ -34,7 +34,7 @@ runtime dependency, but its code ships inside the repo: - `vendor/versionone-sdk/` holds the SDK's ESM build (`index.js`, which imports nothing) and types (`index.d.ts`), plus `SOURCE.json` naming the commit they came from. `lib/api.js` imports from there. -- `package.json` lists `@versionone/sdk` under `devDependencies`, pinned +- `package.json` lists `@versionone/agility-sdk` under `devDependencies`, pinned to a full commit, so the lockfile records exactly what was vendored. - To move the pin: change it in `package.json`, then `npm install && npm run vendor:sdk`, and commit the copy. diff --git a/plans/003-adopt-sdk-k6i-agility-slack-bot.md b/plans/003-adopt-sdk-k6i-agility-slack-bot.md index 353b998..32a9db3 100644 --- a/plans/003-adopt-sdk-k6i-agility-slack-bot.md +++ b/plans/003-adopt-sdk-k6i-agility-slack-bot.md @@ -1,4 +1,4 @@ -# Plan 003: k6i-agility-slack-bot adopts `@versionone/sdk` (Track B) +# Plan 003: k6i-agility-slack-bot adopts `@versionone/agility-sdk` (Track B) Brief for a session on `digital-ai/k6i-agility-slack-bot`. Start once the SDK PR that adds this file has merged into `proposed_type_script_api_version`. @@ -7,7 +7,7 @@ Source plan: `plans/001-sdk-adoption-campaign.md`, Track B. Work on ## Goal -`services/v1.js`'s `createV1Client` runs on `@versionone/sdk`, with the same +`services/v1.js`'s `createV1Client` runs on `@versionone/agility-sdk`, with the same exported surface, so listeners and features do not change. The bot's hand-decoding and rest-1 write path go. @@ -27,9 +27,9 @@ hand-decoding and rest-1 write path go. ## Dependency No registry package yet. Pin a commit: -`"@versionone/sdk": "github:versionone/VersionOne.SDK.JavaScript#"`. +`"@versionone/agility-sdk": "github:versionone/VersionOne.SDK.JavaScript#"`. The SDK's `prepare` script builds `dist` on install. The bot is ESM, so -`import { AgilityApiClient } from "@versionone/sdk"`. +`import { AgilityApiClient } from "@versionone/agility-sdk"`. ## Steps, one PR each diff --git a/plans/handoffs/gh-v1.md b/plans/handoffs/gh-v1.md index aad5218..0c84838 100644 --- a/plans/handoffs/gh-v1.md +++ b/plans/handoffs/gh-v1.md @@ -7,7 +7,7 @@ Paste everything below the line into the session working on --- -Context: gh-v1 will become the first consumer of `@versionone/sdk` (the +Context: gh-v1 will become the first consumer of `@versionone/agility-sdk` (the TypeScript rewrite of the Agility JS SDK). Before that swap, gh-v1 should adopt these migrations so the swap later touches only `lib/api.js`. Do them as separate PRs, in order, each with `npm test` green (99 tests diff --git a/plans/handoffs/k6i-agility-slack-bot.md b/plans/handoffs/k6i-agility-slack-bot.md index 1dcd988..f33851d 100644 --- a/plans/handoffs/k6i-agility-slack-bot.md +++ b/plans/handoffs/k6i-agility-slack-bot.md @@ -8,7 +8,7 @@ in `versionone/VersionOne.SDK.JavaScript` (branch --- Context: after the Bolt migration, `services/v1.js` (`createV1Client`) will -be re-implemented on `@versionone/sdk`, the TypeScript rewrite of the +be re-implemented on `@versionone/agility-sdk`, the TypeScript rewrite of the Agility JS SDK, keeping its exported surface. These four migrations come first, so that swap is one file. Work on (or branch from) `bolt-migration`, not `main`. One PR each, `npm test` green, and follow AGENTS.md and @@ -51,5 +51,5 @@ https://docs.digital.ai/agility-api/apis/asset-bulk/updating-assets API reference for all of this: https://docs.digital.ai/agility-api/ (the whole site as text: https://docs.digital.ai/agility-api/llms-full.txt). -Do not add `@versionone/sdk` yet (milestone B1, run from the SDK session +Do not add `@versionone/agility-sdk` yet (milestone B1, run from the SDK session after S2 and S3 merge). Report back with the PR links. diff --git a/test/unit/cjs.test.ts b/test/unit/cjs.test.ts index a5cfb25..4f93cbc 100644 --- a/test/unit/cjs.test.ts +++ b/test/unit/cjs.test.ts @@ -8,9 +8,9 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; /** * CommonJS consumers (gh-v1 is `require`-based) load the package through * the "require" condition of its exports map. This builds the CJS bundle - * the way the package does, installs it under node_modules/@versionone/sdk + * the way the package does, installs it under node_modules/@versionone/agility-sdk * in a temp directory with the real package.json, and has a plain .cjs - * script `require("@versionone/sdk")` and use it — so a bundle that only + * script `require("@versionone/agility-sdk")` and use it — so a bundle that only * works as ESM (top-level await, import.meta, an ESM-only dependency) or an * exports map that points nowhere fails here, not in a consumer. */ @@ -20,7 +20,7 @@ let dir = ""; beforeAll(async () => { dir = mkdtempSync(join(tmpdir(), "sdk-cjs-")); - const installed = join(dir, "node_modules", "@versionone", "sdk"); + const installed = join(dir, "node_modules", "@versionone", "agility-sdk"); await build({ config: false, entry: [join(root, "src", "index.ts")], format: ["cjs"], outDir: join(installed, "dist"), target: "node18", dts: false, sourcemap: false, silent: true }); copyFileSync(join(root, "package.json"), join(installed, "package.json")); }, 30_000); @@ -29,7 +29,7 @@ afterAll(() => { if (dir) rmSync(dir, { recursive: true, force: true }); }); // The consumer: CommonJS, no transpiler, a fake fetch standing in for the server. const CONSUMER = String.raw` const assert = require("node:assert/strict"); -const sdk = require("@versionone/sdk"); +const sdk = require("@versionone/agility-sdk"); for (const name of ["AgilityApiClient", "createFetchTransport", "ApiError", "CommandFailureError", "assetApi", "rest1", "meta", "loc2", "Schema", "parseOid", "momentless"]) { assert.ok(sdk[name], "missing export " + name); } @@ -55,7 +55,7 @@ const fetchImpl = async (url) => { `; describe("CommonJS consumers", () => { - it("require('@versionone/sdk') resolves through the exports map and the bundle works with no ESM in sight", () => { + it("require('@versionone/agility-sdk') resolves through the exports map and the bundle works with no ESM in sight", () => { const script = join(dir, "consumer.cjs"); writeFileSync(script, CONSUMER); const r = spawnSync(process.execPath, [script], { cwd: dir, encoding: "utf8" }); From e1d968b7a6c41515c984a7cacd11c0b6241f623a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 12:44:50 +0000 Subject: [PATCH 4/7] A6: publish workflow; version 1.0.0-alpha.0 .github/workflows/publish.yml runs on a v* tag: checks the tag equals package.json's version, runs verify, takes the dist-tag from the prerelease id (alpha, beta, rc; latest only for a plain version), and publishes with provenance through npm trusted publishing (NPM_TOKEN as a fallback for the first publish). package.json: 1.0.0-alpha.0 and publishConfig { access: public, provenance: true }. README: Releasing. Not published yet: the license is undecided (master's LICENSE.md is BSD 3-clause; package.json says MIT with no license file). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- .github/workflows/publish.yml | 60 ++++++++++++++++++++++++++++++ README.md | 18 +++++++++ package-lock.json | 4 +- package.json | 6 ++- plans/001-sdk-adoption-campaign.md | 2 +- 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0cb2d16 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,60 @@ +name: Publish + +# Publishes @versionone/agility-sdk to npmjs when a version tag is pushed: +# git tag v1.0.0-alpha.0 && git push origin v1.0.0-alpha.0 +# +# A prerelease (1.0.0-alpha.0, -beta.1, -rc.2) goes out on its own dist-tag +# (alpha, beta, rc), so `npm install @versionone/agility-sdk` never picks it +# up; only a plain version moves `latest`. +# +# Authentication is npm trusted publishing (OIDC): link this repository and +# this workflow file on the package's npmjs settings page, and no token is +# stored anywhere. The package must exist before it can be linked, so the +# very first version may need one manual publish (see the README's +# "Releasing" section); an NPM_TOKEN secret, if set, is used as a fallback. + +on: + push: + tags: ["v*"] + +permissions: + contents: read + id-token: write # OIDC for trusted publishing and provenance + +concurrency: + group: publish + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + # Trusted publishing needs npm 11.5.1 or later. + - run: npm install -g npm@^11.5.1 + - run: npm ci + - name: The tag names the version in package.json + run: | + version="$(node -p "require('./package.json').version")" + if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then + echo "Tag ${GITHUB_REF_NAME} does not match package.json version ${version}" >&2 + exit 1 + fi + echo "VERSION=${version}" >> "$GITHUB_ENV" + - run: npm run verify + - name: Choose the dist-tag + run: | + case "${VERSION}" in + *-*) tag="$(echo "${VERSION#*-}" | sed 's/[.0-9]*$//')" ;; + *) tag=latest ;; + esac + if [ -z "${tag}" ]; then tag=next; fi + echo "DIST_TAG=${tag}" >> "$GITHUB_ENV" + echo "Publishing ${VERSION} with dist-tag ${tag}" + - run: npm publish --provenance --access public --tag "${DIST_TAG}" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 41ef522..3093997 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,24 @@ API reference: https://docs.digital.ai/agility-api/ No registry package yet. Pin a commit: `npm install github:versionone/VersionOne.SDK.JavaScript#` builds `dist` on install (the `prepare` script). CI also uploads an `npm pack` tarball on every run. +## Releasing + +Versions are published to npmjs by `.github/workflows/publish.yml` when a +tag `v` is pushed; the tag must equal `package.json`'s version. A +prerelease (`1.0.0-alpha.0`, `-beta.N`, `-rc.N`) goes out on its own +dist-tag (`alpha`, `beta`, `rc`), so `latest` is never a prerelease. + +```bash +npm version 1.0.0-alpha.1 --no-git-tag-version # bump, commit, merge +git tag v1.0.0-alpha.1 && git push origin v1.0.0-alpha.1 +``` + +Authentication is npm trusted publishing: on npmjs, the package's settings +link this repository and `publish.yml`, and no token is stored. A package +must exist before it can be linked, so the very first version is published +once by hand by an `@versionone` org member (`npm publish --access public +--tag alpha` from a clean checkout of the tagged commit), then linked. + ## Before PR - Run `npm run verify` to validate typecheck, build, and tests before opening a pull request. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6000665..f35214b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@versionone/agility-sdk", - "version": "0.1.0", + "version": "1.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@versionone/agility-sdk", - "version": "0.1.0", + "version": "1.0.0-alpha.0", "license": "MIT", "devDependencies": { "@types/node": "^22.13.10", diff --git a/package.json b/package.json index f723857..7823b5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@versionone/agility-sdk", - "version": "0.1.0", + "version": "1.0.0-alpha.0", "description": "TypeScript SDK for the Digital.ai Agility (VersionOne) API: queries, batches, schema-translated rows, guarded writes, attachments", "keywords": [ "agility", @@ -64,5 +64,9 @@ "homepage": "https://github.com/versionone/VersionOne.SDK.JavaScript#readme", "bugs": { "url": "https://github.com/versionone/VersionOne.SDK.JavaScript/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/plans/001-sdk-adoption-campaign.md b/plans/001-sdk-adoption-campaign.md index 4e2dd29..66d35e3 100644 --- a/plans/001-sdk-adoption-campaign.md +++ b/plans/001-sdk-adoption-campaign.md @@ -475,7 +475,7 @@ ESM" here means finishing the edges: - [x] A4 slack-shaped reads — `queryV1` wire module, `client.queryV1.query`/`queries` (read-only, idempotent, same row translation as api/asset) - [x] A5 services port from gh-v1 — `resolve` (`client.resolve.numbers`/`.ref`), `guard` (`client.assets.sendGuarded`), `batch` (`client.batch.run`/`.reversible`) - [x] A7 attachments and embedded images — `attachment` wire module (`attachment.img` bytes, `EmbeddedImage.mvc/Upload`, URL helpers, browser-safe Base64), `client.attachments` (list/info/get/add/put/remove/url; inline up to `INLINE_LIMIT`, two-step above) and `client.embeddedImages` (upload/url/normalize) -- [ ] A6 CI + prerelease publish — CI (`.github/workflows/ci.yml`: verify on Node 20/22/24, `npm pack` artifact), git-SHA installs (`prepare`) and the dependency audit done; publish `@versionone/agility-sdk@1.0.0-alpha.0` to npmjs with `--tag alpha` next (needs an org owner's token or trusted publisher) +- [ ] A6 CI + prerelease publish — CI, git-SHA installs, the dependency audit and the publish workflow (`.github/workflows/publish.yml`: on a `v*` tag, verify, dist-tag from the prerelease id, `npm publish --provenance`) done; version `1.0.0-alpha.0`. Left: **the license** (master's `LICENSE.md` is BSD 3-clause, VersionOne 2012; `package.json` says MIT with no license file — Jim/Digital.ai to decide), then the first publish by hand and the trusted-publisher link on npmjs - [x] G0–G3 gh-v1 migrations — versionone/gh-v1#29–#32 - [x] S1–S4 slack-bot migrations — digital-ai/k6i-agility-slack-bot#21–#24 (on `bolt-migration`) - [x] B1 slack bot client swap — digital-ai/k6i-agility-slack-bot#29 From 16cf6802faea40a253660fa054757a0f6d22d14f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 13:25:28 +0000 Subject: [PATCH 5/7] License: MIT Jim chose MIT for @versionone/agility-sdk. LICENSE holds the text (npm packs it automatically); package.json already says MIT. The 2.x code's BSD 3-clause LICENSE.md on master is unaffected. Plan 001 records the decision; A6 has only the first publish and the npmjs link left. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- LICENSE | 21 +++++++++++++++++++++ plans/001-sdk-adoption-campaign.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc080f5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012-2026 VersionOne, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plans/001-sdk-adoption-campaign.md b/plans/001-sdk-adoption-campaign.md index 66d35e3..a87fafc 100644 --- a/plans/001-sdk-adoption-campaign.md +++ b/plans/001-sdk-adoption-campaign.md @@ -475,7 +475,7 @@ ESM" here means finishing the edges: - [x] A4 slack-shaped reads — `queryV1` wire module, `client.queryV1.query`/`queries` (read-only, idempotent, same row translation as api/asset) - [x] A5 services port from gh-v1 — `resolve` (`client.resolve.numbers`/`.ref`), `guard` (`client.assets.sendGuarded`), `batch` (`client.batch.run`/`.reversible`) - [x] A7 attachments and embedded images — `attachment` wire module (`attachment.img` bytes, `EmbeddedImage.mvc/Upload`, URL helpers, browser-safe Base64), `client.attachments` (list/info/get/add/put/remove/url; inline up to `INLINE_LIMIT`, two-step above) and `client.embeddedImages` (upload/url/normalize) -- [ ] A6 CI + prerelease publish — CI, git-SHA installs, the dependency audit and the publish workflow (`.github/workflows/publish.yml`: on a `v*` tag, verify, dist-tag from the prerelease id, `npm publish --provenance`) done; version `1.0.0-alpha.0`. Left: **the license** (master's `LICENSE.md` is BSD 3-clause, VersionOne 2012; `package.json` says MIT with no license file — Jim/Digital.ai to decide), then the first publish by hand and the trusted-publisher link on npmjs +- [ ] A6 CI + prerelease publish — CI, git-SHA installs, the dependency audit and the publish workflow (`.github/workflows/publish.yml`: on a `v*` tag, verify, dist-tag from the prerelease id, `npm publish --provenance`) done; version `1.0.0-alpha.0`. License decided 2026-11-07: MIT (`LICENSE`; `master`'s BSD 3-clause `LICENSE.md` stays with the 2.x code). Left: the first publish by hand and the trusted-publisher link on npmjs - [x] G0–G3 gh-v1 migrations — versionone/gh-v1#29–#32 - [x] S1–S4 slack-bot migrations — digital-ai/k6i-agility-slack-bot#21–#24 (on `bolt-migration`) - [x] B1 slack bot client swap — digital-ai/k6i-agility-slack-bot#29 From c06ccf6ed2451604df614038ec2fb0c3cc239b75 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 13:30:04 +0000 Subject: [PATCH 6/7] License: Digital.ai Software, Inc. as the copyright holder Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index fc080f5..c912d5e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012-2026 VersionOne, Inc. +Copyright (c) 2012-2026 Digital.ai Software, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 28201384b6c1b6a3b7238d6f7aab5d5222e08861 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 13:35:19 +0000 Subject: [PATCH 7/7] CJS test on Windows; CI runs Windows too tsup reads entries as glob patterns, where a backslash is an escape, so on Windows the CJS test's absolute entry path (D:\...\src\index.ts) matched nothing ("Cannot find ...\src\index.ts", reported by Jim). It now hands tsup a forward-slash path. CI adds a Windows / Node 24 job so this kind of break shows up in a PR, and the tarball upload uses the renamed package's file name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016NSWj9gL1oCubj5e2anVkb --- .github/workflows/ci.yml | 17 ++++++++++++----- test/unit/cjs.test.ts | 5 ++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5696b6d..d35f62c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,17 @@ permissions: jobs: verify: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: + os: [ubuntu-latest] node: [20, 22, 24] + # Windows too: tools that read paths as globs (tsup's entries) break + # on backslashes, and people develop the SDK there. + include: + - os: windows-latest + node: 24 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -24,10 +31,10 @@ jobs: # Unit tier only: the integration tier needs a testing instance and writes fixtures. - run: npm run verify - name: Pack - if: matrix.node == 24 + if: matrix.node == 24 && matrix.os == 'ubuntu-latest' run: npm pack - uses: actions/upload-artifact@v4 - if: matrix.node == 24 + if: matrix.node == 24 && matrix.os == 'ubuntu-latest' with: - name: versionone-sdk-tarball - path: versionone-sdk-*.tgz + name: versionone-agility-sdk-tarball + path: versionone-agility-sdk-*.tgz diff --git a/test/unit/cjs.test.ts b/test/unit/cjs.test.ts index 4f93cbc..537e078 100644 --- a/test/unit/cjs.test.ts +++ b/test/unit/cjs.test.ts @@ -21,7 +21,10 @@ let dir = ""; beforeAll(async () => { dir = mkdtempSync(join(tmpdir(), "sdk-cjs-")); const installed = join(dir, "node_modules", "@versionone", "agility-sdk"); - await build({ config: false, entry: [join(root, "src", "index.ts")], format: ["cjs"], outDir: join(installed, "dist"), target: "node18", dts: false, sourcemap: false, silent: true }); + // tsup reads entries as glob patterns, where "\\" is an escape: on Windows an + // absolute path (D:\\source\\…) matches nothing. Forward slashes work everywhere. + const entry = join(root, "src", "index.ts").replace(/\\/g, "/"); + await build({ config: false, entry: [entry], format: ["cjs"], outDir: join(installed, "dist"), target: "node18", dts: false, sourcemap: false, silent: true }); copyFileSync(join(root, "package.json"), join(installed, "package.json")); }, 30_000); afterAll(() => { if (dir) rmSync(dir, { recursive: true, force: true }); });