Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
contents: read
strategy:
matrix:
node: ['20','22']
node: ['20', '22']
name: Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v6
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Format Check
run: pnpm format:check
- name: Type Check
run: pnpm typecheck
- name: Lint
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
coverage/
dist/
node_modules/
pnpm-lock.yaml
temp_*/
docs/
26 changes: 14 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,26 @@ Some operations need explicit `isDryRun()` checks:
- User experience optimizations (e.g., skipping sleep timers)

<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/opencode-lore) -->

## Long-term Knowledge

### Gotcha
### Architecture

<!-- lore:019cb31a-14ce-7892-b22a-0327cfcebc13 -->

<!-- lore:019ca05d-1ee2-759e-8a57-596470be9a3a -->
* **Craft changelog: commits without PRs were forced into leftovers regardless of category match**: In \`src/utils/changelog.ts\`, the leftovers guard at the categorization step had \`if (!categoryTitle || !raw.pr)\` — the \`|| !raw.pr\` condition forced ALL commits without associated PRs into the "Other" leftovers section, even when they matched a category via \`commit\_patterns\` or labels. This meant direct pushes (no PR) like \`feat(auth): add SSO\` would correctly contribute to version bump calculation (which runs before the leftovers check) but would appear under "Other" in the changelog instead of their matched category. Fix: remove \`|| !raw.pr\` so only \`!categoryTitle\` controls leftover placement. The downstream rendering already handles PR-less commits gracefully — \`createPREntriesFromRaw\` uses \`raw.pr ?? ''\`, and \`formatChangelogEntry\` falls back to commit-hash links when \`prNumber\` is falsy (empty string).
- **Registry target: repo_url auto-derived from git remote, not user-configurable**: \`repo_url\` in registry manifests is always set by Craft as \`https://github.com/${owner}/${repo}\`. Resolution: (1) explicit \`github: { owner, repo }\` in \`.craft.yml\` (rare), (2) fallback: auto-detect from git \`origin\` remote URL via \`git-url-parse\` library (\`git.ts:194-217\`, \`config.ts:286-316\`). Works with HTTPS and SSH remote URLs. Always overwritten on every publish — existing manifest values are replaced (\`registry.ts:417-418\`). Result is cached globally with \`Object.freeze\`. If remote isn't \`github.com\` and no explicit config exists, throws \`ConfigurationError\`. Most repos need no configuration — the git origin remote is sufficient.

<!-- lore:019c9ba5-515b-70c8-bc1a-1221f6858b42 -->
* **Edit tool triggers Prettier reformatting on the entire file**: When using the Edit tool to make a targeted change to a file in a repo with Prettier configured, the tool may reformat the entire file (e.g., aligning markdown table columns, changing quote styles). This causes the git diff to show cosmetic changes far beyond the intended edit. To keep commits clean: either accept the Prettier reformatting as a net improvement (if the file passes \`prettier --check\`), or use \`git checkout -- \<file>\` to restore the original and re-apply the change via bash/sed for surgical precision. In this codebase, Prettier is configured (\`.prettierrc.yml\`) but the lint CI workflow does NOT run \`prettier --check\`, only ESLint and typecheck.
<!-- lore:019cb31a-14c8-7ba9-b1c4-81b2e8bf7e85 -->

- **Registry target: urlTemplate generates artifact download URLs in manifest**: \`urlTemplate\` in the registry target config generates download URLs for release artifacts in the registry manifest's \`files\` field. Uses Mustache rendering with variables \`{{version}}\`, \`{{file}}\`, \`{{revision}}\`. Primarily useful for apps (standalone binaries) and CDN-hosted assets — SDK packages published to public registries (npm, PyPI, gem) typically don't need it. If neither \`urlTemplate\` nor \`checksums\` is configured, Craft skips adding file data entirely (warns at \`registry.ts:341-349\`). Real-world pattern: \`https://downloads.sentry-cdn.com/\<product>/{{version}}/{{file}}\`.

### Gotcha

<!-- lore:019c9eb7-a640-776a-929d-89120f81733a -->
* **pnpm-lock.yaml merge conflicts: regenerate don't manually merge**: pnpm gotchas: (1) Lock file conflicts: never manually resolve — \`git checkout --theirs pnpm-lock.yaml\` then \`pnpm install\` to regenerate. \`git stash pop\` after merge can re-conflict; drop the stash and re-run instead. (2) Overrides: \`>=\` crosses major versions — use \`^\` to stay in-major. Version-range selectors don't reliably force re-resolution of compatible transitive deps; use blanket overrides when all consumers are on same major. (3) Overrides go stale on tree changes — audit with \`pnpm why\` and remove orphans.
<!-- lore:019c9f57-aa0c-7a2a-8a10-911b13b48fc0 -->

### Pattern
- **ESM modules prevent vi.spyOn of child_process.spawnSync — use test subclass pattern**: In ESM (Vitest or Bun), you cannot \`vi.spyOn\` exports from Node built-in modules — throws 'Module namespace is not configurable'. Workaround: create a test subclass that overrides the method calling the built-in and injects controllable values. \`vi.mock\` at module level works but affects all tests in the file.

<!-- lore:019c9ba5-515c-7131-a1e6-10f93443d0e2 -->
* **AGENTS.md lore section: only include project-relevant entries**: The \`\<!-- lore-managed section -->\` in AGENTS.md is auto-maintained by the lore tool and can accumulate cross-project entries (React, Kubernetes, etc.) that are irrelevant to the Craft codebase. When committing AGENTS.md changes, review the lore section and strip entries that don't pertain to this repo. Only project-specific patterns (like the \`publish\_repo: self\` sentinel) should be included.
<!-- lore:019c9be1-33d1-7b6e-b107-ae7ad42a4ea4 -->

<!-- lore:019c9b36-8f9d-71c9-a43a-d2715aa249d0 -->
* **Craft publish\_repo 'self' sentinel resolves to GITHUB\_REPOSITORY at runtime**: The composite action's \`publish\_repo\` input supports a special sentinel value \`"self"\` which resolves to \`$GITHUB\_REPOSITORY\` at runtime in the bash script of the 'Request publish' step. This allows repos to create publish request issues in themselves rather than in a separate \`{owner}/publish\` repo. The resolution happens in bash (not in the GitHub Actions expression) because the expression layer sets \`PUBLISH\_REPO\` via \`inputs.publish\_repo || format('{0}/publish', github.repository\_owner)\` — the string \`"self"\` passes through as-is and gets resolved to the actual repo name in the shell. Useful for personal/small repos where the default GITHUB\_TOKEN already has write access to the repo itself.
- **pnpm overrides with >= can cross major versions — use ^ to constrain**: pnpm overrides gotchas: (1) \`>=\` crosses major versions — use \`^\` to constrain within same major. (2) Version-range selectors don't reliably force re-resolution of compatible transitive deps; use blanket overrides when safe. (3) Overrides become stale — audit with \`pnpm why \<pkg>\` after dependency changes. (4) Never manually resolve pnpm-lock.yaml conflicts — \`git checkout --theirs\` then \`pnpm install\` to regenerate deterministically.
<!-- End lore-managed section -->
10 changes: 5 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,15 @@

### Bug Fixes 🐛

- fix(changelog): Unscoped entries should be grouped under "other" by @BYK in [#659](https://github.com/getsentry/craft/pull/659)
- fix(changelog): Unscoped entries should be grouped under "other" by @BYK in [#659](https://github.com/getsentry/craft/pull/659)

### Build / dependencies / internal 🔧

- ci: Update action-prepare-release to v1.6.5 by @BYK in [#654](https://github.com/getsentry/craft/pull/654)

### Other

- fix(docker): Support regional Artifact Registry endpoints in isGoogleCloudRegistry by @BYK in [#661](https://github.com/getsentry/craft/pull/661)
- fix(docker): Support regional Artifact Registry endpoints in isGoogleCloudRegistry by @BYK in [#661](https://github.com/getsentry/craft/pull/661)

## 2.13.1

Expand Down Expand Up @@ -779,7 +779,7 @@

### Various fixes & improvements

- ref: Pin cocoapods version (#496) by @brustolin
- ref: Pin cocoapods version (#496) by @brustolin

## 1.6.0

Expand All @@ -795,14 +795,14 @@
- build(deps): bump semver from 6.3.0 to 6.3.1 (#470) by @dependabot
- build(deps): bump @babel/traverse from 7.22.5 to 7.23.2 (#494) by @dependabot
- ref: remove volta from CI (#493) by @asottile-sentry
- fix: Handle `{major}.json` and `{minor}.json` symlinks when publishing older versions (#483) by @cleptric
- fix: Handle `{major}.json` and `{minor}.json` symlinks when publishing older versions (#483) by @cleptric
- Bump symbol collector 1.12.0 (#491) by @bruno-garcia

## 1.4.4

### Various fixes & improvements

- fix(brew): Replace version in artifact names with '__VERSION__' to access checksums from mustache (#488) by @romtsn
- fix(brew): Replace version in artifact names with '**VERSION**' to access checksums from mustache (#488) by @romtsn

## 1.4.3

Expand Down
4 changes: 3 additions & 1 deletion blog-post-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ And for teams that prefer calendar-based versioning, Craft now supports `calver`
versioning:
policy: calver
calver:
format: "%y.%-m" # e.g., 24.12 for December 2024
format: '%y.%-m' # e.g., 24.12 for December 2024
```

