From 3a8acc3eeaa4c4a50a4f899c576a6a42215fe953 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 16 Mar 2026 10:45:45 +0000 Subject: [PATCH 1/7] meta: Bump new development version #skip-changelog --- docs/src/content/docs/targets/registry.md | 103 ++++++++++++---------- package.json | 2 +- src/utils/__tests__/registry.test.ts | 12 ++- src/utils/registry.ts | 40 ++++++++- 4 files changed, 105 insertions(+), 52 deletions(-) diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index e30ea150..986e5d1d 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -16,19 +16,19 @@ Avoid having multiple `registry` targets—it supports batching multiple apps an | `apps` | Dict of app configs keyed by canonical name (e.g., `app:craft`) | | `sdks` | Dict of SDK configs keyed by canonical name (e.g., `maven:io.sentry:sentry`) | -### App/SDK Options +### Per-package options | Option | Description | -|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `urlTemplate` | URL template for artifact download links in the manifest. Supports `{{version}}`, `{{file}}`, and `{{revision}}` variables. Primarily for apps and CDN-hosted assets—not needed for SDK packages installed from public registries (npm, PyPI, etc.) | -| `linkPrereleases` | Update for preview releases. Default: `false` | -| `checksums` | List of checksum configs | -| `onlyIfPresent` | Only run if matching file exists | -| `name` | Human-readable name (used when creating new packages) | +| `linkPrereleases` | By default, the registry target is skipped when the version is a pre-release (e.g. `1.0.0-beta.1`). Set to `true` to publish registry entries for pre-releases as well. | +| `checksums` | List of checksum configs (see [Checksum Configuration](#checksum-configuration)) | +| `onlyIfPresent` | Only run if artifact matches the given filename pattern | +| `name` | Human-readable name for the platform or package (e.g. `"Sentry Browser SDK"` or `"Sentry Craft"`) - (used when creating new packages) | | `sdkName` | SDK identifier matching the SDK's `sdk_info.name` field in Sentry events (e.g., `sentry.javascript.react`). Will create the `sdks/` symlink. (used when creating new packages) | -| `packageUrl` | Link to package registry page, e.g., npmjs.com (used when creating new packages) | -| `mainDocsUrl` | Link to main documentation (used when creating new packages) | -| `apiDocsUrl` | Link to API documentation (used when creating new packages) | +| `packageUrl` | Link to the package registry page (e.g. npmjs.com, PyPI, crates.io). †Not required for `app:` types or `github:` canonicals, but set it when a separate registry or docs page exists. (used when creating new packages) | +| `mainDocsUrl` | Link to the primary documentation page. If omitted, Craft falls back to `repo_url` and emits a warning. (used when creating new packages) | +| `apiDocsUrl` | Link to the API documentation (e.g. pkg.go.dev, javadoc.io) - (used when creating new packages) | ### Checksum Configuration @@ -38,31 +38,50 @@ checksums: format: hex # or base64 ``` -## Example +## How a registry entry is built + +Every time Craft publishes a release, it writes a version file (e.g. `packages/npm/@sentry/browser/1.2.3.json`) to the registry. The fields in that file come from different sources: + +| Field | Source | +| --------------- | --------------------------------------------------------------------------------------------------- | +| `canonical` | The dict key in `.craft.yml` (e.g. `npm:@sentry/browser`). Set once on first publish (not updated). | +| `version` | The release version being published | +| `created_at` | The current timestamp at publish time | +| `repo_url` | Auto-detected from the `origin` git remote. Overwritten on every publish | +| `name` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `package_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `main_docs_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `api_docs_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | + +The `canonical` field is the only one that cannot be changed after the first publish—it is written once and then validated for consistency on every subsequent run. To rename a canonical, you must manually update both the registry and your `.craft.yml` at the same time. + +`repo_url` is always resolved automatically and cannot be configured per-package. By default, Craft reads the `origin` git remote (both HTTPS and SSH formats are supported). If auto-detection is not possible, configure it via a top-level `github` block: ```yaml -targets: - - name: registry - sdks: - 'npm:@sentry/browser': - apps: - 'app:craft': - urlTemplate: 'https://downloads.sentry-cdn.com/craft/{{version}}/{{file}}' - checksums: - - algorithm: sha256 - format: hex +github: + owner: getsentry + repo: sentry-javascript ``` -## Package Types +## Adding a new package -- **sdk**: Package uploaded to public registries (PyPI, NPM, etc.) -- **app**: Standalone application with version files in the registry +When a package does not yet exist in the registry, Craft creates the directory structure and initial manifest automatically on the first publish. No manual registry changes are needed. -## Creating New Packages +For this to succeed, certain fields must be present in your `.craft.yml` before you publish for the first time. + +:::caution[Required metadata on first publish] + +- **`name`** — required for all package types. +- **`sdkName`** — required for SDK packages. +- **`mainDocsUrl`** — required for all package types. If omitted, Craft falls back to `repo_url` and emits a warning, but you should always set it explicitly. +- **`packageUrl`** — required for most SDK packages +- **`apiDocsUrl`** — required packages with separate API docs (e.g. `pkg.go.dev` for Go modules, `javadoc.io` for Java packages). Optional for other packages. + +::: -When you introduce a new package that doesn't yet exist in the release registry, Craft will automatically create the required directory structure and initial manifest on the first publish. +After the first publish, you can add or update any of these fields in `.craft.yml` and they will be applied to the manifest on the next release. -Supply `name`, `packageUrl`, `sdkName` and `mainDocsUrl` so the release registry entry is added to the registry for the first time (existing packages just need `onlyIfPresent` since the manifest already exists): +### Example: New SDK package ```yaml targets: @@ -75,23 +94,17 @@ targets: mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/' ``` -## Manifest Metadata +### Example: New App with downloadable artifacts -### `repo_url` - -The `repo_url` field is automatically set on every publish—it is not user-configurable per target. Craft resolves it in two ways: - -1. **Auto-detection (default):** Craft reads the `origin` git remote URL and extracts the owner and repo. Both HTTPS (`https://github.com/org/repo.git`) and SSH (`git@github.com:org/repo.git`) formats are supported. For most repositories, no configuration is needed. - -2. **Explicit config (rare):** If auto-detection isn't possible (e.g., the remote is not on `github.com`), you can provide it via a top-level `github` block in `.craft.yml`: - ```yaml - github: - owner: getsentry - repo: sentry-javascript - ``` - -The value is always overwritten on every publish, so it stays in sync with the actual repository. - -### Other metadata - -When specified, the metadata fields (`name`, `sdkName`, `packageUrl`, `mainDocsUrl`, `apiDocsUrl`) are applied to every release, allowing you to update package metadata by changing your `.craft.yml` configuration. +```yaml +targets: + - name: registry + apps: + 'app:craft': + name: 'Sentry Craft' + mainDocsUrl: 'https://github.com/getsentry/craft' + urlTemplate: 'https://downloads.sentry-cdn.com/craft/{{version}}/{{file}}' + checksums: + - algorithm: sha256 + format: hex +``` diff --git a/package.json b/package.json index ba10a127..37a8a99f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/craft", - "version": "2.24.2", + "version": "2.25.0-dev.0", "description": "The universal sentry workflow CLI", "main": "dist/craft", "repository": "https://github.com/getsentry/craft", diff --git a/src/utils/__tests__/registry.test.ts b/src/utils/__tests__/registry.test.ts index 44efbebb..4af65eae 100644 --- a/src/utils/__tests__/registry.test.ts +++ b/src/utils/__tests__/registry.test.ts @@ -219,7 +219,11 @@ describe('getPackageManifest', () => { initialData, ); - const symlinkPath = path.join(tempDir, 'sdks', 'sentry.javascript.hono'); + const symlinkPath = path.join( + tempDir, + 'sdks', + 'sentry.javascript.hono', + ); expect(fs.existsSync(symlinkPath)).toBe(true); expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true); expect(fs.readlinkSync(symlinkPath)).toBe( @@ -248,7 +252,11 @@ describe('getPackageManifest', () => { it('skips symlink creation when the symlink already exists', async () => { fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); - const symlinkPath = path.join(tempDir, 'sdks', 'sentry.javascript.hono'); + const symlinkPath = path.join( + tempDir, + 'sdks', + 'sentry.javascript.hono', + ); const existingTarget = path.join( '..', 'packages', diff --git a/src/utils/registry.ts b/src/utils/registry.ts index c880c3c8..d1d2edf8 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -102,6 +102,34 @@ export async function getPackageManifest( throw new Error('Unreachable'); } + if (!initialManifestData.name) { + logger.warn( + `"name" is not set for "${canonicalName}". ` + + `Add \`name\` to the registry target config in your .craft.yml to avoid this warning.`, + ); + } + + if (type === RegistryPackageType.SDK && !initialManifestData.packageUrl) { + logger.warn( + `"packageUrl" is not set for "${canonicalName}". ` + + `Add \`packageUrl\` to the registry target config in your .craft.yml to avoid this warning.`, + ); + } + + // Fall back to repo_url for mainDocsUrl if not explicitly set + let effectiveManifestData = initialManifestData; + if (!initialManifestData.mainDocsUrl) { + logger.warn( + `"mainDocsUrl" is not set for "${canonicalName}". ` + + `Falling back to repo_url ("${initialManifestData.repoUrl}"). ` + + `Set \`mainDocsUrl\` in the registry target config in .craft.yml to avoid this warning.`, + ); + effectiveManifestData = { + ...initialManifestData, + mainDocsUrl: initialManifestData.repoUrl, + }; + } + // Create directory structure if it doesn't exist if (!existsSync(fullPackageDir)) { logger.info( @@ -111,12 +139,16 @@ export async function getPackageManifest( } // Create the sdks/ symlink when an sdkName is provided - if (initialManifestData.sdkName) { - const sdkSymlinkPath = path.join(baseDir, 'sdks', initialManifestData.sdkName); + if (effectiveManifestData.sdkName) { + const sdkSymlinkPath = path.join( + baseDir, + 'sdks', + effectiveManifestData.sdkName, + ); if (!existsSync(sdkSymlinkPath)) { const relativeTarget = path.join('..', packageDirPath); logger.info( - `Creating sdks symlink "${initialManifestData.sdkName}" -> "${relativeTarget}"...`, + `Creating sdks symlink "${effectiveManifestData.sdkName}" -> "${relativeTarget}"...`, ); symlinkSync(relativeTarget, sdkSymlinkPath); } @@ -127,7 +159,7 @@ export async function getPackageManifest( ); return { versionFilePath, - packageManifest: createInitialManifest(initialManifestData), + packageManifest: createInitialManifest(effectiveManifestData), }; } From 630f53124a6761ec483880956c0f69063b2e123f Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:54:44 +0100 Subject: [PATCH 2/7] revert version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37a8a99f..ba10a127 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/craft", - "version": "2.25.0-dev.0", + "version": "2.24.2", "description": "The universal sentry workflow CLI", "main": "dist/craft", "repository": "https://github.com/getsentry/craft", From a20258cb8436ba0a20a4374159a76c542630d288 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:40:39 +0100 Subject: [PATCH 3/7] meta: Bump new development version perform SDK validation for new packages --- src/utils/__tests__/registry.test.ts | 127 ++++++++++++++++++++++++++- src/utils/registry.ts | 60 +++++++------ 2 files changed, 159 insertions(+), 28 deletions(-) diff --git a/src/utils/__tests__/registry.test.ts b/src/utils/__tests__/registry.test.ts index 4af65eae..87509e78 100644 --- a/src/utils/__tests__/registry.test.ts +++ b/src/utils/__tests__/registry.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import { mkdtempSync, rmSync } from 'fs'; @@ -9,6 +9,7 @@ import { RegistryPackageType, InitialManifestData, } from '../registry'; +import { logger } from '../../logger'; describe('getPackageManifest', () => { let tempDir: string; @@ -208,6 +209,7 @@ describe('getPackageManifest', () => { canonical: 'npm:@sentry/hono', repoUrl: 'https://github.com/getsentry/sentry-javascript', name: 'Sentry Hono SDK', + packageUrl: 'https://www.npmjs.com/package/@sentry/hono', sdkName: 'sentry.javascript.hono', }; @@ -269,6 +271,8 @@ describe('getPackageManifest', () => { const initialData: InitialManifestData = { canonical: 'npm:@sentry/hono', repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Hono SDK', + packageUrl: 'https://www.npmjs.com/package/@sentry/hono', sdkName: 'sentry.javascript.hono', }; @@ -318,5 +322,126 @@ describe('getPackageManifest', () => { expect(fs.readdirSync(sdksDir)).toHaveLength(0); }); }); + + describe('SDK validation when sdkName is provided', () => { + it('throws an error when sdkName is provided but name is missing', async () => { + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/new-sdk', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + packageUrl: 'https://www.npmjs.com/package/@sentry/new-sdk', + sdkName: 'sentry.javascript.new-sdk', + }; + + await expect( + getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/new-sdk', + '1.0.0', + initialData, + ), + ).rejects.toThrow( + '"name" is required for new SDK "npm:@sentry/new-sdk"', + ); + }); + + it('throws an error when sdkName is provided but packageUrl is missing', async () => { + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/new-sdk', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry New SDK', + sdkName: 'sentry.javascript.new-sdk', + }; + + await expect( + getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/new-sdk', + '1.0.0', + initialData, + ), + ).rejects.toThrow( + '"packageUrl" is required for new SDK "npm:@sentry/new-sdk"', + ); + }); + + it('warns and falls back to repoUrl when sdkName is provided but mainDocsUrl is missing', async () => { + fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); + const warnSpy = vi.spyOn(logger, 'warn'); + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/new-sdk', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry New SDK', + packageUrl: 'https://www.npmjs.com/package/@sentry/new-sdk', + sdkName: 'sentry.javascript.new-sdk', + }; + + const result = await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/new-sdk', + '1.0.0', + initialData, + ); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('"mainDocsUrl" is not set'), + ); + expect(result.packageManifest.main_docs_url).toBe( + 'https://github.com/getsentry/sentry-javascript', + ); + warnSpy.mockRestore(); + }); + + it('creates initial manifest with all required fields for new SDK with sdkName', async () => { + fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/new-sdk', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry New SDK', + packageUrl: 'https://www.npmjs.com/package/@sentry/new-sdk', + mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/', + sdkName: 'sentry.javascript.new-sdk', + }; + + const result = await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/new-sdk', + '1.0.0', + initialData, + ); + + expect(result.packageManifest).toEqual({ + canonical: 'npm:@sentry/new-sdk', + repo_url: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry New SDK', + package_url: 'https://www.npmjs.com/package/@sentry/new-sdk', + main_docs_url: 'https://docs.sentry.io/platforms/javascript/', + }); + }); + + it('does not enforce SDK validation when sdkName is absent (existing behavior)', async () => { + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/minimal', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + // No name, no packageUrl, no sdkName — should not throw + }; + + const result = await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/minimal', + '1.0.0', + initialData, + ); + + expect(result.packageManifest).toEqual({ + canonical: 'npm:@sentry/minimal', + repo_url: 'https://github.com/getsentry/sentry-javascript', + }); + }); + }); }); }); diff --git a/src/utils/registry.ts b/src/utils/registry.ts index d1d2edf8..dd08fc18 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -102,32 +102,35 @@ export async function getPackageManifest( throw new Error('Unreachable'); } - if (!initialManifestData.name) { - logger.warn( - `"name" is not set for "${canonicalName}". ` + - `Add \`name\` to the registry target config in your .craft.yml to avoid this warning.`, - ); - } - - if (type === RegistryPackageType.SDK && !initialManifestData.packageUrl) { - logger.warn( - `"packageUrl" is not set for "${canonicalName}". ` + - `Add \`packageUrl\` to the registry target config in your .craft.yml to avoid this warning.`, - ); - } - - // Fall back to repo_url for mainDocsUrl if not explicitly set let effectiveManifestData = initialManifestData; - if (!initialManifestData.mainDocsUrl) { - logger.warn( - `"mainDocsUrl" is not set for "${canonicalName}". ` + - `Falling back to repo_url ("${initialManifestData.repoUrl}"). ` + - `Set \`mainDocsUrl\` in the registry target config in .craft.yml to avoid this warning.`, - ); - effectiveManifestData = { - ...initialManifestData, - mainDocsUrl: initialManifestData.repoUrl, - }; + + if (initialManifestData.sdkName) { + // Strict validation for new SDKs — sdkName signals this is a registry SDK entry + if (!initialManifestData.name) { + reportError( + `"name" is required for new SDK "${canonicalName}". ` + + `Add \`name\` to the registry target config in your .craft.yml.`, + ); + throw new Error('Unreachable'); + } + if (!initialManifestData.packageUrl) { + reportError( + `"packageUrl" is required for new SDK "${canonicalName}". ` + + `Add \`packageUrl\` to the registry target config in your .craft.yml.`, + ); + throw new Error('Unreachable'); + } + if (!initialManifestData.mainDocsUrl) { + logger.warn( + `"mainDocsUrl" is not set for "${canonicalName}". ` + + `Falling back to repo_url ("${initialManifestData.repoUrl}"). ` + + `Set \`mainDocsUrl\` in the registry target config in .craft.yml to avoid this warning.`, + ); + effectiveManifestData = { + ...initialManifestData, + mainDocsUrl: initialManifestData.repoUrl, + }; + } } // Create directory structure if it doesn't exist @@ -138,7 +141,7 @@ export async function getPackageManifest( mkdirSync(fullPackageDir, { recursive: true }); } - // Create the sdks/ symlink when an sdkName is provided + // Create the `sdks/` symlink when an sdkName is provided if (effectiveManifestData.sdkName) { const sdkSymlinkPath = path.join( baseDir, @@ -163,7 +166,10 @@ export async function getPackageManifest( }; } - logger.debug('Reading the current configuration from', packageManifestPath); + logger.debug( + 'Reading the already existing configuration from', + packageManifestPath, + ); return { versionFilePath, packageManifest: From 24183a32691a7f9fe5e132b794182ed3efadd681 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:07:18 +0100 Subject: [PATCH 4/7] update docs --- docs/src/content/docs/targets/registry.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index 986e5d1d..d74aeb26 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -27,7 +27,7 @@ Avoid having multiple `registry` targets—it supports batching multiple apps an | `name` | Human-readable name for the platform or package (e.g. `"Sentry Browser SDK"` or `"Sentry Craft"`) - (used when creating new packages) | | `sdkName` | SDK identifier matching the SDK's `sdk_info.name` field in Sentry events (e.g., `sentry.javascript.react`). Will create the `sdks/` symlink. (used when creating new packages) | | `packageUrl` | Link to the package registry page (e.g. npmjs.com, PyPI, crates.io). †Not required for `app:` types or `github:` canonicals, but set it when a separate registry or docs page exists. (used when creating new packages) | -| `mainDocsUrl` | Link to the primary documentation page. If omitted, Craft falls back to `repo_url` and emits a warning. (used when creating new packages) | +| `mainDocsUrl` | Link to the main documentation page. If omitted, Craft falls back to `repo_url` and emits a warning. (used when creating new packages) | | `apiDocsUrl` | Link to the API documentation (e.g. pkg.go.dev, javadoc.io) - (used when creating new packages) | ### Checksum Configuration @@ -70,14 +70,12 @@ When a package does not yet exist in the registry, Craft creates the directory s For this to succeed, certain fields must be present in your `.craft.yml` before you publish for the first time. :::caution[Required metadata on first publish] - - **`name`** — required for all package types. -- **`sdkName`** — required for SDK packages. - **`mainDocsUrl`** — required for all package types. If omitted, Craft falls back to `repo_url` and emits a warning, but you should always set it explicitly. -- **`packageUrl`** — required for most SDK packages -- **`apiDocsUrl`** — required packages with separate API docs (e.g. `pkg.go.dev` for Go modules, `javadoc.io` for Java packages). Optional for other packages. -::: +- **`sdkName`** — required for SDK packages. +- **`packageUrl`** — required for SDK packages + ::: After the first publish, you can add or update any of these fields in `.craft.yml` and they will be applied to the manifest on the next release. @@ -92,6 +90,8 @@ targets: sdkName: 'sentry.javascript.wasm' packageUrl: 'https://www.npmjs.com/package/@sentry/wasm' mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/' + # Optional fields for SDKs with API docs: + apiDocsUrl: 'https://pkg.go.dev/github.com/getsentry/sentry-go' ``` ### Example: New App with downloadable artifacts From 7dc0586848b264fe82af68d772ebcc1e8f145098 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:17:59 +0100 Subject: [PATCH 5/7] require `name` and docs url for all new packages --- docs/src/content/docs/targets/registry.md | 3 +- src/utils/__tests__/registry.test.ts | 107 ++++++++++++---------- src/utils/registry.ts | 54 +++++------ 3 files changed, 90 insertions(+), 74 deletions(-) diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index d74aeb26..ec29a986 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -70,12 +70,13 @@ When a package does not yet exist in the registry, Craft creates the directory s For this to succeed, certain fields must be present in your `.craft.yml` before you publish for the first time. :::caution[Required metadata on first publish] + - **`name`** — required for all package types. - **`mainDocsUrl`** — required for all package types. If omitted, Craft falls back to `repo_url` and emits a warning, but you should always set it explicitly. - **`sdkName`** — required for SDK packages. - **`packageUrl`** — required for SDK packages - ::: +::: After the first publish, you can add or update any of these fields in `.craft.yml` and they will be applied to the manifest on the next release. diff --git a/src/utils/__tests__/registry.test.ts b/src/utils/__tests__/registry.test.ts index 87509e78..771b6e87 100644 --- a/src/utils/__tests__/registry.test.ts +++ b/src/utils/__tests__/registry.test.ts @@ -125,6 +125,8 @@ describe('getPackageManifest', () => { const initialData: InitialManifestData = { canonical: 'npm:@sentry/minimal', repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Minimal', + // mainDocsUrl omitted — falls back to repoUrl }; const result = await getPackageManifest( @@ -138,6 +140,8 @@ describe('getPackageManifest', () => { expect(result.packageManifest).toEqual({ canonical: 'npm:@sentry/minimal', repo_url: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Minimal', + main_docs_url: 'https://github.com/getsentry/sentry-javascript', }); }); @@ -159,6 +163,7 @@ describe('getPackageManifest', () => { canonical: 'app:craft', repoUrl: 'https://github.com/getsentry/craft', name: 'Craft', + // mainDocsUrl omitted — falls back to repoUrl }; const result = await getPackageManifest( @@ -177,6 +182,7 @@ describe('getPackageManifest', () => { canonical: 'app:craft', repo_url: 'https://github.com/getsentry/craft', name: 'Craft', + main_docs_url: 'https://github.com/getsentry/craft', }); }); @@ -184,6 +190,8 @@ describe('getPackageManifest', () => { const initialData: InitialManifestData = { canonical: 'npm:@sentry/core', repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Core', + mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/', apiDocsUrl: 'https://docs.sentry.io/api/', }; @@ -198,6 +206,8 @@ describe('getPackageManifest', () => { expect(result.packageManifest).toEqual({ canonical: 'npm:@sentry/core', repo_url: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Core', + main_docs_url: 'https://docs.sentry.io/platforms/javascript/', api_docs_url: 'https://docs.sentry.io/api/', }); }); @@ -238,6 +248,7 @@ describe('getPackageManifest', () => { const initialData: InitialManifestData = { canonical: 'npm:@sentry/hono', repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Hono SDK', }; await getPackageManifest( @@ -323,34 +334,62 @@ describe('getPackageManifest', () => { }); }); - describe('SDK validation when sdkName is provided', () => { - it('throws an error when sdkName is provided but name is missing', async () => { + describe('required field validation for all new packages', () => { + it('throws an error when name is missing', async () => { const initialData: InitialManifestData = { - canonical: 'npm:@sentry/new-sdk', - repoUrl: 'https://github.com/getsentry/sentry-javascript', - packageUrl: 'https://www.npmjs.com/package/@sentry/new-sdk', - sdkName: 'sentry.javascript.new-sdk', + canonical: 'app:sentry-cli', + repoUrl: 'https://github.com/getsentry/sentry-cli', + // No name, no sdkName — error applies to all package types }; await expect( getPackageManifest( tempDir, - RegistryPackageType.SDK, - 'npm:@sentry/new-sdk', + RegistryPackageType.APP, + 'app:sentry-cli', '1.0.0', initialData, ), ).rejects.toThrow( - '"name" is required for new SDK "npm:@sentry/new-sdk"', + '"name" is required for new package "app:sentry-cli"', + ); + }); + + it('warns and falls back to repoUrl when mainDocsUrl is missing', async () => { + const warnSpy = vi.spyOn(logger, 'warn'); + const initialData: InitialManifestData = { + canonical: 'app:sentry-cli', + repoUrl: 'https://github.com/getsentry/sentry-cli', + name: 'Sentry CLI', + // mainDocsUrl omitted — applies to all package types + }; + + const result = await getPackageManifest( + tempDir, + RegistryPackageType.APP, + 'app:sentry-cli', + '1.0.0', + initialData, + ); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('"mainDocsUrl" is not set'), + ); + expect(result.packageManifest.main_docs_url).toBe( + 'https://github.com/getsentry/sentry-cli', ); + warnSpy.mockRestore(); }); + }); - it('throws an error when sdkName is provided but packageUrl is missing', async () => { + describe('additional SDK validation when sdkName is provided', () => { + it('throws an error when packageUrl is missing', async () => { const initialData: InitialManifestData = { canonical: 'npm:@sentry/new-sdk', repoUrl: 'https://github.com/getsentry/sentry-javascript', name: 'Sentry New SDK', sdkName: 'sentry.javascript.new-sdk', + // packageUrl omitted }; await expect( @@ -366,32 +405,29 @@ describe('getPackageManifest', () => { ); }); - it('warns and falls back to repoUrl when sdkName is provided but mainDocsUrl is missing', async () => { - fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); - const warnSpy = vi.spyOn(logger, 'warn'); + it('does not require packageUrl when sdkName is absent', async () => { const initialData: InitialManifestData = { - canonical: 'npm:@sentry/new-sdk', + canonical: 'npm:@sentry/minimal', repoUrl: 'https://github.com/getsentry/sentry-javascript', - name: 'Sentry New SDK', - packageUrl: 'https://www.npmjs.com/package/@sentry/new-sdk', - sdkName: 'sentry.javascript.new-sdk', + name: 'Sentry Minimal', + mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/', + // No sdkName, no packageUrl — should not throw }; const result = await getPackageManifest( tempDir, RegistryPackageType.SDK, - 'npm:@sentry/new-sdk', + 'npm:@sentry/minimal', '1.0.0', initialData, ); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('"mainDocsUrl" is not set'), - ); - expect(result.packageManifest.main_docs_url).toBe( - 'https://github.com/getsentry/sentry-javascript', - ); - warnSpy.mockRestore(); + expect(result.packageManifest).toEqual({ + canonical: 'npm:@sentry/minimal', + repo_url: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Minimal', + main_docs_url: 'https://docs.sentry.io/platforms/javascript/', + }); }); it('creates initial manifest with all required fields for new SDK with sdkName', async () => { @@ -421,27 +457,6 @@ describe('getPackageManifest', () => { main_docs_url: 'https://docs.sentry.io/platforms/javascript/', }); }); - - it('does not enforce SDK validation when sdkName is absent (existing behavior)', async () => { - const initialData: InitialManifestData = { - canonical: 'npm:@sentry/minimal', - repoUrl: 'https://github.com/getsentry/sentry-javascript', - // No name, no packageUrl, no sdkName — should not throw - }; - - const result = await getPackageManifest( - tempDir, - RegistryPackageType.SDK, - 'npm:@sentry/minimal', - '1.0.0', - initialData, - ); - - expect(result.packageManifest).toEqual({ - canonical: 'npm:@sentry/minimal', - repo_url: 'https://github.com/getsentry/sentry-javascript', - }); - }); }); }); }); diff --git a/src/utils/registry.ts b/src/utils/registry.ts index dd08fc18..d98d03c6 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -104,33 +104,33 @@ export async function getPackageManifest( let effectiveManifestData = initialManifestData; - if (initialManifestData.sdkName) { - // Strict validation for new SDKs — sdkName signals this is a registry SDK entry - if (!initialManifestData.name) { - reportError( - `"name" is required for new SDK "${canonicalName}". ` + - `Add \`name\` to the registry target config in your .craft.yml.`, - ); - throw new Error('Unreachable'); - } - if (!initialManifestData.packageUrl) { - reportError( - `"packageUrl" is required for new SDK "${canonicalName}". ` + - `Add \`packageUrl\` to the registry target config in your .craft.yml.`, - ); - throw new Error('Unreachable'); - } - if (!initialManifestData.mainDocsUrl) { - logger.warn( - `"mainDocsUrl" is not set for "${canonicalName}". ` + - `Falling back to repo_url ("${initialManifestData.repoUrl}"). ` + - `Set \`mainDocsUrl\` in the registry target config in .craft.yml to avoid this warning.`, - ); - effectiveManifestData = { - ...initialManifestData, - mainDocsUrl: initialManifestData.repoUrl, - }; - } + // Required for all new packages + if (!initialManifestData.name) { + reportError( + `"name" is required for new package "${canonicalName}". ` + + `Add \`name\` to the registry target config in your .craft.yml.`, + ); + throw new Error('Unreachable'); + } + if (!initialManifestData.mainDocsUrl) { + logger.warn( + `"mainDocsUrl" is not set for "${canonicalName}". ` + + `Falling back to repo_url ("${initialManifestData.repoUrl}"). ` + + `Set \`mainDocsUrl\` in the registry target config in .craft.yml to avoid this warning.`, + ); + effectiveManifestData = { + ...initialManifestData, + mainDocsUrl: initialManifestData.repoUrl, + }; + } + + // Additional requirement for SDK packages + if (initialManifestData.sdkName && !initialManifestData.packageUrl) { + reportError( + `"packageUrl" is required for new SDK "${canonicalName}". ` + + `Add \`packageUrl\` to the registry target config in your .craft.yml.`, + ); + throw new Error('Unreachable'); } // Create directory structure if it doesn't exist From 590b41134df6046cff6500273331c976e24f8418 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:48:45 +0100 Subject: [PATCH 6/7] update doc --- docs/src/content/docs/targets/registry.md | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index ec29a986..c51b9766 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -18,17 +18,17 @@ Avoid having multiple `registry` targets—it supports batching multiple apps an ### Per-package options -| Option | Description | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Option | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `urlTemplate` | URL template for artifact download links in the manifest. Supports `{{version}}`, `{{file}}`, and `{{revision}}` variables. Primarily for apps and CDN-hosted assets—not needed for SDK packages installed from public registries (npm, PyPI, etc.) | -| `linkPrereleases` | By default, the registry target is skipped when the version is a pre-release (e.g. `1.0.0-beta.1`). Set to `true` to publish registry entries for pre-releases as well. | -| `checksums` | List of checksum configs (see [Checksum Configuration](#checksum-configuration)) | -| `onlyIfPresent` | Only run if artifact matches the given filename pattern | -| `name` | Human-readable name for the platform or package (e.g. `"Sentry Browser SDK"` or `"Sentry Craft"`) - (used when creating new packages) | -| `sdkName` | SDK identifier matching the SDK's `sdk_info.name` field in Sentry events (e.g., `sentry.javascript.react`). Will create the `sdks/` symlink. (used when creating new packages) | -| `packageUrl` | Link to the package registry page (e.g. npmjs.com, PyPI, crates.io). †Not required for `app:` types or `github:` canonicals, but set it when a separate registry or docs page exists. (used when creating new packages) | -| `mainDocsUrl` | Link to the main documentation page. If omitted, Craft falls back to `repo_url` and emits a warning. (used when creating new packages) | -| `apiDocsUrl` | Link to the API documentation (e.g. pkg.go.dev, javadoc.io) - (used when creating new packages) | +| `linkPrereleases` | By default, the registry target is skipped when the version is a pre-release (e.g. `1.0.0-beta.1`). Set to `true` to publish registry entries for pre-releases as well. | +| `checksums` | List of checksum configs (see [Checksum Configuration](#checksum-configuration)) | +| `onlyIfPresent` | Only run if artifact matches the given filename pattern | +| `name` | Human-readable name for the platform or package (e.g. `"Sentry Browser SDK"` or `"Sentry Craft"`) - (used when creating new packages) | +| `sdkName` | SDK identifier matching the SDK's `sdk_info.name` field in Sentry events (e.g., `sentry.javascript.react`). Will create the `sdks/` symlink. (used when creating new packages) | +| `packageUrl` | Link to the package registry page (e.g. npmjs.com, PyPI, crates.io). Not required for `app` types (used when creating new packages) | +| `mainDocsUrl` | Link to the main documentation page. If omitted, Craft falls back to `repo_url` and emits a warning. (used when creating new packages) | +| `apiDocsUrl` | Link to the API documentation (e.g. pkg.go.dev, javadoc.io) - (used when creating new packages) | ### Checksum Configuration @@ -82,6 +82,8 @@ After the first publish, you can add or update any of these fields in `.craft.ym ### Example: New SDK package +A package uploaded to public registries (PyPI, NPM, etc.) + ```yaml targets: - name: registry @@ -97,6 +99,8 @@ targets: ### Example: New App with downloadable artifacts +A standalone application with version files in the registry + ```yaml targets: - name: registry From d939675c608b07fc02857059ad73203164c931f1 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:30:30 +0100 Subject: [PATCH 7/7] add registry variable column --- docs/src/content/docs/targets/registry.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index c51b9766..c836b824 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -42,16 +42,16 @@ checksums: Every time Craft publishes a release, it writes a version file (e.g. `packages/npm/@sentry/browser/1.2.3.json`) to the registry. The fields in that file come from different sources: -| Field | Source | -| --------------- | --------------------------------------------------------------------------------------------------- | -| `canonical` | The dict key in `.craft.yml` (e.g. `npm:@sentry/browser`). Set once on first publish (not updated). | -| `version` | The release version being published | -| `created_at` | The current timestamp at publish time | -| `repo_url` | Auto-detected from the `origin` git remote. Overwritten on every publish | -| `name` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | -| `package_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | -| `main_docs_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | -| `api_docs_url` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| Registry field | `.craft.yml` option | Source | +|-----------------|---------------------|-----------------------------------------------------------------------------------------------------| +| `canonical` | _(dict key)_ | The dict key in `.craft.yml` (e.g. `npm:@sentry/browser`). Set once on first publish (not updated). | +| `version` | _(automatic)_ | The release version being published | +| `created_at` | _(automatic)_ | The current timestamp at publish time | +| `repo_url` | _(automatic)_ | Auto-detected from the `origin` git remote. Overwritten on every publish | +| `name` | `name` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `package_url` | `packageUrl` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `main_docs_url` | `mainDocsUrl` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | +| `api_docs_url` | `apiDocsUrl` | Your `.craft.yml` config. Applied on every publish, can be updated at any time | The `canonical` field is the only one that cannot be changed after the first publish—it is written once and then validated for consistency on every subsequent run. To rename a canonical, you must manually update both the registry and your `.craft.yml` at the same time.