Dashboard › craft › Session 1IcDjnCRwulq
1IcDjnCRwulqxvWEeYou are an adversarial senior code reviewer. Critically review a NEW Cloudflare release target added to the getsentry/craft repo (a TypeScript release-automation CLI). Be skeptical and thorough — find real bugs, security issues, inconsistencies with existing patterns, and missing edge cases. Do NOT rubber-stamp.
Files to review (read them in full):
extractZipArchiveWithFlattening)Reference/context files to compare against for pattern consistency:
Design intent (verify the code actually matches this):
deployType: 'pages' | 'worker' (default 'pages'). Pages requires projectName.wrangler pages deploy <dir> --project-name <name> --branch <productionBranch> --commit-hash <revision> --commit-message "Release <version>" --commit-dirty false. The --branch <productionBranch> (default 'main') forces a PRODUCTION deploy, not a preview. This is the Cloudflare environment selector, NOT a git branch.wrangler deploy run with cwd = deployDir.I'll conduct a thorough adversarial review. Let me start by reading all the files in full.
<path>/home/byk/Code/getsentry/craft/src/targets/cloudflare.ts</path>
<type>file</type>
<content>
1: import { join } from 'path';
2:
3: import {
4: GitHubGlobalConfig,
5: TargetConfig,
6: TypedTargetConfig,
7: } from '../schemas/project_config';
8: import { checkEnvForPrerequisite } from '../utils/env';
9: import { ConfigurationError, reportError } from '../utils/errors';
10: import { withTempDir } from '../utils/files';
11: import {
12: checkExecutableIsPresent,
13: extractZipArchiveWithFlattening,
14: resolveExecutable,
15: spawnProcess,
16: } from '../utils/system';
17: import { BaseTarget } from './base';
18: import { BaseArtifactProvider } from '../artifact_providers/base';
19:
20: /**
21: * Secrets required to authenticate with the Cloudflare API.
22: *
23: * Exported so tests (and documentation tooling) can reference the canonical
24: * list of environment variables this target consumes. ...
<path>/home/byk/Code/getsentry/craft/src/targets/__tests__/cloudflare.test.ts</path>
<type>file</type>
<content>
1: import { vi } from 'vitest';
2:
3: import { CloudflareTarget, targetSecrets } from '../cloudflare';
4: import { NoneArtifactProvider } from '../../artifact_providers/none';
5: import * as system from '../../utils/system';
6:
7: const TMP_DIR = '/tmp/craft-cloudflare-test';
8: const DEFAULT_SECRET_VALUE = 'secret_value';
9:
10: vi.mock('../../utils/system', async importOriginal => {
11: const actual = await importOriginal<typeof import('../../utils/system')>();
12: return {
13: ...actual,
14: checkExecutableIsPresent: vi.fn(),
15: spawnProcess: vi.fn(async () => undefined),
16: extractZipArchiveWithFlattening: vi.fn(async () => undefined),
17: };
18: });
19:
20: vi.mock('../../utils/files', async importOriginal => {
21: const actual = await importOriginal<typeof import('../../utils/files')>();
22: return {
23: ...actual,
24: ...
<path>/home/byk/Code/getsentry/craft/src/utils/system.ts</path>
<type>file</type>
<content>
1: import { spawn, SpawnOptions } from 'child_process';
2: import { createHash, Hash } from 'crypto';
3: import * as fs from 'fs';
4: import * as path from 'path';
5: import split from 'split';
6: import * as tar from 'tar';
7: import StreamZip from 'node-stream-zip';
8:
9: import { logger } from '../logger';
10:
11: import { reportError } from './errors';
12: import { isDryRun } from './helpers';
13: import { isInWorktreeMode } from './dryRun';
14: import { sanitizeSpawnEnv } from './dynamicLinkerEnv';
15:
16: /**
17: * Types of supported hashing algorithms
18: */
19: export enum HashAlgorithm {
20: /** SHA256 */
21: SHA256 = 'sha256',
22: /** SHA384 */
23: SHA384 = 'sha384',
24: /** SHA512 */
25: SHA512 = 'sha512',
26: }
27:
28: /**
29: * Types of supported digest formats
30: */
31: export enum HashOutputFormat {
32: /** Hex digest, consists of [0-9a-f] characters */
33: ...
<path>/home/byk/Code/getsentry/craft/src/targets/ghPages.ts</path>
<type>file</type>
<content>
1: import * as fs from 'fs';
2:
3: import { Octokit } from '@octokit/rest';
4:
5: import {
6: GitHubGlobalConfig,
7: TargetConfig,
8: TypedTargetConfig,
9: } from '../schemas/project_config';
10: import { ConfigurationError, reportError } from '../utils/errors';
11: import { withTempDir } from '../utils/files';
12: import {
13: getGitHubApiToken,
14: getGitHubClient,
15: GitHubRemote,
16: } from '../utils/githubApi';
17: import { cloneRepo } from '../utils/git';
18: import { extractZipArchiveWithFlattening } from '../utils/system';
19: import { BaseTarget } from './base';
20: import { BaseArtifactProvider } from '../artifact_providers/base';
21:
22: /** GH Pages target configuration fields */
23: interface GhPagesConfigFields extends Record<string, unknown> {
24: branch?: string;
25: githubOwner?: string;
26: githubRepo?: string;
27: }
28:
29: /**
30: * Regex for docs ...
Now let me read the remaining files and check git history for the ghPages refactor.
<path>/home/byk/Code/getsentry/craft/src/targets/index.ts</path>
<type>file</type>
<content>
1: import { BaseTarget } from './base';
2: import { BrewTarget } from './brew';
3: import { CloudflareTarget } from './cloudflare';
4: import { CocoapodsTarget } from './cocoapods';
5: import { CratesTarget } from './crates';
6: import { DockerTarget } from './docker';
7: import { GcsTarget } from './gcs';
8: import { GemTarget } from './gem';
9: import { GhPagesTarget } from './ghPages';
10: import { GitHubTarget } from './github';
11: import { NpmTarget } from './npm';
12: import { NugetTarget } from './nuget';
13: import { SentryPypiTarget } from './sentryPypi';
14: import { PypiTarget } from './pypi';
15: import { RegistryTarget } from './registry';
16: import { AwsLambdaLayerTarget } from './awsLambdaLayer';
17: import { UpmTarget } from './upm';
18: import { MavenTarget } from './maven';
19: import { SymbolCollector } from './symbolCollector';
20: import { PubDevTarget } from ...
<path>/home/byk/Code/getsentry/craft/Dockerfile</path>
<type>file</type>
<content>
1: FROM node:24.18.0-bookworm-slim AS builder
2:
3: WORKDIR /usr/local/lib
4:
5: # Install pnpm (version from Volta config in package.json)
6: COPY package.json ./
7: RUN npm install -g pnpm@$(node -p "require('./package.json').volta.pnpm")
8:
9: COPY package.json pnpm-lock.yaml ./
10: RUN pnpm install --frozen-lockfile --quiet
11:
12: COPY . .
13:
14: RUN \
15: NODE_ENV=production \
16: NODE_PATH=/usr/local/lib/node_modules \
17: PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/lib/node_modules/.bin" \
18: pnpm build
19:
20: FROM node:24.18.0-bookworm
21:
22: ENV DEBIAN_FRONTEND=noninteractive \
23: DOTNET_CLI_TELEMETRY_OPTOUT=1 \
24: # See https://github.com/CocoaPods/CocoaPods/issues/6795
25: COCOAPODS_ALLOW_ROOT=1 \
26: CARGO_HOME=/root/.cargo \
27: RUSTUP_HOME=/root/.rustup \
28: PATH=${PATH}:/root/.cargo/bin:/opt/flutter/bin:/venv/bin
29: ...
<path>/home/byk/Code/getsentry/craft/src/targets/pubDev.ts</path>
<type>file</type>
<content>
1: import {
2: constants,
3: existsSync,
4: promises as fsPromises,
5: readFileSync,
6: writeFileSync,
7: } from 'fs';
8: import { homedir, platform } from 'os';
9: import { join, dirname } from 'path';
10: import { load, dump } from 'js-yaml';
11: import { createGitClient } from '../utils/git';
12: import { BaseTarget } from './base';
13: import { BaseArtifactProvider } from '../artifact_providers/base';
14: import {
15: GitHubGlobalConfig,
16: TargetConfig,
17: TypedTargetConfig,
18: } from '../schemas/project_config';
19: import { forEachChained } from '../utils/async';
20: import { checkEnvForPrerequisite } from '../utils/env';
21: import { withTempDir } from '../utils/files';
22: import { checkExecutableIsPresent, spawnProcess } from '../utils/system';
23: import { isDryRun } from '../utils/helpers';
24: import { logDryRun } from '../utils/dryRun';
25: import { logger } ...
<path>/home/byk/Code/getsentry/craft/src/targets/crates.ts</path>
<type>file</type>
<content>
1: import * as fs from 'fs';
2: import * as path from 'path';
3:
4: import { createGitClient } from '../utils/git';
5:
6: import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config';
7: import { forEachChained, sleep, withRetry } from '../utils/async';
8: import { ConfigurationError } from '../utils/errors';
9: import { withTempDir } from '../utils/files';
10: import {
11: checkExecutableIsPresent,
12: resolveExecutable,
13: runWithExecutable,
14: spawnProcess,
15: } from '../utils/system';
16: import { BaseTarget } from './base';
17: import { BaseArtifactProvider } from '../artifact_providers/base';
18:
19: /** Cargo executable configuration */
20: const CARGO_CONFIG = {
21: name: 'cargo',
22: envVar: 'CARGO_BIN',
23: errorHint: 'Install cargo or define a custom preReleaseCommand in .craft.yml',
24: } as const;
25:
26: /** Resolved cargo binary path */
27: ...
Now let me check the git history for the ghPages refactor to verify the flatten semantics are preserved, and check the base target and errors util.
883f05b fix: replace extract-zip with node-stream-zip to fix Node 24.16.0 hang (#831)
2f55139 security(spawn): strip dynamic-linker env vars from subprocess env (#800)
4760b72 ci: Add TypeScript type checking to CI (#747)
d62ca92 feat: Automatic version bumping from configured targets (#707)
d01d144 build(deps-dev): bump tar from 6.2.1 to 7.5.3 (#724)
373929f feat(dry-run): Add worktree-based dry-run mode with real diff output (#692)
d01268a ref: Centralize dry-run logic with Proxy-based abstraction (#685)
f60aba9 feat(docker): Add support for multiple registries (#657)
---DIFF ghPages against master---
diff --git a/src/targets/ghPages.ts b/src/targets/ghPages.ts
index 2c5f791..cd4938a 100644
--- a/src/targets/ghPages.ts
+++ b/src/targets/ghPages.ts
@@ -1,5 +1,4 @@
import * as fs from 'fs';
-import * as path from 'path';
import { Octokit } from '@octokit/rest';
@@ -16,7 +15,7 @@ import {
GitHubRemote,
} from '../utils/githubApi';
import { cloneRepo } from ...
<path>/home/byk/Code/getsentry/craft/src/targets/base.ts</path>
<type>file</type>
<content>
1: import { logger as loggerRaw } from '../logger';
2: import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config';
3: import {
4: parseFilterOptions,
5: RawFilterOptions,
6: ParsedFilterOptions,
7: } from '../artifact_providers/base';
8: import { stringToRegexp } from '../utils/filters';
9: import {
10: BaseArtifactProvider,
11: RemoteArtifact,
12: } from '../artifact_providers/base';
13:
14: /**
15: * Base class for all remote targets
16: */
17: export class BaseTarget {
18: public readonly id: string;
19: protected readonly logger: typeof loggerRaw;
20: /** Artifact provider */
21: public readonly artifactProvider: BaseArtifactProvider;
22: /** Unparsed target configuration */
23: public readonly config: TargetConfig;
24: /** Artifact filtering options for the target */
25: public readonly filterOptions: ParsedFilterOptions;
26: /** GitHub repo ...
<path>/home/byk/Code/getsentry/craft/src/utils/errors.ts</path>
<type>file</type>
<content>
1: import { logger } from '../logger';
2: import { isDryRun } from './helpers';
3: import { captureException } from '@sentry/node';
4:
5: /**
6: * Custom error class that describes client configuration errors
7: */
8: export class ConfigurationError extends Error {
9: // We have to do the following because of: https://github.com/Microsoft/TypeScript/issues/13965
10: // Otherwise we cannot use instanceof later to catch a given type
11: /** Error prototype */
12: public __proto__: Error;
13:
14: public constructor(message?: string) {
15: const trueProto = new.target.prototype;
16: super(message);
17:
18: this.__proto__ = trueProto;
19: }
20: }
21:
22: /**
23: * Writes an error or message to "error" log if in dry-mode, throws an error
24: * otherwise
25: *
26: * @param error Error object or error message
27: * @param errorLogger Optional logger to use
28: */
29: ...
The ghPages refactor is verified — behavior is byte-for-byte identical (empty-dir check stays in ghPages, flatten moves to the shared util). Now let me check the env.ts for checkEnvForPrerequisite, the schema registration, docs, and run the compiler + tests.
<path>/home/byk/Code/getsentry/craft/src/utils/env.ts</path>
<type>file</type>
<content>
1: import { existsSync } from 'fs';
2: import { join } from 'path';
3: // XXX(BYK): This is to be able to spy on `homedir()` in tests
4: // TODO(BYK): Convert this to ES6 imports
5: import os = require('os');
6:
7: import { getConfigFileDir } from '../config';
8: import { ConfigurationError } from './errors';
9: import { logger } from '../logger';
10:
11: /**
12: * Legacy filename no longer read by Craft. Retained as a constant for the
13: * startup warning helper below.
14: */
15: const LEGACY_ENV_FILE_NAME = '.craft.env';
16:
17: /**
18: * A token, key, or other value which can be stored either in an env file or
19: * directly in the environment
20: */
21: export interface RequiredConfigVar {
22: /**
23: * The currently-preferred name of the variable, generally something in
24: * UPPER_SNAKE_CASE
25: */
26: name: string;
27: /** A deprecated (but still allowed) name for ...
No files found
No files found
Targets aren't individually enumerated in schemas — let me verify how the schema handles target config (to confirm no schema change needed), then run the compiler and tests.
project_config.ts --- 54:export const TargetConfigSchema = z 63:export type TargetConfig = z.infer<typeof TargetConfigSchema>; 67: * Combines base TargetConfig fields with target-specific fields. 74: * const config = this.config as TypedTargetConfig<BrewConfigFields>; 76:export type TypedTargetConfig<T extends Record<string, unknown>> = 77: TargetConfig & T; 172: targets: z.array(TargetConfigSchema).optional(),
EXIT: 0
TypeScript compiles clean. Let me check the schema more closely (does TargetConfigSchema allow arbitrary keys?), and run the tests.
<path>/home/byk/Code/getsentry/craft/src/schemas/project_config.ts</path>
<type>file</type>
<content>
54: export const TargetConfigSchema = z
55: .object({
56: name: z.string(),
57: id: z.string().optional(),
58: includeNames: z.string().optional(),
59: excludeNames: z.string().optional(),
60: })
61: .passthrough(); // Allow additional properties for target-specific config
62:
63: export type TargetConfig = z.infer<typeof TargetConfigSchema>;
64:
65: /**
66: * Utility type for strongly-typed target configurations.
67: * Combines base TargetConfig fields with target-specific fields.
68: *
69: * @example
70: * interface BrewConfigFields {
71: * tap?: string;
72: * template: string;
73: * }
74: * const config = this.config as TypedTargetConfig<BrewConfigFields>;
75: */
76: export type TypedTargetConfig<T extends Record<string, unknown>> =
77: TargetConfig & T;
78:
79: /**
80: * Which service should be used for status checks
81: */
82: export ...
> @sentry/craft@2.27.0-dev.0 test /home/byk/Code/getsentry/craft
> vitest run src/targets/__tests__/cloudflare.test.ts
RUN v4.1.8 /home/byk/Code/getsentry/craft
Test Files 1 passed (1)
Tests 15 passed (15)
Start at 14:52:07
Duration 1.40s (transform 381ms, setup 0ms, import 1.14s, tests 38ms, environment 0ms)
EXIT: 0
Both pass. Now let me investigate the critical concerns more deeply. First, the secret expansion via replaceEnvVariable vulnerability and the dry-run question. Let me check helpers.ts (isDryRun) and how spawnProcess handles env expansion with secrets, plus check the docs for the target.
<path>/home/byk/Code/getsentry/craft/src/utils/helpers.ts</path>
<type>file</type>
<content>
1: import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
2: import path from 'path';
3:
4: import prompts from 'prompts';
5: import { logger, LogLevel, setLevel } from '../logger';
6:
7: /**
8: * Maximum size (in bytes) for step output values passed through GITHUB_OUTPUT.
9: * Values exceeding this are truncated to avoid E2BIG errors when GitHub Actions
10: * expands them into environment variables for subsequent steps.
11: *
12: * 64 KB is well under the ~2 MB ARG_MAX kernel limit and also under GitHub's
13: * ~65 536-character issue body limit, so truncated changelogs still render.
14: */
15: export const MAX_STEP_OUTPUT_BYTES = 64 * 1024;
16:
17: const FALSY_ENV_VALUES = new Set(['', 'undefined', 'null', '0', 'false', 'no']);
18: export function envToBool(envVar: unknown): boolean {
19: const normalized = String(envVar).toLowerCase();
20: return ...
configuration.md contributing.md getting-started.md github-actions.md index.mdx targets --- docs/.astro/collections/docs.schema.json docs/dist/configuration/index.html docs/dist/contributing/index.html docs/dist/getting-started/index.html docs/dist/github-actions/index.html docs/dist/targets/index.html docs/dist/targets/aws-lambda-layer/index.html docs/dist/targets/brew/index.html docs/dist/targets/cloudflare/index.html docs/dist/targets/cocoapods/index.html ---git status--- M .lore.md M Dockerfile M docs/src/content/docs/targets/index.md M src/targets/ghPages.ts M src/targets/index.ts M src/utils/system.ts ?? .craft-issue-842-plan.md ?? .opencode/ ?? docs/src/content/docs/targets/cloudflare.md ?? src/targets/__tests__/cloudflare.test.ts ?? src/targets/cloudflare.ts
There's a docs file. Let me read it and the docs index change to verify docs consistency, and confirm the --commit-dirty false flag semantics against wrangler.
<path>/home/byk/Code/getsentry/craft/docs/src/content/docs/targets/cloudflare.md</path> <type>file</type> <content> 1: --- 2: title: Cloudflare 3: description: Deploy static sites or Workers to Cloudflare 4: --- 5: 6: Deploys a release artifact to Cloudflare, either as a [Cloudflare Pages](https://developers.cloudflare.com/pages/) site or as a [Cloudflare Worker](https://developers.cloudflare.com/workers/) with static assets. 7: 8: The target extracts a ZIP artifact and shells out to the [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) CLI to perform the deployment. `wrangler` is bundled in the Craft Docker image. 9: 10: ## Configuration 11: 12: | Option | Description | 13: |--------|-------------| 14: | `deployType` | `pages` (default) or `worker`. | 15: | `projectName` | Cloudflare Pages project name. **Required** when `deployType` is `pages`. | 16: | `productionBranch` | The Pages project's production branch name. ...
Now I have a complete picture. Let me verify two remaining technical concerns: (1) does the docs index list cloudflare, and (2) the --commit-dirty false — wrangler expects --commit-dirty as a boolean flag; passing false as a separate arg. Let me confirm the wrangler CLI arg format and check the dry-run behavior of the withTempDir/extraction path.
diff --git a/docs/src/content/docs/targets/index.md b/docs/src/content/docs/targets/index.md
index 13758a2..8218e9c 100644
--- a/docs/src/content/docs/targets/index.md
+++ b/docs/src/content/docs/targets/index.md
@@ -18,6 +18,7 @@ Targets define where Craft publishes your release artifacts. Configure them in `
| [Homebrew](./brew/) | Update Homebrew formulas |
| [GCS](./gcs/) | Upload to Google Cloud Storage |
| [GitHub Pages](./gh-pages/) | Deploy static sites |
+| [Cloudflare](./cloudflare/) | Deploy static sites or Workers to Cloudflare |
| [CocoaPods](./cocoapods/) | Publish iOS/macOS pods |
| [Ruby Gems](./gem/) | Publish Ruby gems |
| [Maven](./maven/) | Publish to Maven Central |
---withTempDir---
28:export async function scan(
57:export async function listFiles(directory: string): Promise<string[]> {
77:export async function withTempDir<T>(
113:export async function withTempFile<T>(
136:export function detectContentType(artifactName: string): string | undefined ...
<path>/home/byk/Code/getsentry/craft/src/utils/files.ts</path>
<type>file</type>
<content>
77: export async function withTempDir<T>(
78: callback: (arg: string) => T | Promise<T>,
79: cleanup = true,
80: prefix = 'craft-',
81: ): Promise<T> {
82: const directory = await mkdtemp(path.join(os.tmpdir(), prefix));
83: try {
84: return await callback(directory);
85: } finally {
86: if (cleanup) {
87: fs.rm(directory, { recursive: true, force: true }, err => {
88: // XXX(BYK): intentionally DO NOT await unlinking as we do not want
89: // to block (both in terms of waiting for IO and the success of the
90: // operation) finishing the task at hand. If unlinking fails, we honestly
91: // don't care as this is already a temporary file and will be removed
92: // eventually by the OS. And it doesn't make sense to wait until this op
93: // finishes then as nothing relies on the removal of this file. ...
I now have everything I need. Let me compile my findings.
Build & test verification:
npx tsc --noEmit -p tsconfig.build.json → PASS (exit 0)pnpm test src/targets/__tests__/cloudflare.test.ts → PASS (15/15)The code compiles and all tests are green, but green tests hide several real problems. Findings below, prioritized.
C1. Dry-run mode still performs a REAL production deploy in worktree mode — and the design brief's assumption is wrong.
cloudflare.ts:261 calls spawnProcess whose dry-run guard is isDryRun() && !enableInDryRunMode && !isInWorktreeMode() (system.ts:140-144). The brief claims "the deploy is actually skipped in dry-run." That is only true in plain dry-run. In worktree dry-run mode (isInWorktreeMode() true — which craft enables for its dry-run diff feature, commit 373929f), spawnProcess runs the command for real. For git/gcs this is safe because worktree mode isolates them to a local scratch clone. But wrangler pages deploy / wrangler deploy talk to the Cloudflare production API — there is no local isolation. Worktree mode will push a live production deploy while the user believes they are doing a dry run.
Every other target that shells out to a network-publishing command either (a) has no worktree concept because it's git-only, or (b) guards explicitly. pubDev.ts:263 and crates.ts rely on the command's own semantics. Here nothing stops a real deploy.
Suggested fix: guard the deploy explicitly, e.g. at cloudflare.ts:258:
if (isDryRun()) {
logDryRun(`wrangler ${args.join(' ')}`);
return;
}
(mirror pubDev.ts:180/logDryRun), or pass a flag that forces skipping even in worktree mode. This is the single most important fix — it can cause an unintended production deployment. **Confirm the correct behavior with the intended design; per AGENTS.md dry-run rules a network publish must never run in ...
* 🔴 (14:51) User stated: replace extract-zip with node-stream-zip to fix Node 24. * 🔴 (14:52) User stated: always targets the **production** environment. * 🔴 (14:52) User stated: never on the command line. * 🔴 [requested-tests] (14:56) User asked for tests after implementing Cloudflare target. * 🔴 (14:57) Cloudflare target has two deploy modes: "pages" and "worker". * 🔴 (14:58) Cloudflare p…
* 🔴 (14:51) User stated always be consistent. * 🟡 (14:53) User asked about dry-run behavior for Cloudflare target. * 🟡 (14:54) User inquired about secret handling in Cloudflare target. * 🔴 (14:55) Cloudflare target requires WRANGLER_BIN env var or CLI install. * 🔴 (14:55) Cloudflare target uses wrangler binary for deployments. * 🟡 (14:56) User requested tests for Cloudflare target implement…
* 🔴 (14:51) User stated always targets the production environment for Cloudflare deployments. * 🔴 (14:51) User stated switch to branch "${branch}" for deployments. * 🔴 [tool-requested] (14:51) User requested code review of Cloudflare target. * 🔴 (14:52) Cloudflare target configuration includes deployType: 'pages' | 'worker'. * 🔴 (14:52) Cloudflare target uses secrets CLOUDFLARE_API_TOKEN and…
<observations> * 🟡 (14:50) User requested a thorough code review of a new Cloudflare release target added to the getsentry/craft repo, specifically asking to scrutinize dry-run correctness, secret leakage, config validation edge cases, and test quality. * 🔴 (14:50) Files to review: - /home/byk/Code/getsentry/craft/src/targets/cloudflare.ts - /home/byk/Code/getsentry/craft/src/targets/__te…