← back to blog

My resume is code now

#tech·typst·ci-cd·resume

For years my resume lived in a Word doc. Every edit was a small fight. Add one bullet and the whole thing reflows, a job slides onto a second page, the gap between two sections quietly stops matching the others. I was spending more time nudging margins than writing.

So I did the obvious DevOps thing and turned it into code.

## Meet Typst

Typst is a typesetting system, a younger and friendlier take on what LaTeX does. You write plain text, it gives you a clean, precise PDF. The same typeset quality, without the LaTeX pain.

For a resume that is the whole point. The words are just text I can edit anywhere, and the layout lives in code I control instead of a word processor guessing what I meant. It compiles in well under a second, so previewing a change feels instant.

## How a change ships

Because the resume is just a text file in git, every change runs through the same path my code does.

### One file, with history

I can diff it. I can look back and see exactly what changed between the version I sent in March and the one I sent last week. No more resume_final_v3.docx.

The layout is codified too. A few spacing constants and one entry() helper keep every job block consistent, so I never hand-space anything:

resume/resume.typ
// one renderer for every org/role block, so spacing is identical everywhere
#let entry(org, loc, role, first: false) = ...

#entry("Acme Corp", "San Francisco, CA", "Senior Software Engineer")

### On every push

A push to main that touches the resume wakes up GitHub Actions. The workflow only runs for the file that matters:

.github/workflows/resume.yml
on:
  push:
    paths: ['resume/resume.typ']

From there it does three things.

  1. Bump a version tag. A patch by default, or #minor / #major from the commit message.

  2. Compile the PDF with Typst, stamping that tag into the footer.

    typst compile resume/resume.typ cv.pdf --input version=$TAG
  3. Publish it to Cloudflare R2 — the latest at a fixed URL, and a versioned archive copy beside it so old versions never disappear. My site links the latest one.

Resume publish pipeline

A push to main becomes a published, versioned PDF
The publish steps, verbatim

Two wrangler calls, one per destination. The archive copy gets an immutable cache header, because that URL will never point at different bytes again.

.github/workflows/resume.yml
- name: Publish latest to R2
  uses: cloudflare/wrangler-action@v4
  with:
    command: >-
      r2 object put public-assets/resume/cv.pdf --file=cv.pdf
      --cache-control="public, max-age=300" --remote

- name: Archive versioned PDF to R2
  uses: cloudflare/wrangler-action@v4
  with:
    command: >-
      r2 object put public-assets/resume/cv-${{ steps.tag.outputs.new_tag }}.pdf
      --file=cv.pdf --cache-control="public, max-age=31536000, immutable" --remote

## Making it ATS-friendly

A resume usually meets an applicant tracking system before it meets a person. The ATS reads your PDF by pulling the text back out of it, so what matters is whether that text comes out clean and in the right order.

The first time I ran that test, a letter-spaced title came back as spaced-out glyphs the parser can misread as separate words. Dropping the tracking brought it back clean:

What the ATS extracted

S O F T W A R E   E N G I N E E R

After dropping the tracking

SOFTWARE ENGINEER

One property, one line of layout code:

resume/resume.typ
#text(tracking: 0.15em)[SOFTWARE ENGINEER] 
#text[SOFTWARE ENGINEER] 

The fixes are boring on purpose: single column, real selectable text, standard headings. Typst keeps that easy, and the copy-paste test catches the rest before it reaches a recruiter.

## Where this goes next

Versioning was never really the point. The point is what codifying unlocks.

If the resume is structured content plus a layout, I can build automation on top of it. Given a job description, it could surface the experience I already have that best fits the role, phrased in their words. Tailored, not spammed across fifty applications.

There is one hard rule:

It can reorder, emphasize, and reword. It cannot invent.

No skill I do not have, no achievement I did not earn. A resume that lies falls apart in the interview anyway. I want to show the real me in the best light for each role, not become someone I am not on paper.

That part is still ahead of me. But it only became possible once the resume stopped being a document I fight and started being something I can build.