Skip to content

Commit 384ea09

Browse files
feat: add track validation and integrate into action execution
1 parent 2aabfc0 commit 384ea09

File tree

5 files changed

+61
-3
lines changed

5 files changed

+61
-3
lines changed

docs/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Use Context7 MCP for up to date documentation.
229229
Emit `multiple_tracks_detected`, `runners_missing`, `no_matches_found`, `already_latest`, `pr_exists`, `pr_creation_failed`.
230230
Verify: Tests assert outputs and logs.
231231

232-
37. [ ] **Config validation with zod**
232+
37. [x] **Config validation with zod**
233233
Validate `track` as `/^\d+\.\d+$/`.
234234
Verify: Bad inputs fail fast.
235235

src/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from 'zod';
2+
3+
const trackSchema = z
4+
.string()
5+
.regex(/^[0-9]+\.[0-9]+$/, 'Track must be in the form X.Y (for example 3.13).');
6+
7+
export function validateTrack(track: string): string {
8+
const result = trackSchema.safeParse(track);
9+
if (!result.success) {
10+
throw new Error(`Input "track" must match X.Y (e.g. 3.13). Received "${track}".`);
11+
}
12+
13+
return result.data;
14+
}

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveLatestPatch,
1717
} from './versioning';
1818
import { createOrUpdatePullRequest, findExistingPullRequest } from './git';
19+
import { validateTrack } from './config';
1920

2021
const DEFAULT_TRACK = '3.13';
2122
const DEFAULT_PATHS = [
@@ -153,6 +154,7 @@ export async function run(): Promise<void> {
153154
try {
154155
const trackInput = core.getInput('track').trim();
155156
const track = trackInput === '' ? DEFAULT_TRACK : trackInput;
157+
const validatedTrack = validateTrack(track);
156158

157159
const includePrerelease = getBooleanInput('include_prerelease', false);
158160
const automerge = getBooleanInput('automerge', false);
@@ -166,7 +168,7 @@ export async function run(): Promise<void> {
166168

167169
core.startGroup('Configuration');
168170
core.info(`workspace: ${workspace}`);
169-
core.info(`track: ${track}`);
171+
core.info(`track: ${validatedTrack}`);
170172
core.info(`include_prerelease: ${includePrerelease}`);
171173
core.info(`paths (${effectivePaths.length}): ${effectivePaths.join(', ')}`);
172174
core.info(`automerge: ${automerge}`);
@@ -185,7 +187,7 @@ export async function run(): Promise<void> {
185187
const result = await executeAction(
186188
{
187189
workspace,
188-
track,
190+
track: validatedTrack,
189191
includePrerelease,
190192
paths: effectivePaths,
191193
dryRun,

tests/config.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { validateTrack } from '../src/config';
4+
5+
describe('validateTrack', () => {
6+
it('returns the track when format is valid', () => {
7+
expect(validateTrack('3.13')).toBe('3.13');
8+
expect(validateTrack('3.12')).toBe('3.12');
9+
});
10+
11+
it('throws an error when track does not match X.Y', () => {
12+
expect(() => validateTrack('3')).toThrow(/must match X\.Y/i);
13+
expect(() => validateTrack('3.13.1')).toThrow(/must match X\.Y/i);
14+
expect(() => validateTrack('banana')).toThrow(/must match X\.Y/i);
15+
});
16+
});

tests/index.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ const actionExecutionMocks = vi.hoisted(() => ({
1515
executeAction: vi.fn(),
1616
}));
1717

18+
const configMocks = vi.hoisted(() => ({
19+
validateTrack: vi.fn((value: string) => value),
20+
}));
21+
1822
vi.mock('@actions/core', () => coreMocks);
1923
vi.mock('../src/action-execution', () => ({
2024
executeAction: actionExecutionMocks.executeAction,
2125
}));
26+
vi.mock('../src/config', () => ({
27+
validateTrack: configMocks.validateTrack,
28+
}));
2229

2330
const mockGetInput = coreMocks.getInput;
2431
const mockGetMultilineInput = coreMocks.getMultilineInput;
@@ -29,6 +36,7 @@ const mockSetOutput = coreMocks.setOutput;
2936
const mockSetFailed = coreMocks.setFailed;
3037
const mockWarning = coreMocks.warning;
3138
const mockExecuteAction = actionExecutionMocks.executeAction as ReturnType<typeof vi.fn>;
39+
const mockValidateTrack = configMocks.validateTrack as ReturnType<typeof vi.fn>;
3240

3341
import { run } from '../src/index';
3442

@@ -46,6 +54,7 @@ describe('run', () => {
4654
filesChanged: ['Dockerfile'],
4755
dryRun: true,
4856
});
57+
mockValidateTrack.mockImplementation((value: string) => value);
4958
});
5059

5160
it('uses default configuration when inputs are empty', async () => {
@@ -134,4 +143,21 @@ describe('run', () => {
134143

135144
expect(mockSetFailed).toHaveBeenCalledWith('run failure');
136145
});
146+
147+
it('fails fast when track input is invalid', async () => {
148+
mockGetInput.mockImplementation((name: string) => {
149+
if (name === 'track') return 'invalid';
150+
return '';
151+
});
152+
mockValidateTrack.mockImplementation(() => {
153+
throw new Error('Input "track" must match X.Y (e.g. 3.13). Received "invalid".');
154+
});
155+
156+
await run();
157+
158+
expect(mockExecuteAction).not.toHaveBeenCalled();
159+
expect(mockSetFailed).toHaveBeenCalledWith(
160+
'Input "track" must match X.Y (e.g. 3.13). Received "invalid".',
161+
);
162+
});
137163
});

0 commit comments

Comments
 (0)