Skip to content

Commit 5288d54

Browse files
feat: enhance CI configuration for cross-platform support and improve path normalization in scanning functions
1 parent b25ddad commit 5288d54

File tree

8 files changed

+58
-12
lines changed

8 files changed

+58
-12
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ on:
88

99
jobs:
1010
build:
11-
runs-on: ubuntu-latest
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, windows-latest]
1216
steps:
1317
- uses: actions/checkout@v4
1418
- name: Use Node.js 20

docs/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Use Context7 MCP for up to date documentation.
259259
Output JSON of changed files and new version.
260260
Verify: Example consumes output.
261261

262-
44. [ ] **Windows path handling**
262+
44. [x] **Windows path handling**
263263
Use `node:path`. Add Windows CI job.
264264
Verify: Windows runner green.
265265

src/scanning/patterns/python-version.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import path from 'node:path';
2+
13
export interface VersionMatch {
24
file: string;
35
line: number;
@@ -103,13 +105,12 @@ export const pythonVersionPatterns: PatternDefinition[] = [
103105
];
104106

105107
function normalizePath(filePath: string): string {
106-
return filePath.replace(/\\\\/g, '/');
108+
return filePath.split(path.win32.sep).join(path.posix.sep);
107109
}
108110

109111
function getBasename(filePath: string): string {
110112
const normalized = normalizePath(filePath);
111-
const index = normalized.lastIndexOf('/');
112-
const base = index === -1 ? normalized : normalized.slice(index + 1);
113+
const base = path.posix.basename(normalized);
113114
return base.toLowerCase();
114115
}
115116

src/scanning/scanner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,18 @@ export async function scanForPythonVersions(options: ScanOptions): Promise<ScanR
4646

4747
for (const relative of relativeFiles) {
4848
const absolute = path.join(root, relative);
49+
const relativePosix = relative.split(path.win32.sep).join(path.posix.sep);
4950
const content = await readFileSafe(absolute);
5051
if (content === null) {
5152
continue;
5253
}
5354

5455
filesScanned += 1;
55-
const fileMatches = findPythonVersionMatches(relative, content);
56+
const fileMatches = findPythonVersionMatches(relativePosix, content);
5657
matches.push(
5758
...fileMatches.map((match) => ({
5859
...match,
59-
file: relative,
60+
file: relativePosix,
6061
})),
6162
);
6263
}

src/versioning/release-notes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ export async function fetchReleaseNotes(
5353
}
5454

5555
if (!response.ok) {
56-
throw new Error(`Failed to fetch release notes for ${normalizedTag} (status ${response.status}).`);
56+
throw new Error(
57+
`Failed to fetch release notes for ${normalizedTag} (status ${response.status}).`,
58+
);
5759
}
5860

5961
const payload = (await response.json()) as GitHubReleasePayload;

tests/python-version-patterns.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ describe('findPythonVersionMatches', () => {
1212
expect(matches[0]?.matched).toBe('3.13.2');
1313
});
1414

15+
it('normalizes Windows-style workflow paths', () => {
16+
const content = `jobs:\n build:\n steps:\n - uses: actions/setup-python@v4\n with:\n python-version: "3.13.5"`;
17+
18+
const matches = findPythonVersionMatches('.github\\workflows\\ci.yml', content);
19+
20+
expect(matches).toHaveLength(1);
21+
expect(matches[0]?.matched).toBe('3.13.5');
22+
});
23+
1524
it('captures versions within Dockerfiles', () => {
1625
const content = `FROM python:3.13.2-slim\nARG PYTHON_VERSION="3.12.8"\nENV PYTHON_VERSION=3.12.8`;
1726

tests/release-notes.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { describe, expect, it } from 'vitest';
33
import { fetchReleaseNotes } from '../src/versioning';
44

55
describe('fetchReleaseNotes', () => {
6-
const createFetch = (response: { status: number; body?: unknown }) =>
7-
async () => ({
6+
function createFetch(response: { status: number; body?: unknown }) {
7+
return async () => ({
88
status: response.status,
99
ok: response.status >= 200 && response.status < 300,
1010
async json() {
1111
return response.body ?? {};
1212
},
1313
});
14+
}
1415

1516
it('returns release note body when available', async () => {
1617
const fetchImpl = createFetch({ status: 200, body: { body: 'Security fix included.' } });

tests/scanner-errors.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ const missingError = Object.assign(new Error('missing'), { code: 'ENOENT' });
55
describe('scanForPythonVersions error handling', () => {
66
it('ignores files that cannot be read', async () => {
77
vi.resetModules();
8-
vi.mock('../src/scanning/glob-discovery', () => ({
8+
await vi.doMock('../src/scanning/glob-discovery', () => ({
99
__esModule: true,
1010
discoverFiles: vi.fn(async () => ['missing.txt']),
1111
}));
12-
vi.mock('node:fs/promises', () => ({
12+
await vi.doMock('node:fs/promises', () => ({
1313
__esModule: true,
1414
readFile: vi.fn(async () => {
1515
throw missingError;
@@ -25,4 +25,32 @@ describe('scanForPythonVersions error handling', () => {
2525
vi.unmock('../src/scanning/glob-discovery');
2626
vi.unmock('node:fs/promises');
2727
});
28+
29+
it('normalizes Windows-style separators from glob discovery', async () => {
30+
vi.resetModules();
31+
const discoverFilesMock = vi.fn(async () => ['.github\\workflows\\ci.yml']);
32+
await vi.doMock('../src/scanning/glob-discovery', () => ({
33+
__esModule: true,
34+
discoverFiles: discoverFilesMock,
35+
}));
36+
await vi.doMock('node:fs/promises', () => ({
37+
__esModule: true,
38+
readFile: vi.fn(async () => 'python-version: "3.13.2"'),
39+
}));
40+
41+
const { scanForPythonVersions } = await import('../src/scanning/scanner');
42+
const result = await scanForPythonVersions({
43+
root: '.',
44+
patterns: ['**/*.yml'],
45+
});
46+
47+
expect(discoverFilesMock).toHaveBeenCalled();
48+
expect(result.filesScanned).toBe(1);
49+
expect(result.matches).toEqual([
50+
expect.objectContaining({ file: '.github/workflows/ci.yml', matched: '3.13.2' }),
51+
]);
52+
53+
vi.unmock('../src/scanning/glob-discovery');
54+
vi.unmock('node:fs/promises');
55+
});
2856
});

0 commit comments

Comments
 (0)