Address second round of PR review feedback

- Replace getConfig<T> with getMetadata/setMetadata on CliAuth, removing
  the unsafe type parameter in favor of returning unknown
- Move updateInstanceConfig from cli-module-auth public API to
  CliAuth.setMetadata, removing the cross-package dependency
- Rename 'config' to 'metadata' in StoredInstance and storage schemas
- Add zod validation at consumer sites (cli-module-actions) for
  type-safe metadata access
- Fix zod imports to use zod/v3 for compatibility with zod v4
- Add proper-lockfile to cli-node for metadata write locking
- Refactor cli-node storage from fs-extra to node:fs
- Remove @backstage/cli-module-auth dependency from cli-module-actions

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-17 19:24:26 +01:00
parent 2b90358730
commit 4f6e7de133
18 changed files with 168 additions and 86 deletions
+2 -2
View File
@@ -33,10 +33,10 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/cli-module-auth": "workspace:^",
"@backstage/cli-node": "workspace:^",
"@backstage/errors": "workspace:^",
"cleye": "^2.3.0"
"cleye": "^2.3.0",
"zod": "^3.25.76 || ^4.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -16,7 +16,9 @@
import { cli } from 'cleye';
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
import { updateInstanceConfig } from '@backstage/cli-module-auth';
import { z } from 'zod/v3';
const pluginSourcesSchema = z.array(z.string()).default([]);
export default async ({ args, info }: CliCommandContext) => {
const parsed = cli(
@@ -31,7 +33,9 @@ export default async ({ args, info }: CliCommandContext) => {
const pluginId = parsed._[0];
const auth = await CliAuth.create();
const existing = (await auth.getConfig<string[]>('pluginSources')) ?? [];
const existing = pluginSourcesSchema.parse(
await auth.getMetadata('pluginSources'),
);
if (existing.includes(pluginId)) {
process.stderr.write(
@@ -40,10 +44,7 @@ export default async ({ args, info }: CliCommandContext) => {
return;
}
await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [
...existing,
pluginId,
]);
await auth.setMetadata('pluginSources', [...existing, pluginId]);
process.stdout.write(`Added plugin source "${pluginId}".\n`);
};
@@ -16,12 +16,17 @@
import { cli } from 'cleye';
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
import { z } from 'zod/v3';
const pluginSourcesSchema = z.array(z.string()).default([]);
export default async ({ args, info }: CliCommandContext) => {
cli({ help: info }, undefined, args);
const auth = await CliAuth.create();
const sources = (await auth.getConfig<string[]>('pluginSources')) ?? [];
const sources = pluginSourcesSchema.parse(
await auth.getMetadata('pluginSources'),
);
if (!sources.length) {
process.stderr.write('No plugin sources configured.\n');
@@ -16,7 +16,9 @@
import { cli } from 'cleye';
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
import { updateInstanceConfig } from '@backstage/cli-module-auth';
import { z } from 'zod/v3';
const pluginSourcesSchema = z.array(z.string()).default([]);
export default async ({ args, info }: CliCommandContext) => {
const parsed = cli(
@@ -31,15 +33,16 @@ export default async ({ args, info }: CliCommandContext) => {
const pluginId = parsed._[0];
const auth = await CliAuth.create();
const existing = (await auth.getConfig<string[]>('pluginSources')) ?? [];
const existing = pluginSourcesSchema.parse(
await auth.getMetadata('pluginSources'),
);
if (!existing.includes(pluginId)) {
process.stderr.write(`Plugin source "${pluginId}" is not configured.\n`);
return;
}
await updateInstanceConfig(
auth.getInstanceName(),
await auth.setMetadata(
'pluginSources',
existing.filter(s => s !== pluginId),
);
@@ -37,7 +37,7 @@ describe('resolveAuth', () => {
getInstanceName: jest.fn().mockReturnValue('production'),
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']),
getMetadata: jest.fn().mockResolvedValue(['catalog', 'scaffolder']),
} as unknown as CliAuth);
const result = await resolveAuth();
@@ -56,7 +56,7 @@ describe('resolveAuth', () => {
getInstanceName: jest.fn().mockReturnValue('staging'),
getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'),
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
getConfig: jest.fn().mockResolvedValue([]),
getMetadata: jest.fn().mockResolvedValue([]),
} as unknown as CliAuth);
await resolveAuth('staging');
@@ -73,7 +73,7 @@ describe('resolveAuth', () => {
.mockRejectedValue(
new Error('No access token found. Run "auth login" to authenticate.'),
),
getConfig: jest.fn().mockResolvedValue([]),
getMetadata: jest.fn().mockResolvedValue([]),
} as unknown as CliAuth);
await expect(resolveAuth()).rejects.toThrow(
@@ -86,7 +86,7 @@ describe('resolveAuth', () => {
getInstanceName: jest.fn().mockReturnValue('production'),
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
getConfig: jest.fn().mockResolvedValue(undefined),
getMetadata: jest.fn().mockResolvedValue(undefined),
} as unknown as CliAuth);
const result = await resolveAuth();
@@ -15,6 +15,9 @@
*/
import { CliAuth } from '@backstage/cli-node';
import { z } from 'zod/v3';
const pluginSourcesSchema = z.array(z.string()).default([]);
export async function resolveAuth(instanceFlag?: string): Promise<{
baseUrl: string;
@@ -24,7 +27,9 @@ export async function resolveAuth(instanceFlag?: string): Promise<{
}> {
const auth = await CliAuth.create({ instanceName: instanceFlag });
const accessToken = await auth.getAccessToken();
const pluginSources = (await auth.getConfig<string[]>('pluginSources')) ?? [];
const pluginSources = pluginSourcesSchema.parse(
await auth.getMetadata('pluginSources'),
);
return {
baseUrl: auth.getBaseUrl(),