Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c084944
feat(cli-runtime): add typed agent command engine
yyyyaaa Jul 28, 2026
a1e478f
fix(cache): scope Graphile storage state per runtime
yyyyaaa Jul 28, 2026
588818d
fix(graphql): inject per-instance runtime environments
yyyyaaa Jul 28, 2026
8b7c755
feat(codegen): add transactional generation operations
yyyyaaa Jul 28, 2026
90b350b
feat(cli): migrate command families to agent protocol v1
yyyyaaa Jul 28, 2026
720e004
docs(cli): document registry-backed agent workflows
yyyyaaa Jul 28, 2026
7505993
ci(cli): verify published agent protocol artifacts
yyyyaaa Jul 28, 2026
3daffba
fix(codegen): roll back staged manifest publication
yyyyaaa Jul 21, 2026
7917a17
fix(codegen): serialize output planning and writes
yyyyaaa Jul 21, 2026
495684d
fix(cli): resolve auth from one state snapshot
yyyyaaa Jul 21, 2026
ec79f41
fix(cli-runtime): validate command metadata on registration
yyyyaaa Jul 21, 2026
0408af8
fix(cli): refuse reclaiming live state locks
yyyyaaa Jul 21, 2026
acd7331
fix(codegen): validate and classify config files
yyyyaaa Jul 21, 2026
4523174
fix(codegen): separate target planning from apply
yyyyaaa Jul 21, 2026
2e426dc
refactor: decompose oversized protocol modules
yyyyaaa Jul 21, 2026
93d52bf
fix(codegen): port transactional generation to current main
yyyyaaa Aug 20, 2026
2cfa6b8
fix(cli): activate agent protocol on current main
yyyyaaa Aug 20, 2026
8e65f2a
fix(graphql-server): keep cache tests type-resolvable
yyyyaaa Aug 20, 2026
13dd818
ci(cnc): cover protocol support packages
yyyyaaa Aug 20, 2026
b2b298a
fix(cli): preserve Node 18 release compatibility
yyyyaaa Aug 20, 2026
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
99 changes: 95 additions & 4 deletions .github/workflows/run-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ jobs:
- batch: uploads
packages: 'uploads/mime-bytes uploads/uuid-hash uploads/uuid-stream uploads/etag-hash uploads/etag-stream uploads/stream-to-etag uploads/content-type-stream uploads/upload-names uploads/s3-utils'
- batch: packages-core
packages: 'packages/url-domains packages/coerce packages/csrf packages/oauth packages/12factor-env packages/orm packages/express-context packages/errors packages/llm-env packages/node-type-registry packages/query-spec packages/server-utils postgres/pg-cache postgres/pg-env'
packages: 'packages/url-domains packages/coerce packages/csrf packages/oauth packages/12factor-env packages/orm packages/express-context packages/errors packages/llm-env packages/node-type-registry packages/query-spec packages/server-utils postgres/pg-cache postgres/pg-env pgpm/logger'
- batch: packages-services
packages: 'packages/postmaster packages/smtppostmaster packages/csv-to-pg packages/cli postgres/pgsql-client postgres/pg-ast'
packages: 'packages/postmaster packages/smtppostmaster packages/csv-to-pg packages/cli-runtime packages/cli postgres/pgsql-client postgres/pg-ast'
- batch: graphql
packages: 'graphql/query graphql/codegen'
packages: 'graphql/query graphql/codegen graphql/explorer'
- batch: graphile-unit
packages: 'graphile/graphile-plugin-utils graphile/graphile-realtime-subscriptions graphile/graphile-sql-expression-validator graphile/graphile-upload-plugin graphile/graphile-storage-registry'
packages: 'graphile/graphile-plugin-utils graphile/graphile-realtime-subscriptions graphile/graphile-sql-expression-validator graphile/graphile-upload-plugin graphile/graphile-storage-registry graphile/graphile-cache'
- batch: agentic
packages: 'agentic/protocol agentic/agentic-kit agentic/agent agentic/harness agentic/chat agentic/cli agentic/db-tools agentic/pi agentic/dsh agentic/react agentic/agentic-server agentic/anthropic agentic/openai agentic/ollama agentic/run-log agentic/metering'
- batch: pgpm-unit
Expand Down Expand Up @@ -150,6 +150,97 @@ jobs:
echo "::endgroup::"
done

# Pack once from the Node 22 build, then exercise the published package
# boundary on every supported Node line. This intentionally installs the
# tarballs in an empty project so workspace links cannot mask packaging bugs.
cli-package-artifacts:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download workspace
uses: actions/download-artifact@v4
with:
name: workspace-build

