Add release pipeline and upgrade toolchain to TypeScript 7
Publish snapshot / snapshot (push) Failing after 1m58s
Publish snapshot / snapshot (push) Failing after 1m58s
Publishing infrastructure - Three Gitea workflows: ci.yml (PRs, non-main pushes), publish-snapshot.yml (main -> Gitea under dist-tag @main) and release.yml (tags -> Gitea + npmjs) - Tag-driven releases as <package-dir>-v<version>; the manifest stays the source of truth and release.yml refuses to run if tag and manifest disagree - Every publish is idempotent: each step checks the registry first, so a run that fails on the second registry can simply be re-run - Hard coverage gate (85% branches) shared by CI, the pre-push hook and local runs, since the thresholds live in vitest.config.ts rather than a CI flag - README.PUBLISH.md documents the whole mechanism Toolchain - TypeScript 7 native compiler; drop tsgo and ts-node, use tsx for dev runs - Biome 1.9 -> 2.x, Vitest 1 -> 4, zod 3 -> 4, inquirer 8 -> 14, pnpm 11.17.0 - Replace inquirer-checkbox-plus-prompt, which is peer-capped at inquirer <9, with enquirer's AutoComplete; the CubeSelection contract is unchanged - Stand in for zod 4's removed z.AnyZodObject with a local AnyObjectSchema Repo hygiene - Stop tracking dist/; ignore coverage/, *.tsbuildinfo, .npmrc* and release.json - Drop package-lock.json in favour of pnpm-lock.yaml Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Edge cases for BuildContext: unknown cubes, session replay and auth flags.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
import { BuildContext } from '../src/cubes/dependencies.js';
|
||||
import { Cube, Manifest } from '../src/cubes/types.js';
|
||||
import { Variables } from '../src/nopy.common.js';
|
||||
import type { NopyConfig } from '../src/nopy.config.js';
|
||||
import type { NopySession } from '../src/nopy.session.js';
|
||||
|
||||
vi.mock('../src/nopy.prompts.js', async () => {
|
||||
const actual = await vi.importActual('../src/nopy.prompts.js');
|
||||
return { ...actual, VariableAssignment: vi.fn() };
|
||||
});
|
||||
|
||||
import { VariableAssignment } from '../src/nopy.prompts.js';
|
||||
|
||||
const testCube = (id: string, schema = z.object({})) =>
|
||||
new Cube(Manifest.create({ id, name: `Test ${id}`, schema }), `/test/${id}`, 'deploy.py');
|
||||
|
||||
const config = { env: {} } as NopyConfig;
|
||||
const session = (cubes: NopySession['cubes'] = []) => ({ cubes }) as NopySession;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('BuildContext error handling', () => {
|
||||
it('throws when the requested cube does not exist', async () => {
|
||||
const context = new BuildContext({}, new Variables(), session(), config, { method: 'ssh' });
|
||||
|
||||
await expect(context.resolveCube('ghost', 'host1')).rejects.toThrow('Cube not found: ghost');
|
||||
});
|
||||
|
||||
it('throws when a dependency does not exist', async () => {
|
||||
const cubeB = new Cube(
|
||||
Manifest.create({
|
||||
id: 'cube-b',
|
||||
name: 'B',
|
||||
schema: z.object({}),
|
||||
dependencies: () => ['ghost'],
|
||||
}),
|
||||
'/test/cube-b',
|
||||
'deploy.py'
|
||||
);
|
||||
const context = new BuildContext({ 'cube-b': cubeB }, new Variables(), session(), config, {
|
||||
method: 'ssh',
|
||||
});
|
||||
|
||||
await expect(context.resolveCube('cube-b', 'host1')).rejects.toThrow('Cube not found: ghost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BuildContext session replay', () => {
|
||||
it('takes variables from the session instead of prompting', async () => {
|
||||
const cube = testCube('cube-a', z.object({ PORT: z.string().default('3000') }));
|
||||
const vars = new Variables();
|
||||
const context = new BuildContext(
|
||||
{ 'cube-a': cube },
|
||||
vars,
|
||||
session([{ key: 'cube-a', variables: { PORT: '9090' } }]),
|
||||
config,
|
||||
{ method: 'ssh' },
|
||||
{ isSessionReplay: true }
|
||||
);
|
||||
|
||||
await context.resolveCube('cube-a', 'host1');
|
||||
|
||||
expect(VariableAssignment).not.toHaveBeenCalled();
|
||||
expect(context.deployCalls[0].env.PORT).toBe('9090');
|
||||
});
|
||||
|
||||
it('falls back to schema defaults when the session has no entry for the cube', async () => {
|
||||
const cube = testCube('cube-a', z.object({ PORT: z.string().default('3000') }));
|
||||
const context = new BuildContext(
|
||||
{ 'cube-a': cube },
|
||||
new Variables(),
|
||||
session([{ key: 'other', variables: { PORT: '9090' } }]),
|
||||
config,
|
||||
{ method: 'ssh' },
|
||||
{ isSessionReplay: true }
|
||||
);
|
||||
|
||||
await context.resolveCube('cube-a', 'host1');
|
||||
|
||||
expect(VariableAssignment).not.toHaveBeenCalled();
|
||||
expect(context.deployCalls[0].env.PORT).toBe('3000');
|
||||
});
|
||||
|
||||
it('prompts when not replaying', async () => {
|
||||
const context = new BuildContext(
|
||||
{ 'cube-a': testCube('cube-a') },
|
||||
new Variables(),
|
||||
session(),
|
||||
config,
|
||||
{ method: 'ssh' }
|
||||
);
|
||||
|
||||
await context.resolveCube('cube-a', 'host1');
|
||||
|
||||
expect(VariableAssignment).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BuildContext command construction', () => {
|
||||
const build = (auth: { method: string; username?: string; password?: string }) => {
|
||||
const context = new BuildContext(
|
||||
{ 'cube-a': testCube('cube-a') },
|
||||
new Variables(),
|
||||
session(),
|
||||
config,
|
||||
auth
|
||||
);
|
||||
return context.resolveCube('cube-a', 'host1').then(() => context);
|
||||
};
|
||||
|
||||
it('adds --user/--password for complete password auth', async () => {
|
||||
const context = await build({ method: 'password', username: 'deploy', password: 'pw' });
|
||||
|
||||
expect(context.deployCalls[0].command.join(' ')).toContain('--user deploy --password pw');
|
||||
});
|
||||
|
||||
it('omits credentials for ssh auth', async () => {
|
||||
const context = await build({ method: 'ssh' });
|
||||
|
||||
expect(context.deployCalls[0].command.join(' ')).not.toContain('--user');
|
||||
});
|
||||
|
||||
it('omits credentials when the password is missing', async () => {
|
||||
const context = await build({ method: 'password', username: 'deploy' });
|
||||
|
||||
expect(context.deployCalls[0].command.join(' ')).not.toContain('--user');
|
||||
});
|
||||
|
||||
it('omits credentials when the username is missing', async () => {
|
||||
const context = await build({ method: 'password', password: 'pw' });
|
||||
|
||||
expect(context.deployCalls[0].command.join(' ')).not.toContain('--user');
|
||||
});
|
||||
|
||||
it('passes cube variables as --data flags and points at the deploy script', async () => {
|
||||
const cube = testCube('cube-a', z.object({ PORT: z.string().default('3000') }));
|
||||
const context = new BuildContext({ 'cube-a': cube }, new Variables(), session(), config, {
|
||||
method: 'ssh',
|
||||
});
|
||||
|
||||
await context.resolveCube('cube-a', 'host1');
|
||||
const command = context.deployCalls[0].command.join(' ');
|
||||
|
||||
expect(command).toContain('--data "PORT=3000"');
|
||||
expect(command).toContain('--chdir /test/cube-a');
|
||||
expect(command).toContain('/test/cube-a/deploy.py');
|
||||
expect(context.deployCalls[0].cwd).toBe('/test/cube-a');
|
||||
});
|
||||
|
||||
it('builds a separate call per host but records the cube session once', async () => {
|
||||
const context = new BuildContext(
|
||||
{ 'cube-a': testCube('cube-a') },
|
||||
new Variables(),
|
||||
session(),
|
||||
config,
|
||||
{ method: 'ssh' }
|
||||
);
|
||||
|
||||
await context.resolveCube('cube-a', 'host1');
|
||||
await context.resolveCube('cube-a', 'host2');
|
||||
|
||||
expect(context.deployCalls.map((c) => c.host)).toEqual(['host1', 'host2']);
|
||||
expect(context.cubeSessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('applies caller overrides as params', async () => {
|
||||
const cube = testCube('cube-a', z.object({ PORT: z.string().default('3000') }));
|
||||
const context = new BuildContext({ 'cube-a': cube }, new Variables(), session(), config, {
|
||||
method: 'ssh',
|
||||
});
|
||||
|
||||
await context.resolveCube('cube-a', 'host1', { PORT: '8080' });
|
||||
|
||||
expect(context.deployCalls[0].env.PORT).toBe('8080');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user