diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f44959c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +# Two trailing spaces are a hard line break in Markdown. +trim_trailing_whitespace = false + +[*.py] +indent_size = 4 + +[{Gemfile.lock,*.bib}] +# Generated or exported by other tools: keep them byte-for-byte. +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1ba3eca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner for everything in the repository. +* @estcarisimo diff --git a/.github/REVIEW.md b/.github/REVIEW.md new file mode 100644 index 0000000..c545460 --- /dev/null +++ b/.github/REVIEW.md @@ -0,0 +1,96 @@ +# Code review brief + +Every pull request to `main` is reviewed by an **independent reviewer with no context +from the authoring session** before it is merged. In practice that is a fresh AI +coding-agent session (currently a Claude Sonnet subagent) started for the review alone, +or a human. This file is the brief that session is given. + +The loop: PR ready → fresh reviewer → **its full verdict is posted on the PR as a +comment** (by the author or the launching process: `gh pr comment --body-file -`) +→ author fixes and pushes, rebutting anything not fixed in the same thread → **another** +fresh reviewer (never the same session), which reads the earlier verdicts and checks +that each finding was fixed or rebutted with evidence → repeat until `VERDICT: APPROVE` +on the final commit → merge. A review of an earlier commit does not count. + +--- + +You are an independent code reviewer for the **CoNexDat website**, a static Jekyll 4 +site (academicpages theme, jekyll-polyglot for ES/EN/FR, jekyll-scholar for BibTeX +publications) published at . You have no prior context on +this PR; that is deliberate. Be thorough and skeptical, as a strict maintainer would be: +concrete, evidence-based findings with file:line, and no praise. + +## Setup + +1. Resolve the branch from the PR number: `gh pr view --json headRefName -q .headRefName`. + Work in a throwaway checkout so the main checkout is untouched: + `git fetch origin && git worktree add --detach /wt- origin/`, then + `cd` there. `` is the scratch directory your environment assigned you, or + `mktemp -d`. +2. Read `AGENTS.md` (conventions, layout, "things that are easy to get wrong") and the PR + description **with its comments**: `gh pr view --comments`. Earlier review + rounds are there. For every finding of an earlier round, check that the current + commit fixes it or that the rebuttal holds; an unaddressed or wrongly rebutted + finding is itself a finding. +3. The diff under review is `git diff origin/main...HEAD` in your worktree. +4. Build and check, and report the results verbatim: + - `export PATH="/usr/local/opt/ruby@3.3/bin:$PATH"` (macOS/Homebrew; Ruby 3.3 only), + `bundle install`, then `JEKYLL_ENV=production bundle exec jekyll build`. The repeated + "Error reading file _layouts/single" warnings are a known, harmless polyglot quirk. + - The shell lines of every `run:` step of `.github/workflows/build.yml` against the + result (the topic-id check and the sanity checks). + - `lychee --config lychee.toml --offline --root-dir "$PWD/_site" --remap + 'https://conexdat\.github\.io/(.*) file://'"$PWD"'/_site/$1' --exclude '/_site/LaNet-vi/' + '_site/**/*.html'` (internal links), and `curl -sIL` on every external URL the diff adds. + - `uvx pre-commit run --all-files` and, if workflows changed, `uvx zizmor .github/workflows/`. +5. For anything visual or interactive, build with `--config _config.yml,_config.dev.yml`, + serve `_site/` on **port 4000** (the dev config hard-codes that origin), and look at the + page in ES, EN and FR, light and dark mode, and at phone width (375 px). + +## What to check + +- Correctness: broken or wrong links (a 200 on a generic home page, a sign-in wall or a + parked domain is broken), Liquid errors, markup that polyglot rewrites (`href="/…"` gets + `/en/` or `/fr/`), JavaScript edge cases, behavior changes not described in the PR. + Verify claims in the PR description by running things, not by reading. +- Conventions from `AGENTS.md`: **ES/EN/FR in lock-step** for every content change; + **American English** in English text, comments, identifiers and docs (published paper + titles and proper names exempt); **no e-mail addresses** anywhere on the site; **no new + links into `cnet.fi.uba.ar`** except the pages AGENTS.md lists; new publications carry + `topics = {…}` with ids from `_data/publication_topics.yml`. +- Workflows: actions pinned to a full commit SHA with the version in a comment, + `persist-credentials: false`, least-privilege `permissions`, no untrusted input + interpolated into `run:`. +- Accessibility of new UI: keyboard operation, visible focus, contrast in both themes. +- Anything misleading in docs the PR adds or changes (numbers, commands, paths). Run the + commands. + +Do not comment on style the hooks already enforce. Do not pad the review. + +## Rules + +- Read-only with respect to the branch: do not commit, push, or edit files outside your + worktree experiments. Remove the worktree when done (`git worktree remove --force`). +- Do not post to GitHub. + +## Output (the final message, nothing else) + +``` +VERDICT: APPROVE | CHANGES REQUESTED +CHECKS: build= ci-steps= internal-links= pre-commit= zizmor= + +FINDINGS (most severe first; omit if none) +1. [blocking|should-fix|nit] : — + Evidence: + Suggestion: + +EARLIER ROUNDS (omit on round 1) +- round finding : fixed in | rebuttal holds | NOT addressed (see finding ) + +VERIFIED CLAIMS +- — + +NOTES (optional, non-blocking) +``` + +`APPROVE` only if there are no blocking or should-fix findings. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4b01b58..4d100e9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,29 @@ version: 2 updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + cooldown: + default-days: 7 # let new releases settle before proposing them + labels: ["dependencies", "github_actions"] + groups: + actions: + patterns: ["*"] + - package-ecosystem: bundler directory: "/" schedule: - interval: monthly + interval: weekly + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: ["dependencies", "ruby"] ignore: # Pinned to 2.x on purpose: the academicpages partials do not compile under # dart-sass (jekyll-sass-converter 3.x). See the Gemfile and AGENTS.md. - dependency-name: jekyll-sass-converter update-types: ["version-update:semver-major"] groups: - jekyll: - patterns: ["*"] - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly + ruby-minor-and-patch: + update-types: ["minor", "patch"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01151e6..d7ea350 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,7 @@ name: Build site +# Required check on every pull request (ruleset "Protect main"): build the site +# exactly as deploy.yml does, then check the output. on: pull_request: workflow_dispatch: @@ -16,26 +18,36 @@ jobs: name: Build site runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ruby-version: "3.3" - bundler-cache: true + persist-credentials: false - name: Check publication topic ids run: | # Every id in a `topics = {...}` field must exist in # _data/publication_topics.yml; an unknown id renders no chip silently. + fields=$(grep -ciE '^[[:space:]]*topics[[:space:]]*=' _bibliography/conexdat.bib || true) + if [ "$fields" -eq 0 ]; then + echo "::error::No topics fields found in _bibliography/conexdat.bib"; exit 1 + fi known=$(sed -n 's/^- id: *//p' _data/publication_topics.yml | sort -u) - used=$(grep -oE '^[[:space:]]*topics[[:space:]]*=[[:space:]]*\{[^}]*\}' _bibliography/conexdat.bib \ + used=$(grep -oiE '^[[:space:]]*topics[[:space:]]*=[[:space:]]*\{[^}]*\}' _bibliography/conexdat.bib \ | sed -E 's/.*\{(.*)\}/\1/' | tr ',' '\n' | tr -d ' ' | sed '/^$/d' | sort -u) + matched=$(grep -ciE '^[[:space:]]*topics[[:space:]]*=[[:space:]]*\{[^}]*\}' _bibliography/conexdat.bib || true) + if [ "$matched" -ne "$fields" ]; then + echo "::error::$((fields - matched)) topics field(s) are not on one line as topics = {id1, id2}"; exit 1 + fi unknown=$(comm -13 <(echo "$known") <(echo "$used")) if [ -n "$unknown" ]; then echo "::error::Unknown publication topic id(s): $unknown"; exit 1 fi - echo "OK: $(echo "$used" | wc -l) topic ids used, all known" + echo "OK: $fields entries tagged, $(echo "$used" | wc -l) topic ids used, all known" + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + ruby-version: "3.3" + bundler-cache: true - name: Build site (Jekyll + polyglot + scholar) env: @@ -57,12 +69,29 @@ jobs: # entries (the filter splits on whitespace). grep -q 'data-topic="latam"' _site/publications/index.html grep -Eq 'data-topics="[a-z-]+ +[a-z-]+' _site/en/publications/index.html - # Group decision: no e-mail addresses anywhere on the site. + # Group decision: no e-mail addresses in any HTML page. if grep -rl 'mailto:' _site --include='*.html'; then echo "::error::mailto: link found (the site publishes no e-mail addresses)"; exit 1 fi # Repository docs must not be published with the site. - for f in AGENTS.md CLAUDE.md CONTRIBUTING.md CODE_OF_CONDUCT.md SECURITY.md; do + for f in AGENTS.md CLAUDE.md CONTRIBUTING.md CODE_OF_CONDUCT.md SECURITY.md lychee.toml; do test ! -e "_site/$f" done echo "OK: $(find _site -name '*.html' | wc -l) HTML files" + + # Every internal link and asset must resolve inside the build. Absolute + # links to this site are mapped to the local files; external links are + # checked weekly by link-check.yml instead, so a PR never fails because + # some other server is down. + - name: Check internal links + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + args: >- + --config lychee.toml --offline + --root-dir ${{ github.workspace }}/_site + --remap 'https://conexdat\.github\.io/(.*) file://${{ github.workspace }}/_site/$1' + --remap 'https://conexdat\.github\.io$ file://${{ github.workspace }}/_site/' + --exclude '/_site/LaNet-vi/' + '_site/**/*.html' + fail: true + jobSummary: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9146441..0696126 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,8 +5,7 @@ on: branches: [main] workflow_dispatch: -permissions: - contents: write +permissions: {} concurrency: group: pages @@ -14,12 +13,17 @@ concurrency: jobs: build-deploy: + name: Build and deploy runs-on: ubuntu-latest + permissions: + contents: write # push the built site to the gh-pages branch steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 with: ruby-version: "3.3" bundler-cache: true @@ -30,7 +34,7 @@ jobs: run: bundle exec jekyll build --trace - name: Deploy to gh-pages - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_branch: gh-pages diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..6f2c2bc --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,86 @@ +name: Link check + +# Weekly check of every link on the site, internal and external. Broken links +# are reported in a single open issue labeled "broken-link", which is updated +# each week and closed automatically once everything passes again. +on: + schedule: + - cron: "41 7 * * 1" # Mondays 07:41 UTC + workflow_dispatch: + +permissions: {} + +concurrency: + group: link-check + cancel-in-progress: true + +jobs: + links: + name: Check links + runs-on: ubuntu-latest + permissions: + contents: read + issues: write # open, update or close the broken-link report + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + ruby-version: "3.3" + bundler-cache: true + + - name: Build site + env: + JEKYLL_ENV: production + run: bundle exec jekyll build + + - name: Check links + id: lychee + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + args: >- + --config lychee.toml + --root-dir ${{ github.workspace }}/_site + '_site/**/*.html' + output: lychee-report.md + format: markdown + fail: false + jobSummary: true + + - name: Open, update or close the broken-link issue + env: + GH_TOKEN: ${{ github.token }} + EXIT_CODE: ${{ steps.lychee.outputs.exit_code }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + existing=$(gh issue list --label broken-link --state open --json number --jq '.[0].number // empty') + if [ "$EXIT_CODE" != "0" ]; then + { + echo "The weekly link check found broken links on https://conexdat.github.io." + echo "Fix them in the source (see AGENTS.md: never link into cnet.fi.uba.ar;" + echo "prefer the new home of a project or a Wayback Machine snapshot)." + echo + echo "Run: $RUN_URL" + echo + # GitHub caps issue bodies at 65,536 characters. + if [ "$(wc -c < lychee-report.md)" -gt 60000 ]; then + head -c 60000 lychee-report.md + echo + echo + echo "_Report truncated; the full list is in the run's job summary._" + else + cat lychee-report.md + fi + } > issue-body.md + if [ -n "$existing" ]; then + gh issue edit "$existing" --body-file issue-body.md + gh issue comment "$existing" --body "Still failing as of $(date -u +%F). Report updated: $RUN_URL" + else + gh issue create --title "Broken links on the website" --label broken-link --body-file issue-body.md + fi + elif [ -n "$existing" ]; then + gh issue close "$existing" --comment "All links pass as of $(date -u +%F): $RUN_URL" + fi diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..fc3f6ba --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,57 @@ +name: Security + +# Two audits, like the other CoNexDat repositories: +# - bundler-audit: the locked gems (Gemfile.lock) against the Ruby advisory +# database. Runs when the lockfile changes and weekly, so advisories +# published after the last lock change still surface. +# - zizmor: static analysis of the workflow files themselves (unpinned +# actions, credential persistence, excessive permissions, template injection). +on: + push: + branches: [main] + paths: ["Gemfile", "Gemfile.lock", ".github/workflows/**"] + pull_request: + paths: ["Gemfile", "Gemfile.lock", ".github/workflows/**"] + schedule: + - cron: "23 6 * * 1" # Mondays 06:23 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + name: bundler-audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + ruby-version: "3.3" + bundler-cache: true # bundler-audit is locked in Gemfile.lock (group :audit) + + - name: Audit locked gems + run: bundle exec bundle-audit check --update + + workflows: + name: zizmor + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + + - name: Lint workflows + # Same version as the pre-commit hook; bump both together. + run: uvx zizmor@1.30.1 --min-severity low .github/workflows/ diff --git a/.gitignore b/.gitignore index bffd200..ac600c9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ _site/ .sass-cache/ .bundle/ /vendor/ -Gemfile.lock .DS_Store *~ node_modules/ @@ -12,3 +11,4 @@ node_modules/ # Claude Code subagent worktrees .claude/worktrees/ +.lycheecache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7b1eaca --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +# Install once with `uvx pre-commit install`; hooks then run on every commit. +# Run everything by hand with `uvx pre-commit run --all-files`. +# Vendored theme files, prebuilt JS, PDFs and fonts are left untouched. +exclude: ^(_sass/vendor/|assets/js/(main\.min\.js|plugins/|vendor/|_main\.js)|assets/(fonts|webfonts|pdf|css)/|files/|_site/) + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + # Whitespace hooks only touch files this project owns; the academicpages + # templates stay byte-for-byte upstream (a newline at the end of a Liquid + # include can change the rendered HTML). + - id: trailing-whitespace + files: '^(\.github/|_data/|_pages/|_news/|_plugins/|_scripts/|_bibliography/|assets/js/publications\.js$|_sass/_conexdat\.scss$|_includes/(people-list|projects-list|publications-filter|research-area|theses-list|news|masthead)\.html$|_layouts/bib_entry\.html$|[^/]+$)' + exclude: \.(md|bib)$|^(Gemfile\.lock|_data/ui-text\.yml)$ # ui-text.yml is upstream + - id: end-of-file-fixer + files: '^(\.github/|_data/|_pages/|_news/|_plugins/|_scripts/|_bibliography/|assets/js/publications\.js$|_sass/_conexdat\.scss$|_includes/(people-list|projects-list|publications-filter|research-area|theses-list|news|masthead)\.html$|_layouts/bib_entry\.html$|[^/]+$)' + exclude: ^(Gemfile\.lock|LICENSE)$ + - id: check-yaml + - id: check-json + exclude: ^\.devcontainer/ # JSON with comments + - id: check-merge-conflict + - id: mixed-line-ending + - id: check-added-large-files + args: ["--maxkb=2000"] + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.30.1 + hooks: + - id: zizmor diff --git a/AGENTS.md b/AGENTS.md index 94398ac..5651b1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,13 @@ _includes/ masthead (language switcher), people/projects/theses publications-filter.html, research-area.html assets/js/publications.js search / topic / person filter on /publications/ _sass/_conexdat.scss all site-specific styles (colors, cards, chips, dark mode) -.github/workflows/ build.yml (PR check "Build site"), deploy.yml (push to main) +_plugins/people_photos.rb lists images/people/*.jpg as site.data.people_photos +images/ logo, favicons (generated from the logo), manifest.json, people/ +lychee.toml link-checker settings (PR internal check + weekly online check) +.github/workflows/ build.yml (PR check "Build site": build, sanity checks, internal + links), deploy.yml (push to main), security.yml (bundler-audit, + zizmor), link-check.yml (weekly; opens a "broken-link" issue) +.github/REVIEW.md brief for the independent reviewer of every PR ``` ## Commands @@ -43,8 +49,15 @@ export PATH="/usr/local/opt/ruby@3.3/bin:$PATH" # macOS/Homebrew; Ruby 3.3 onl bundle install bundle exec jekyll serve --livereload --config _config.yml,_config.dev.yml # preview JEKYLL_ENV=production bundle exec jekyll build # what CI runs +uvx pre-commit install # once per clone; hooks run on every commit +uvx pre-commit run --all-files # whitespace, YAML/JSON, zizmor on workflows +bundle exec bundle-audit check --update # known vulnerabilities in Gemfile.lock +lychee --config lychee.toml --root-dir "$PWD/_site" '_site/**/*.html' # every link ``` +`Gemfile.lock` is committed and CI installs exactly what it pins. Update gems with +`bundle update --conservative ` (or let Dependabot do it); never hand-edit the lock. + Ruby 3.3 is required: the system Ruby is too old for Bundler 4, and Ruby 4 cannot compile the `sassc` native gem. A full build takes about 30 seconds. The repeated "Error reading file _layouts/single" warnings are a harmless polyglot quirk. To preview @@ -94,8 +107,18 @@ hard-codes that origin for CSS and assets. - **No `//` comments inside inline `