Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c33d64
Add resume PDF generation tooling
hcrosse Aug 24, 2026
6402fe5
Refine resume contact details
hcrosse Aug 24, 2026
b56c051
Merge main into resume PDF tooling
hcrosse Aug 24, 2026
9b840d6
Increase light resume metadata contrast
hcrosse Aug 24, 2026
34dcd23
Group resume tools by capability
hcrosse Aug 24, 2026
8fa2ab0
Refine resume tool spacing
hcrosse Aug 24, 2026
8beccd3
Add resume education and tool inventory
hcrosse Aug 24, 2026
100dab6
Expand resume content structure
hcrosse Aug 24, 2026
02c854b
Refine resume education layout
hcrosse Aug 24, 2026
7921daf
Tighten resume education spacing
hcrosse Aug 24, 2026
fd99c8e
Fix resume education spacing
hcrosse Aug 24, 2026
5248cd6
Adjust resume tool label spacing
hcrosse Aug 24, 2026
d418c12
Increase resume section heading size
hcrosse Aug 24, 2026
2baa5e2
Refine resume section spacing
hcrosse Aug 24, 2026
031d0cf
Add selected open source work
hcrosse Aug 24, 2026
ace57c3
Embed selected work contribution links
hcrosse Aug 24, 2026
6e92bd2
Clarify selected open source work
hcrosse Aug 24, 2026
1f174ee
Add property testing for tool sorting
hcrosse Aug 25, 2026
b3f5eab
Add Docker resume highlights
hcrosse Aug 25, 2026
ed0a074
Add Calendly resume highlights
hcrosse Aug 25, 2026
55db716
Add earlier employment highlights
hcrosse Aug 25, 2026
c6a6871
Trim resume employment copy
hcrosse Aug 25, 2026
b78a90b
Balance resume column spacing
hcrosse Aug 25, 2026
23623f8
Refine Calendly data contracts copy
hcrosse Aug 25, 2026
1f546f4
Refine employment details and links
hcrosse Aug 25, 2026
2ff0f5e
Simplify resume generation to typed content
hcrosse Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ node_modules/

# project
TODO.md
.resume/
.superpowers/
.worktrees/

Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,33 @@ The token and account ID must be build environment variables, not runtime
bindings or Secrets Store entries. They are required only while downloading the
private font files from R2. An R2 S3 API token created from the R2 dashboard is
not compatible with Wrangler's `CLOUDFLARE_API_TOKEN` authentication.

## Resume PDFs

Install the local browser once:

```sh
bunx playwright install chromium
```

Generate the canonical light and dark resumes from the committed general
content:

```sh
bun run resume:pdf
```

This replaces the stable tracked files only after both generated PDFs pass
validation:

- `public/harrison-crosse-resume-light.pdf`
- `public/harrison-crosse-resume-dark.pdf`

They are published at
`https://crosse.dev/harrison-crosse-resume-light.pdf` and
`https://crosse.dev/harrison-crosse-resume-dark.pdf`. The website has no resume
HTML route or navigation item.

Generated files must be parseable, non-empty US Letter PDFs. A resume that spans
more than one Letter page is retained and reported with a warning so content can
be adjusted manually.
233 changes: 142 additions & 91 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"check": "prek run --all-files && bun run format:check",
"check": "prek run --all-files && bun run format:check && bun run test:resume",
"check:build": "bun run check && bun run build",
"dev": "astro dev",
"build": "astro build",
Expand All @@ -15,12 +15,16 @@
"preview": "astro preview",
"astro": "astro",
"deploy": "sh scripts/fetch-fonts.sh && bun run build && wrangler pages deploy dist --project-name website",
"resume:pdf": "bun scripts/generate-resume-pdfs.ts",
"test:resume": "bun test src/resume/content.test.ts scripts/generate-resume-pdfs.test.ts",
"setup": "bun ci && prek install"
},
"dependencies": {
"@astrojs/markdown-remark": "7.2.2",
"@astrojs/rss": "4.0.19",
"@astrojs/sitemap": "3.7.3",
"@fontsource/archivo": "5.3.0",
"@fontsource/chivo-mono": "5.3.0",
"@resvg/resvg-js": "2.6.2",
"@tailwindcss/typography": "0.5.20",
"@tailwindcss/vite": "4.3.3",
Expand All @@ -31,8 +35,11 @@
"wrangler": "4.123.0"
},
"devDependencies": {
"@hegeldev/hegel": "0.4.5",
"oxfmt": "0.63.0",
"oxlint": "1.78.0"
"oxlint": "1.78.0",
"pdf-lib": "1.17.1",
"playwright": "1.62.1"
},
"packageManager": "bun@1.3.14"
}
Binary file added public/harrison-crosse-resume-dark.pdf
Binary file not shown.
Binary file added public/harrison-crosse-resume-light.pdf
Binary file not shown.
147 changes: 147 additions & 0 deletions scripts/generate-resume-pdfs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { PDFDocument } from "pdf-lib";
import {
createHarnessEnvironment,
generateResumePdfs,
verifyPdf,
type ResumeOutput,
} from "./generate-resume-pdfs";

