mirror of
https://github.com/prdlk/jobyssey.git
synced 2026-11-02 13:12:49 +00:00
ci(ci): add CI workflow with linting, tests, LaTeX compilation, and dependency review
This commit is contained in:
commit
f091cc110a
2 files changed
+303
No files matched your search
Executable
+218
@@ -0,0 +1,218 @@
|
||||
# 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/content assertions run only on the upstream template repo.
|
||||
# Compile success, lint correctness, and an extractable PDF text layer 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).
|
||||
# CI runs typechecks and the checked-in fixture/mock test suites only; live
|
||||
# portal testing stays a local, on-demand step.
|
||||
#
|
||||
# Security posture: this template ships pre-approved Claude Code permissions
|
||||
# and CLI code that every fork user executes, so the security-guards job
|
||||
# fails PRs that widen settings.json permissions, weaken the personal-data
|
||||
# gitignore rules, or add package lifecycle scripts; dependency-review flags
|
||||
# newly introduced vulnerable/malicious dependencies. Honest limit: a PR can
|
||||
# edit this workflow itself, so these guards catch accidents and casual
|
||||
# attempts, not a determined author - branch protection with required checks
|
||||
# and human review of workflow/settings diffs remain the real backstop.
|
||||
# Actions are pinned to commit SHAs; the token is read-only.
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint skills, commands, settings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: pip install pyyaml
|
||||
- run: python tools/lint_skills.py
|
||||
- name: Framework version guard (upstream template only)
|
||||
if: github.repository == 'MadsLorentzen/ai-job-search'
|
||||
run: python tools/check_framework_version.py
|
||||
|
||||
security-guards:
|
||||
name: Security guards (permissions, gitignore, manifests)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: python tools/security_guards.py
|
||||
|
||||
python-tests:
|
||||
name: Python tool tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: python -m unittest discover -s tests -t . -v
|
||||
|
||||
dependency-review:
|
||||
name: Dependency review
|
||||
# Requires the repo's Dependency graph, which not every repo (upstream or
|
||||
# fork) has enabled - so the graph is probed first, and the job warns and
|
||||
# passes instead of hard-failing if it's unavailable (the same
|
||||
# graceful-skip pattern the workflow uses for optional tools), rather than
|
||||
# being gated to a specific repository. Enabling Dependency graph under
|
||||
# Settings -> Advanced Security activates the real check on any repo.
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Probe Dependency graph availability
|
||||
id: graph
|
||||
run: |
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/dependency-graph/sbom")
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Dependency graph is not enabled on this repository (HTTP $code). Dependency review was skipped - enable Dependency graph under Settings -> Advanced Security to activate this check."
|
||||
fi
|
||||
- name: Dependency review
|
||||
if: steps.graph.outputs.enabled == 'true'
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
with:
|
||||
fail-on-severity: high
|
||||
|
||||
latex-smoke:
|
||||
name: Compile example CV and cover letter
|
||||
runs-on: ubuntu-latest
|
||||
container: texlive/texlive:latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Install PDF inspection tools
|
||||
run: |
|
||||
if ! command -v pdfinfo >/dev/null || ! command -v pdftotext >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends poppler-utils
|
||||
fi
|
||||
- name: Compile CV example (lualatex)
|
||||
run: |
|
||||
cd cv
|
||||
lualatex -interaction=nonstopmode -halt-on-error 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 -halt-on-error 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: Verify extractable PDF text
|
||||
run: |
|
||||
python3 tools/verify_pdf.py cv/main_example.pdf --min-chars 100
|
||||
python3 tools/verify_pdf.py cover_letters/cover_example.pdf --min-chars 100
|
||||
- name: Assert stock PDF structure (upstream template only)
|
||||
if: github.repository == 'MadsLorentzen/ai-job-search'
|
||||
run: |
|
||||
python3 tools/verify_pdf.py cv/main_example.pdf \
|
||||
--pages 2 \
|
||||
--contains '[your.email@example.com]' \
|
||||
--contains 'Professional Experience' \
|
||||
--contains 'Achievement'
|
||||
python3 tools/verify_pdf.py cover_letters/cover_example.pdf \
|
||||
--pages 1 \
|
||||
--contains 'your.email@example.com' \
|
||||
--contains 'Dear [Hiring Manager / Team]'
|
||||
|
||||
discover-clis:
|
||||
# The matrix is discovered, not hardcoded, so a portal CLI added in a fork
|
||||
# (the /add-portal path) gets typechecked and tested without the fork
|
||||
# having to edit this workflow - the same reason security-guards globs
|
||||
# .agents/**/package.json instead of naming the shipped portals.
|
||||
name: Discover portal CLIs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tools: ${{ steps.list.outputs.tools }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- id: list
|
||||
run: |
|
||||
tools=$(find .agents/skills -mindepth 3 -maxdepth 3 -path '*/cli/package.json' \
|
||||
| cut -d/ -f3 | sort | jq -R . | jq -cs .)
|
||||
echo "Discovered portal CLIs: $tools"
|
||||
echo "tools=$tools" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cli-checks:
|
||||
name: CLI checks ${{ matrix.tool }}
|
||||
needs: discover-clis
|
||||
if: needs.discover-clis.outputs.tools != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tool: ${{ fromJSON(needs.discover-clis.outputs.tools) }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- run: bun install
|
||||
working-directory: .agents/skills/${{ matrix.tool }}/cli
|
||||
- run: bun run typecheck
|
||||
working-directory: .agents/skills/${{ matrix.tool }}/cli
|
||||
- name: Run fixture/mock tests when present
|
||||
run: |
|
||||
if find tests -type f -name '*.test.ts' | grep -q .; then
|
||||
bun test
|
||||
else
|
||||
echo "No Bun tests found for ${{ matrix.tool }}; skipping"
|
||||
fi
|
||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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 '\\name{\[First\]}{\[Last\]}'
|
||||
check cv/main_example.tex '\\email{\[your\.email@example\.com\]}'
|
||||
check cover_letters/cover_example.tex '\[YOUR NAME\]'
|
||||
check .claude/skills/job-application-assistant/01-candidate-profile.md '\[YOUR_EMAIL\]'
|
||||
check .claude/skills/job-application-assistant/04-job-evaluation.md '\[YOUR_PRIMARY_SKILLS\]'
|
||||
exit $fail
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
# Weekly upstream triage. Reports only - it NEVER merges, pushes, or edits code.
|
||||
#
|
||||
# It fetches the upstream template, runs tools/upstream_triage.py to sort the
|
||||
# commits this fork lacks into "worth reviewing" vs "probably skip" (dropping
|
||||
# cherry-picks already applied and changes that only touch files this fork
|
||||
# removed), and writes the result into a single rolling issue. You read it and
|
||||
# port anything worth porting by hand.
|
||||
#
|
||||
# The report/act boundary is deliberate and load-bearing: the report stops at
|
||||
# ready-to-run cherry-pick lines and never opens a draft PR or merges. On a
|
||||
# fork "applies cleanly" is not "correct" - a commit for portals the fork
|
||||
# dropped can cherry-pick fine and still be wrong, and that silent-wrong case
|
||||
# is worse than a conflict. Merges stay a human decision, the same posture
|
||||
# /apply keeps (it drafts, never submits). Keep it that way.
|
||||
#
|
||||
# This is the commit-level companion to tools/check_upstream_updates.py, which
|
||||
# tracks personalized-file version stamps. Two tools, two questions.
|
||||
#
|
||||
# Runs only on forks (guarded below), so the upstream template never triggers
|
||||
# it against itself - GitHub also leaves inherited workflows disabled on a fork
|
||||
# until the owner enables Actions, so the guard is a second fence, not the only
|
||||
# one. Token is the built-in GITHUB_TOKEN, scoped to reading contents and
|
||||
# writing issues in this repo only: the digest can never be written outside the
|
||||
# fork.
|
||||
|
||||
name: Upstream watch
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 * * 1" # 08:00 UTC every Monday
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: Triage upstream commits
|
||||
# No-op on the upstream template itself. Pinned by
|
||||
# tests/test_upstream_triage.py so a template clone never runs it by surprise.
|
||||
if: github.repository != 'MadsLorentzen/ai-job-search'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Fetch upstream template
|
||||
run: |
|
||||
git remote add upstream https://github.com/MadsLorentzen/ai-job-search.git 2>/dev/null || true
|
||||
git fetch --quiet upstream master
|
||||
|
||||
- name: Build triage report
|
||||
run: |
|
||||
{
|
||||
echo "_Last checked: $(date -u '+%Y-%m-%d %H:%M UTC') · [run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})_"
|
||||
echo
|
||||
python tools/upstream_triage.py --remote upstream --branch master
|
||||
} > report.md
|
||||
cat report.md
|
||||
|
||||
- name: Open or update the rolling issue
|
||||
env:
|
||||
# Built-in token is scoped to this repo only, so the digest can never
|
||||
# be written outside the fork.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
# Pin to this fork. Without it, the `upstream` git remote added above
|
||||
# makes gh's remote resolution target the base repo, so the digest
|
||||
# would land on upstream's tracker instead of the fork's.
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
title="Upstream sync watch"
|
||||
existing=$(gh issue list --state open --search "in:title \"$title\"" \
|
||||
--json number,title --jq ".[] | select(.title==\"$title\") | .number" | head -n1)
|
||||
if [ -n "$existing" ]; then
|
||||
gh issue edit "$existing" --body-file report.md
|
||||
echo "Updated issue #$existing"
|
||||
else
|
||||
gh issue create --title "$title" --body-file report.md
|
||||
echo "Created a new rolling issue"
|
||||
fi
|
||||
Reference in new issue
Block a user