Dashboard › craft › Session 1PkNcoT4cJuI
1PkNcoT4cJuIpMo2MResearch the getsentry/craft codebase (working dir /home/byk/Code/getsentry/craft, branch feat/prefixed-tags) to inform a design for native "workspaces" support — letting ONE repository release multiple independently-versioned products (e.g. cli@1.2.3, mcp@2.0.0) ideally from a SINGLE top-level .craft.yml, with a product/workspace selector.
I need a precise, file:line-cited map of every place that would need to become workspace-aware. Do NOT write code. Read the actual source.
Investigate and report on:
Config schema & loading (src/config.ts, src/schemas/project_config.ts):
targets, github (global), releaseBranchPrefix, changelog, versioning, minVersion live.getConfiguration(), loadConfigurationFromString(), getConfigFileDir(), findConfigFile(), getGlobalGitHubConfig(), getGitTagPrefix(), getVersioningPolicy() — signatures + what they read from the cached config..passthrough() extension points?The prepare command (src/commands/prepare.ts): full flow — how it computes the new version (auto/calver/explicit), reads the latest tag, creates the release branch (releaseBranchPrefix), writes the changelog, commits, pushes. Every call to getGitTagPrefix/getLatestTag/getVersioningPolicy/getGlobalGitHubConfig. Where a "which product" selector would have to enter.
The publish command (src/commands/publish.ts): full flow — how it finds the release branch, resolves the version, iterates targets, and the publish-issue / publish_repo mechanism. How targets are instantiated and run. How the github target creates the tag/release (versionToTag, tagPrefix, isLatestRelease, make_latest). Cite the publish-issue polling/label flow if present.
...
I'll research the craft codebase systematically. Let me start by ensuring I'm on the right branch and getting an overview.
feat/prefixed-tags M .lore.md ?? .craft-issue-842-plan.md ?? .opencode/ artifact_providers commands config.ts index.ts instrument.ts logger.ts __mocks__ schemas status_providers targets __tests__ types utils
I'm on the right branch. Let me set up a todo list and start reading in parallel.
[
{
"content": "Grep for 'workspace' across repo",
"status": "in_progress",
"priority": "high"
},
{
"content": "Config schema & loading (config.ts, project_config.ts)",
"status": "pending",
"priority": "high"
},
{
"content": "prepare command flow",
"status": "pending",
"priority": "high"
},
{
"content": "publish command flow",
"status": "pending",
"priority": "high"
},
{
"content": "publish_repo / publish issue / action.yml",
"status": "pending",
"priority": "high"
},
{
"content": "CLI entry & arg parsing (index.ts + yargs)",
"status": "pending",
"priority": "medium"
},
{
"content": "Version/tag utilities",
"status": "pending",
"priority": "high"
},
{
"content": "Precedents for grouping / per-target overrides",
"status": "pending",
"priority": "medium"
},
{
"content": "Tests needing change",
"status": "pending",
"priority": "medium"
},
{
...
Found 100 matches (more matches available) /home/byk/Code/getsentry/craft/src/config.ts: Line 572: * to implement their own expansion logic (e.g., npm workspace expansion). /home/byk/Code/getsentry/craft/CHANGELOG.md: Line 117: - (npm) Tolerate workspace:* deps in version bump and bun.lock patching by @BYK in [#805](https://github.com/getsentry/craft/pull/805) Line 566: - feat(npm): Add workspaces support by @BYK in [#645](https://github.com/getsentry/craft/pull/645) Line 1177: - fix(crates): Resume workspace publish (#392) by @jan-auer /home/byk/Code/getsentry/craft/blog-post-draft.md: Line 106: For those of you maintaining monorepos with multiple packages, Craft now supports npm workspaces natively. This one's a game-changer for repos like sentry-javascript and sentry-wizard. Line 133: workspaces: true Line 137: That's a ~200 line reduction in config, with automatic dependency ordering. Craft reads your `package.json` workspaces config, figures out the ...
There's already an npm-workspaces concept (package-publishing, not release-versioning). Key finding: expandWorkspaceTargets exists. Let me read the core files.
<path>/home/byk/Code/getsentry/craft/src/config.ts</path>
<type>file</type>
<content>
1: import { existsSync, lstatSync, readFileSync } from 'fs';
2: import path from 'path';
3:
4: import { load } from 'js-yaml';
5: import GitUrlParse from 'git-url-parse';
6: import { createGitClient } from './utils/git';
7: import { ZodError } from 'zod';
8:
9: import { logger } from './logger';
10: import {
11: CraftProjectConfig,
12: CraftProjectConfigSchema,
13: GitHubGlobalConfig,
14: ArtifactProviderName,
15: StatusProviderName,
16: TargetConfig,
17: ChangelogPolicy,
18: VersioningPolicy,
19: } from './schemas/project_config';
20: import { ConfigurationError } from './utils/errors';
21: import { isCompiledGitHubAction } from './utils/detection';
22: import {
23: getPackageVersion,
24: parseVersion,
25: versionGreaterOrEqualThan,
26: } from './utils/version';
27: // Note: We import getTargetByName lazily in expandWorkspaceTargets to avoid
28: // circular dependency: config ...
Now the schema. Then prepare and publish.
<path>/home/byk/Code/getsentry/craft/src/schemas/project_config.ts</path>
<type>file</type>
<content>
1: import { z } from 'zod';
2:
3: /**
4: * DEPRECATED: Use changelog.policy instead. Different policies for changelog management
5: */
6: export const enum ChangelogPolicy {
7: Auto = 'auto',
8: Simple = 'simple',
9: None = 'none',
10: }
11:
12: /**
13: * Name of the status provider
14: */
15: export const enum StatusProviderName {
16: GitHub = 'github',
17: }
18:
19: /**
20: * Name of the artifact provider
21: */
22: export const enum ArtifactProviderName {
23: GCS = 'gcs',
24: GitHub = 'github',
25: None = 'none',
26: }
27:
28: /**
29: * Default versioning policy when no version argument is provided. ...
Note: the top-level schema is a plain z.object (NOT .passthrough()), so unknown top-level keys would be stripped/rejected. Only TargetConfigSchema uses .passthrough(). Now the commands.
<path>/home/byk/Code/getsentry/craft/src/commands/prepare.ts</path>
<type>file</type>
<content>
1: import { existsSync, promises as fsPromises } from 'fs';
2: import { join, relative } from 'path';
3:
4: import * as shellQuote from 'shell-quote';
5: import type { SimpleGit, StatusResult } from 'simple-git';
6: import type { Arguments, Argv, CommandBuilder } from 'yargs';
7: import { createDryRunIsolation, safeFs } from '../utils/dryRun';
8:
9: import {
10: CONFIG_FILE_NAME,
11: DEFAULT_RELEASE_BRANCH_NAME,
12: getConfigFileDir,
13: getConfiguration,
14: getGitTagPrefix,
15: getGlobalGitHubConfig,
16: getVersioningPolicy,
17: loadConfigurationFromString,
18: requiresMinVersion,
19: } from '../config';
20: import { logger } from '../logger';
21: import {
22: ChangelogPolicy,
23: type TargetConfig,
24: VersioningPolicy,
25: } from '../schemas/project_config';
26: import { sleep } from '../utils/async';
27: import {
28: type BumpType,
29: ...
Now the publish command.
<path>/home/byk/Code/getsentry/craft/src/commands/publish.ts</path>
<type>file</type>
<content>
1: import { Arguments, Argv, CommandBuilder } from 'yargs';
2: import chalk from 'chalk';
3: import { existsSync, mkdirSync, readFileSync } from 'fs';
4:
5: import { safeFs } from '../utils/dryRun';
6: import { dirname, join } from 'path';
7: import * as shellQuote from 'shell-quote';
8: import stringLength from 'string-length';
9:
10: import {
11: getConfiguration,
12: getStatusProviderFromConfig,
13: getArtifactProviderFromConfig,
14: DEFAULT_RELEASE_BRANCH_NAME,
15: getGlobalGitHubConfig,
16: expandWorkspaceTargets,
17: getNoMergeConfig,
18: } from '../config';
19: import { formatTable, logger } from '../logger';
20: import { TargetConfig } from '../schemas/project_config';
21: import { getAllTargetNames, getTargetByName, SpecialTarget } from '../targets';
22: import { BaseTarget } from '../targets/base';
23: import {
24: ConfigurationError,
25: handleGlobalError,
26: ...
Now let me update todos and read the version/git/utility files, targets.ts command, the github target, index.ts, and search for publish_repo/action.yml in parallel.
[
{
"content": "Grep for 'workspace' across repo",
"status": "completed",
"priority": "high"
},
{
"content": "Config schema & loading (config.ts, project_config.ts)",
"status": "completed",
"priority": "high"
},
{
"content": "prepare command flow",
"status": "completed",
"priority": "high"
},
{
"content": "publish command flow",
"status": "completed",
"priority": "high"
},
{
"content": "publish_repo / publish issue / action.yml",
"status": "in_progress",
"priority": "high"
},
{
"content": "CLI entry & arg parsing (index.ts + yargs)",
"status": "pending",
"priority": "medium"
},
{
"content": "Version/tag utilities",
"status": "in_progress",
"priority": "high"
},
{
"content": "Precedents for grouping / per-target overrides + targets.ts + github target",
"status": "in_progress",
"priority": "medium"
},
{
"content": "Tests needing change",
"status": ...
<path>/home/byk/Code/getsentry/craft/src/utils/version.ts</path>
<type>file</type>
<content>
1: import { getGitTagPrefix } from '../config';
2:
3: /**
4: * Regular expression for matching semver versions.
5: *
6: * Modified to match version components
7: * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
8: * @see https://github.com/sindresorhus/semver-regex
9: */
10: const semverRegex = () =>
11: /\bv?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-?([\da-z-]+(?:\.[\da-z-]+)*))?(?:\+([\da-z-]+(?:\.[\da-z-]+)*))?\b/gi;
12:
13: /**
14: * Extracts a version number from the given text.
15: *
16: * In case the version contains a leading "v", it is stripped from the result.
17: * All semantic versions are supported. See {@link http://semver.org/} for
18: * more information. ...
<path>/home/byk/Code/getsentry/craft/src/utils/git.ts</path>
<type>file</type>
<content>
1: import simpleGit, {
2: type SimpleGit,
3: type LogOptions,
4: type Options,
5: type StatusResult,
6: } from 'simple-git';
7:
8: import { getConfigFileDir } from '../config';
9: import { ConfigurationError } from './errors';
10: import { createDryRunGit } from './dryRun';
11: import { logger } from '../logger';
12: import { distance as levenshtein } from 'fastest-levenshtein';
13:
14: export interface GitChange {
15: hash: string;
16: title: string;
17: body: string;
18: pr: string | null;
19: }
20:
21: // This regex relies on the default GitHub behavior where it appends the PR
22: // number to the end of the commit title as: `fix: Commit title (#123)`.
23: // This makes it very cheap and quick to extract the associated PR number just
24: // from the commit log locally. ...
Now let me read calver.ts, changelog.ts, autoVersion.ts, the targets.ts command, github target, index.ts, and search for publish_repo / action.yml in parallel.
<path>/home/byk/Code/getsentry/craft/src/utils/calver.ts</path>
<type>file</type>
<content>
1: import type { SimpleGit } from 'simple-git';
2:
3: import { getGitTagPrefix } from '../config';
4: import { logger } from '../logger';
5:
6: /**
7: * Configuration for CalVer versioning
8: */
9: export interface CalVerConfig {
10: /** Days to go back for date calculation */
11: offset: number;
12: /** strftime-like format for date part */
13: format: string;
14: }
15:
16: /**
17: * Default CalVer configuration
18: */
19: export const DEFAULT_CALVER_CONFIG: CalVerConfig = {
20: offset: 14,
21: format: '%y.%-m',
22: };
23:
24: /**
25: * Formats a date according to a strftime-like format string.
26: *
27: * Supported format specifiers:
28: * - %y: 2-digit year (e.g., "24" for 2024)
29: * - %Y: 4-digit year (e.g., "2024")
30: * - %m: Zero-padded month (e.g., "01" for January)
31: * - %-m: Month without zero padding (e.g., "1" for January)
32: * - %d: Zero-padded day ...
<path>/home/byk/Code/getsentry/craft/src/commands/targets.ts</path>
<type>file</type>
<content>
1: import { getConfiguration, expandWorkspaceTargets } from '../config';
2: import { formatJson } from '../utils/strings';
3: import { getAllTargetNames } from '../targets';
4: import { BaseTarget } from '../targets/base';
5:
6: export const command = ['targets'];
7: export const description = 'List defined targets as JSON array';
8:
9: export async function handler(): Promise<any> {
10: let definedTargets = getConfiguration().targets || [];
11:
12: // Expand workspace targets (e.g., npm workspaces)
13: definedTargets = await expandWorkspaceTargets(definedTargets);
14:
15: const possibleTargetNames = new Set(getAllTargetNames());
16: const allowedTargetNames = definedTargets
17: .filter(target => target.name && possibleTargetNames.has(target.name))
18: .map(BaseTarget.getId);
19:
20: console.log(formatJson(allowedTargetNames));
21: }
(End of file - total 21 ...
Now the github target, index.ts, changelog.ts, autoVersion.ts, and grep for publish_repo/action files.
<path>/home/byk/Code/getsentry/craft/src/targets/github.ts</path>
<type>file</type>
<content>
1: import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
2: import { createReadStream, promises, statSync } from 'fs';
3: import { basename } from 'path';
4:
5: import { getConfiguration } from '../config';
6: import {
7: ChangelogPolicy,
8: GitHubGlobalConfig,
9: TargetConfig,
10: TypedTargetConfig,
11: } from '../schemas/project_config';
12: import {
13: Changeset,
14: DEFAULT_CHANGELOG_PATH,
15: findChangeset,
16: } from '../utils/changelog';
17: import { getGitHubClient } from '../utils/githubApi';
18: import { isDryRun } from '../utils/helpers';
19: import { safeExec } from '../utils/dryRun';
20: import {
21: isPreviewRelease,
22: parseVersion,
23: SemVer,
24: versionGreaterOrEqualThan,
25: versionToTag,
26: } from '../utils/version';
27: import { BaseTarget } from './base';
28: import { BaseArtifactProvider } from '../artifact_providers/base';
29: ...
Now index.ts, changelog.ts, autoVersion.ts, and search for publish_repo & action.yml.
<path>/home/byk/Code/getsentry/craft/src/index.ts</path>
<type>file</type>
<content>
1: #!/usr/bin/env node
2: // Import Sentry instrumentation first, before any other code
3: import './instrument';
4:
5: import isCI from 'is-ci';
6: import yargs from 'yargs';
7:
8: import { logger, LogLevel } from './logger';
9: import {
10: sanitizeDynamicLinkerEnv,
11: warnIfCraftEnvFileExists,
12: } from './utils/env';
13: import { envToBool, setGlobals } from './utils/helpers';
14: import { getPackageVersion } from './utils/version';
15: import { withTracing } from './utils/tracing';
16:
17: // Commands
18: import * as prepare from './commands/prepare';
19: import * as publish from './commands/publish';
20: import * as targets from './commands/targets';
21: import * as config from './commands/config';
22: import * as artifacts from './commands/artifacts';
23: import * as changelog from './commands/changelog';
24:
25: function printVersion(): void {
26: if (!process.argv.includes('-v') ...
Note .env('CRAFT') — env var layering with CRAFT_ prefix. Now search for publish_repo and action files, and read changelog.ts & autoVersion.ts.
Found 9 matches /home/byk/Code/getsentry/craft/.lore.md: Line 93: * **getsentry/craft publish recovery: check completed targets before retry to avoid duplicate-publish 403**: When a craft publish run fails mid-way (e.g., docker\[release] 400 Bad Request), some targets (npm, gcs, registry) may have already succeeded. Recovery steps: (1) verify which targets completed (check npm registry, GCS bucket, release-registry repo); (2) update the publish issue body to check off completed targets; (3) re-add the 'accepted' label to re-trigger the publish workflow — it will skip already-checked targets. Always verify npm publish before retry to avoid duplicate-publish 403. Docker Hub 400 during blob upload is a known transient infrastructure flake — retry resolves it. /home/byk/Code/getsentry/craft/CHANGELOG.md: Line 255: - (actions) Support `publish_repo: self` for single-repo setups by @BYK in ...
<path>/home/byk/Code/getsentry/craft/action.yml</path>
<type>file</type>
<content>
1: name: 'Craft Prepare Release'
2: description: 'Prepare a new release using Craft'
3:
4: inputs:
5: version:
6: description: >
7: Version to release. Can be a semver string (e.g., "1.2.3"),
8: a bump type ("major", "minor", "patch"), or "auto" for automatic detection.
9: required: false
10: merge_target:
11: description: Target branch to merge into. Uses the default branch as a fallback. ...
This is the key finding for part 4. The issue title is publish: ${GITHUB_REPOSITORY}${SUBDIRECTORY}@${RESOLVED_VERSION} — note SUBDIRECTORY is inputs.path. Now let me check the release.yml workflow (the accept side) and read changelog.ts & autoVersion.ts.
<path>/home/byk/Code/getsentry/craft/.github/workflows/release.yml</path>
<type>file</type>
<content>
1: name: Release
2: concurrency: ${{ github.workflow }}-${{ github.ref }}
3:
4: on:
5: # For Craft's own releases (dogfooding)
6: workflow_dispatch:
7: inputs:
8: version:
9: description: Version to release
10: required: true
11: default: 'auto'
12: force:
13: description: Force a release even when there are release-blockers (optional)
14: required: false
15:
16: # For external repos to call this workflow
17: workflow_call:
18: inputs:
19: version:
20: description: Version to release (semver, bump type, or "auto")
21: type: string
22: required: false
23: force:
24: description: Force a release even when there are release-blockers
25: type: string
26: required: false
27: default: 'false'
28: merge_target:
29: description: Target branch ...
This workflow is only the "prepare/request" side. The "accept" side (publish workflow triggered by the accepted label) lives in the consuming repo, not here. Let me check the docs for the publish workflow and read changelog.ts + autoVersion.ts.
<path>/home/byk/Code/getsentry/craft/src/utils/autoVersion.ts</path>
<type>file</type>
<content>
1: import * as semver from 'semver';
2: import type { SimpleGit } from 'simple-git';
3:
4: import { logger } from '../logger';
5: import {
6: generateChangesetFromGit,
7: BUMP_TYPES,
8: isBumpType,
9: type BumpType,
10: type ChangelogResult,
11: } from './changelog';
12:
13: // Re-export for convenience
14: export { BUMP_TYPES, isBumpType, type BumpType, type ChangelogResult };
15:
16: /**
17: * Calculates the next version by applying the bump type to the current version.
18: *
19: * @param currentVersion The current version string (e.g., "1.2.3")
20: * @param bumpType The type of bump to apply
21: * @returns The new version string
22: * @throws Error if the version cannot be incremented
23: */
24: export function calculateNextVersion(
25: currentVersion: string,
26: bumpType: BumpType,
27: ): string {
28: // Handle empty/missing current version (new project)
29: ...
Now let me look at changelog.ts (the parts relevant to tagPrefix / getLatestTag / config file dir) and grep for getGitTagPrefix/getConfigFileDir usages across the codebase.
Found 100 matches (more matches available)
/home/byk/Code/getsentry/craft/src/config.ts:
Line 110: export function getConfigFileDir(): string | undefined {
Line 147: export function getConfiguration(clearCache = false): CraftProjectConfig {
Line 230: const config = getConfiguration();
Line 266: export function getVersioningPolicy(): VersioningPolicy {
Line 267: const config = getConfiguration();
Line 284: export async function getGlobalGitHubConfig(
Line 299: let repoGitHubConfig = getConfiguration(clearCache).github || null;
Line 302: const configDir = getConfigFileDir() || '.';
Line 326: return getGlobalGitHubConfig();
Line 339: export function getGitTagPrefix(): string {
Line 340: const targets = getConfiguration().targets || [];
Line 367: const projectConfig = getConfiguration();
Line 374: const githubRepo = await getGlobalGitHubConfig();
Line 402: const config = getConfiguration();
Line 403: const githubConfig = ...
Let me read the relevant parts of changelog.ts (getLatestTag usage, config file dir, DEFAULT_RELEASE_CONFIG) and the base target + registry/index of targets, plus the npm target's expand method.
Found 82 matches
/home/byk/Code/getsentry/craft/src/utils/git.ts:
Line 8: import { getConfigFileDir } from '../config';
Line 45: export async function getLatestTag(
Line 47: tagPrefix = '',
Line 52: if (tagPrefix) {
Line 57: args.push('--match', `${tagPrefix}*`);
Line 119: const configFileDir = getConfigFileDir() || '.';
/home/byk/Code/getsentry/craft/src/utils/env.ts:
Line 7: import { getConfigFileDir } from '../config';
Line 106: const configFileDir = getConfigFileDir();
/home/byk/Code/getsentry/craft/src/utils/changelog.ts:
Line 10: getConfigFileDir,
Line 837: * Release configuration structure matching GitHub's release.yml format
Line 863: * Used when .github/release.yml doesn't exist
Line 869: export const DEFAULT_RELEASE_CONFIG: ReleaseConfig = {
Line 952: /** True if a custom .github/release.yml was loaded (not using defaults) */
Line 957: * Reads and parses .github/release.yml from the repository root
Line ...
Now let me look at the base target (getId), the npm target's expand static method, and the targets registry index.
<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 ...
Now let me look at the npm target's expand static (the precedent for target expansion) and the targets index/registry.
Found 56 matches
/home/byk/Code/getsentry/craft/src/targets/npm.ts:
Line 23: discoverWorkspaces,
Line 29: } from '../utils/workspaces';
Line 104: workspaces?: boolean;
Line 172: * Expand an npm target config into multiple targets if workspaces is enabled.
Line 173: * This static method is called during config loading to expand workspace targets.
Line 179: public static async expand(
Line 183: // If workspaces is not enabled, return the config as-is
Line 184: if (!config.workspaces) {
Line 188: const result = await discoverWorkspaces(rootDir);
Line 192: 'npm target has workspaces enabled but no workspace packages were found',
Line 307: * Supports workspaces - bumps root and all workspace packages.
Line 329: const workspaces = await discoverWorkspaces(rootDir);
Line 331: workspaces.type !== 'none' && workspaces.packages.length > 0;
Line 344: // npm 7+ supports --workspaces flag
Line 347: ...
<path>/home/byk/Code/getsentry/craft/src/targets/npm.ts</path>
<type>file</type>
<content>
160: /**
161: * Target responsible for publishing releases on NPM
162: */
163: export class NpmTarget extends BaseTarget {
164: /** Target name */
165: public readonly name: string = 'npm';
166: /** Target options */
167: public readonly npmConfig: NpmTargetOptions;
168: /** Parsed npm version, set during checkRequirements() */
169: protected npmVersion: SemVer | null = null;
170:
171: /**
172: * Expand an npm target config into multiple targets if workspaces is enabled.
173: * This static method is called during config loading to expand workspace targets. ...
That's the expand precedent (npm-package-level, not release-level). Now let me check the targets registry index and the config command, then look at test patterns for config/prepare/publish.
<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 ...
Now let me quickly check the config command and the docs for github-actions publish workflow (the accept side), plus look at test file structure for config/prepare/publish.
<path>/home/byk/Code/getsentry/craft/src/commands/config.ts</path>
<type>file</type>
<content>
1: import { getConfiguration, getGlobalGitHubConfig } from '../config';
2: import { formatJson } from '../utils/strings';
3:
4: export const command = ['config'];
5: export const description =
6: 'Print the parsed, processed, and validated Craft config for the current project in pretty-JSON.';
7:
8: export async function handler(): Promise<void> {
9: const github = await getGlobalGitHubConfig();
10: const config = {
11: ...getConfiguration(),
12: github,
13: };
14: console.log(formatJson(config));
15: }
(End of file - total 15 lines)
</content>
<path>/home/byk/Code/getsentry/craft/docs/src/content/docs/github-actions.md</path> <type>file</type> <content> 1: --- 2: title: GitHub Actions 3: description: Automate releases and changelog previews with Craft GitHub Actions 4: --- 5: 6: Craft provides GitHub Actions for automating releases and previewing changelog entries in pull requests. 7: 8: For a real-world example of using Craft's GitHub Actions, see the [getsentry/publish](https://github.com/getsentry/publish) repository. 9: 10: ## Prepare Release 11: 12: Craft offers two ways to automate releases in GitHub Actions: 13: 14: | Option | Best For | Flexibility | 15: | --------------------- | ---------------------------------- | ---------------------------------- | 16: | **Reusable Workflow** | Quick setup, standard release flow | Low - runs as a complete job | 17: | **Composite Action** | Custom workflows, pre/post steps | High - composable with other ...
The publish/accept side lives in getsentry/publish (external), triggered by the accepted label — craft itself only creates the request issue. Now let me look at the config.test.ts (getGitTagPrefix tests) and check the plan file that already exists to understand context, plus quickly scan the test file list.
Found 37 matches
/home/byk/Code/getsentry/craft/src/__tests__/config.test.ts:
Line 8: getGitTagPrefix,
Line 9: loadConfigurationFromString,
Line 10: validateConfiguration,
Line 15: describe('validateConfiguration', () => {
Line 19: expect(validateConfiguration(data)).toEqual(data);
Line 28: expect(validateConfiguration(data)).toEqual(data);
Line 40: expect(validateConfiguration(data)).toEqual(data);
Line 48: expect(validateConfiguration(data)).toEqual(data);
Line 62: expect(validateConfiguration(data)).toEqual(data);
Line 67: validateConfiguration({ github: { owner: 'getsentry' } }),
Line 72: expect(() => validateConfiguration({ minVersion: 'invalid' })).toThrow(
Line 79: validateConfiguration({ changelog: { policy: 'invalid' } }),
Line 84: describe('CraftProjectConfigSchema', () => {
Line 105: describe('noMerge config', () => {
Line 108: expect(validateConfiguration(data)).toEqual(data);
Line ...
<path>/home/byk/Code/getsentry/craft/src/__tests__/config.test.ts</path>
<type>file</type>
<content>
127: describe('getGitTagPrefix', () => {
128: afterEach(() => {
129: vi.restoreAllMocks();
130: });
131:
132: function loadWithTargets(targets: unknown[]): void {
133: loadConfigurationFromString(
134: [
135: 'github:',
136: ' owner: getsentry',
137: ' repo: craft',
138: 'targets:',
139: ...targets.map(t => ` - ${JSON.stringify(t)}`),
140: ].join('\n'),
141: );
142: }
143:
144: test('returns empty string when no github target has a tagPrefix', () => {
145: loadWithTargets([{ name: 'npm' }, { name: 'github' }]);
146: expect(getGitTagPrefix()).toBe('');
147: });
148:
149: test("returns the github target's tagPrefix", () => {
150: loadWithTargets([{ name: 'npm' }, { name: 'github', tagPrefix: 'cli@' }]);
151: expect(getGitTagPrefix()).toBe('cli@');
152: });
153:
154: test('does not ...
Now let me read the existing plan file for context on the reviewer's concern about the publish repo, and check the publishState util and getPublishStatePath.
<path>/home/byk/Code/getsentry/craft/.craft-issue-842-plan.md</path> <type>file</type> <content> 1: # Issue #842 — Cloudflare deploy target + prefixed multi-product releases 2: 3: Part of the `getsentry/toolkit` monorepo merge (sentry-cli + sentry-mcp). 4: Master: getsentry/cli#1248. Gist: https://gist.github.com/BYK/8c0ce6cec64608f41fe5654b7827228e 5: 6: Two logically-independent deliverables → **two separate PRs off `master`**. 7: 8: - **PR 1** — `cloudflare` release target (config-selectable Pages vs Worker). 9: - **PR 2** — Full monorepo prefixed-tag support (`cli@x.y.z` / `mcp@x.y.z`). 10: 11: Branch naming: `feat/cloudflare-target`, `feat/prefixed-tags` (per `fix/<scope>-<issue>` / `feat/...` convention). 12: 13: --- 14: 15: ## Background (confirmed from code) 16: 17: - Targets extend `BaseTarget` (`src/targets/base.ts:17`); constructor `(config, artifactProvider, githubRepo?)`; override `async publish(version, revision)`. ...
I now have full context. The current model is "separate .craft.yml per product". Let me verify one more thing about the publish state path (keying) and check the getPublishStatePath signature, since publish-issue titles include the version and the state file is keyed by owner/repo + version.
<path>/home/byk/Code/getsentry/craft/src/utils/publishState.ts</path> <type>file</type> <content> 1: /** 2: * Helpers for locating Craft's publish-state file in a path that is NOT 3: * writable by the repository being published. 4: * 5: * Background: `craft publish` writes a small JSON file listing targets 6: * that have completed so a resumed run can skip them. Before this module 7: * existed, the file lived at `.craft-publish-<version>.json` in the 8: * project's cwd. That path is inside the repository checkout, so any 9: * committed file at the same path (or any earlier CI step) could 10: * pre-populate the "published" set and trick Craft into silently 11: * skipping targets. 12: * 13: * The file now lives under `$XDG_STATE_HOME/craft/` (falling back to 14: * `$HOME/.local/state/craft/`). The filename is keyed on 15: * owner, repo, a hash of cwd (to disambiguate monorepo subpaths), and 16: * the version being published. ...
I have everything I need. Let me do a final quick check: the test file inventory for prepare/publish to complete part 8, and confirm there's no publish workflow (accept-side) in this repo.
src/commands/__tests__/: changelog-versioning-policy.test.ts prepare.test.ts publish.test.ts targets.test.ts src/__tests__/: config.test.ts index.test.ts prepare-dry-run.e2e.test.ts __snapshots__ versionBump.test.ts ---workflows--- build.yml changelog-preview.yml docs-preview.yml enforce-license-compliance.yml image.yml lint.yml release.yml
Confirmed: no publish/accept workflow in this repo (it lives in getsentry/publish). Let me quickly scan the prepare.test.ts and publish.test.ts structure to characterize test patterns for part 8.
1:import { vi, describe, test, expect, beforeEach, type Mock } from 'vitest';
2:import { spawnProcess } from '../../utils/system';
3:import { runPreReleaseCommand, checkVersionOrPart } from '../prepare';
5:vi.mock('../../utils/system');
7:describe('runPreReleaseCommand', () => {
177:describe('checkVersionOrPart', () => {
=== PUBLISH TEST ===
12:vi.mock('../../utils/system');
13:vi.mock('../../utils/git', () => ({
20:describe('runPostReleaseCommand', () => {
47: describe('default script', () => {
165:describe('handleReleaseBranch', () => {
407:describe('MergeConflictError', () => {
433:describe('PushError', () => {
I have all the information needed. Let me mark todos complete and write the report.
[
{
"content": "Grep for 'workspace' across repo",
"status": "completed",
"priority": "high"
},
{
"content": "Config schema & loading (config.ts, project_config.ts)",
"status": "completed",
"priority": "high"
},
{
"content": "prepare command flow",
"status": "completed",
"priority": "high"
},
{
"content": "publish command flow",
"status": "completed",
"priority": "high"
},
{
"content": "publish_repo / publish issue / action.yml",
"status": "completed",
"priority": "high"
},
{
"content": "CLI entry & arg parsing (index.ts + yargs)",
"status": "completed",
"priority": "medium"
},
{
"content": "Version/tag utilities",
"status": "completed",
"priority": "high"
},
{
"content": "Precedents for grouping / per-target overrides + targets.ts + github target",
"status": "completed",
"priority": "medium"
},
{
"content": "Tests needing change",
"status": ...
Branch feat/prefixed-tags. All cites are file:line. No code was written.
_configCache, config.ts:56) and one cached global GitHub config (_globalGitHubConfigCache, config.ts:283).feat/prefixed-tags work makes the read paths prefix-aware (getLatestTag, CalVer, changelog base) and threads getGitTagPrefix() through them, but the "which product" selector is entirely implicit: it's whatever the single github target's tagPrefix is (config.ts:339).workspace concept — but it means npm package workspaces (publishing many npm packages from one target), NOT independently-versioned products. See §3. The static-expand() mechanism (config.ts:577, npm.ts:179) is the closest existing precedent for a per-product expansion but operates at target-config level, one version..craft-issue-842-plan.md:141) is separate .craft.yml per product. A single-file/workspace-selector model does not exist and would require threading a selector through everything below.findConfigFile() walks up from cwd for .craft.yml (config.ts:63-88), caches path in _configPathCache. CONFIG_FILE_NAME = '.craft.yml' (config.ts:41). Note the // TODO support multiple configuration files (one per configuration) at config.ts:40.getConfiguration() reads + validateConfiguration() → CraftProjectConfigSchema.parse → caches _configCache (config.ts:147-161, 125-142). loadConfigurationFromString() is the --config-from variant (config.ts:170-178).getConfigFileDir() = dirname of that path (config.ts:110-116) — used as rootDir ...Date: Jul 21, 2026 * 🟡 (16:13) [tool-result] Grep for getConfigFileDir/getGitTagPrefix usage (82 matches) across craft codebase — key details (file:line): src/utils/git.ts imports getConfigFileDir (line 8), getLatestTag() defined line 45 with param tagPrefix='' (line 47), uses `args.push('--match', `${tagPrefix}*`)` (line 57), `const configFileDir = getConfigFileDir() || '.';` (line 119); src/ut…
<observations> Date: Jul 21, 2026 * 🔴 (16:11) User surfaced (via grep tool result) a documented recovery procedure in /home/byk/Code/getsentry/craft/.lore.md line 93 titled "getsentry/craft publish recovery: check completed targets before retry to avoid duplicate-publish 403": When a craft publish run fails mid-way (e.g., docker[release] 400 Bad Request), some targets (npm, gcs, registry) may ha…
Date: Jul 21, 2026 * 🟢 (16:10) Assistant stated next step: "Now index.ts, changelog.ts, autoVersion.ts, and search for publish_repo & action.yml." * 🟡 (16:10) [tool-result] Assistant read GitHub target implementation file (src/targets/github.ts, partial view via elided dump) — key structures found (file:line): - `import { getGitHubClient } from '../utils/githubApi';` (line 17). - `export in…
<observations> Date: Jul 21, 2026 * 🟡 (16:08) [tool-result] Todo list status update returned: "Grep for 'workspace' across repo" (completed, high), "Config schema & loading (config.ts, project_config.ts)" (completed, high), "prepare command flow" (completed, high), "publish command flow" (completed, high), "publish_repo / publish issue / action.yml" (in_progress, high), "CLI entry & arg parsing …
Date: Jul 21, 2026 * 🟡 (16:08) [tool-result] Assistant read src/commands/publish.ts (part of todo item 4: "publish command flow") — key structures found (file:line): - Imports include `getNoMergeConfig` (line 17); import block ends `} from '../utils/system';` (line 36). - `export const command = ['publish NEW-VERSION'];` (line 55). - `SpecialTarget.All` referenced in yargs options (line 74…
Date: Jul 21, 2026 * 🟡 (16:06) [tool-result] Assistant read src/commands/prepare.ts (part of todo item 3: "prepare command flow") — key structures found (file:line): - Imports include `ConfigurationError`, `getVersion`, `isValidVersion` from '../utils/version' (line 66). - Yargs option description: "a bump type (\"major\", \"minor\", or \"patch\"), \"auto\" to determine automatically" (line …
Date: Jul 21, 2026 * 🔴 (16:04) [tool-result] Confirmed branch feat/prefixed-tags via git status; working tree shows modified .lore.md, untracked .craft-issue-842-plan.md and .opencode/ directory, plus source dirs (artifact_providers, commands, config.ts, index.ts, instrument.ts, logger.ts, __mocks__, schemas, status_providers, targets, __tests__, types, utils). * 🟡 (16:04) Assistant created a 1…
Date: Jul 21, 2026 * 🔴 (16:02) User stated working directory is /home/byk/Code/getsentry/craft on branch feat/prefixed-tags. * 🔴 (16:02) User's goal: design native "workspaces" support for the getsentry/craft codebase — letting ONE repository release multiple independently-versioned products (examples: cli@1.2.3, mcp@2.0.0), ideally from a SINGLE top-level .craft.yml, with a product/workspace s…