The best part? When using auto-versioning, you get a preview in your PR with a "Semver Impact of This PR" section so you know exactly what _your specific change_ will do to the version.
Expand Down Expand Up @@ -202,5 +202,7 @@ Drop me a message, file an issue, or just leave a comment. I'm actively working
LFG 🚀

[^1]: It's not actually living there. That would be creepy. It just reads the messages. Still creepy? OK moving on.

[^2]: It doesn't. But it handles reverts correctly, which is almost the same thing in software.

[^3]: The Vitest migration alone touches ~30 test files. Don't ask me how I know this.
10 changes: 7 additions & 3 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ if (process.env.SENTRY_AUTH_TOKEN) {
assets: ['dist/craft.js', 'dist/craft.js.map'],
filesToDeleteAfterUpload: ['dist/**/*.map'],
},
})
}),
);
} else {
console.log('[build] SENTRY_AUTH_TOKEN not found, skipping source map upload');
console.log(
'[build] SENTRY_AUTH_TOKEN not found, skipping source map upload',
);
}

// Build to .js file first so Sentry plugin can properly handle source maps
Expand All @@ -33,7 +35,9 @@ await esbuild.build({
'import.meta.url': 'import_meta_url',
'process.env.NODE_ENV': JSON.stringify('production'),
...(process.env.CRAFT_BUILD_SHA && {
'process.env.CRAFT_BUILD_SHA': JSON.stringify(process.env.CRAFT_BUILD_SHA),
'process.env.CRAFT_BUILD_SHA': JSON.stringify(
process.env.CRAFT_BUILD_SHA,
),
}),
},
outfile: 'dist/craft.js',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"clean": "rm -rf dist coverage",
"lint": "eslint --cache --cache-strategy content",
"fix": "pnpm lint --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
Expand Down
2 changes: 0 additions & 2 deletions src/__mocks__/@aws-sdk/client-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


const PUBLISHED_LAYER_TEST = {
Version: 1,
LayerVersionArn: 'test:layer:version:arn',
Expand Down
32 changes: 16 additions & 16 deletions src/artifact_providers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface ParsedFilterOptions {
* Returns parsed the given raw filters.
*/
export function parseFilterOptions(
rawFilters: RawFilterOptions
rawFilters: RawFilterOptions,
): ParsedFilterOptions {
const parsedFilters: ParsedFilterOptions = {};
if (rawFilters.includeNames) {
Expand Down Expand Up @@ -193,7 +193,7 @@ export abstract class BaseArtifactProvider {
*/
public async downloadArtifact(
artifact: RemoteArtifact,
downloadDirectory?: string
downloadDirectory?: string,
): Promise<string> {
let finalDownloadDirectory;
if (downloadDirectory) {
Expand All @@ -210,14 +210,14 @@ export abstract class BaseArtifactProvider {
return cached;
}
this.logger.debug(
`Downloading \`${artifact.filename}\` to \`${finalDownloadDirectory}\``
`Downloading \`${artifact.filename}\` to \`${finalDownloadDirectory}\``,
);
const promise = this.doDownloadArtifact(
artifact,
finalDownloadDirectory
finalDownloadDirectory,
).catch(err => {
this.logger.error(
`Unable to download ${artifact.filename} from artifact provider!`
`Unable to download ${artifact.filename} from artifact provider!`,
);
throw err;
});
Expand All @@ -237,7 +237,7 @@ export abstract class BaseArtifactProvider {
*/
protected abstract doDownloadArtifact(
artifact: RemoteArtifact,
downloadDirectory: string
downloadDirectory: string,
): Promise<string>;

/**
Expand All @@ -254,12 +254,12 @@ export abstract class BaseArtifactProvider {
*/
public async downloadArtifacts(
artifacts: RemoteArtifact[],
downloadDirectory?: string
downloadDirectory?: string,
): Promise<string[]> {
return Promise.all(
artifacts.map(async artifact =>
this.downloadArtifact(artifact, downloadDirectory)
)
this.downloadArtifact(artifact, downloadDirectory),
),
);
}

Expand All @@ -271,7 +271,7 @@ export abstract class BaseArtifactProvider {
* @returns List of artifacts associated with that commit
*/
public async listArtifactsForRevision(
revision: string
revision: string,
): Promise<RemoteArtifact[]> {
this.logger.debug(`Fetching artifact list for revision \`${revision}\`.`);
// check the cache first
Expand All @@ -288,7 +288,7 @@ export abstract class BaseArtifactProvider {
artifacts = await this.fileListCache[revision];
} catch (err) {
this.logger.error(
`Unable to retrieve artifact list for revision ${revision}!`
`Unable to retrieve artifact list for revision ${revision}!`,
);
throw err;
}
Expand All @@ -314,7 +314,7 @@ export abstract class BaseArtifactProvider {
* be found
*/
protected abstract doListArtifactsForRevision(
revision: string
revision: string,
): Promise<RemoteArtifact[]>;

/**
Expand All @@ -330,7 +330,7 @@ export abstract class BaseArtifactProvider {
public async getChecksum(
artifact: RemoteArtifact,
algorithm: HashAlgorithm,
format: HashOutputFormat
format: HashOutputFormat,
): Promise<string> {
const filePath = await this.downloadArtifact(artifact);
const checksumKey = `${algorithm}__${format}`;
Expand All @@ -356,7 +356,7 @@ export abstract class BaseArtifactProvider {
*/
public async filterArtifactsForRevision(
revision: string,
filterOptions?: ParsedFilterOptions
filterOptions?: ParsedFilterOptions,
): Promise<RemoteArtifact[]> {
let filteredArtifacts = await this.listArtifactsForRevision(revision);
if (!filterOptions || filteredArtifacts.length === 0) {
Expand All @@ -365,12 +365,12 @@ export abstract class BaseArtifactProvider {
const { includeNames, excludeNames } = filterOptions;
if (includeNames) {
filteredArtifacts = filteredArtifacts.filter(artifact =>
includeNames.test(artifact.filename)
includeNames.test(artifact.filename),
);
}
if (excludeNames) {
filteredArtifacts = filteredArtifacts.filter(
artifact => !excludeNames.test(artifact.filename)
artifact => !excludeNames.test(artifact.filename),
);
}
return filteredArtifacts;
Expand Down
12 changes: 6 additions & 6 deletions src/artifact_providers/gcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export class GCSArtifactProvider extends BaseArtifactProvider {
},
{
name: 'CRAFT_GCS_STORE_CREDS_PATH',
}
},
);

// TODO (kmclb) get rid of this check once config validation is working
if (!config.bucket) {
throw new ConfigurationError(
'No GCS bucket provided in artifact provider config!'
'No GCS bucket provided in artifact provider config!',
);
}

Expand All @@ -51,11 +51,11 @@ export class GCSArtifactProvider extends BaseArtifactProvider {
*/
protected async doDownloadArtifact(
artifact: RemoteArtifact,
downloadDirectory: string
downloadDirectory: string,
): Promise<string> {
const result = await this.gcsClient.downloadArtifact(
artifact.storedFile.downloadFilepath,
downloadDirectory
downloadDirectory,
);
// In dry-run mode, downloadArtifact returns null. Return a placeholder path
// that indicates the file would have been downloaded here.
Expand All @@ -69,13 +69,13 @@ export class GCSArtifactProvider extends BaseArtifactProvider {
* @inheritDoc
*/
protected async doListArtifactsForRevision(
revision: string
revision: string,
): Promise<RemoteArtifact[]> {
const { repoName, repoOwner } = this.config;
return this.gcsClient.listArtifactsForRevision(
repoOwner,
repoName,
revision
revision,
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/artifact_providers/none.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class NoneArtifactProvider extends BaseArtifactProvider {
repoName: 'none',
repoOwner: 'none',
name: 'none',
}
},
) {
super(config);
}
Expand All @@ -24,10 +24,10 @@ export class NoneArtifactProvider extends BaseArtifactProvider {
*/
protected async doDownloadArtifact(
_artifact: RemoteArtifact,
_downloadDirectory: string
_downloadDirectory: string,
): Promise<string> {
return Promise.reject(
new Error('NoneProvider does not suuport file downloads!')
new Error('NoneProvider does not suuport file downloads!'),
);
}

Expand All @@ -37,7 +37,7 @@ export class NoneArtifactProvider extends BaseArtifactProvider {
* @returns An empty array
*/
protected async doListArtifactsForRevision(
_revision: string
_revision: string,
): Promise<RemoteArtifact[]> {
return [];
}
Expand Down
16 changes: 11 additions & 5 deletions src/commands/__tests__/targets.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { vi, describe, test, expect, beforeEach, afterEach, type Mock, type MockInstance } from 'vitest';
import {
vi,
describe,
test,
expect,
beforeEach,
afterEach,
type Mock,
type MockInstance,
} from 'vitest';
import { handler } from '../targets';

vi.mock('../../config', () => ({
Expand Down Expand Up @@ -29,10 +38,7 @@ describe('targets command', () => {
});

test('lists targets without expansion when no workspaces', async () => {
const targets = [
{ name: 'npm' },
{ name: 'github' },
];
const targets = [{ name: 'npm' }, { name: 'github' }];

mockedGetConfiguration.mockReturnValue({ targets });
mockedExpandWorkspaceTargets.mockResolvedValue(targets);
Expand Down
Loading
Loading