- name: Extract workspace
run: tar -xzf workspace.tar.gz && rm workspace.tar.gz

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Pack CNC release set
run: |
mkdir -p /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/cli-runtime pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/graphql-codegen pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/graphql-explorer pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/graphql-query pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/graphql-server pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/express-context pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @pgpmjs/env pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @pgpmjs/logger pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @pgpmjs/server-utils pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter pg-cache pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter pg-env pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter graphile-cache pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter graphile-bucket-provisioner-plugin pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter graphile-schema pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter graphile-settings pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter graphile-presigned-url-plugin pack --pack-destination /tmp/cnc-package-artifacts
npm_config_ignore_scripts=true pnpm --filter @constructive-io/cli pack --pack-destination /tmp/cnc-package-artifacts
cp packages/cli/scripts/verify-packed-artifacts.mjs /tmp/cnc-package-artifacts/
cp packages/cli/scripts/packed-acceptance-support.mjs /tmp/cnc-package-artifacts/

- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: cnc-package-artifacts
path: /tmp/cnc-package-artifacts
retention-days: 1

cli-package-matrix:
needs: cli-package-artifacts
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- node-version: '18.17.0'
suite: core
- node-version: '20'
suite: core
- node-version: '22'
suite: core
- node-version: '22.19.0'
suite: full
steps:
- name: Download package artifacts
uses: actions/download-artifact@v4
with:
name: cnc-package-artifacts
path: cnc-package-artifacts

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Verify installed package entrypoints
run: node cnc-package-artifacts/verify-packed-artifacts.mjs --artifacts cnc-package-artifacts --suite ${{ matrix.suite }}

