refactor(cli): address PR review feedback on image storage · znat/gitpulse@e148118 · GitHub
Skip to content

Commit e148118

Browse files
znatclaude
andcommitted
refactor(cli): address PR review feedback on image storage
- Drop unused storeId field on VercelBlobStorage (greptile) - URL-encode path segments in urlFor() so keys with spaces, ?, # or unicode produce valid URLs — required once PR 2/3 introduce branch names in key paths (coderabbit) - Harden storeIdToHost(): trim whitespace, throw on empty or prefix-only input (coderabbit) - Retry the post-delete 404 check up to 10s in the integration test; Vercel Blob is eventually consistent at the CDN, so an immediate re-fetch can briefly return cached bytes (coderabbit) - Extend (not replace) vitest default excludes via configDefaults.exclude so node_modules, dist, .idea, etc. stay excluded by default (coderabbit) Skipped: SHA-pinning GitHub Actions in storage-integration.yml — would be inconsistent with the rest of the repo (ci.yml, deploy-vercel*.yml all use floating @v6 tags). Belongs in a separate repo-wide PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 806526a commit e148118

4 files changed

Lines changed: 57 additions & 8 deletions

File tree

cli/src/image/storage/vercel-blob.integration.test.ts

Lines changed: 16 additions & 2 deletions

cli/src/image/storage/vercel-blob.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ describe('storeIdToHost', () => {
2525
it('handles ids without store_ prefix (defensive)', () => {
2626
expect(storeIdToHost('XyZ')).toBe('xyz.public.blob.vercel-storage.com');
2727
});
28+
29+
it('trims whitespace', () => {
30+
expect(storeIdToHost(' store_abc ')).toBe(
31+
'abc.public.blob.vercel-storage.com',
32+
);
33+
});
34+
35+
it('throws on empty or whitespace-only input', () => {
36+
expect(() => storeIdToHost('')).toThrow(/storeId is required/);
37+
expect(() => storeIdToHost(' ')).toThrow(/storeId is required/);
38+
});
39+
40+
it('throws when the prefix is the entire value', () => {
41+
expect(() => storeIdToHost('store_')).toThrow(/Invalid storeId/);
42+
});
2843
});
2944

3045
describe('VercelBlobStorage', () => {
@@ -66,6 +81,18 @@ describe('VercelBlobStorage', () => {
6681
);
6782
});
6883

84+
it('urlFor encodes special characters in path segments but preserves slashes', () => {
85+
const storage = new VercelBlobStorage(
86+
{ storeId: STORE_ID },
87+
{ BLOB_READ_WRITE_TOKEN: TOKEN },
88+
);
89+
// Branch names in PR 2 keys can include spaces, ?, #, etc. Each segment
90+
// gets percent-encoded; slashes stay as path separators.
91+
expect(storage.urlFor('feature/foo bar/#hash?.webp')).toBe(
92+
'https://abcdef123.public.blob.vercel-storage.com/feature/foo%20bar/%23hash%3F.webp',
93+
);
94+
});
95+
6996
it('list returns pathnames and follows cursors across pages', async () => {
7097
vi.mocked(blobList)
7198
.mockResolvedValueOnce({

cli/src/image/storage/vercel-blob.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export interface VercelBlobStorageOptions {
66
}
77

88
export class VercelBlobStorage implements ImageStorage {
9-
private readonly storeId: string;
109
private readonly token: string;
1110
private readonly host: string;
1211

@@ -17,7 +16,6 @@ export class VercelBlobStorage implements ImageStorage {
1716
'BLOB_READ_WRITE_TOKEN env var is required for vercel-blob storage',
1817
);
1918
}
20-
this.storeId = opts.storeId;
2119
this.token = token;
2220
this.host = storeIdToHost(opts.storeId);
2321
}
@@ -33,7 +31,7 @@ export class VercelBlobStorage implements ImageStorage {
3331
}
3432

3533
urlFor(key: string): string {
36-
return `https://${this.host}/${key}`;
34+
return `https://${this.host}/${encodePath(key)}`;
3735
}
3836

3937
async list(prefix: string): Promise<string[]> {
@@ -58,6 +56,16 @@ export class VercelBlobStorage implements ImageStorage {
5856
// https://<lowercase-storeId-without-prefix>.public.blob.vercel-storage.com/<key>
5957
// confirmed by derisking against the real API.
6058
export function storeIdToHost(storeId: string): string {
61-
const slug = storeId.replace(/^store_/, '').toLowerCase();
59+
const trimmed = storeId.trim();
60+
if (!trimmed) throw new Error('storeId is required');
61+
const slug = trimmed.replace(/^store_/, '').toLowerCase();
62+
if (!slug) throw new Error(`Invalid storeId: ${storeId}`);
6263
return `${slug}.public.blob.vercel-storage.com`;
6364
}
65+
66+
// Encode a slash-delimited key as URL path segments, preserving the slashes
67+
// as path separators. Keys can include branch names or other arbitrary
68+
// strings in PR 2/3, so we must handle spaces, ?, #, and unicode safely.
69+
function encodePath(key: string): string {
70+
return key.split('/').map(encodeURIComponent).join('/');
71+
}

cli/vitest.config.ts

Lines changed: 2 additions & 2 deletions

0 commit comments

Comments
 (0)