Back to blog
FILE 0x0E·YOUR CV UPDATES ITSELF ON EVERY GIT PUSH

Your CV updates itself on every git push

June 9, 2026 · evercv, github-actions, ci, saas, side-project

The nightly rebuild is fine. But if you merged something worth talking about and you're about to send your CV somewhere, you want it current now.

I added a CI endpoint to EverCV: POST /api/ci/refresh, authenticated with a Bearer token. One step in your GitHub Actions workflow and your CV rebuilds within 30 seconds of every push to main.


Setup

  1. Dashboard → Profile → copy your CI Token (auto-generated on first visit).
  2. GitHub repo → Settings → Secrets → Add EVERCV_CI_TOKEN.
  3. Add .github/workflows/evercv-refresh.yml:
name: Refresh EverCV

on:
  push:
    branches: [main]

jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger EverCV CV rebuild
        run: |
          curl -s -f -X POST https://evercv.io/api/ci/refresh \
            -H "Authorization: Bearer ${{ secrets.EVERCV_CI_TOKEN }}" \
          && echo "EverCV rebuild triggered" \
          || echo "EverCV refresh failed (non-blocking)"

The || echo tail means a failed rebuild never blocks your deploy. The CV refresh is a best-effort side effect, not a gate.


How the endpoint works

def handle_ci_refresh(event: dict) -> dict:
    auth_header = (event.get("headers") or {}).get("authorization", "")
    if not auth_header.lower().startswith("bearer "):
        return _json(401, {"error": "Authorization: Bearer {ci_token} required"})
    raw_token = auth_header[7:].strip()

    # Token lookup — scan_all_users() is fine at small scale;
    # add a GSI on ci_token if the table grows past ~10k users
    all_users = db.scan_all_users()
    user = next((u for u in all_users if u.get("ci_token") == raw_token), None)
    if not user:
        return _json(401, {"error": "Invalid or expired CI token"})

    result = refresh_cv.refresh_one(user)
    return _json(200, {"ok": result.ok, "error": result.error})

The CI token is separate from your session cookie and GitHub OAuth token. It only grants the ability to trigger a rebuild — nothing else. If you suspect it leaked, generate a new one from the dashboard (the old one stops working immediately).


The token

def get_or_create_ci_token(user: dict) -> str:
    existing = user.get("ci_token", "")
    if existing:
        return existing
    token = str(uuid.uuid4())
    update_user(user["user_id"], {"ci_token": token})
    return token

Single field on the existing user record. No new table. First call generates and stores it; subsequent calls return the same token. This is a read-modify-write, but since it's append-only (we don't rotate unless the user asks), no race condition matters.


What happens after the push

  1. GitHub Actions runs the curl step after your deploy step.
  2. The Lambda authenticates the token, finds your user record, calls refresh_cv.refresh_one(user).
  3. refresh_one fetches your latest commits from GitHub, re-runs the CV summarization pipeline, saves the result.
  4. Your public profile at evercv.io/p/{slug} shows the updated CV.

The whole thing takes ~15-25 seconds depending on how many commits the summarizer processes.


The badge tie-in

Combine this with the README badge:

[![EverCV](https://evercv.io/badge/YOUR_SLUG)](https://evercv.io/p/YOUR_SLUG)

The badge is cached for 3600 seconds by GitHub's camo proxy, so it might lag by an hour. But the profile page itself updates in real time. The badge shows the date of the last refresh — so after a push, within ~2 hours anyone clicking the badge sees your fresh CV.


EverCV is at evercv.io. Pro is $8/mo. The CI token endpoint and GitHub Actions workflow are included on all paid plans.