From 6477987608da99ae3762bcd9fd9bf2e0509dfc29 Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Wed, 8 Jul 2026 16:09:33 +0100 Subject: [PATCH] ci: add GitHub Actions workflow - LaTeX smoke compiles, skill lint, CLI typechecks, placeholder integrity (#59) Every PR to this repo is currently verified by hand. This adds the checks a machine can do: - latex-smoke: compiles cv/main_example.tex (lualatex) and the new cover_letters/cover_example.tex (xelatex) in the texlive/texlive container, failing on any LaTeX error. Exact page-count assertions (CV=2, cover letter=1) run on the upstream repo only - lint (tools/lint_skills.py, also runnable locally): every SKILL.md has parseable YAML frontmatter with name+description (frontmatter breakage happened before - 37a0eed), allowed-tools 'bun run ' targets exist, command files start with a '# /' title, settings.json is valid JSON with a permissions.allow list - cli-typecheck: bun install + tsc --noEmit for all five portal CLIs (matrix, fail-fast off) - placeholder-integrity (upstream only): tracked template files still carry their placeholder tokens, catching accidental personal-data commits before they land Fork-friendly by design: /setup personalizes CLAUDE.md, the skill files, and main_example.tex in forks, so placeholder checks and exact page counts are guarded with github.repository == upstream; compile success and lint run everywhere. Live CLI smoke tests are deliberately excluded: network-flaky, and linkedin-search is personal-use-only per its own ToS warning - CI-automated requests would violate it. CLIs are typechecked instead. The cover letter previously had no tracked example (cover_*.tex is gitignored), so cover_example.tex is new: a placeholder letter following the documented 06 structure, demonstrating the correct itemize-outside- lettercontent pattern. It doubles as the structural reference /apply Step 2 looks for on fresh clones, which until now matched nothing. The gitignore exception is ordered after Cover_*.tex because case-insensitive filesystems match that pattern against cover_example.tex too. Writing it surfaced a latent bug in the documented template itself: 06-cover-letter-templates.md's structure ends with \closing{Kind regards,\} - but cover.cls appends its own \, and the doubled break produces '! LaTeX Error: There's no line here to end.' on every compile (nonstopmode swallows it, so it went unnoticed). Fixed in 06 and noted in the example. --- .../06-cover-letter-templates.md | 4 +- .github/workflows/ci.yml | 108 ++++++++++++++++++ .gitignore | 1 + README.md | 5 + cover_letters/cover_example.tex | 61 ++++++++++ tools/lint_skills.py | 107 +++++++++++++++++ 6 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cover_letters/cover_example.tex create mode 100644 tools/lint_skills.py diff --git a/.claude/skills/job-application-assistant/06-cover-letter-templates.md b/.claude/skills/job-application-assistant/06-cover-letter-templates.md index 293ba95..02d46d5 100644 --- a/.claude/skills/job-application-assistant/06-cover-letter-templates.md +++ b/.claude/skills/job-application-assistant/06-cover-letter-templates.md @@ -99,7 +99,9 @@ The font wrapper is mandatory — if you just move `\begin{itemize}` outside `\l \lettercontent{I look forward to hearing from you.} \begin{flushright} -\closing{Kind regards,\\} +% No trailing \\ inside \closing{} - cover.cls appends its own \\, and a +% doubled break triggers "! LaTeX Error: There's no line here to end." +\closing{Kind regards,} \signature{[YOUR_NAME]} \end{flushright} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b6b607 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +# CI for the framework itself: LaTeX smoke compiles, skill/command lint, +# CLI typechecks, and (upstream only) placeholder integrity. +# +# Fork-friendly by design: forks personalize CLAUDE.md, the skill files, and +# cv/main_example.tex via /setup, so the placeholder-integrity job and the +# exact page-count assertions run only on the upstream template repo. Compile +# success and lint correctness are asserted everywhere. +# +# Deliberately NOT here: live smoke tests of the job-portal CLIs. They hit +# real portals (network-flaky, and the linkedin-search skill is personal-use +# only per its own ToS warning - CI-automated requests would violate that). +# CLIs get typechecked instead; live testing stays a local, on-demand step. + +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + lint: + name: Lint skills, commands, settings + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install pyyaml + - run: python tools/lint_skills.py + + latex-smoke: + name: Compile example CV and cover letter + runs-on: ubuntu-latest + container: texlive/texlive:latest + steps: + - uses: actions/checkout@v4 + - name: Compile CV example (lualatex) + run: | + cd cv + lualatex -interaction=nonstopmode main_example.tex + test -f main_example.pdf + if grep -q '^!' main_example.log; then + echo '::error::lualatex reported errors compiling cv/main_example.tex' + grep -A3 '^!' main_example.log + exit 1 + fi + - name: Compile cover letter example (xelatex) + run: | + cd cover_letters + xelatex -interaction=nonstopmode cover_example.tex + test -f cover_example.pdf + if grep -q '^!' cover_example.log; then + echo '::error::xelatex reported errors compiling cover_letters/cover_example.tex' + grep -A3 '^!' cover_example.log + exit 1 + fi + - name: Assert exact page counts (upstream template only) + if: github.repository == 'MadsLorentzen/ai-job-search' + run: | + grep -q 'Output written on main_example.pdf (2 pages' cv/main_example.log \ + || { echo '::error::cv/main_example.tex no longer compiles to exactly 2 pages'; exit 1; } + grep -q 'Output written on cover_example.pdf (1 page' cover_letters/cover_example.log \ + || { echo '::error::cover_letters/cover_example.tex no longer compiles to exactly 1 page'; exit 1; } + + cli-typecheck: + name: Typecheck ${{ matrix.tool }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tool: + - jobbank-search + - jobdanmark-search + - jobindex-search + - jobnet-search + - linkedin-search + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install + working-directory: .agents/skills/${{ matrix.tool }}/cli + - run: bun run typecheck + working-directory: .agents/skills/${{ matrix.tool }}/cli + + placeholder-integrity: + name: Placeholder integrity (upstream template only) + if: github.repository == 'MadsLorentzen/ai-job-search' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Tracked template files must keep their placeholder tokens + run: | + fail=0 + check() { + if ! grep -q "$2" "$1"; then + echo "::error file=$1::expected placeholder token $2 - personal data may have been committed" + fail=1 + fi + } + check CLAUDE.md '\[YOUR_NAME\]' + check cv/main_example.tex '\[YOUR_NAME\]' + check cover_letters/cover_example.tex '\[YOUR NAME\]' + check .claude/skills/job-application-assistant/01-candidate-profile.md '