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
10 changes: 7 additions & 3 deletions docs/src/content/docs/targets/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -59,14 +60,17 @@ 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:
- name: registry
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/'
```
Expand All @@ -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.
3 changes: 3 additions & 0 deletions src/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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,
Expand Down
110 changes: 110 additions & 0 deletions src/utils/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
16 changes: 15 additions & 1 deletion src/utils/registry.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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}"...`,
);
Expand Down
Loading