# =========================================================================
# TIER 1b – Windows compatibility (native runner, no Linux containers)
#
Expand Down
1 change: 1 addition & 0 deletions graphile/graphile-bucket-provisioner-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export { BucketProvisionerPlugin, createBucketProvisionerPlugin } from './plugin
export { BucketProvisionerPreset } from './preset';
export type {
BucketAccessType,
AllowedOriginsOrGetter,
BucketNameResolver,
BucketProvisionerPluginOptions,
ConnectionConfigOrGetter,
Expand Down
4 changes: 2 additions & 2 deletions graphile/graphile-bucket-provisioner-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,15 @@ async function resolveDatabaseId(pgClient: any): Promise<string | null> {
function resolveAllowedOrigins(
bucketOrigins: string[] | null | undefined,
storageModuleOrigins: string[] | null | undefined,
pluginOrigins: string[],
pluginOrigins: string[] | (() => string[]),
): string[] {
if (bucketOrigins && bucketOrigins.length > 0) {
return bucketOrigins;
}
if (storageModuleOrigins && storageModuleOrigins.length > 0) {
return storageModuleOrigins;
}
return pluginOrigins;
return typeof pluginOrigins === 'function' ? pluginOrigins() : pluginOrigins;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion graphile/graphile-bucket-provisioner-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export type ConnectionConfigOrGetter =
| StorageConnectionConfig
| (() => StorageConnectionConfig);

/** CORS origins or a runtime-scoped getter for them. */
export type AllowedOriginsOrGetter = string[] | (() => string[]);

/**
* Function to derive the actual S3 bucket name from a logical bucket key.
*
Expand All @@ -51,7 +54,7 @@ export interface BucketProvisionerPluginOptions {
* These are the domains where your app runs (e.g., ["https://app.example.com"]).
* Required for browser-based presigned URL uploads.
*/
allowedOrigins: string[];
allowedOrigins: AllowedOriginsOrGetter;

/**
* Optional prefix for S3 bucket names.
Expand Down
114 changes: 114 additions & 0 deletions graphile/graphile-cache/src/__tests__/ownership.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Express } from 'express';
import type { GrafservBase } from 'grafserv';
import type { Server as HttpServer } from 'node:http';
import type { Pool } from 'pg';
import type { PostGraphileInstance } from 'postgraphile';
import { PgPoolCacheManager } from 'pg-cache';

import {
type GraphileCacheEntry,
GraphileCacheManager,
} from '../graphile-cache';

const createEntry = (
cacheKey: string,
release: () => Promise<void> = async () => {},
pgPoolKey?: string
): GraphileCacheEntry =>
({
cacheKey,
pgPoolKey,
createdAt: Date.now(),
pgl: { release } as unknown as PostGraphileInstance,
serv: {} as GrafservBase,
handler: {} as Express,
httpServer: { listening: false } as HttpServer,
realtimeManager: null,
}) as GraphileCacheEntry;

const createPool = (): Pool => {
let ended = false;
return {
get ended() {
return ended;
},
end: jest.fn(async () => {
ended = true;
}),
} as unknown as Pool;
};

describe('GraphileCacheManager ownership', () => {
it('closing one cache does not release entries owned by another cache', async () => {
const firstPg = new PgPoolCacheManager(undefined, {});
const secondPg = new PgPoolCacheManager(undefined, {});
const first = new GraphileCacheManager({ pgCache: firstPg });
const second = new GraphileCacheManager({ pgCache: secondPg });
const releaseFirst = jest.fn(async () => {});
const releaseSecond = jest.fn(async () => {});

first.set('shared-key', createEntry('shared-key', releaseFirst));
second.set('shared-key', createEntry('shared-key', releaseSecond));

await first.close();

expect(releaseFirst).toHaveBeenCalledTimes(1);
expect(releaseSecond).not.toHaveBeenCalled();
expect(second.has('shared-key')).toBe(true);

await second.close();
await Promise.all([firstPg.close(), secondPg.close()]);
});

it('releases a Graphile instance before its backing pool ends', async () => {
const pgCache = new PgPoolCacheManager(undefined, {});
const cache = new GraphileCacheManager({ pgCache });
const pool = createPool();
let finishRelease!: () => void;
const releaseGate = new Promise<void>((resolve) => {
finishRelease = resolve;
});
const release = jest.fn(() => releaseGate);

pgCache.set('pool-a', pool);
cache.set('api-a', createEntry('api-a', release, 'pool-a'));

const closePromise = pgCache.close();
await Promise.resolve();
await Promise.resolve();

expect(release).toHaveBeenCalledTimes(1);
expect(pool.end).not.toHaveBeenCalled();

finishRelease();
await closePromise;

expect(pool.end).toHaveBeenCalledTimes(1);
expect(cache.has('api-a')).toBe(false);
});

it('tracks asynchronous LRU disposal until release completes', async () => {
const pgCache = new PgPoolCacheManager(undefined, {});
const cache = new GraphileCacheManager({
pgCache,
config: { max: 1, ttl: 60_000 },
});
let finishRelease!: () => void;
const releaseGate = new Promise<void>((resolve) => {
finishRelease = resolve;
});
const release = jest.fn(() => releaseGate);

cache.set('first', createEntry('first', release));
cache.set('second', createEntry('second'));

const disposalPromise = cache.waitForDisposals();
await Promise.resolve();
expect(release).toHaveBeenCalledTimes(1);

finishRelease();
await disposalPromise;
await cache.close();
await pgCache.close();
});
});
21 changes: 16 additions & 5 deletions graphile/graphile-cache/src/create-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const log = new Logger('graphile-cache:create');
interface GraphileInstanceOptions {
preset: any;
cacheKey: string;
/** Exact key used by the owning pg-cache manager. */
pgPoolKey?: string;
/**
* When true, a RealtimeManager is created and started alongside the
* PostGraphile instance. The pool is extracted from the preset's
Expand Down Expand Up @@ -39,7 +41,7 @@ interface GraphileInstanceOptions {
export const createGraphileInstance = async (
opts: GraphileInstanceOptions
): Promise<GraphileCacheEntry> => {
const { preset, cacheKey, enableRealtime = false } = opts;
const { preset, cacheKey, pgPoolKey, enableRealtime = false } = opts;

const pgl = postgraphile(preset);
const serv = pgl.createServ(grafserv);
Expand All @@ -55,12 +57,14 @@ export const createGraphileInstance = async (
handler,
httpServer,
cacheKey,
...(pgPoolKey ? { pgPoolKey } : {}),
createdAt: Date.now(),
};

if (enableRealtime) {
try {
const { RealtimeManager } = await import('graphile-realtime-subscriptions');
const { RealtimeManager } =
await import('graphile-realtime-subscriptions');

// Extract PgSubscriber and pool from the resolved preset's pgServices.
// The pool is the same instance managed by pg-cache (via getPgPool)
Expand All @@ -71,9 +75,13 @@ export const createGraphileInstance = async (
const pool = pgService?.adaptorSettings?.pool ?? null;

if (!pgSubscriber) {
log.warn(`PostGraphile[${cacheKey}] has no pgSubscriber — RealtimeManager will not be started`);
log.warn(
`PostGraphile[${cacheKey}] has no pgSubscriber — RealtimeManager will not be started`
);
} else if (!pool) {
log.warn(`PostGraphile[${cacheKey}] has no pool in pgService — RealtimeManager will not be started`);
log.warn(
`PostGraphile[${cacheKey}] has no pool in pgService — RealtimeManager will not be started`
);
} else {
const manager = new RealtimeManager({
pgSubscriber,
Expand All @@ -87,7 +95,10 @@ export const createGraphileInstance = async (
log.info(`RealtimeManager started for PostGraphile[${cacheKey}]`);
}
} catch (err) {
log.error(`Failed to start RealtimeManager for PostGraphile[${cacheKey}]:`, err);
log.error(
`Failed to start RealtimeManager for PostGraphile[${cacheKey}]:`,
err
);
}
}

Expand Down
Loading