test("removes inherited resume overrides from the harness environment", () => {
expect(
createHarnessEnvironment({
PATH: "/bin",
RESUME_CONTENT_PATH: "/tmp/private.json",
}),
).toEqual({ ASTRO_DEV_BACKGROUND: "0", PATH: "/bin" });
});

describe("generateResumePdfs", () => {
test("installs stable canonical PDFs and removes temporary files", async () => {
const workspace = await createTestWorkspace();

try {
await generateResumePdfs({
outputDirectory: workspace.temporaryDirectory,
publicDirectory: workspace.publicDirectory,
renderPdfs: writeLetterPdfs,
});

expect((await readdir(workspace.publicDirectory)).toSorted()).toEqual([
"harrison-crosse-resume-dark.pdf",
"harrison-crosse-resume-light.pdf",
]);
expect(await readdir(workspace.temporaryDirectory)).toEqual([]);
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});

test("cleans temporary PDFs when rendering fails", async () => {
const workspace = await createTestWorkspace();

try {
await expect(
generateResumePdfs({
outputDirectory: workspace.temporaryDirectory,
publicDirectory: workspace.publicDirectory,
renderPdfs: async (outputs) => {
await writeFile(outputs[0].temporaryPath, "partial PDF");
throw new Error("render failed");
},
}),
).rejects.toThrow("render failed");
expect(await readdir(workspace.temporaryDirectory)).toEqual([]);
expect(await readdir(workspace.publicDirectory)).toEqual([]);
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});
});

describe("verifyPdf", () => {
test("accepts a one-page US Letter PDF", async () => {
const workspace = await createTestWorkspace();
const path = resolve(workspace.temporaryDirectory, "one-page.pdf");

try {
await writePdf(path, [[612, 792]]);
await expect(verifyPdf(path)).resolves.toBeUndefined();
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});

test("rejects a zero-byte PDF", async () => {
const workspace = await createTestWorkspace();
const path = resolve(workspace.temporaryDirectory, "empty.pdf");

try {
await writeFile(path, "");
await expect(verifyPdf(path)).rejects.toThrow("Invalid PDF");
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});

test("warns and accepts a multi-page US Letter PDF", async () => {
const workspace = await createTestWorkspace();
const path = resolve(workspace.temporaryDirectory, "multi-page.pdf");
const warnings: string[] = [];

try {
await writePdf(path, [
[612, 792],
[612, 792],
]);
await verifyPdf(path, (warning) => warnings.push(warning));
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain("2 pages");
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});

test("rejects malformed and non-Letter PDFs", async () => {
const workspace = await createTestWorkspace();
const malformedPath = resolve(workspace.temporaryDirectory, "malformed.pdf");
const a4Path = resolve(workspace.temporaryDirectory, "a4.pdf");

try {
await writeFile(malformedPath, "not a PDF");
await writePdf(a4Path, [[595.28, 841.89]]);
await expect(verifyPdf(malformedPath)).rejects.toThrow("Invalid PDF");
await expect(verifyPdf(a4Path)).rejects.toThrow("US Letter");
} finally {
await rm(workspace.root, { recursive: true, force: true });
}
});
});

async function createTestWorkspace(): Promise<{
root: string;
publicDirectory: string;
temporaryDirectory: string;
}> {
await mkdir(".resume", { recursive: true });
const root = await mkdtemp(resolve(".resume/generator-test-"));
const publicDirectory = resolve(root, "public");
const temporaryDirectory = resolve(root, "temporary");
await Promise.all([
mkdir(publicDirectory, { recursive: true }),
mkdir(temporaryDirectory, { recursive: true }),
]);
return { root, publicDirectory, temporaryDirectory };
}

async function writeLetterPdfs(outputs: ResumeOutput[]): Promise<void> {
await Promise.all(outputs.map(({ temporaryPath }) => writePdf(temporaryPath, [[612, 792]])));
}

async function writePdf(path: string, sizes: Array<[number, number]>): Promise<void> {
const document = await PDFDocument.create();
for (const size of sizes) document.addPage(size);
await writeFile(path, await document.save());
}
Loading