diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md index 2ac98bf8..e30ea150 100644 --- a/docs/src/content/docs/targets/registry.md +++ b/docs/src/content/docs/targets/registry.md @@ -19,12 +19,13 @@ Avoid having multiple `registry` targets—it supports batching multiple apps an ### App/SDK 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) | +| `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) | @@ -59,7 +60,9 @@ targets: ## Creating New 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. +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. + +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): ```yaml targets: @@ -67,6 +70,7 @@ targets: sdks: 'npm:@sentry/wasm': name: 'Sentry WASM' + sdkName: 'sentry.javascript.wasm' packageUrl: 'https://www.npmjs.com/package/@sentry/wasm' mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/' ``` @@ -90,4 +94,4 @@ The value is always overwritten on every publish, so it stays in sync with the a ### Other metadata -When specified, the metadata fields (`name`, `packageUrl`, `mainDocsUrl`, `apiDocsUrl`) are applied to every release, allowing you to update package metadata by changing your `.craft.yml` configuration. +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. diff --git a/src/targets/registry.ts b/src/targets/registry.ts index af92f189..893d37c8 100644 --- a/src/targets/registry.ts +++ b/src/targets/registry.ts @@ -67,6 +67,8 @@ export interface RegistryConfig { onlyIfPresent?: RegExp; /** Human-readable name for new packages */ name?: string; + /** SDK identifier used in the `sdk_info.name` field of the event (e.g. "sentry.javascript.react"). Used to create an entry in the registry's sdks/ directory. */ + sdkName?: string; /** Link to package registry (PyPI, npm, etc.) */ packageUrl?: string; /** Link to main documentation */ @@ -459,6 +461,7 @@ export class RegistryTarget extends BaseTarget { canonical: registryConfig.canonicalName, repoUrl: `https://github.com/${owner}/${repo}`, name: registryConfig.name, + sdkName: registryConfig.sdkName, packageUrl: registryConfig.packageUrl, mainDocsUrl: registryConfig.mainDocsUrl, apiDocsUrl: registryConfig.apiDocsUrl, diff --git a/src/utils/__tests__/registry.test.ts b/src/utils/__tests__/registry.test.ts index 0dc4c988..44efbebb 100644 --- a/src/utils/__tests__/registry.test.ts +++ b/src/utils/__tests__/registry.test.ts @@ -200,5 +200,115 @@ describe('getPackageManifest', () => { api_docs_url: 'https://docs.sentry.io/api/', }); }); + + describe('sdkName symlink creation', () => { + it('creates a sdks/ symlink when sdkName is provided', async () => { + fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/hono', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + name: 'Sentry Hono SDK', + sdkName: 'sentry.javascript.hono', + }; + + await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/hono', + '1.0.0', + initialData, + ); + + 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( + path.join('..', 'packages', 'npm', '@sentry', 'hono'), + ); + }); + + it('does not create a sdks/ symlink when sdkName is not provided', async () => { + fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/hono', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + }; + + await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/hono', + '1.0.0', + initialData, + ); + + const sdksDir = path.join(tempDir, 'sdks'); + expect(fs.readdirSync(sdksDir)).toHaveLength(0); + }); + + 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 existingTarget = path.join( + '..', + 'packages', + 'npm', + '@sentry', + 'hono', + ); + fs.symlinkSync(existingTarget, symlinkPath); + + const initialData: InitialManifestData = { + canonical: 'npm:@sentry/hono', + repoUrl: 'https://github.com/getsentry/sentry-javascript', + sdkName: 'sentry.javascript.hono', + }; + + // Should not throw or overwrite the existing symlink + await expect( + getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/hono', + '1.0.0', + initialData, + ), + ).resolves.not.toThrow(); + + expect(fs.readlinkSync(symlinkPath)).toBe(existingTarget); + }); + + it('does not create a sdks/ symlink for existing packages (only new ones)', async () => { + fs.mkdirSync(path.join(tempDir, 'sdks'), { recursive: true }); + + // Set up an existing package with latest.json + const packageDir = path.join( + tempDir, + 'packages', + 'npm', + '@sentry', + 'browser', + ); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'latest.json'), + JSON.stringify({ + canonical: 'npm:@sentry/browser', + version: '1.0.0', + }), + ); + + await getPackageManifest( + tempDir, + RegistryPackageType.SDK, + 'npm:@sentry/browser', + '1.1.0', + // sdkName is only consulted when the package is new + ); + + const sdksDir = path.join(tempDir, 'sdks'); + expect(fs.readdirSync(sdksDir)).toHaveLength(0); + }); + }); }); }); diff --git a/src/utils/registry.ts b/src/utils/registry.ts index 81babe96..c880c3c8 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -1,4 +1,4 @@ -import { promises as fsPromises, existsSync, mkdirSync } from 'fs'; +import { promises as fsPromises, existsSync, mkdirSync, symlinkSync } from 'fs'; import * as path from 'path'; import { logger } from '../logger'; @@ -29,6 +29,8 @@ export interface InitialManifestData { mainDocsUrl?: string; /** Link to API documentation */ apiDocsUrl?: string; + /** SDK identifier used in the `sdk_info.name` field of the event (e.g. "sentry.javascript.react"). Used to create an entry in the registry's sdks/ directory. */ + sdkName?: string; } /** @@ -108,6 +110,18 @@ export async function getPackageManifest( mkdirSync(fullPackageDir, { recursive: true }); } + // Create the sdks/ symlink when an sdkName is provided + if (initialManifestData.sdkName) { + const sdkSymlinkPath = path.join(baseDir, 'sdks', initialManifestData.sdkName); + if (!existsSync(sdkSymlinkPath)) { + const relativeTarget = path.join('..', packageDirPath); + logger.info( + `Creating sdks symlink "${initialManifestData.sdkName}" -> "${relativeTarget}"...`, + ); + symlinkSync(relativeTarget, sdkSymlinkPath); + } + } + logger.info( `Creating initial manifest for new package "${canonicalName}"...`, );