Dashboard › craft › Session 12TZZpomX65u
12TZZpomX65uZ0sdHPerform a read-only adversarial correctness/security review of the uncommitted changes in /home/byk/Code/getsentry/craft-workspace-action-propagation on feat/workspace-action-propagation, compared with origin/master. Do NOT modify files. Scope: action.yml and .github/workflows/release.yml workspace propagation. Verify exact backward compatibility without a workspace; action input and manual/reusable forwarding; shell quoting/injection; title grammar compatibility with publish controller: legacy publish: getsentry/repo/path@version, workspace publish: getsentry/repo/path [workspace: <JSON string>] @version; targets and prepare selection; workspace newline/unsafe behavior must fail before prepare side effects; dry run; issues/state identity collision. Inspect relevant source/test context too. Report findings first by severity with exact file:line evidence, PASS/FAIL per concern, testing gaps, and a clear MERGE/DO-NOT-MERGE verdict. No rubber stamp.
<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 .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 (32 entries) </entries>
M .github/workflows/release.yml M action.yml feat/workspace-action-propagation 21270a1d12a60b461c51fba93bd076b90d9cf4af .github/workflows/release.yml | 9 +++++++++ action.yml | 30 +++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) .github/workflows/release.yml | 9 +++++++++ action.yml | 30 +++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4e6c11e..323e8ba 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,142 +1,151 @@
name: Release
concurrency: ${{ github.workflow }}-${{ github.ref }}
on:
# For Craft's own releases (dogfooding)
workflow_dispatch:
inputs:
version:
description: Version to release
required: true
default: 'auto'
force:
description: Force a release even when there are release-blockers (optional)
required: false
+ workspace:
+ description: Named Craft release workspace to prepare and publish
+ required: false
# For external repos to call this workflow
workflow_call:
inputs:
version:
description: Version to release (semver, bump type, or "auto")
type: string
required: false
force:
description: Force a release even when there are ...
<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. ...
Found 100 matches (more matches available)
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/workspaces.ts:
Line 15: /** Information about a workspace package */
Line 25: /** Dependencies that are also workspace packages */
Line 26: workspaceDependencies: string[];
Line 29: /** Result of workspace discovery */
Line 31: /** The type of workspace manager detected */
Line 37: /** Structure of pnpm-workspace.yaml */
Line 45: workspaces?: string[] | { packages?: string[] };
Line 93: * Extract workspaces array from package.json workspaces field
Line 97: workspaces: string[] | { packages?: string[] } | undefined,
Line 99: if (!workspaces) {
Line 102: if (Array.isArray(workspaces)) {
Line 103: return workspaces;
Line 105: return workspaces.packages || [];
Line 115: // First: collect all workspace package names and locations
Line 116: const workspaceLocations: Array<{
Line 120: const workspaceNames = ...
Found 100 matches (more matches available) /home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/publishState.ts: Line 82: export function getPublishStateFilename( Line 119: getPublishStateFilename(version, githubConfig, cwd, workspace), /home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/helpers.ts: Line 25: * Extracts the `--workspace` selection from the raw argv (or the Line 33: * Supports `--workspace foo` and `--workspace=foo`. It uses Node's built-in Line 35: * `--workspace --dry-run`) is never mistaken for a workspace name. The CLI Line 42: export function extractWorkspaceSelection( Line 65: // `--workspace=-foo` is a valid inline value; `--workspace --foo` is not. /home/byk/Code/getsentry/craft-workspace-action-propagation/src/schemas/project_config.ts: Line 227: * When present, a release run must select one via `--workspace <name>` ...
<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: ...
<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. ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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: Workspace,
20: } from './schemas/project_config';
21: import { ConfigurationError } from './utils/errors';
22: import { isCompiledGitHubAction } from './utils/detection';
23: import {
24: getPackageVersion,
25: parseVersion,
26: versionGreaterOrEqualThan,
27: SemVer,
28: } from './utils/version';
29: // Note: We import getTargetByName lazily in ...
<path>/home/byk/Code/getsentry</path> <type>directory</type> <entries> cli-api-1423/ cli-wt/ cli/ codecov-action/ craft-vercel-prebuilt-output/ craft-vercel-project-config/ craft-workspace-action-propagation/ craft-workspaces-schema/ craft/ devinfra-coder-infra/ devinfra-deployment-service/ getsentry/ ops/ package_grant_publish_repo/ publish-workspace-acceptance/ publish/ security-as-code/ self-hosted/ sentry-api-schema/ sentry-docs/ sentry-forge/ sentry-go/ sentry-javascript/ sentry-orbital/ sentry-python/ sentry-release-registry/ sentry-wizard/ sentry/ snuba/ spotlight/ sqlish/ static-sites/ stripe-app/ symbolic/ (34 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 src/ vitest.config.js yarn.lock (14 entries) </entries>
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/AGENTS.md</path> <type>file</type> <content> 1: # AGENTS.md - Coding Agent Guidelines 2: 3: This document provides guidelines for AI coding agents working in this repository. 4: 5: ## Project Overview 6: 7: This is the **Sentry Publish Repository** - a GitHub Actions-based approval system for publishing releases across multiple Sentry projects using [Craft](https://github.com/getsentry/craft). It's a pure JavaScript (Node.js 24.0.0) project using CommonJS modules. 8: 9: ## Build/Lint/Test Commands 10: 11: ### Package Manager 12: 13: - **Yarn 1.22.22** (classic) - Do NOT use npm 14: - Version management via Volta (pinned in package.json) 15: 16: ### Commands 17: 18: ```bash 19: # Install dependencies 20: yarn install 21: 22: # Run all tests 23: yarn test 24: 25: # Run tests in watch mode 26: yarn test:watch 27: 28: # Run a single test file 29: yarn test src/modules/__tests__/update-issue.js 30: 31: # Run tests ...
M .github/workflows/publish.yml M src/modules/__tests__/details-from-context.js M src/modules/details-from-context.js feat/workspace-acceptance 83d210b build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174) .github/workflows/publish.yml | 9 ++- src/modules/__tests__/details-from-context.js | 97 ++++++++++++++++++++++++++- src/modules/details-from-context.js | 24 +++++-- 3 files changed, 123 insertions(+), 7 deletions(-)
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/details-from-context.js</path>
<type>file</type>
<content>
1: /**
2: * Matches the entire "Targets" section of a github publish issue body.
3: */
4: const TARGETS_SECTION_PARSER_REGEX =
5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
6:
7: /**
8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
9: * The "id" of the targets is captured within a capture group.
10: */
11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim;
12:
13: /**
14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
15: * The "id" of the targets is captured within a capture group.
16: */
17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim;
18:
19: async function detailsFromContext({ context }) {
20: if (!context || ...
<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/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/publish-workspace-acceptance/src/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
1: import { test, expect } from "vitest";
2:
3: const { detailsFromContext } = require("../details-from-context.js");
4:
5: const inputsArgs = {
6: context: {
7: repo: { owner: "getsentry", repo: "publish" },
8: payload: {
9: issue: {
10: number: "223",
11: title: "publish: getsentry/sentry@21.3.1",
12: body: `
13: Requested by: @BYK
14:
15: Merge target: custom-branch
16:
17: Quick links:
18: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
19: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
20:
21: Assign the **accepted** label to this issue to approve the release. ...
Found 100 matches (more matches available)
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/system.ts:
Line 109: /** Force the process to run in dry-run mode */
Line 145: logger.info('[dry-run] Not spawning process:', `${command} ${argsString}`);
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/registry.ts:
Line 100: // reportError throws in non-dry-run mode, but TypeScript doesn't know that
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/helpers.ts:
Line 35: * `--workspace --dry-run`) is never mistaken for a workspace name. The CLI
Line 76: 'dry-run'?: boolean;
Line 83: 'dry-run': boolean;
Line 89: 'dry-run': false,
Line 95: if (argv['dry-run'] !== undefined) {
Line 96: GLOBAL_FLAGS['dry-run'] = argv['dry-run'];
Line 110: return GLOBAL_FLAGS['dry-run'];
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/githubApi.ts:
Line 118: // Wrap ...
No files found
<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 ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/prepare.ts</path>
<type>file</type>
<content>
800: logger.warn(
801: `Loading .craft.yml from remote branch "${argv.configFrom}". ` +
802: `Its preReleaseCommand will be executed by Craft — ensure the ` +
803: `branch is trusted.`,
804: );
805: try {
806: await git.fetch([argv.remote, argv.configFrom]);
807: const configContent = await git.show([
808: `${argv.remote}/${argv.configFrom}:${CONFIG_FILE_NAME}`,
809: ]);
810: loadConfigurationFromString(configContent);
811: } catch (error: any) {
812: throw new ConfigurationError(
813: `Failed to load ${CONFIG_FILE_NAME} from branch "${argv.configFrom}": ${error.message}`,
814: );
815: }
816: }
817:
818: // Get repo configuration
819: const config = getConfiguration();
820: const githubConfig = await getGlobalGitHubConfig();
821:
822: const defaultBranch = ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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 ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/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: ...
<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/vitest.config.mts</path>
<type>file</type>
<content>
1: import { defineConfig } from 'vitest/config';
2:
3: export default defineConfig({
4: test: {
5: globals: true,
6: environment: 'node',
7: include: ['src/**/*.test.ts'],
8: exclude: ['**/node_modules/**', '**/dist/**', '**/fixtures/**'],
9: testTimeout: 30000,
10: alias: {
11: '^marked$': 'marked/lib/marked.umd.js',
12: },
13: },
14: });
(End of file - total 14 lines)
</content>
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/config.test.ts
Found 83 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/versionBump.test.ts:
Line 218: describe('workspace:* handling (issue #804)', () => {
Line 220: * Set up a workspace monorepo:
Line 221: * /package.json (workspaces: ["packages/*"])
Line 223: * /packages/app/package.json (@scope/app, 0.0.1, deps: core @ workspace:*)
Line 237: workspaces: ['packages/*'],
Line 258: dependencies: { '@scope/core': 'workspace:*' },
Line 273: // Simulate `npm version --workspaces`: write the new version to every
Line 277: if (args.includes('--workspaces')) {
Line 284: workspaces: ['packages/*'],
Line 296: dependencies: { '@scope/core': 'workspace:*' },
Line 300: 'npm error code EUNSUPPORTEDPROTOCOL\nnpm error Unsupported URL Type "workspace:": workspace:*',
Line 315: const workspaceCalls = ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/config.test.ts</path>
<type>file</type>
<content>
220: { name: 'github', id: 'second' },
221: ]);
222: // A mixed defined/undefined prefix is still ambiguous.
223: expect(getGitTagPrefix()).toBe('cli@');
224: expect(warnSpy).toHaveBeenCalledTimes(1);
225: });
226: });
227:
228: describe('workspaces', () => {
229: afterEach(() => {
230: setActiveWorkspace(undefined);
231: vi.restoreAllMocks();
232: });
233:
234: const WS_CONFIG = [
235: `minVersion: ${WORKSPACES_MIN_VERSION}`,
236: 'github:',
237: ' owner: getsentry',
238: ' repo: toolkit',
239: 'changelog: CHANGELOG.md',
240: 'workspaces:',
241: ' cli:',
242: ' releaseBranchPrefix: release/cli',
243: ' github:',
244: ' projectPath: cli',
245: ' targets:',
246: ' - name: github',
247: ' tagPrefix: "cli@"',
248: ' mcp:',
249: ' ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/prepare.ts</path>
<type>file</type>
<content>
740:
741: const calverFormat =
742: config.versioning?.calver?.format ?? DEFAULT_CALVER_CONFIG.format;
743:
744: return calculateCalVer(git, {
745: offset: calverOffset,
746: format: calverFormat,
747: });
748: }
749:
750: // Handle automatic version detection or version bump types
751: if (version === 'auto' || isBumpType(version)) {
752: if (!requiresMinVersion(AUTO_VERSION_MIN_VERSION)) {
753: const featureName = isBumpType(version)
754: ? 'Version bump types'
755: : 'Auto-versioning';
756: throw new ConfigurationError(
757: `${featureName} requires minVersion >= ${AUTO_VERSION_MIN_VERSION} in .craft.yml. ` +
758: 'Please update your configuration or specify the version explicitly.',
759: );
760: }
761:
762: const latestTag = await getLatestTag(git, ...
<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/craft-workspace-action-propagation/src/commands/publish.ts</path>
<type>file</type>
<content>
650: }
651: }
652:
653: const revision = await git.revparse('HEAD');
654: logger.debug('Revision to publish: ', revision);
655:
656: const statusProvider = await getStatusProviderFromConfig();
657: const artifactProvider = await getArtifactProviderFromConfig();
658:
659: // Check status of all CI builds linked to the revision
660: await checkRevisionStatus(statusProvider, revision, argv.noStatusCheck);
661:
662: await printRevisionSummary(artifactProvider, revision);
663:
664: await checkRequiredArtifacts(artifactProvider, revision, config.requireNames);
665:
666: // Find targets
667: let targetsToPublish: Set<string> = new Set(
668: (typeof argv.target === 'string' ? [argv.target] : argv.target) || [
669: SpecialTarget.All,
670: ],
671: );
672:
673: // Treat "all"/"none" specially
674: for (const ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/__tests__/publishState.test.ts</path>
<type>file</type>
<content>
1: import { describe, test, expect, beforeEach, afterEach } from 'vitest';
2: import { homedir } from 'os';
3: import { join } from 'path';
4:
5: import {
6: getCraftStateDir,
7: getPublishStateFilename,
8: getPublishStatePath,
9: } from '../publishState';
10:
11: describe('publishState', () => {
12: const savedEnv = { ...process.env };
13:
14: beforeEach(() => {
15: delete process.env.XDG_STATE_HOME;
16: });
17:
18: afterEach(() => {
19: process.env = { ...savedEnv };
20: });
21:
22: describe('getCraftStateDir', () => {
23: test('defaults to $HOME/.local/state/craft when XDG_STATE_HOME is unset', () => {
24: expect(getCraftStateDir()).toBe(
25: join(homedir(), '.local', 'state', 'craft'),
26: );
27: });
28:
29: test('honours XDG_STATE_HOME when set', () => {
30: ...
RUN v4.1.8 /home/byk/Code/getsentry/craft-workspace-action-propagation
Test Files 3 passed (3)
Tests 99 passed (99)
Start at 15:07:32
Duration 1.72s (transform 706ms, setup 0ms, import 2.36s, tests 105ms, environment 0ms)
<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: "test": "vitest run",
19: "test:watch": "vitest",
20: "lint": "eslint src .github --ignore-pattern '!.github'",
21: "prettier": "prettier --write src"
22: },
23: "devDependencies": {
24: "eslint": "^8.9.0",
25: "eslint-config-prettier": "^8.3.0",
26: "eslint-plugin-yml": "^0.13.0",
27: "prettier": "^2.2.1",
28: "vitest": "^4.1.0"
29: },
30: "dependencies": {
31: ...
yarn run v1.22.22 info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. error Command "vitest" not found.
yarn run v1.22.22 $ vitest run src/modules/__tests__/details-from-context.js info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. /bin/sh: 1: vitest: not found error Command failed with exit code 127.
{"repo":"toolkit","path":".","version":"1.2.3","dry_run":"","merge_target":"","workspace":"cli [preview] \"next\""}
{"repo":"repo","path":"./path","version":"1.2.3","dry_run":"","merge_target":""}
--dry-run [{"kind":"positional","index":0,"value":"prepare"},{"kind":"option","name":"workspace","rawName":"--workspace","index":1,"value":"--dry-run","inlineValue":false}]
--config-from=untrusted [{"kind":"positional","index":0,"value":"prepare"},{"kind":"option","name":"workspace","rawName":"--workspace","index":1,"value":"--config-from=untrusted","inlineValue":false}]
-cli [{"kind":"positional","index":0,"value":"prepare"},{"kind":"option","name":"workspace","rawName":"--workspace","index":1,"value":"-cli","inlineValue":false}]
node:internal/modules/cjs/loader:1520
throw err;
^
Error: Cannot find module 'yargs-parser'
Require stack:
- /home/byk/Code/getsentry/craft-workspace-action-propagation/[eval]
at Module._resolveFilename (node:internal/modules/cjs/loader:1517:15)
at wrapResolveFilename (node:internal/modules/cjs/loader:1071:27)
at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1095:10)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1122:12)
at Module._load (node:internal/modules/cjs/loader:1294:5)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.require (node:internal/modules/cjs/loader:1617:12)
at require (node:internal/modules/helpers:153:16)
at [eval]:1:13
at runScriptInThisContext (node:internal/vm:219:10) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/home/byk/Code/getsentry/craft-workspace-action-propagation/[eval]'
]
}
Node.js v24.18.0
--dry-run {"_":["prepare"],"workspace":"","dry-run":true,"dryRun":true,"$0":""}
--config-from=untrusted {"_":["prepare"],"workspace":"","config-from":"untrusted","configFrom":"untrusted","$0":""}
-cli {"_":["prepare"],"workspace":"","c":true,"l":true,"i":true,"$0":""}
{"title":"publish: getsentry/toolkit [workspace: \"x\\\"] @999.0.0 # $(printf pwned)\"] @1.2.3","result":{"repo":"toolkit","path":".","version":"1.2.3","dry_run":"","merge_target":"","workspace":"x\"] @999.0.0 # $(printf pwned)"}}
YAML parse passed
M .github/workflows/release.yml M action.yml 9 0 .github/workflows/release.yml 27 3 action.yml
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/.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: workspace:
16: description: Named Craft release workspace to prepare and publish
17: required: false
18:
19: # For external repos to call this workflow
20: workflow_call:
21: inputs:
22: version:
23: description: Version to release (semver, bump type, or "auto")
24: type: string
25: required: false
26: force:
27: description: Force a release even when there are ...
# Reuse workflows Learn how to avoid duplication when creating a workflow by reusing existing workflows. ## Creating a reusable workflow Reusable workflows are YAML-formatted files, very similar to any other workflow file. As with other workflow files, you locate reusable workflows in the `.github/workflows` directory of a repository. Subdirectories of the `workflows` directory are not supported. For a workflow to be reusable, the values for `on` must include `workflow_call`: ```yaml on: workflow_call: ``` ## Using inputs and secrets in a reusable workflow You can define inputs and secrets, which can be passed from the caller workflow and then used within the called workflow. There are three stages to using an input or a secret in a reusable workflow. 1. In the reusable workflow, use the `inputs` and `secrets` keywords to define inputs or secrets that will be passed from a caller workflow. ...
# Contexts reference Find information about contexts available in GitHub Actions workflows, including available properties, access methods, and usage examples. ## Available contexts | Context name | Type | Description | | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `github` | `object` | Information about the workflow run. For more information, see [`github` context](#github-context). | | `env` | `object` | Contains variables set in a workflow, job, or step. For more information, see [`env` context](#env-context). ...
diff --git a/dev/stdin b/src/modules/details-from-context.js
index 0000000..492311c 100644
--- a/dev/stdin
+++ b/src/modules/details-from-context.js
@@ -0,0 +1,80 @@
+/**
+ * Matches the entire "Targets" section of a github publish issue body.
+ */
+const TARGETS_SECTION_PARSER_REGEX =
+ /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
+
+/**
+ * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
+ * The "id" of the targets is captured within a capture group.
+ */
+const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim;
+
+/**
+ * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
+ * The "id" of the targets is captured within a capture group.
+ */
+const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim;
+
+async function detailsFromContext({ context }) {
+ if (!context || !context.payload || ...
chore/fix-security-advisories * feat/workspace-acceptance fix/add-workflow-permissions main security/dual-write-publish-state + security/remove-legacy-publish-state-write remotes/origin/HEAD -> origin/main remotes/origin/add-agents-md remotes/origin/chore/fix-security-advisories remotes/origin/ci/switch-to-sentry-internal-app remotes/origin/docs/deprecate-action-prepare-release remotes/origin/docs/mermaid-release-flow remotes/origin/fix/add-packages-write-permission remotes/origin/fix/add-workflow-permissions remotes/origin/fix/auto-approve-token remotes/origin/fix/undici-security-vulnerability remotes/origin/fix/update-node-version-in-workflows remotes/origin/main remotes/origin/migrate-secrets remotes/origin/ref/remove-unused-requester-field remotes/origin/refactor/auto-approve-repo-list remotes/origin/remove/retract-command remotes/origin/security/dual-write-publish-state ...
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cff6bc3..37519e9 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -153,136 +153,143 @@ jobs:
fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v9' ||
fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v8' ||
fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v7' ||
fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'master' ||
fromJSON(steps.inputs.outputs.result).repo == 'sentry-python' && fromJSON(steps.inputs.outputs.result).merge_target == 'alpha' ||
fromJSON(steps.inputs.outputs.result).repo == 'sentry-wizard' && ...
M .github/workflows/publish.yml M src/modules/__tests__/details-from-context.js M src/modules/__tests__/update-issue.js M src/modules/details-from-context.js M src/modules/update-issue.js
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const crypto = require("crypto");
3: const path = require("path");
4: const {
5: TARGETS_SECTION_PARSER_REGEX,
6: TARGETS_PARSER_REGEX,
7: } = require("./details-from-context");
8:
9: const CONTAINER_REPO_PATH = "/github/workspace/__repo__";
10:
11: function sanitiseForFilename(value) {
12: return value
13: .toLowerCase()
14: .replace(/[^a-z0-9._-]+/g, "_")
15: .replace(/^_+|_+$/g, "");
16: }
17:
18: function getCraftStateFilePath({
19: repo,
20: path: publishPath,
21: version,
22: workspace,
23: }) {
24: const containerCwd = path.posix.resolve(CONTAINER_REPO_PATH, publishPath);
25: if (
26: containerCwd !== CONTAINER_REPO_PATH &&
27: !containerCwd.startsWith(`${CONTAINER_REPO_PATH}/`)
28: ) {
29: throw new Error(`Invalid publish issue path: '${publishPath}'`);
30: }
31:
32: const ...
Found 13 matches
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/update-issue.js:
Line 136: test("isolates state files for workspaces and canonical paths", () => {
Line 142: workspace: "cli/v2",
Line 145: ".craft-state/craft/publish-state-getsentry-toolkit-21cf7beaeda4-workspace-Y2xpL3Yy-1.2.3.json"
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/details-from-context.js:
Line 104: test("parses a human-readable workspace from the title", async () => {
Line 111: title: 'publish: getsentry/toolkit [workspace: "cli/v2"] @1.2.3',
Line 122: workspace: "cli/v2",
Line 126: test("parses escaped workspace characters from the title", async () => {
Line 134: 'publish: getsentry/toolkit [workspace: "cli [preview] \\"next\\""] @1.2.3',
Line 142: expect(result.workspace).toBe('cli [preview] "next"');
Line 162: test("rejects an empty or control-character workspace", async () => ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/details-from-context.js</path>
<type>file</type>
<content>
19: async function detailsFromContext({ context }) {
20: if (!context || !context.payload || !context.payload.issue) {
21: throw new Error("Issue context is not defined");
22: }
23:
24: const titleParser =
25: /^publish: (?:getsentry\/)?(?<repo>[A-Za-z0-9_.-]+)(?<path>\/[\w./-]+)?(?: \[workspace: (?<workspace>"(?:[^"\\]|\\.)*")\] )?@(?<version>[\w.+-]+)$/;
26: const titleMatch = context.payload.issue.title.match(titleParser);
27: if (!titleMatch || !titleMatch.groups) {
28: throw new Error(
29: `Invalid publish issue title: '${context.payload.issue.title}'`
30: );
31: }
32: const { workspace: workspaceJson, ...titleDetails } = titleMatch.groups;
33: let workspace = "";
34: if (workspaceJson) {
35: workspace = JSON.parse(workspaceJson);
36: if (!workspace || /[\p{Cc}\p{Zl}\p{Zp}]/u.test(workspace)) {
37: ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
95: dry_run: "",
96: merge_target: "",
97: path: ".",
98: repo: "sentry",
99: targets: ["github", "docker[latest]"],
100: version: "21.3.1",
101: });
102: });
103:
104: test("parses a human-readable workspace from the title", async () => {
105: const result = await detailsFromContext({
106: context: {
107: repo: { owner: "getsentry", repo: "publish" },
108: payload: {
109: issue: {
110: number: "123",
111: title: 'publish: getsentry/toolkit [workspace: "cli/v2"] @1.2.3',
112: body: "Requested by: @example",
113: labels: [],
114: },
115: },
116: },
117: });
118:
119: expect(result).toMatchObject({
120: repo: "toolkit",
121: version: "1.2.3",
122: workspace: "cli/v2",
123: });
124: });
125:
126: test("parses escaped ...
action.yml:184-186 and action.yml:204-208 build --workspace and its value as separate argv entries. A workspace value such as --config-from=untrusted becomes a Craft option, not a workspace name.
Craft explicitly rejects separate option-like workspace values in src/utils/helpers.ts:63-68; yargs then parses the injected flag. I reproduced:
prepare --workspace --config-from=untrusted
→ workspace: ""
→ config-from: "untrusted"
This can reach the remote-config fetch at src/commands/prepare.ts:796-810, which can later run that config’s preReleaseCommand at :902-910. It also affects target selection.
Use one inline argument in both steps, e.g. CRAFT_ARGS+=("--workspace=$WORKSPACE"), so Craft receives option-like names as values as its parser intends.
action.yml:171-174 rejects only CR/LF. It permits tabs, other control characters, and Unicode line separators. The paired controller rejects all control characters and Unicode line/paragraph separators at publish-workspace-acceptance/src/modules/details-from-context.js:34-40.
A configured workspace with a tab or U+2028 can therefore pass the action check, reach git remote set-head (action.yml:176-177) and craft prepare (:194), then create a publish issue that the controller rejects. This fails the requirement to reject unsafe workspace names before prepare side effects.
Validate the same character class as the controller before line 176.
action.yml:258-265 emits the new workspace title grammar. The compatible parser and workspace state forwarding currently exist only as uncommitted edits in /home/byk/Code/getsentry/publish-workspace-acceptance:
src/modules/details-from-context.js:25-40Date: Aug 25, 2026 * 🟡 (15:24) Inspected `/home/byk/Code/getsentry/craft-workspace-action-propagation/.github/workflows/release.yml:1-151`: `Release` workflow has concurrency `${{ github.workflow }}-${{ github.ref }}` and supports both `workflow_dispatch` and `workflow_call`. * 🟡 (15:24) `workflow_dispatch` inputs include required `version` (default `'auto'`), optional `force`, and optional `wo…
Date: Aug 25, 2026 * 🟡 (14:54) Inspected `/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/prepare.ts:1-130`: command is `prepare [NEW-VERSION]`, aliases are `p`, `prerelease`, `prepublish`, `prepare`, `release`, and description is `🚢 Prepare a new release branch`. * 🟡 (14:54) `prepare` positional `NEW-VERSION` accepts semver (example `"1.2.3"`), bump types (`"major"`, …
Date: Aug 25, 2026 * 🔴 (14:53) User directed that CalVer changelog content destined for publish issues replace `@`-mentions with bold formatting, preventing contributor notifications; committed `CHANGELOG.md` content must retain original `@`-mentions. * 🟡 (14:53) Inspected `/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/prepare.ts:800-1003`. * 🟡 (14:53) `prepareMain` …
Date: Aug 25, 2026 * 🟡 (14:39) Inspected `/home/byk/Code/getsentry/craft-workspace-action-propagation/src/config.ts` lines 1-270. `CONFIG_FILE_NAME = '.craft.yml'`; `DEFAULT_RELEASE_BRANCH_NAME = 'release'`; `WORKSPACES_MIN_VERSION = '2.29.0'`. * 🟡 (14:39) `src/config.ts:69-95` stores selected workspace in `_activeWorkspaceName: string | undefined`; `setActiveWorkspace(name)` sets it and invali…
Date: Aug 25, 2026 * 🔴 (14:38) User stated `--workspace --dry-run` must never be mistaken for a workspace name. * 🟡 (14:38) `src/utils/helpers.ts:24-72` documents and implements `extractWorkspaceSelection(argv, env = process.env)`: it resolves `--workspace foo` and `--workspace=foo` from raw argv before yargs builders/config access, using `node:util` `parseArgs()` tokenization with `options: { …
Date: Aug 25, 2026 * 🟡 [requested-security-review] (14:30) User requested a read-only adversarial correctness/security review of uncommitted changes in `/home/byk/Code/getsentry/craft-workspace-action-propagation` on branch `feat/workspace-action-propagation`, compared with `origin/master`; explicitly directed that no files be modified. * 🟡 [requested-security-review] (14:30) Review scope reque…