Dashboard › craft › Session 0qxwIy69SGpB
0qxwIy69SGpBZQKbJPerform the final strict READ-ONLY adversarial audit. Do NOT edit files, run formatters/generators, or change git state. Review complete current uncommitted diffs in BOTH exact worktrees:
The prior audit found and the implementation claims to fix:
A) resolvePublishLocation() now validates workspaceNames BEFORE its root-path return, so invalid discovery must fail for root and non-root releases before state/publish side effects.
B) Craft configured literal workspace keys with glob-only invalid characters (], !, ^) must fail. Safe actual glob patterns including *, ?, [cm], [!a], [^a], and ** remain accepted. All literal/concrete workspace names must obey nonempty ASCII [A-Za-z0-9_.-]+ segments excluding ., .., __proto__, and leading -.
Audit all contracts:
Return ONLY severity-ordered findings with exact file:line citations. Include PASS/CONCERN/MUST-FIX for each numbered contract. End exactly MERGE or DO-NOT-MERGE. No generic praise and no empty result.
<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/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>
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
action.yml | 4 +-
docs/src/content/docs/targets/github.md | 20 ++--
src/__tests__/action.test.ts | 59 ++++++++---
src/__tests__/config.test.ts | 153 ++++++++++++++++++++++++++-
src/commands/prepare.ts | 3 +-
src/config.ts | 177 +++++++++++++++++++++++++++++---
src/schemas/project_config.ts | 20 ++++
7 files changed, 395 insertions(+), 41 deletions(-)
diff --git a/action.yml b/action.yml
index 94f3158..84377a8 100644
--- a/action.yml
+++ b/action.yml
@@ -81,8 +81,8 @@ runs:
echo "::error::The path and workspace inputs cannot be used together."
exit 1
fi
- if [[ -n "$WORKSPACE" ]] && ! node -e ...
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 .github/workflows/publish.yml | 7 +- docs/publish-issue-format.md | 52 +++--- src/modules/__tests__/ci-poller-input.js | 4 +- src/modules/__tests__/details-from-context.js | 191 ++-------------------- src/modules/__tests__/publish-location.js | 94 +++++------ src/modules/__tests__/publish-workflow.js | 18 ++- src/modules/details-from-context.js | 27 +--- src/modules/publish-issue-title.js | 225 +++----------------------- src/modules/publish-issue-title.peggy | 19 ...
<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/craft-workspace-action-propagation/action.yml</path>
<type>file</type>
<content>
175: - name: Craft Prepare
176: id: craft
177: shell: bash
178: env:
179: CRAFT_LOG_LEVEL: Debug
180: CRAFT_CONFIG_FROM_MERGE_TARGET: ${{ inputs.craft_config_from_merge_target }}
181: MERGE_TARGET: ${{ inputs.merge_target }}
182: VERSION: ${{ inputs.version }}
183: WORKSPACE: ${{ inputs.workspace }}
184: working-directory: ${{ inputs.path }}
185: run: |
186: # Ensure we have origin/HEAD set
187: git remote set-head origin --auto
188:
189: # Build command with optional flags
190: CRAFT_ARGS=()
191: if [[ "$CRAFT_CONFIG_FROM_MERGE_TARGET" == 'true' && -n "$MERGE_TARGET" ]]; then
192: CRAFT_ARGS=(--config-from "$MERGE_TARGET")
193: fi
194: if [[ -n "$WORKSPACE" ]]; then
195: CRAFT_ARGS+=("--workspace=$WORKSPACE")
196: ...
<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: import { globSync, hasMagic } from 'glob';
4:
5: import { load } from 'js-yaml';
6: import GitUrlParse from 'git-url-parse';
7: import { createGitClient } from './utils/git';
8: import { ZodError } from 'zod';
9:
10: import { logger } from './logger';
11: import {
12: CraftProjectConfig,
13: CraftProjectConfigSchema,
14: GitHubGlobalConfig,
15: ArtifactProviderName,
16: StatusProviderName,
17: TargetConfig,
18: ChangelogPolicy,
19: VersioningPolicy,
20: Workspace,
21: } from './schemas/project_config';
22: import { ConfigurationError } from './utils/errors';
23: import { isCompiledGitHubAction } from './utils/detection';
24: import {
25: getPackageVersion,
26: parseVersion,
27: versionGreaterOrEqualThan,
28: SemVer,
29: } from './utils/version';
30: // ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/schemas/project_config.ts</path>
<type>file</type>
<content>
180: */
181: const releaseUnitFields = {
182: github: GitHubGlobalConfigSchema.optional(),
183: targets: z.array(TargetConfigSchema).optional(),
184: preReleaseCommand: z.string().optional(),
185: postReleaseCommand: z.string().optional(),
186: releaseBranchPrefix: z.string().optional(),
187: changelog: ChangelogConfigSchema.optional(),
188: changelogPolicy: z.enum(['auto', 'simple', 'none']).optional(),
189: requireNames: z.array(z.string()).optional(),
190: statusProvider: BaseStatusProviderSchema.optional(),
191: artifactProvider: BaseArtifactProviderSchema.optional(),
192: versioning: VersioningConfigSchema.optional(),
193: /**
194: * Do not merge the release branch after publishing.
195: * Defaults to true for compiled GitHub Actions (Node.js actions with dist/ folder).
196: */
197: noMerge: ...
<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/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 (!workspaceNames.every(isWorkspaceName)) {
7: throw new Error(
8: "Craft workspace discovery returned an invalid workspace list."
9: );
10: }
11:
12: if (path === ".") {
13: return { path };
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/details-from-context.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const { isPublishPath } = require("./publish-location");
3:
4: /**
5: * Matches the entire "Targets" section of a github publish issue body.
6: */
7: const TARGETS_SECTION_PARSER_REGEX =
8: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
9:
10: /**
11: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
12: * The "id" of the targets is captured within a capture group.
13: */
14: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim;
15:
16: /**
17: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
18: * The "id" of the targets is captured within a capture group. ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/publish-issue-title.peggy</path>
<type>file</type>
<content>
1: {
2: function join(characters) {
3: return characters.join("");
4: }
5: }
6:
7: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
8: // the controller resolves the complete suffix as a workspace after checking
9: // out the CI-approved revision.
10: PublishIssueTitle
11: = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. {
12: return {
13: repo,
14: path: path || "",
15: version,
16: };
17: }
18:
19: Repository
20: = characters:RepositoryCharacter+ { return join(characters); }
21:
22: RepositoryCharacter
23: = [A-Za-z0-9_.-]
24:
25: Path
26: = segments:("/" segment:PathSegment { return `/${segment}`; })+ { return join(segments); }
27:
28: PathSegment
29: = characters:RepositoryCharacter+ { return join(characters); }
30:
31: ...
<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": ...
<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/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/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/craft-workspace-action-propagation/src/commands/prepare.ts</path>
<type>file</type>
<content>
730: }
731:
732: // Build CalVer config with overrides
733: const calverOffset =
734: options.calverOffset ??
735: (process.env.CRAFT_CALVER_OFFSET
736: ? Number.parseInt(process.env.CRAFT_CALVER_OFFSET, 10)
737: : undefined) ??
738: config.versioning?.calver?.offset ??
739: DEFAULT_CALVER_CONFIG.offset;
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: ? ...
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 20 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/helpers.ts:
Line 42: export function extractWorkspaceSelection(
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/__tests__/helpers.test.ts:
Line 10: extractWorkspaceSelection,
Line 37: describe('extractWorkspaceSelection', () => {
Line 42: extractWorkspaceSelection(['publish', '1.0.0'], NO_ENV),
Line 48: extractWorkspaceSelection(['publish', '--workspace', 'cli'], NO_ENV),
Line 53: expect(extractWorkspaceSelection(['--workspace=mcp'], NO_ENV)).toBe('mcp');
Line 57: expect(extractWorkspaceSelection(['--workspace=-cli'], NO_ENV)).toBe(
Line 61: extractWorkspaceSelection(['--workspace', '-cli'], NO_ENV),
Line 67: extractWorkspaceSelection(['--workspace', 'cli'], {
Line 75: extractWorkspaceSelection(
Line 84: extractWorkspaceSelection(
Line 93: extractWorkspaceSelection(['publish'], {
Line ...
<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/__tests__/config.test.ts</path>
<type>file</type>
<content>
1: import { describe, test, expect, vi, afterEach, beforeEach } from 'vitest';
2: import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs';
3: import { tmpdir } from 'os';
4: import { join } from 'path';
5: /**
6: * Tests of our ability to read craft config files. (This is NOT general test
7: * configuration).
8: */
9:
10: import {
11: getGitTagPrefix,
12: loadConfigurationFromString,
13: validateConfiguration,
14: setActiveWorkspace,
15: getActiveWorkspace,
16: getConfiguration,
17: getVersioningPolicy,
18: getWorkspaceNames,
19: WORKSPACES_MIN_VERSION,
20: } from '../config';
21: import { CraftProjectConfigSchema } from '../schemas/project_config';
22: import { logger } from '../logger';
23:
24: describe('validateConfiguration', () => {
25: test('parses minimal configuration', () => {
26: const data = { github: { ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/__tests__/action.test.ts</path>
<type>file</type>
<content>
1: import {
2: chmodSync,
3: mkdtempSync,
4: mkdirSync,
5: readFileSync,
6: rmSync,
7: writeFileSync,
8: } from 'fs';
9: import { tmpdir } from 'os';
10: import { join } from 'path';
11: import { spawnSync } from 'child_process';
12:
13: import { load } from 'js-yaml';
14: import { afterEach, expect, test } from 'vitest';
15:
16: interface ActionStep {
17: env?: Record<string, string>;
18: name?: string;
19: run?: string;
20: }
21:
22: function getActionSteps(): ActionStep[] {
23: const action = load(
24: readFileSync(join(__dirname, '../../action.yml'), 'utf8'),
25: ) as {
26: runs?: { steps?: ActionStep[] };
27: };
28: return action.runs?.steps || [];
29: }
30:
31: function getActionStep(name: string): ActionStep {
32: const step = getActionSteps().find(step => step.name === name);
33: if (!step?.run) {
34: ...
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
180:
181: return (
182: "Expected " +
183: describeExpected(expected) +
184: " but " +
185: describeFound(found) +
186: " found."
187: );
188: }
189: }
190:
191: function peg$parse(input, options) {
192: options = options !== undefined ? options : {};
193:
194: const peg$FAILED = {};
195: const peg$source = options.grammarSource;
196:
197: const peg$startRuleFunctions = {
198: PublishIssueTitle: peg$parsePublishIssueTitle,
199: };
200: let peg$startRuleFunction = peg$parsePublishIssueTitle;
201:
202: const peg$c0 = "publish: ";
203: const peg$c1 = "getsentry/";
204: const peg$c2 = "@";
205: const peg$c3 = "/";
206:
207: const peg$r0 = /^[A-Za-z0-9_.\-]/;
208: const peg$r1 = /^[A-Za-z0-9_.+\-]/;
209:
210: const peg$e0 = peg$literalExpectation("publish: ", false);
211: const peg$e1 ...
Found 13 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/utils/publishState.ts:
Line 34: * `scripts/print-publish-state-path.ts` if present) that needs to
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`;
/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: expect(name).toMatch(/^publish-state-o-r-[0-9a-f]{12}-1\.2\.3\.json$/);
Line 118: expect(name).toMatch(/^publish-state-weird_owner-re_po-[0-9a-f]{12}-/);
Line 123: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/publish.ts</path>
<type>file</type>
<content>
130: description: 'Do not check for build status',
131: type: 'boolean',
132: })
133: .option('no-git-checks', {
134: default: false,
135: description: 'Ignore local git changes and unsynchronized remotes',
136: type: 'boolean',
137: })
138: .check(checkVersion)
139: .demandOption('new-version', 'Please specify the version to publish');
140: };
141:
142: /** Command line options. */
143: export interface PublishOptions {
144: /** The git remote to use when pushing */
145: remote: string;
146: /** Revision to publish (can be commit, tag, etc.) */
147: rev?: string;
148: /** Target branch to merge the release into, auto detected when empty */
149: mergeTarget?: string;
150: /** One or more targets we want to publish */
151: target?: string | string[];
152: /** The new version to publish */
153: ...
<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/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/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/modules/ci-poller-input.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const {
3: getReleaseRevision,
4: updateReleaseRevision,
5: } = require("./release-revision");
6:
7: function getCiPollerInput({ title, issueBody, revision }) {
8: let parsedTitle;
9: try {
10: parsedTitle = parsePublishIssueTitle(title);
11: } catch {
12: throw new Error(`Invalid publish issue title: '${title}'`);
13: }
14: const { repo, version } = parsedTitle;
15: const currentRevision = getReleaseRevision({ issueBody, repo });
16:
17: return {
18: ...(revision
19: ? { issueBody: updateReleaseRevision({ issueBody, repo, revision }) }
20: : {}),
21: repo: `getsentry/${repo}`,
22: revision: currentRevision,
23: version,
24: };
25: }
26:
27: module.exports = { getCiPollerInput };
(End of file - total 27 lines)
</content>
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/docs/publish-issue-format.md</path>
<type>file</type>
<content>
1: # Publish Issue Format
2:
3: The Publish workflow treats a publish issue as a release request. The title identifies
4: the release; the body supplies the merge target and selected targets. Do not edit the
5: title by hand unless it remains valid under this format.
6:
7: ## Title
8:
9: Every title starts with `publish: `. This EBNF is canonical:
10:
11: ```text
12: title = "publish: ", [ "getsentry/" ], repository, [ path ],
13: "@", version ;
14: repository = token, { token } ;
15: path = "/", path-segment, { "/", path-segment } ;
16: path-segment = token, { token } ;
17: version = version-character, { version-character } ;
18: token = ? ASCII letter, digit, ".", "_", or "-" ? ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/docs/src/content/docs/targets/github.md</path> <type>file</type> <content> 55: owner: getsentry 56: repo: toolkit 57: releaseBranchPrefix: release/cli 58: targets: 59: - name: github 60: tagPrefix: "cli@" 61: ``` 62: 63: ```yaml 64: # .craft.yml for the MCP product 65: github: 66: owner: getsentry 67: repo: toolkit 68: releaseBranchPrefix: release/mcp 69: targets: 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`. ...
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
<path>/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3: import { tmpdir } from "os";
4: import { join } from "path";
5:
6: import { afterEach, expect, test } from "vitest";
7:
8: const { resolvePublishLocation } = require("../publish-location.js");
9:
10: const temporaryDirectories = [];
11:
12: function getWorkflow() {
13: return readFileSync(
14: join(__dirname, "../../../.github/workflows/publish.yml"),
15: "utf8"
16: );
17: }
18:
19: function getSetTargetsScript() {
20: const workflow = getWorkflow();
21: const section = workflow.match(
22: / {6}- name: Set targets[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- uses: docker:\/\/getsentry\/craft:latest/
23: );
24: if (!section?.groups?.script) {
25: throw new Error("Missing Set targets workflow ...
Found 5 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/schemas/project_config.ts:
Line 44: export const GitHubGlobalConfigSchema = z.object({
Line 46: repo: z.string(),
Line 50: export type GitHubGlobalConfig = z.infer<typeof GitHubGlobalConfigSchema>;
Line 182: github: GitHubGlobalConfigSchema.optional(),
Line 210: github: GitHubGlobalConfigSchema.partial()
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/schemas/project_config.ts</path>
<type>file</type>
<content>
1: import { z } from 'zod';
2: import { hasMagic } from 'glob';
3:
4: /**
5: * DEPRECATED: Use changelog.policy instead. Different policies for changelog management
6: */
7: export const enum ChangelogPolicy {
8: Auto = 'auto',
9: Simple = 'simple',
10: None = 'none',
11: }
12:
13: /**
14: * Name of the status provider
15: */
16: export const enum StatusProviderName {
17: GitHub = 'github',
18: }
19:
20: /**
21: * Name of the artifact provider
22: */
23: export const enum ArtifactProviderName {
24: GCS = 'gcs',
25: GitHub = 'github',
26: None = 'none',
27: }
28:
29: /**
30: * Default versioning policy when no version argument is provided. ...
Found 22 matches
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/ci-poller-input.js:
Line 26: version: "1.2.3",
Line 37: version: "1.2.3",
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/publish-location.js:
Line 58: ).toThrow("Invalid publish path.");
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/publish-workflow.js:
Line 30: function runSetTargets({ path, repo, version, workspace = "" }) {
Line 42: CRAFT_PUBLISH_VERSION: version,
Line 93: "craft publish ${{ fromJSON(steps.inputs.outputs.result).version }} --rev ${{ steps.release-revision.outputs.revision }}"
Line 111: version: "21.3.1",
Line 128: version: "1.2.3",
/home/byk/Code/getsentry/publish-workspace-acceptance/src/modules/__tests__/update-issue.js:
Line 9: inputs: { repo: "sentry", version: "21.3.1" ...
<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 1 matches /home/byk/Code/getsentry/craft-workspace-action-propagation/package.json: Line 49: "glob": "^11.0.0",
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/package.json</path> <type>file</type> <content> 30: "@types/node-fetch": "^2.5.10", 31: "@types/ora": "^1.3.4", 32: "@types/prompts": "^2.0.11", 33: "@types/semver": "^7.7.1", 34: "@types/shell-quote": "^1.6.0", 35: "@types/tar": "^4.0.0", 36: "@types/tmp": "^0.0.33", 37: "@types/yargs": "^17", 38: "async": "3.2.2", 39: "aws4": "^1.11.0", 40: "chalk": "4.1.1", 41: "cli-table": "0.3.1", 42: "consola": "2.15.3", 43: "esbuild": "^0.28.1", 44: "eslint": "^9.17.0", 45: "eslint-config-prettier": "^9.1.0", 46: "eslint-formatter-github-annotations": "^0.1.0", 47: "fast-xml-parser": "^5.8.0", 48: "git-url-parse": "^16.1.0", 49: "glob": "^11.0.0", 50: "is-ci": "^2.0.0", 51: "js-yaml": "4.3.1", 52: "mkdirp": "^1.0.4", 53: "mustache": "3.0.1", 54: "nock": "^13.2.4", 55: "node-fetch": "^2.6.1", 56: "node-stream-zip": ...
packages/*: true packages/?li: true packages/[cm]: true packages/[!a]: true packages/[^a]: true packages/**: true packages/foo]: false packages/foo!: false packages/foo^: false packages/*]: true packages/*!: true
!packages/*: true
packages/!foo: false
packages/!(foo): true
packages/@(foo): true
packages/+(foo): true
packages/{foo,bar}: true
packages/[a-z]: true
No files found
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/node_modules/glob</path> <type>directory</type> <entries> dist/ LICENSE.md package.json README.md (4 entries) </entries>
Found 11 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/node_modules/glob/README.md:
Line 198: -f --follow Follow symlinked directories when expanding '**'
Line 522: > When `follow` and `nodir` are both set, then symbolic
Line 558: - `follow` Follow symlinked directories when expanding `**`
Line 562: By default, a `**` in a pattern will follow 1 symbolic link if
Line 564: first item in the pattern, following the same behavior as Bash.
Line 567: > When `follow` and `nodir` are both set, then symbolic
Line 683: The following characters have special magic meaning when used in
Line 714: `{follow:true}` is passed in the options object. A pattern
Line 717: or 0 if it is the first item, unless `follow:true` is set, in
Line 718: which case it follows all symbolic links.
Line 772: > setting `{follow:true}` in the options.
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/node_modules/glob/README.md</path> <type>file</type> <content> 530: 531: - `ignore` string or string[], or an object with `ignored` and 532: `childrenIgnored` methods. 533: 534: If a string or string[] is provided, then this is treated as 535: a glob pattern or array of glob patterns to exclude from 536: matches. To ignore all children within a directory, as well 537: as the entry itself, append `'/**'` to the ignore pattern. 538: 539: If an object is provided that has `ignored(path)` and/or 540: `childrenIgnored(path)` methods, then these methods will be 541: called to determine whether any Path is a match or if its 542: children should be traversed, respectively. 543: 544: The `path` argument to the methods will be a 545: [`path-scurry`](https://isaacs.github.io/path-scurry/index.html) 546: [`Path`](https://isaacs.github.io/path-scurry/classes/PathBase) 547: object, which extends 548: ...
Usage: realpath [OPTION]... FILE...
Print the resolved absolute file name;
all but the last component must exist
-e, --canonicalize-existing all components of the path must exist
-m, --canonicalize-missing no path components need exist or be a directory
-L, --logical resolve '..' components before symlinks
-P, --physical resolve symlinks as encountered (default)
-q, --quiet suppress most error messages
--relative-to=DIR print the resolved path relative to DIR
--relative-base=DIR print absolute paths unless paths below DIR
-s, --strip, --no-symlinks don't expand symlinks
-z, --zero end each output line with NUL, not newline
--help display this help and exit
--version output version information and exit
GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation <https://www.gnu.org/software/coreutils/realpath>
or available ...
Found 18 matches
/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/publish.ts:
Line 54: import { getPublishStatePath } from '../utils/publishState';
Line 177: export function getPublishStateGitHubConfig(
Line 716: let publishStateGithubConfig = null;
Line 718: publishStateGithubConfig = await getGlobalGitHubConfig();
Line 720: // Fall through with null — getPublishStatePath() handles this by
Line 724: const publishStateFile = getPublishStatePath(
Line 726: getPublishStateGitHubConfig(publishStateGithubConfig),
Line 732: logger.debug(`Publish state file path: ${publishStateFile}`);
Line 744: `Craft now stores publish state at "${publishStateFile}". ` +
Line 749: const earlierStateExists = existsSync(publishStateFile);
Line 750: let publishState: PublishState;
Line 753: publishState = JSON.parse(readFileSync(publishStateFile).toString());
Line 756: publishState = { published: ...
<path>/home/byk/Code/getsentry/craft-workspace-action-propagation/src/commands/publish.ts</path>
<type>file</type>
<content>
680: const revision = await git.revparse('HEAD');
681: logger.debug('Revision to publish: ', revision);
682:
683: const statusProvider = await getStatusProviderFromConfig();
684: const artifactProvider = await getArtifactProviderFromConfig();
685:
686: // Check status of all CI builds linked to the revision
687: await checkRevisionStatus(statusProvider, revision, argv.noStatusCheck);
688:
689: await printRevisionSummary(artifactProvider, revision);
690:
691: await checkRequiredArtifacts(artifactProvider, revision, config.requireNames);
692:
693: // Find targets
694: let targetsToPublish: Set<string> = new Set(
695: (typeof argv.target === 'string' ? [argv.target] : argv.target) || [
696: SpecialTarget.All,
697: ],
698: );
699:
700: // Treat "all"/"none" specially
701: for (const specialTarget of [SpecialTarget.All, ...
MUST-FIX — Contract 3: Title parsing accepts unsafe repository identities and option-like/non-semver versions. Only path is validated before checkout/state writes; repo reaches checkout and version reaches state creation and craft publish. src/modules/publish-issue-title.peggy:19-32, src/modules/details-from-context.js:38-40, .github/workflows/publish.yml:159-166, .github/workflows/publish.yml:184-230, .github/workflows/publish.yml:242-244.
MUST-FIX — Contract 4: Glob root containment is lexical, not physical. A supported ** glob can traverse a symlinked directory; path.resolve() and lstatSync() on its descendant accept an outside directory without a realpath() containment check. src/config.ts:241-255.
PASS — Contract 1: Action rejects simultaneous path/workspace inputs and emits the full workspace path in titles; acceptance resolves only exact full-path matches. action.yml:80-86, action.yml:268, src/modules/publish-location.js:16-21.
PASS — Contract 2: The CI-approved revision resolves before target checkout and discovery; root configuration discovery occurs only after checkout and invalid discovery fails before root-path return. .github/workflows/publish.yml:138-182, src/modules/publish-location.js:6-19.
PASS — Contract 5: Publishing requires a fresh ci-ready label event plus accepted/ready and no pending/failed labels. .github/workflows/publish.yml:102-114.
PASS — Contract 6: State identity includes repository, physical cwd hash, encoded workspace, and version; publishing uses the resolved revision via --rev. .github/workflows/publish.yml:212-230, .github/workflows/publish.yml:242-256.
PASS — Contract 7: The canonical grammar has path-only titles and no legacy JSON workspace production or parsing. src/modules/publish-issue-title.peggy:7-32, docs/publish-issue-format.md:12-43.
PASS — Contract 8: Regression coverage includes invalid root discovery and invalid literal ], !, ^ keys; generated parser ...
Date: Sep 4, 2026 * 🔴 (10:35) User stated that Action publish titles should always carry the full concrete workspace path. * 🟡 (10:35) Read-only audit evidence: `/home/byk/Code/getsentry` contains 34 entries, including the two audited worktrees `craft-workspace-action-propagation/` and `publish-workspace-acceptance/`. * 🟡 (10:35) Read-only audit evidence: `/home/byk/Code/getsentry/craft-worksp…
Date: Sep 4, 2026 * 🔴 [requested-review] [enforced-read-only-audit] (10:34) User requested a final strict READ-ONLY adversarial audit of complete current uncommitted diffs in both exact worktrees: `/home/byk/Code/getsentry/craft-workspace-action-propagation` and `/home/byk/Code/getsentry/publish-workspace-acceptance`. User prohibited editing files, running formatters/generators, and changing git…