From 96400a9e42143483d3f686fcbc59cf586a78baef Mon Sep 17 00:00:00 2001 From: Steve Landey Date: Fri, 16 Oct 2026 09:44:11 -0700 Subject: [PATCH 1/2] Let the Linear label name be configured `linear.labelName` sets the label Dependicus puts on its Linear issues and finds them by. It defaults to "Dependicus", as before. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + docs/docs/configuration.md | 10 ++++++++++ src/cli.ts | 6 ++++++ src/linear/LinearService.test.ts | 18 ++++++++++++++++++ src/linear/LinearService.ts | 18 +++++++++++------- src/linear/issueReconciler.ts | 7 ++++++- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b50bb3c..3bfe52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - The Snyk source records which versions fix each advisory, and the lowest version that fixes them all. Tickets and dependency pages show them, and `getFixVersion(store, name, version)` returns it for use as `minimumVersion`. - `getFixVersion` takes `{ minSeverity }`, to ask for the release that fixes the high and critical advisories and not wait for one that also fixes a medium. - `VersionContext.dependencyVersions` lists every version of the dependency in the repo, with the packages on each, so a spec filing one issue per team can see what else that team is on. +- `linear.labelName` sets the label Dependicus puts on its Linear issues, and finds them by, instead of "Dependicus". ### Changed diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index cb751b3..4b1bb55 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -46,6 +46,16 @@ This field is optional because [plugins](./plugins.md) can provide the same func If you do not write a plugin or pass a `getLinearIssueSpec()` function, then Dependicus cannot create Linear tickets for you. +### `linear.labelName` (optional, defaults to `"Dependicus"`) + +The Linear label Dependicus puts on every issue it files. It is also how Dependicus finds its issues again, so changing it on a workspace with open Dependicus issues leaves those issues behind. If the workspace has no label of that name, Dependicus creates one, which needs an API key with write access. + +```ts +linear: { + labelName: 'Dependency Bump', +} +``` + ### `linear.cooldownDays` (optional, defaults to `0`) Wait at least this many days after a release before creating or updating tickets to include it. diff --git a/src/cli.ts b/src/cli.ts index 656fa23..4692038 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -60,6 +60,11 @@ export interface DependicusCliConfig { context: VersionContext, store: FactStore, ) => LinearIssueSpec | LinearIssueSpec[] | undefined; + /** + * The Linear label on every issue Dependicus files, and how it finds them again. + * Created if the workspace has no label of that name. Defaults to "Dependicus". + */ + labelName?: string; /** Number of days to wait before creating a new issue for a newly-published version. */ cooldownDays?: number; /** Whether to allow new issue creation. Defaults to `true`. */ @@ -432,6 +437,7 @@ export function dependicusCli(config: DependicusCliConfig): { store, { linearApiKey, + labelName: linearConfig.labelName, dryRun: options.dryRun, getDetailUrl, providerInfoMap, diff --git a/src/linear/LinearService.test.ts b/src/linear/LinearService.test.ts index 2adda74..3a95847 100644 --- a/src/linear/LinearService.test.ts +++ b/src/linear/LinearService.test.ts @@ -51,6 +51,24 @@ describe('LinearService', () => { }); }); + it('uses the label name it was given', async () => { + mockClient.issueLabels.mockResolvedValue({ nodes: [] }); + mockClient.createIssueLabel.mockResolvedValue({ + issueLabel: Promise.resolve({ id: 'bump-789' }), + }); + const named = new LinearService('test-api-key', { labelName: 'Dependency Bump' }); + + const labelId = await named.ensureLabel(); + expect(labelId).toBe('bump-789'); + expect(mockClient.issueLabels).toHaveBeenCalledWith({ + filter: { name: { eq: 'Dependency Bump' } }, + }); + expect(mockClient.createIssueLabel).toHaveBeenCalledWith({ + name: 'Dependency Bump', + color: '#6B7280', + }); + }); + it('caches label id after first call', async () => { mockClient.issueLabels.mockResolvedValue({ nodes: [{ id: 'label-123', name: 'Dependicus' }], diff --git a/src/linear/LinearService.ts b/src/linear/LinearService.ts index 94ad400..b313b7b 100644 --- a/src/linear/LinearService.ts +++ b/src/linear/LinearService.ts @@ -7,7 +7,7 @@ import { type IssueCreateInput = LinearDocument.IssueCreateInput; -const DEPENDICUS_LABEL_NAME = 'Dependicus'; +const DEFAULT_LABEL_NAME = 'Dependicus'; const TITLE_PREFIX = '[Dependicus]'; export interface DependicusIssue { @@ -50,18 +50,22 @@ export interface CreateIssueParams { export class LinearService { private client: LinearClient; private labelId: string | undefined; + private labelName: string; private dryRun: boolean; - constructor(apiKey: string, options?: { dryRun?: boolean }) { + constructor(apiKey: string, options?: { dryRun?: boolean; labelName?: string }) { if (!apiKey) { throw new Error('LINEAR_API_KEY is required'); } this.client = new LinearClient({ apiKey }); + this.labelName = options?.labelName ?? DEFAULT_LABEL_NAME; this.dryRun = options?.dryRun ?? false; } /** - * Get or create the "Dependicus" label (workspace-level). + * Get or create the label that marks an issue as Dependicus's. Every issue + * gets it, and it is how the issues are found again. "Dependicus" unless + * the constructor was given another name. */ async ensureLabel(): Promise { if (this.labelId) { @@ -70,11 +74,11 @@ export class LinearService { // Search for existing label const labels = await this.client.issueLabels({ - filter: { name: { eq: DEPENDICUS_LABEL_NAME } }, + filter: { name: { eq: this.labelName } }, }); const existingLabel = labels.nodes.find( - (l) => l.name.toLowerCase() === DEPENDICUS_LABEL_NAME.toLowerCase(), + (l) => l.name.toLowerCase() === this.labelName.toLowerCase(), ); if (existingLabel) { @@ -84,13 +88,13 @@ export class LinearService { // Create new label const result = await this.client.createIssueLabel({ - name: DEPENDICUS_LABEL_NAME, + name: this.labelName, color: '#6B7280', // gray }); const newLabel = await result.issueLabel; if (!newLabel) { - throw new Error('Failed to create Dependicus label'); + throw new Error(`Failed to create ${this.labelName} label`); } this.labelId = newLabel.id; diff --git a/src/linear/issueReconciler.ts b/src/linear/issueReconciler.ts index 6ec56a7..5d82dbe 100644 --- a/src/linear/issueReconciler.ts +++ b/src/linear/issueReconciler.ts @@ -42,6 +42,8 @@ import { export interface IssueReconcilerConfig { linearApiKey: string; + /** The label on every issue Dependicus files. Defaults to "Dependicus". */ + labelName?: string; dryRun?: boolean; /** Builds the full detail page URL for a given package version. */ getDetailUrl: DetailUrlFn; @@ -264,7 +266,10 @@ export async function reconcileIssues( const skipStateNamesSet = new Set((config.skipStateNames ?? []).map((s) => s.toLowerCase())); - const linearService = new LinearService(config.linearApiKey, { dryRun }); + const linearService = new LinearService(config.linearApiKey, { + dryRun, + labelName: config.labelName, + }); // Find out-of-date dependencies (group by ecosystem::name to avoid // merging packages that share a name across different registries, and by From 637b944ed77b312e836265998f658e8ab8d00366 Mon Sep 17 00:00:00 2001 From: Steve Landey Date: Mon, 19 Oct 2026 14:57:24 -0700 Subject: [PATCH 2/2] Take the Linear label by id as well as by name Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 +- docs/docs/configuration.md | 14 +++++++++++--- src/cli.ts | 10 ++++++---- src/linear/LinearService.test.ts | 12 +++++++++++- src/linear/LinearService.ts | 20 +++++++++++++++----- src/linear/index.ts | 2 +- src/linear/issueReconciler.ts | 8 ++++---- 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfe52b..1ed905e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ - The Snyk source records which versions fix each advisory, and the lowest version that fixes them all. Tickets and dependency pages show them, and `getFixVersion(store, name, version)` returns it for use as `minimumVersion`. - `getFixVersion` takes `{ minSeverity }`, to ask for the release that fixes the high and critical advisories and not wait for one that also fixes a medium. - `VersionContext.dependencyVersions` lists every version of the dependency in the repo, with the packages on each, so a spec filing one issue per team can see what else that team is on. -- `linear.labelName` sets the label Dependicus puts on its Linear issues, and finds them by, instead of "Dependicus". +- `linear.label` sets the label Dependicus puts on its Linear issues, and finds them by, instead of "Dependicus". Given by id, the label is used as is, so a read-only key is enough. Given by name, it is found or created. ### Changed diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 4b1bb55..e75bbc7 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -46,13 +46,21 @@ This field is optional because [plugins](./plugins.md) can provide the same func If you do not write a plugin or pass a `getLinearIssueSpec()` function, then Dependicus cannot create Linear tickets for you. -### `linear.labelName` (optional, defaults to `"Dependicus"`) +### `linear.label` (optional, defaults to the name `"Dependicus"`) -The Linear label Dependicus puts on every issue it files. It is also how Dependicus finds its issues again, so changing it on a workspace with open Dependicus issues leaves those issues behind. If the workspace has no label of that name, Dependicus creates one, which needs an API key with write access. +The Linear label Dependicus puts on every issue it files. It is also how Dependicus finds its issues again, so changing it on a workspace with open Dependicus issues leaves those issues behind. + +Give the label by id or by name. By id, Dependicus uses the label as is, so a read-only API key is enough for a dry run. By name, Dependicus finds the label in the workspace and creates it if it is missing, which needs an API key with write access. + +```ts +linear: { + label: { id: 'a1b2c3d4-…' }, +} +``` ```ts linear: { - labelName: 'Dependency Bump', + label: { name: 'Dependency Bump' }, } ``` diff --git a/src/cli.ts b/src/cli.ts index 4692038..3bd9300 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,7 +20,7 @@ import { UvProvider } from './providers-python/index'; import { GoProvider } from './provider-go/index'; import { CargoProvider } from './provider-rust/index'; import { reconcileIssues } from './linear/index'; -import type { VersionContext, LinearIssueSpec } from './linear/index'; +import type { VersionContext, LinearIssueSpec, LinearLabelConfig } from './linear/index'; import { reconcileGitHubIssues } from './github-issues/index'; import type { GitHubIssueSpec } from './github-issues/index'; import type { VersionContext as GitHubVersionContext } from './github-issues/index'; @@ -62,9 +62,11 @@ export interface DependicusCliConfig { ) => LinearIssueSpec | LinearIssueSpec[] | undefined; /** * The Linear label on every issue Dependicus files, and how it finds them again. - * Created if the workspace has no label of that name. Defaults to "Dependicus". + * `{ id }` uses that label as is. `{ name }` finds the label in the workspace and + * creates it if it is missing, which needs an API key with write access. + * Defaults to the name "Dependicus". */ - labelName?: string; + label?: LinearLabelConfig; /** Number of days to wait before creating a new issue for a newly-published version. */ cooldownDays?: number; /** Whether to allow new issue creation. Defaults to `true`. */ @@ -437,7 +439,7 @@ export function dependicusCli(config: DependicusCliConfig): { store, { linearApiKey, - labelName: linearConfig.labelName, + label: linearConfig.label, dryRun: options.dryRun, getDetailUrl, providerInfoMap, diff --git a/src/linear/LinearService.test.ts b/src/linear/LinearService.test.ts index 3a95847..f733ebb 100644 --- a/src/linear/LinearService.test.ts +++ b/src/linear/LinearService.test.ts @@ -51,12 +51,22 @@ describe('LinearService', () => { }); }); + it('uses a label id as given, with no lookup', async () => { + const byId = new LinearService('test-api-key', { label: { id: 'bump-789' } }); + + expect(await byId.ensureLabel()).toBe('bump-789'); + expect(mockClient.issueLabels).not.toHaveBeenCalled(); + expect(mockClient.createIssueLabel).not.toHaveBeenCalled(); + }); + it('uses the label name it was given', async () => { mockClient.issueLabels.mockResolvedValue({ nodes: [] }); mockClient.createIssueLabel.mockResolvedValue({ issueLabel: Promise.resolve({ id: 'bump-789' }), }); - const named = new LinearService('test-api-key', { labelName: 'Dependency Bump' }); + const named = new LinearService('test-api-key', { + label: { name: 'Dependency Bump' }, + }); const labelId = await named.ensureLabel(); expect(labelId).toBe('bump-789'); diff --git a/src/linear/LinearService.ts b/src/linear/LinearService.ts index b313b7b..2e211a8 100644 --- a/src/linear/LinearService.ts +++ b/src/linear/LinearService.ts @@ -47,25 +47,35 @@ export interface CreateIssueParams { delegateId?: string; } +/** + * The label on every issue Dependicus files, and how it finds them again. + * By id, nothing is looked up or created, so a read-only API key is enough. + * By name, the label is found in the workspace, or created if it is missing. + */ +export type LinearLabelConfig = { id: string } | { name: string }; + export class LinearService { private client: LinearClient; private labelId: string | undefined; private labelName: string; private dryRun: boolean; - constructor(apiKey: string, options?: { dryRun?: boolean; labelName?: string }) { + constructor(apiKey: string, options?: { dryRun?: boolean; label?: LinearLabelConfig }) { if (!apiKey) { throw new Error('LINEAR_API_KEY is required'); } this.client = new LinearClient({ apiKey }); - this.labelName = options?.labelName ?? DEFAULT_LABEL_NAME; + const label = options?.label; + this.labelId = label && 'id' in label ? label.id : undefined; + this.labelName = label && 'name' in label ? label.name : DEFAULT_LABEL_NAME; this.dryRun = options?.dryRun ?? false; } /** - * Get or create the label that marks an issue as Dependicus's. Every issue - * gets it, and it is how the issues are found again. "Dependicus" unless - * the constructor was given another name. + * The id of the label that marks an issue as Dependicus's. Given an id, + * that is it. Otherwise the label is found by name, "Dependicus" unless + * the constructor was given another, and created if the workspace has + * none. */ async ensureLabel(): Promise { if (this.labelId) { diff --git a/src/linear/index.ts b/src/linear/index.ts index a7cfeae..1f15997 100644 --- a/src/linear/index.ts +++ b/src/linear/index.ts @@ -1,5 +1,5 @@ export { LinearService } from './LinearService'; -export type { DependicusIssue, CreateIssueParams } from './LinearService'; +export type { DependicusIssue, CreateIssueParams, LinearLabelConfig } from './LinearService'; export type { OutdatedDependency, OutdatedGroup, LinearPolicy, IssueAssignment } from './types'; export type { LinearIssueSpec, VersionContext, DescriptionSection } from './types'; export { diff --git a/src/linear/issueReconciler.ts b/src/linear/issueReconciler.ts index 5d82dbe..6767f01 100644 --- a/src/linear/issueReconciler.ts +++ b/src/linear/issueReconciler.ts @@ -23,7 +23,7 @@ import { toSpecList, higherVersion, } from '../core/index'; -import { LinearService, DependicusIssue } from './LinearService'; +import { LinearService, DependicusIssue, type LinearLabelConfig } from './LinearService'; import type { OutdatedDependency, OutdatedGroup, @@ -42,8 +42,8 @@ import { export interface IssueReconcilerConfig { linearApiKey: string; - /** The label on every issue Dependicus files. Defaults to "Dependicus". */ - labelName?: string; + /** The label on every issue Dependicus files, by id or by name. Defaults to "Dependicus". */ + label?: LinearLabelConfig; dryRun?: boolean; /** Builds the full detail page URL for a given package version. */ getDetailUrl: DetailUrlFn; @@ -268,7 +268,7 @@ export async function reconcileIssues( const linearService = new LinearService(config.linearApiKey, { dryRun, - labelName: config.labelName, + label: config.label, }); // Find out-of-date dependencies (group by ecosystem::name to avoid