Skip to content
Open
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
14 changes: 12 additions & 2 deletions packages/deploy-cloudflare/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export type CloudflareTriggerConfig = Readonly<{
settingsHash?: string;
/** Explicit Worker-wide observability settings, published after version activation. */
observability?: Readonly<Record<string, unknown>>;
/** Explicit Worker-wide Logpush setting, published after version activation. */
logpush?: boolean;
workers_dev: boolean;
preview_urls: boolean;
routes: ReadonlyArray<unknown>;
Expand Down Expand Up @@ -208,7 +210,11 @@ export function readCloudflareTriggerConfig(
const config = parseRootWranglerConfig(configPath);
// Build tooling can supply defaults; only authored settings may change the live Worker.
const source = sourceConfigPath ? parseRootWranglerConfig(sourceConfigPath) : config;
const result = triggerConfigFromWrangler({ ...config, observability: source.observability });
const result = triggerConfigFromWrangler({
...config,
observability: source.observability,
logpush: source.logpush,
});
if (!sourceConfigPath) {
return result;
}
Expand Down Expand Up @@ -597,6 +603,7 @@ function triggerConfigFromWrangler(config: Record<string, unknown>): CloudflareT
...(config.preview_urls === undefined ? { preservePreviewUrls: true } : {}),
settingsHash: nonVersionedSettingsHash(config),
...(isRecord(config.observability) ? { observability: config.observability } : {}),
...(typeof config.logpush === 'boolean' ? { logpush: config.logpush } : {}),
workers_dev: workersDev,
preview_urls: typeof config.preview_urls === 'boolean' ? config.preview_urls : workersDev,
routes,
Expand Down Expand Up @@ -639,7 +646,7 @@ function parseTriggerConfig(value: unknown): CloudflareTriggerConfig | null {
return null;
}
if (
['preserve', 'preserveRoutes', 'preserveWorkersDev', 'preservePreviewUrls'].some(
['preserve', 'preserveRoutes', 'preserveWorkersDev', 'preservePreviewUrls', 'logpush'].some(
(key) => value[key] !== undefined && typeof value[key] !== 'boolean',
) ||
(value.settingsHash !== undefined && typeof value.settingsHash !== 'string') ||
Expand All @@ -661,6 +668,7 @@ function parseTriggerConfig(value: unknown): CloudflareTriggerConfig | null {
...(value.preservePreviewUrls === true ? { preservePreviewUrls: true } : {}),
...(typeof value.settingsHash === 'string' ? { settingsHash: value.settingsHash } : {}),
...(isRecord(value.observability) ? { observability: value.observability } : {}),
...(typeof value.logpush === 'boolean' ? { logpush: value.logpush } : {}),
workers_dev: value.workers_dev,
preview_urls: value.preview_urls,
routes: value.routes,
Expand All @@ -685,6 +693,7 @@ function triggerWranglerConfig(
...(triggerConfig.observability === undefined
? {}
: { observability: triggerConfig.observability }),
...(triggerConfig.logpush === undefined ? {} : { logpush: triggerConfig.logpush }),
triggers,
queues,
workflows: [...triggerConfig.workflows],
Expand Down Expand Up @@ -722,6 +731,7 @@ function resolvePreservedTraffic(
...(desired.preserve ? { preserve: true } : {}),
...(desired.settingsHash ? { settingsHash: desired.settingsHash } : {}),
...(desired.observability === undefined ? {} : { observability: desired.observability }),
...(desired.logpush === undefined ? {} : { logpush: desired.logpush }),
workers_dev: desired.preserveWorkersDev
? (previous?.workers_dev ?? desired.workers_dev)
: desired.workers_dev,
Expand Down
53 changes: 53 additions & 0 deletions packages/deploy-cloudflare/test/unit/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,55 @@ describe('Cloudflare version activation', () => {
}
});

it.each([false, true])(
'carries explicit Logpush through the version snapshot and trigger config: %s',
async (logpush) => {
const directory = mkdtempSync(join(tmpdir(), 'void-version-logpush-'));
const configPath = join(directory, 'wrangler.json');
const sourcePath = join(directory, 'source.json');
try {
writeFileSync(configPath, JSON.stringify({ name: 'app', logpush }));
const snapshot = readCloudflareTriggerConfig(configPath);
expect(snapshot.logpush).toBe(logpush);
expect(
parseCloudflareVersionMessage(encodeCloudflareVersionMessage([], snapshot)),
).toMatchObject({
triggerConfig: { logpush },
});
let applied: Record<string, unknown> | undefined;
const publish = (triggerConfig: CloudflareTriggerConfig) =>
deployCloudflareVersionTriggers({
runWrangler: operationFixture(async (args) => {
applied = readConfigArg(args);
return ok();
}),
workerName: 'app',
configPath,
triggerConfig,
});
await publish(snapshot);
expect(applied?.logpush).toBe(logpush);
writeFileSync(sourcePath, JSON.stringify({ name: 'app' }));
const omitted = readCloudflareTriggerConfig(configPath, sourcePath);
expect(omitted).not.toHaveProperty('logpush');
await publish(omitted);
expect(applied).not.toHaveProperty('logpush');
writeFileSync(sourcePath, JSON.stringify({ name: 'app', logpush: !logpush }));
expect(readCloudflareTriggerConfig(configPath, sourcePath).logpush).toBe(!logpush);
applied = undefined;
await publish({ ...snapshot, preserve: true });
expect(applied).toBeUndefined();
expect(
parseCloudflareVersionMessage(
`void:${JSON.stringify({ schema: 2, migrations: [], triggerConfig: { ...snapshot, logpush: 'false' } })}`,
),
).toBeNull();
} finally {
rmSync(directory, { recursive: true, force: true });
}
},
);

it('carries Email Routing addresses through the trigger snapshot and the trigger config', async () => {
const directory = mkdtempSync(join(tmpdir(), 'void-version-addresses-'));
const configPath = join(directory, 'wrangler.json');
Expand Down Expand Up @@ -2919,6 +2968,7 @@ describe('Cloudflare version activation', () => {
routes: [{ pattern: 'old.example.com', custom_domain: true }],
crons: ['0 * * * *'],
observability: { enabled: false },
logpush: true,
});
const liveTraffic = {
workers_dev: false,
Expand Down Expand Up @@ -2959,6 +3009,7 @@ describe('Cloudflare version activation', () => {
preservePreviewUrls: true,
crons: ['*/10 * * * *'],
observability: { enabled: true, traces: { enabled: true } },
logpush: false,
}),
fetch: async () => Response.json({ ready: true }),
});
Expand All @@ -2968,11 +3019,13 @@ describe('Cloudflare version activation', () => {
...liveTraffic,
triggers: { crons: ['*/10 * * * *'] },
observability: { enabled: true, traces: { enabled: true } },
logpush: false,
});
expect(publications[1]).toMatchObject({
...liveTraffic,
triggers: { crons: ['0 * * * *'] },
observability: { enabled: false },
logpush: true,
});
expect(publications[1].queues).toEqual({ consumers: [] });
});
Expand Down
2 changes: 1 addition & 1 deletion packages/void/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ generated entries must preserve the module-boundary rules below.
- **Direct deploy package boundary**: target-neutral intents, artifacts, resources, migrations, and Durable Object plans live in public `@void/deploy-core`; root Wrangler config parsing and verified Cloudflare upload primitives live in public `@void/deploy-cloudflare`. The CLI consumes both packages directly. Hosted platform packages consume `@void/deploy-core` and must not be imported by public SDK packages.
- **Direct activation invariant**: native Cloudflare deploy uploads a Worker Version without traffic, records the validated migration filenames in its annotation, probes that version directly, activates it at 100%, and only then synchronizes triggers. A trigger-sync failure restores the prior active version. Database migrations are forward-only; rollback warns before activating code whose recorded migration set is older or unknown.

Worker-wide observability requires a separate settings update after activation. Replay only the target version's explicit observability snapshot during trigger synchronization and rollback; omitted settings and binding-preserving handoffs retain the live configuration.
Worker-wide Logpush and observability require a separate settings update after activation. Replay only the target version's explicit settings snapshot during trigger synchronization and rollback; omitted settings and binding-preserving handoffs skip that settings update.

- **Platform Worker reuse**: An installed platform may reuse an active immutable Worker Version only when its annotation proves the complete upload input matches: Worker config, managed routes, runtime files and assets, and the pinned uploader toolchain. Secret updates always upload. Reused versions still reconcile mutable triggers and traffic, check live version lineage, and fail closed on drift or incomplete proof.
- **Edge-routing ownership**: managed builds set `VOID_DEPLOY_PROJECT_ID` and omit the generated redirects, rewrites, headers, and fallbacks because platform dispatch applies them. Native Cloudflare, local dev, and non-CF builds compile the same ordered rules into the application runtime. Only managed bundles may trust dispatch-authored `X-Void-Original-URL` metadata.
Expand Down
5 changes: 3 additions & 2 deletions packages/void/src/cli/cf-version-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,8 +1149,9 @@ export async function uploadPinnedCloudflareVersion(options: {
'limits',
'placement',
'cache',
// Worker-level settings that version uploads cannot set. Observability is
// published separately during post-activation trigger synchronization.
// Worker-level settings that version uploads cannot set. Observability and
// Logpush are published separately during post-activation trigger
// synchronization; tail consumers are not applied.
'logpush',
'tailConsumers',
]).has(key),
Expand Down
10 changes: 7 additions & 3 deletions packages/void/src/cli/cf-version-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
uploadPinnedCloudflareVersion,
type CfCommand,
} from './cf-version-api.ts';
import { publishCloudflareObservability } from './cf-worker-settings.ts';
import { publishCloudflareWorkerSettings } from './cf-worker-settings.ts';
import { isCleanCloudflareExit } from './cloudflare-process.ts';

function failed(message: string): CloudflareCommandResult {
Expand Down Expand Up @@ -780,7 +780,10 @@ export function createCfVersionRunner(options: {
command.capture,
directory,
);
if (isCleanCloudflareExit(deployed) && raw.observability !== undefined) {
if (
isCleanCloudflareExit(deployed) &&
(raw.observability !== undefined || raw.logpush !== undefined)
) {
const credentials = options.pinnedUpload;
if (!credentials || !credentials.verifyUploader()) {
return failed('The Cloudflare settings publisher is unavailable.');
Expand All @@ -790,11 +793,12 @@ export function createCfVersionRunner(options: {
if (!token || !credentials.verifyUploader()) {
return failed('Cloudflare session is unavailable for publishing Worker settings.');
}
await publishCloudflareObservability({
await publishCloudflareWorkerSettings({
apiBaseUrl: credentials.apiBaseUrl,
accountId: credentials.accountId,
workerName,
observability: raw.observability,
logpush: raw.logpush,
headers: { Authorization: `Bearer ${token}` },
});
} catch (cause) {
Expand Down
21 changes: 16 additions & 5 deletions packages/void/src/cli/cf-worker-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ import { isRecord } from '@void/deploy-core';
import { getVoidUserAgent } from './cf-user-agent.ts';

/** Version uploads leave Worker-wide settings untouched; publish only explicit configuration. */
export async function publishCloudflareObservability(options: {
export async function publishCloudflareWorkerSettings(options: {
apiBaseUrl: string;
accountId: string;
workerName: string;
observability: unknown;
observability?: unknown;
logpush?: unknown;
headers: Record<string, string>;
}): Promise<void> {
if (!isRecord(options.observability)) {
if (options.observability !== undefined && !isRecord(options.observability)) {
throw new Error('The Cloudflare Worker observability settings were not readable.');
}
if (options.logpush !== undefined && typeof options.logpush !== 'boolean') {
throw new Error('The Cloudflare Worker Logpush setting was not readable.');
}
const settings = {
...(options.observability === undefined ? {} : { observability: options.observability }),
...(options.logpush === undefined ? {} : { logpush: options.logpush }),
};
if (Object.keys(settings).length === 0) {
return;
}
const endpoint = `${options.apiBaseUrl.replace(/\/$/, '')}/accounts/${encodeURIComponent(options.accountId)}/workers/scripts/${encodeURIComponent(options.workerName)}/script-settings`;
const response = await fetch(endpoint, {
method: 'PATCH',
Expand All @@ -20,12 +31,12 @@ export async function publishCloudflareObservability(options: {
...options.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({ observability: options.observability }),
body: JSON.stringify(settings),
redirect: 'error',
signal: AbortSignal.timeout(30_000),
});
const body: unknown = await response.json();
if (!response.ok || !isRecord(body) || body.success !== true) {
throw new Error(`Cloudflare did not update Worker observability (HTTP ${response.status}).`);
throw new Error(`Cloudflare did not update Worker settings (HTTP ${response.status}).`);
}
}
7 changes: 4 additions & 3 deletions packages/void/src/cli/cloudflare-operation-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from './cf-command-values.ts';
import { persistCfKeyringPreference, readCfSessionCredential } from './cf-session.ts';
import { getCfCliVersion, getVoidUserAgent } from './cf-user-agent.ts';
import { publishCloudflareObservability } from './cf-worker-settings.ts';
import { publishCloudflareWorkerSettings } from './cf-worker-settings.ts';
import {
cloudflareDeviceCode,
cloudflareVerificationUrl,
Expand Down Expand Up @@ -1136,12 +1136,13 @@ async function workerChanges(): Promise<Result> {
const deployed = await withBuildOutput(raw, null, (directory) =>
runCf(['workers', 'triggers', 'deploy', '--prebuilt'], directory),
);
if (deployed.status === 0 && raw.observability !== undefined) {
await publishCloudflareObservability({
if (deployed.status === 0 && (raw.observability !== undefined || raw.logpush !== undefined)) {
await publishCloudflareWorkerSettings({
apiBaseUrl: apiBaseUrl(),
accountId: accountId(),
workerName: name,
observability: raw.observability,
logpush: raw.logpush,
headers: await apiHeaders(),
});
}
Expand Down
17 changes: 10 additions & 7 deletions packages/void/test/unit/cf-version-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,24 +341,27 @@ it(
// This exercises multiple fresh cf subprocesses. Windows CI has exceeded 30s
// across those startups; bound each child and give the complete protocol a
// separate Windows budget, including assertion and cleanup overhead.
it(
'uploads an ordinary API-origin revision with genuine cf reads and UUID-pinned inheritance',
async () => {
it.each([
{ observability: { enabled: true, logs: { enabled: true }, traces: { enabled: true } } },
{ logpush: false },
{ logpush: true },
])(
'uploads an API-origin revision and publishes its explicit Worker settings: %j',
async (settings) => {
const current = '11111111-1111-4111-8111-111111111111';
const next = '22222222-2222-4222-8222-222222222222';
const observability = { enabled: true, logs: { enabled: true }, traces: { enabled: true } };
const triggerConfig = {
workers_dev: true,
preview_urls: true,
routes: [],
crons: [],
queueConsumers: [],
workflows: [],
observability,
...settings,
};
const message = encodeCloudflareVersionMessage([], triggerConfig);
const prepared = await output({
observability,
...settings,
secrets: { required: ['AUTH_SECRET'] },
version_metadata: { binding: 'APP_VERSION' },
send_email: [
Expand Down Expand Up @@ -531,7 +534,7 @@ it(
{
method: 'PATCH',
path: '/client/v4/accounts/account-id/workers/scripts/void-test/script-settings',
body: { observability },
body: settings,
},
]);
expect(events.indexOf('activate')).toBeLessThan(events.indexOf('settings'));
Expand Down
Loading
Loading