Dashboard › craft › Session 1WdPDg9kiH9B
1WdPDg9kiH9B5NvEOPerform a final strict READ-ONLY adversarial audit. Do NOT edit files, run formatters/generators, or change git state. Inspect complete current uncommitted diffs in BOTH worktrees:
Audit the exact final trees against this required contract:
publish: <checkout-repository>/<full-concrete-workspace-path>@<version> and preserve exact case/path spelling; workspace and checkout path are mutually exclusive..craft.yml workspace list; exact unnormalized full suffix match selects a workspace, all nonmatches remain checkout paths; no root config means path behavior and discovery errors fail closed.., .., __proto__, leading -, backslash, non-ASCII/control values before checkout/state/publish. No normalizing.--config-from resolves workspace globs from the checked-out repo root. Workspace glob keys never escape base before filesystem access. Regular *, ?, character classes including [!a] and [^a], and globstar remain valid. Literal/configured workspace path safety matches concrete workspace safety.ci-ready label event for an open accepted issue with neither pending nor failed CI; waiting state removes stale ci-ready first.--rev.<path>/home/byk/Code/getsentry/craft-workspace-action-propagation</path> <type>directory</type> <entries> .craft.yml .dockerignore .eslintcache .git .github/ .gitignore .lore.md .nojekyll .npmignore .prettierignore .prettierrc.yml .tsbuildinfo .vscode/ action.yml AGENTS.md blog-post-draft.md build.mjs CHANGELOG.md CONTRIBUTING.md dist/ Dockerfile docs/ eslint.config.mjs img/ LICENSE node_modules/ package.json pnpm-lock.yaml README.md src/ tsconfig.build.json tsconfig.json vitest.config.mts (33 entries) </entries>
<path>/home/byk/Code/getsentry/publish-workspace-acceptance</path> <type>directory</type> <entries> .eslintrc.js .git .github/ .gitignore AGENTS.md auto-approve-repos.txt docs/ LICENSE node_modules/ package.json README.md scripts/ src/ vitest.config.js yarn.lock (15 entries) </entries>
M action.yml M docs/src/content/docs/targets/github.md M src/__tests__/action.test.ts M src/__tests__/config.test.ts M src/commands/prepare.ts M src/config.ts M src/schemas/project_config.ts M action.yml M docs/src/content/docs/targets/github.md M src/__tests__/action.test.ts M src/__tests__/config.test.ts M src/commands/prepare.ts M src/config.ts M src/schemas/project_config.ts
M .github/workflows/publish.yml M docs/publish-issue-format.md M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/publish-location.js M src/modules/__tests__/publish-workflow.js M src/modules/details-from-context.js M src/modules/publish-issue-title.js M src/modules/publish-issue-title.peggy M src/modules/publish-location.js M src/publish/resolve-location.js M .github/workflows/publish.yml M docs/publish-issue-format.md M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/publish-location.js M src/modules/__tests__/publish-workflow.js M src/modules/details-from-context.js M src/modules/publish-issue-title.js M src/modules/publish-issue-title.peggy M src/modules/publish-location.js M src/publish/resolve-location.js
diff --git a/action.yml b/action.yml
index 94f3158..84377a8 100644
--- a/action.yml
+++ b/action.yml
@@ -4,162 +4,162 @@ description: 'Prepare a new release using Craft'
inputs:
version:
description: >
Version to release. Can be a semver string (e.g., "1.2.3"),
a bump type ("major", "minor", "patch"), or "auto" for automatic detection.
required: false
merge_target:
description: Target branch to merge into. Uses the default branch as a fallback.
required: false
force:
description: Force a release even when there are release-blockers
required: false
default: 'false'
blocker_label:
description: Label that blocks releases
required: false
default: 'release-blocker'
publish_repo:
description: Repository for publish issues (owner/repo format)
required: false
git_user_name:
description: Git committer name
required: false
git_user_email:
description: Git committer email
required: ...
diff --git a/src/__tests__/action.test.ts b/src/__tests__/action.test.ts
index 8ca535a..0a1d71b 100644
--- a/src/__tests__/action.test.ts
+++ b/src/__tests__/action.test.ts
@@ -113,136 +113,161 @@ function runRequestPublish(
WORKSPACE: workspace,
},
},
);
}
function runActionStep(
stepName: string,
workspace: string,
environment: ReturnType<typeof createActionEnvironment>,
pathInput = '.',
locale = 'C',
) {
return spawnSync('bash', ['-e', '-c', getActionStep(stepName).run!], {
cwd: environment.directory,
env: {
...process.env,
CRAFT_CALLS: environment.craftCalls,
CRAFT_CONFIG_FROM_MERGE_TARGET: '',
GITHUB_OUTPUT: environment.output,
GIT_CALLS: environment.gitCalls,
LC_ALL: locale,
MERGE_TARGET: '',
PATH: `${environment.binDirectory}:${process.env.PATH}`,
PATH_INPUT: pathInput,
VERSION: '',
WORKSPACE: workspace,
},
});
}
afterEach(() => {
...
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index f6dc96a..c72068a 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -115,126 +115,121 @@ jobs:
timeout-minutes: 90
env:
SENTRY_DSN: "https://303a687befb64dc2b40ce4c96de507c5@o1.ingest.sentry.io/6183838"
steps:
- name: Get repo contents
uses: actions/checkout@v6
with:
path: .__publish__
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: yarn
cache-dependency-path: .__publish__/yarn.lock
- name: Install yarn dependencies
run: yarn install --cwd ".__publish__"
- name: Parse and set inputs
id: inputs
run: node .__publish__/src/publish/inputs.js
- name: Resolve CI-approved release revision
id: release-revision
env:
PUBLISH_ARGS: ${{ ...
diff --git a/src/modules/__tests__/ci-poller-input.js b/src/modules/__tests__/ci-poller-input.js
index 8c80856..77feb3a 100644
--- a/src/modules/__tests__/ci-poller-input.js
+++ b/src/modules/__tests__/ci-poller-input.js
@@ -1,53 +1,53 @@
import { readFileSync } from "fs";
import { join } from "path";
import { expect, test } from "vitest";
const { getCiPollerInput } = require("../ci-poller-input.js");
const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
const issueBody = `Requested by: @byk
Merge target: (default)
Quick links:
- [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
- [View check runs](https://github.com/getsentry/toolkit/commit/${REVISION}/checks/)`;
-test("parses compact and legacy workspace publish titles", () => {
+test("parses root and full-path publish titles", () => {
expect(
getCiPollerInput({
issueBody,
title: "publish: getsentry/toolkit/cli@1.2.3",
})
).toEqual({
...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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. ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/.github/workflows/publish.yml</path>
<type>file</type>
<content>
1: name: Publish
2: on:
3: issues:
4: types: [labeled]
5:
6: concurrency:
7: # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
8: # issues for the same repo@version share a concurrency group.
9: group: ${{ github.event.issue.title }}
10: cancel-in-progress: false
11:
12: permissions:
13: contents: read
14: issues: write
15: packages: write
16:
17: jobs:
18: # When accepted is added to a publish issue:
19: # - Add ci-pending (and remove ci-failed if retrying)
20: # - Enable the poller via CI_POLLER_HAS_PENDING=true
21: # - Comment on the issue
22: # - Trigger the poller immediately so we don't wait for the next cron tick
23: # The publish job below requires ci-ready, so it will not fire until the
24: # poller flips ci-pending → ci-ready (which also prevents publishing without
25: # CI ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/.github/workflows/ci-poller.yml</path> <type>file</type> <content> 1: name: CI Status Poller 2: 3: on: 4: schedule: 5: - cron: "*/5 * * * *" 6: workflow_dispatch: 7: inputs: 8: attempt: 9: description: Self-dispatch attempt counter (internal use) 10: required: false 11: default: "0" 12: 13: permissions: 14: contents: read 15: issues: read 16: 17: jobs: 18: check-ci: 19: runs-on: ubuntu-latest 20: environment: production 21: # Skip entirely (no runner provisioned) when there's nothing to check. 22: # Set to "true" by ci-pending.yml, reset to "false" here when done. 23: # Always allow workflow_dispatch for manual recovery. 24: if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch' 25: concurrency: 26: group: ci-status-poller 27: cancel-in-progress: false 28: steps: 29: # sentry-internal-app token ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/publish/inputs.js</path>
<type>file</type>
<content>
1: const github = require('@actions/github');
2: const core = require('@actions/core');
3: const { detailsFromContext } = require('../modules/details-from-context');
4:
5: async function inputs() {
6: const result = await detailsFromContext({
7: context: github.context,
8: });
9: core.setOutput('result', result);
10: }
11:
12:
13: inputs();
(End of file - total 13 lines)
</content>
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/config.ts</path>
<type>file</type>
<content>
90: }
91:
92: /**
93: * Returns the name of the currently-selected workspace, if any.
94: */
95: export function getActiveWorkspace(): string | undefined {
96: return _activeWorkspaceName;
97: }
98:
99: /**
100: * Merges a workspace's overrides onto the top-level (base) config, producing a
101: * flat `CraftProjectConfig` that the rest of craft consumes unchanged.
102: *
103: * Resolution rules:
104: * - Every release-relevant field defined on the workspace replaces the
105: * top-level value (shallow override; a workspace either declares a field or
106: * inherits it wholesale — we do not deep-merge arrays/objects, to keep
107: * behavior predictable).
108: * - `github` is shallow-merged (owner/repo) so a workspace can override either
109: * value while inheriting the other. ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/schemas/project_config.ts</path>
<type>file</type>
<content>
180: const releaseUnitFields = {
181: github: GitHubGlobalConfigSchema.optional(),
182: targets: z.array(TargetConfigSchema).optional(),
183: preReleaseCommand: z.string().optional(),
184: postReleaseCommand: z.string().optional(),
185: releaseBranchPrefix: z.string().optional(),
186: changelog: ChangelogConfigSchema.optional(),
187: changelogPolicy: z.enum(['auto', 'simple', 'none']).optional(),
188: requireNames: z.array(z.string()).optional(),
189: statusProvider: BaseStatusProviderSchema.optional(),
190: artifactProvider: BaseArtifactProviderSchema.optional(),
191: versioning: VersioningConfigSchema.optional(),
192: /**
193: * Do not merge the release branch after publishing.
194: * Defaults to true for compiled GitHub Actions (Node.js actions with dist/ folder).
195: */
196: noMerge: z.boolean().optional(),
197: } as ...
Found 10 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/index.ts:
Line 20: import { setActiveWorkspace } from './config';
Line 29: import * as workspace from './commands/workspace';
Line 92: // Resolve the active workspace BEFORE parsing. yargs runs command `builder`s
Line 94: // choices from config.targets) *before* middleware, so setting the workspace
Line 96: // config without a selection and fail. We therefore extract --workspace (or
Line 99: setActiveWorkspace(extractWorkspaceSelection(argv));
Line 112: .command(workspace)
Line 126: .option('workspace', {
Line 129: 'Select a named workspace (release unit) from the configuration. ' +
Line 130: 'Required when the config defines "workspaces". Env: CRAFT_WORKSPACE',
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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 {
14: envToBool,
15: setGlobals,
16: extractWorkspaceSelection,
17: } from './utils/helpers';
18: import { getPackageVersion } from './utils/version';
19: import { withTracing } from './utils/tracing';
20: import { setActiveWorkspace } from './config';
21:
22: // Commands
23: import * as prepare from './commands/prepare';
24: import * as publish from './commands/publish';
25: import * as targets from './commands/targets';
26: import * as config from './commands/config';
27: import * as artifacts from './commands/artifacts';
28: ...
Found 1 matches /home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/helpers.ts: Line 42: export function extractWorkspaceSelection(
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/helpers.ts</path>
<type>file</type>
<content>
1: import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
2: import path from 'path';
3: import { parseArgs } from 'node:util';
4:
5: import prompts from 'prompts';
6: import { logger, LogLevel, setLevel } from '../logger';
7:
8: /**
9: * Maximum size (in bytes) for step output values passed through GITHUB_OUTPUT.
10: * Values exceeding this are truncated to avoid E2BIG errors when GitHub Actions
11: * expands them into environment variables for subsequent steps.
12: *
13: * 64 KB is well under the ~2 MB ARG_MAX kernel limit and also under GitHub's
14: * ~65 536-character issue body limit, so truncated changelogs still render. ...
Found 17 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/publishState.ts:
Line 2: * Helpers for locating Craft's publish-state file in a path that is NOT
Line 34: * `scripts/print-publish-state-path.ts` if present) that needs to
Line 68: * Builds the filename for the publish-state file.
Line 71: * `publish-state-<owner>-<repo>-<sha1(cwd)[:12]>-<version>.json`
Line 76: * `publish-state-<sha256(cwd)[:16]>-<version>.json`
Line 95: return `publish-state-${owner}-${repo}-${shortCwdHash(cwd)}-${workspacePrefix}${safeVersion}.json`;
Line 98: return `publish-state-${cwdDigest}-${workspacePrefix}${safeVersion}.json`;
Line 102: * Full absolute path to the publish-state file for the given version.
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/__tests__/publishState.test.ts:
Line 52: /^publish-state-getsentry-craft-[0-9a-f]{12}-1\.2\.3\.json$/,
Line 107: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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. ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/publish/resolve-release-revision.js</path>
<type>file</type>
<content>
1: const core = require("@actions/core");
2: const { getReleaseRevision } = require("../modules/release-revision");
3:
4: function resolveReleaseRevision() {
5: const { repo } = JSON.parse(process.env.PUBLISH_ARGS || "");
6: core.setOutput(
7: "revision",
8: getReleaseRevision({
9: issueBody: process.env.PUBLISH_ISSUE_BODY || "",
10: repo,
11: })
12: );
13: }
14:
15: resolveReleaseRevision();
(End of file - total 15 lines)
</content>
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/release-revision.js</path>
<type>file</type>
<content>
1: const CHECK_RUNS_LINK =
2: /^Requested by: @[^\r\n]+\r?\n(?:[ \t]*\r?\n)?[ \t]*Merge target: [^\r\n]+\r?\n(?:[ \t]*\r?\n)?[ \t]*Quick links:\r?\n(?:[ \t]*\r?\n)?[ \t]*- \[View changes\]\([^\r\n]+\)\r?\n[ \t]*- \[View check runs\]\(https:\/\/github\.com\/getsentry\/(?<repo>[A-Za-z0-9_.-]+)\/commit\/(?<revision>[0-9a-f]{40})\/checks\/?\)(?=\r?\n|$)/;
3: const CHECK_RUNS_LINK_COUNT = /^[ \t]*- \[View check runs\]\(/gm;
4:
5: function getReleaseRevision({ issueBody, repo }) {
6: if ((issueBody.match(CHECK_RUNS_LINK_COUNT) || []).length !== 1) {
7: throw new Error(
8: `Expected exactly one View check runs link in Quick links for getsentry/${repo}.`
9: );
10: }
11:
12: const match = issueBody.match(CHECK_RUNS_LINK);
13: if (!match?.groups || match.groups.repo !== repo) {
14: throw new Error(
15: `Expected a View check runs link ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/package.json</path>
<type>file</type>
<content>
1: {
2: "name": "publish",
3: "private": true,
4: "version": "0.0.1",
5: "description": "Approval-based publishing system for Sentry",
6: "main": "index.js",
7: "repository": "git@github.com:getsentry/publish.git",
8: "author": "Sentry Open Source <oss@sentry.io>",
9: "license": "Apache-2.0",
10: "volta": {
11: "node": "24.0.0",
12: "yarn": "1.22.22"
13: },
14: "resolutions": {
15: "undici": "^6.23.0"
16: },
17: "scripts": {
18: "generate": "node scripts/generate-publish-issue-title-parser.js",
19: "check:generated": "node scripts/generate-publish-issue-title-parser.js --check",
20: "test": "yarn check:generated && vitest run",
21: "test:watch": "vitest",
22: "lint": "eslint src .github --ignore-pattern '!.github'",
23: "prettier": "prettier --write src"
24: },
25: "devDependencies": {
26: "eslint": ...
"cli\n" false false "cli\r" false false "cli " false false "cli " false false
packages/foo] false packages/foo! false packages/foo^ false packages/foo[ false packages/foo* true packages/[!a]* true packages/[^a]* true
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/scripts/generate-publish-issue-title-parser.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const path = require("path");
3: const peggy = require("peggy");
4: const prettier = require("prettier");
5:
6: const grammarPath = path.join(
7: __dirname,
8: "..",
9: "src",
10: "modules",
11: "publish-issue-title.peggy"
12: );
13: const outputPath = path.join(
14: __dirname,
15: "..",
16: "src",
17: "modules",
18: "publish-issue-title.js"
19: );
20: const grammar = fs.readFileSync(grammarPath, "utf8");
21: const parser = peggy.generate(grammar, {
22: format: "commonjs",
23: grammarSource: "publish-issue-title.peggy",
24: output: "source",
25: });
26:
27: const generatedParser = prettier.format(`/* eslint-disable */\n${parser}`, {
28: filepath: outputPath,
29: });
30:
31: if (process.argv.includes("--check")) {
32: const currentParser = fs.readFileSync(outputPath, "utf8");
33: ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/publish-location.js</path>
<type>file</type>
<content>
1: function resolvePublishLocation({ path, workspaceNames }) {
2: if (!isPublishPath(path)) {
3: throw new Error("Invalid publish path.");
4: }
5:
6: if (path === ".") {
7: return { path };
8: }
9:
10: if (!workspaceNames.every(isWorkspaceName)) {
11: throw new Error(
12: "Craft workspace discovery returned an invalid workspace list."
13: );
14: }
15:
16: const workspace = path.slice(2);
17: if (workspaceNames.includes(workspace)) {
18: return { path: ".", workspace };
19: }
20:
21: return { path };
22: }
23:
24: function isPublishPath(path) {
25: return (
26: typeof path === "string" &&
27: (path === "." ||
28: (path.startsWith("./") &&
29: path
30: .slice(2)
31: .split("/")
32: .every(isSafeWorkspaceSegment)))
33: );
34: }
35:
36: function ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
400: function peg$fail(expected) {
401: if (peg$currPos < peg$maxFailPos) {
402: return;
403: }
404:
405: if (peg$currPos > peg$maxFailPos) {
406: peg$maxFailPos = peg$currPos;
407: peg$maxFailExpected = [];
408: }
409:
410: peg$maxFailExpected.push(expected);
411: }
412:
413: function peg$buildSimpleError(message, location) {
414: return new peg$SyntaxError(message, null, null, location);
415: }
416:
417: function peg$buildStructuredError(expected, found, location) {
418: return new peg$SyntaxError(
419: peg$SyntaxError.buildMessage(expected, found),
420: expected,
421: found,
422: location
423: );
424: }
425:
426: function peg$parsePublishIssueTitle() {
427: let s0, s1, s2, s3, s4, s5, s6, s7, s8;
428:
429: s0 = peg$currPos;
430: if ...
Found 2 matches
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/ci-poller-input.js:
Line 1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/details-from-context.js:
Line 1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/publish-location.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2:
3: const { resolvePublishLocation } = require("../publish-location.js");
4:
5: test("classifies an exact full workspace path without normalizing it", () => {
6: expect(
7: resolvePublishLocation({
8: path: "./packages/CLI",
9: workspaceNames: ["packages/cli", "packages/CLI"],
10: })
11: ).toStrictEqual({ path: ".", workspace: "packages/CLI" });
12: });
13:
14: test("keeps a non-workspace suffix as a checkout path", () => {
15: expect(
16: resolvePublishLocation({
17: path: "./packages",
18: workspaceNames: ["cli"],
19: })
20: ).toStrictEqual({ path: "./packages" });
21: });
22:
23: test("keeps a multi-segment suffix that is not an exact workspace path", () => {
24: expect(
25: resolvePublishLocation({
26: path: "./packages/cli",
27: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/config.test.ts</path>
<type>file</type>
<content>
280: loadWithTargets([
281: { name: 'github', tagPrefix: 'cli@' },
282: { name: 'github', id: 'second' },
283: ]);
284: // A mixed defined/undefined prefix is still ambiguous.
285: expect(getGitTagPrefix()).toBe('cli@');
286: expect(warnSpy).toHaveBeenCalledTimes(1);
287: });
288: });
289:
290: describe('workspaces', () => {
291: let originalCwd: string;
292: const temporaryDirectories: string[] = [];
293:
294: beforeEach(() => {
295: originalCwd = process.cwd();
296: });
297:
298: afterEach(() => {
299: process.chdir(originalCwd);
300: for (const directory of temporaryDirectories.splice(0)) {
301: rmSync(directory, { recursive: true, force: true });
302: }
303: setActiveWorkspace(undefined);
304: vi.restoreAllMocks();
305: });
306:
307: const WS_CONFIG = [
308: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/package.json</path>
<type>file</type>
<content>
1: {
2: "name": "@sentry/craft",
3: "version": "2.31.0-dev.0",
4: "description": "The universal sentry workflow CLI",
5: "main": "dist/craft",
6: "repository": "https://github.com/getsentry/craft",
7: "author": "Sentry",
8: "license": "MIT",
9: "bin": {
10: "craft": "dist/craft",
11: "sentry-craft": "dist/craft"
12: },
13: "devDependencies": {
14: "@aws-sdk/client-lambda": "^3.984.0",
15: "@eslint/js": "^9.17.0",
16: "@google-cloud/storage": "^7.14.0",
17: "@octokit/plugin-retry": "^7.1.2",
18: "@octokit/request-error": "^7.0.0",
19: "@octokit/rest": "^21.0.2",
20: "@sentry/esbuild-plugin": "^2.23.0",
21: "@sentry/node": "^10.27.0",
22: "@types/async": "^3.0.1",
23: "@types/aws4": "^1.5.1",
24: "@types/cli-table": "^0.3.0",
25: "@types/git-url-parse": "^9.0.0",
26: "@types/is-ci": "^2.0.0",
27: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/docs/src/content/docs/targets/github.md</path> <type>file</type> <content> 70: - name: github 71: tagPrefix: "mcp@" 72: ``` 73: 74: Releasing `1.2.3` for each product then produces the tags `cli@1.2.3` / `mcp@1.2.3` on release branches `release/cli/1.2.3` / `release/mcp/1.2.3` — no collisions. 75: 76: ## Release Workspaces 77: 78: Use top-level `workspaces:` to define independently versioned release units in 79: one repository. Keys may be literal paths or glob patterns, and Craft expands 80: patterns to concrete directories relative to `.craft.yml`. Select one concrete 81: path explicitly with `--workspace <path>` or 82: `CRAFT_WORKSPACE`: 83: 84: ```yaml 85: minVersion: 2.29.0 86: github: 87: owner: getsentry 88: repo: toolkit 89: workspaces: 90: packages/*: 91: releaseBranchPrefix: release/cli 92: targets: 93: - name: github 94: tagPrefix: "cli@" 95: tools/mcp: 96: ...
M action.yml M docs/src/content/docs/targets/github.md M src/__tests__/action.test.ts M src/__tests__/config.test.ts M src/commands/prepare.ts M src/config.ts M src/schemas/project_config.ts ba5c4249b29220e90994caddec27ab4212a64316
M .github/workflows/publish.yml M docs/publish-issue-format.md M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/publish-location.js M src/modules/__tests__/publish-workflow.js M src/modules/details-from-context.js M src/modules/publish-issue-title.js M src/modules/publish-issue-title.peggy M src/modules/publish-location.js M src/publish/resolve-location.js c5ebdf0d8061dfb6bcf83055468b9ae3f8617b8f
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/action.test.ts</path>
<type>file</type>
<content>
125: ) {
126: return spawnSync('bash', ['-e', '-c', getActionStep(stepName).run!], {
127: cwd: environment.directory,
128: env: {
129: ...process.env,
130: CRAFT_CALLS: environment.craftCalls,
131: CRAFT_CONFIG_FROM_MERGE_TARGET: '',
132: GITHUB_OUTPUT: environment.output,
133: GIT_CALLS: environment.gitCalls,
134: LC_ALL: locale,
135: MERGE_TARGET: '',
136: PATH: `${environment.binDirectory}:${process.env.PATH}`,
137: PATH_INPUT: pathInput,
138: VERSION: '',
139: WORKSPACE: workspace,
140: },
141: });
142: }
143:
144: afterEach(() => {
145: for (const directory of tempDirectories.splice(0)) {
146: rmSync(directory, { recursive: true, force: true });
147: }
148: });
149:
150: test('forwards workspace input to every Craft command', () => {
151: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/action.test.ts</path>
<type>file</type>
<content>
258: test('uses the full workspace path in publish request titles', () => {
259: const rootEnvironment = createActionEnvironment();
260: const workspaceEnvironment = createActionEnvironment();
261:
262: expect(runRequestPublish('', rootEnvironment).status).toBe(0);
263: expect(runRequestPublish('packages/cli', workspaceEnvironment).status).toBe(
264: 0,
265: );
266:
267: expect(readFileSync(rootEnvironment.ghTitles, 'utf8')).toBe(
268: 'publish: getsentry/toolkit@1.2.3\n',
269: );
270: expect(readFileSync(workspaceEnvironment.ghTitles, 'utf8')).toBe(
271: 'publish: getsentry/toolkit/packages/cli@1.2.3\n',
272: );
273: });
(End of file - total 273 lines)
</content>
Contract 2 — MUST-FIX; Contract 3 — MUST-FIX: Root paths return before validating discovered workspace names. An unsafe discovery result therefore reaches state creation and publish. publish-location.js:6-14; state creation follows at .github/workflows/publish.yml:184-230. Validate workspaceNames before the root-path return.
Contract 4 — MUST-FIX: Configured literals such as packages/foo], packages/foo!, and packages/foo^ pass the glob-segment schema but are not glob patterns, so they bypass concrete-path validation and can become workspace names. project_config.ts:218-241; config.ts:208-211,264-279. Literal keys must use the same safety predicate as concrete workspace paths.
Contract 8 — MUST-FIX: Regression coverage misses both failures above: literal unsafe configured paths are not tested (config.test.ts:495-502), and invalid discovery output is only tested for non-root paths (publish-location.test.js:32-39,53-79). The seven historical Craft unused-variable warnings are non-findings.
Contract 1 — PASS: The action rejects simultaneous path/workspace inputs and builds the full workspace-path title without normalizing it. action.yml:80-85,194-195,238-241,268.
Contract 5 — PASS: Acceptance removes stale ci-ready, publishes only on a fresh open accepted ci-ready event without pending/failed labels, and the poller adds that label after removing pending. .github/workflows/publish.yml:42-58,98-114; .github/workflows/ci-poller.yml:203-206.
Contract 6 — PASS: The target checkout uses the resolved revision; Craft receives that SHA through --rev; state is outside __repo__ and keyed by checkout identity, path hash, workspace, and version. .github/workflows/publish.yml:159-179,212-230,242-256.
Contract 7 — PASS: The grammar removes legacy JSON workspace syntax, and the documentation defines path-only titles. src/modules/publish-issue-title.peggy:9-14; docs/publish-issue-format.md:10-39. ...
Date: Sep 4, 2026 * 🔴 (08:26) User directive: workspace glob keys must never escape base before filesystem access. * 🔴 (08:26) User requires a final strict read-only adversarial audit: do not edit files, run formatters/generators, or change git state. * 🟡 [requested-audit] (08:26) User requested inspection of complete current uncommitted diffs in both worktrees: `/home/byk/Code/getsentry/craft…