From f306f0d3dabe0a3458ec99e5a28b2e016db9d589 Mon Sep 17 00:00:00 2001 From: Anne Fouilloux Date: Tue, 9 Jun 2026 12:47:29 +0200 Subject: [PATCH] Fix initialisation guard: single sentinel + shared action, fail loud on silent-green MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "is this template initialised, should CI run?" question was answered by five divergent copies of a repo-wide `{{...}}` grep (3 workflows, the /init-template skill, and CLAUDE.md), each with a different exclusion list. They drifted, and — worse — the grep false-positives on the template's OWN literal token examples in docs/ and .claude/, so a fully-finished replication silently skipped its CI/Docker pipelines while reporting green. Root-cause fix: - Add a `.template-uninitialised` sentinel as the ONE state signal. No more token-grepping to decide initialisation state. - Add `.github/actions/check-ready` composite action as the single source of truth (sentinel check + notebooks/*.py scaffold check). All three workflows call it, so they can't drift again. - Tripwire: if the repo records real published nanopub URIs in nanopubs/PUBLISHED.md but the guard would still skip, fail the run loudly instead of passing green. This is the check that turns the silent-green failure mode into a visible one. - CLAUDE.md first-run guard now checks the sentinel, not a {{...}} grep. - /init-template deletes the sentinel as its final step (activating the workflows); README updated to describe the shared guard. Logic verified across four states: uninitialised template (skip), finished+ scaffold+published (fail), initialised+real (run), initialised+scaffold+ unpublished (skip). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/init-template/SKILL.md | 15 +++++-- .github/actions/check-ready/action.yml | 54 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 34 +++++----------- .github/workflows/docker.yml | 34 ++++++---------- .github/workflows/jupyter-book.yml | 37 ++++++------------ .template-uninitialised | 6 +++ CLAUDE.md | 22 +++++------ README.md | 2 +- 8 files changed, 113 insertions(+), 91 deletions(-) create mode 100644 .github/actions/check-ready/action.yml create mode 100644 .template-uninitialised diff --git a/.claude/skills/init-template/SKILL.md b/.claude/skills/init-template/SKILL.md index 24d6920..8ef9b31 100644 --- a/.claude/skills/init-template/SKILL.md +++ b/.claude/skills/init-template/SKILL.md @@ -161,21 +161,28 @@ The constellation JSON contains all the substantive content inline — there's n Do NOT `git add` any of those paths in Step 10. If you accidentally do, `git status` will show them as new files because `.gitignore` excludes the *unrelated* path; `git add` is permissive about gitignored paths if you list them explicitly. Just don't. -## Step 10 — Self-removal +## Step 10 — Self-removal and activation -This skill should not exist in the resulting repo. Remove the entire `.claude/skills/init-template/` directory: +This skill should not exist in the resulting repo. Remove the entire `.claude/skills/init-template/` directory, **and delete the `.template-uninitialised` sentinel** — that sentinel is what makes CI, Docker, and the Jupyter Book workflow skip their pipelines (and what makes the `CLAUDE.md` first-run guard fire). Deleting it activates them: ```bash rm -rf .claude/skills/init-template +rm -f .template-uninitialised ``` -Stage and commit the deletion as a separate commit: +Stage and commit both deletions as a separate commit: ```bash git add -A -git commit -m "Remove init-template skill (one-shot, no longer needed)" +git commit -m "Remove init-template skill and activation sentinel (one-shot)" ``` +> **Why the sentinel matters:** once it's gone, the workflows run for real on the +> next push. If the notebooks are still scaffolds they'll skip with a `::notice::` +> (expected) — but once you've also published a nanopub chain, a skip becomes a +> hard CI failure on purpose (`.github/actions/check-ready`), so a finished +> replication can never sit on silently-green-but-empty CI. + ## Step 11 — Report Tell the user, in this order, with the push reminder loud and unmissable: diff --git a/.github/actions/check-ready/action.yml b/.github/actions/check-ready/action.yml new file mode 100644 index 0000000..2e43d0b --- /dev/null +++ b/.github/actions/check-ready/action.yml @@ -0,0 +1,54 @@ +name: Check repository readiness +description: >- + Single source of truth for "should this workflow actually run its pipeline?". + Returns ready=true once the template has been initialised (the + .template-uninitialised sentinel is gone) and the notebooks are no longer + scaffolds; otherwise ready=false with an explanatory ::notice::. Fails loudly + if the repo looks like a finished replication (real published nanopub URIs in + nanopubs/PUBLISHED.md) yet would still skip — the silent-green failure mode. + +outputs: + ready: + description: "'true' when the pipeline should run, 'false' to skip." + value: ${{ steps.check.outputs.ready }} + +runs: + using: composite + steps: + - id: check + shell: bash + run: | + ready=true + reason="" + + # 1. Not-initialised: the sentinel is the ONLY signal. No repo-wide + # {{...}} grep — the template legitimately ships literal token + # examples in docs/ and .claude/, which is what used to cause + # false-positive skips (and silent-green CI) downstream. + if [ -f .template-uninitialised ]; then + ready=false + reason="Template not initialised — run /init-template (the .template-uninitialised sentinel is still present)." + + # 2. Scaffold notebooks: initialised, but Phase-2 code not written yet. + # Scoped to notebooks/*.py only, so it can never trip on prose. + elif ls notebooks/*.py >/dev/null 2>&1 \ + && grep -lE 'raise NotImplementedError|""|# Example skeleton — adapt' notebooks/*.py >/dev/null 2>&1; then + ready=false + reason="Notebooks are still scaffolds — implement notebooks/*.py (Phase 2) before the pipeline can run." + fi + + if [ "$ready" = "false" ]; then + echo "::notice::${reason} Skipping the pipeline steps for this run." + + # Tripwire: a finished replication that still wants to skip is a bug, + # not an expected scaffold state. "Finished" = real published nanopub + # URIs recorded in nanopubs/PUBLISHED.md (np/RA + >=10 id chars, so the + # template's own "np/RA…" example placeholders do NOT match). This is + # the check that turns the old silent-green skip into a loud failure. + if grep -qE 'w3id\.org/(sciencelive/)?np/RA[A-Za-z0-9_-]{10,}' nanopubs/PUBLISHED.md 2>/dev/null; then + echo "::error::${reason} BUT nanopubs/PUBLISHED.md already records published nanopub URIs — this repo is a finished replication and must not be skipping its pipeline. This is the silent-green failure mode the template guards against. Failing instead of passing green; fix the sentinel/scaffold state above." + exit 1 + fi + fi + + echo "ready=$ready" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d02691..a0b612f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,32 +16,16 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Skip CI if template has not been initialised or notebooks are scaffolds + # Single source of truth for "should the pipeline run?" — see + # .github/actions/check-ready. Replaces the old per-workflow {{...}} grep + # that false-positived on the template's own doc/skill token examples and + # silently skipped CI green. + - name: Check repository readiness id: guard - run: | - # {{ZENODO_DOI}} is documented to remain unsubstituted until the first - # GitHub release mints the concept DOI — so don't treat it as a blocker. - # See docs/fair4rs-checklist.md. - placeholder_files=$(grep -rln '{{[A-Z_]\+}}' . --include='*.md' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.cff' --include='*.py' --include='*.toml' 2>/dev/null \ - | grep -v 'claude/skills/init-template/' \ - | while read f; do - if grep -oE '\{\{[A-Z_]+\}\}' "$f" | grep -qv '{{ZENODO_DOI}}'; then - echo "$f" - fi - done) - scaffold_files=$(grep -lE 'raise NotImplementedError|""|# Example skeleton — adapt' notebooks/*.py 2>/dev/null || true) - if [ -n "$placeholder_files" ]; then - echo "::notice::Template placeholders detected ({{...}} tokens). Run /init-template inside Claude Code (or substitute manually) before CI runs meaningfully. Skipping the rest of this workflow." - echo "skip=true" >> "$GITHUB_OUTPUT" - elif [ -n "$scaffold_files" ]; then - echo "::notice::Notebooks are still in scaffold state — replace placeholders in notebooks/*.py with your actual replication code (Phase 2). The Snakefile rule outputs will not be produced until then. Skipping pipeline run." - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi + uses: ./.github/actions/check-ready - name: Set up pixi - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' uses: prefix-dev/setup-pixi@v0.9.6 with: pixi-version: v0.68.1 @@ -73,11 +57,11 @@ jobs: # key: input-data-v1 - name: Run pipeline - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' run: pixi run snakemake --cores 1 - name: Upload results - if: always() && steps.guard.outputs.skip != 'true' + if: always() && steps.guard.outputs.ready == 'true' uses: actions/upload-artifact@v5 with: name: ci-outputs diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6ccf010..e90a5a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -20,28 +20,16 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Skip if template has not been initialised + # Single source of truth — see .github/actions/check-ready. A finished + # replication (real URIs in nanopubs/PUBLISHED.md) that would skip here + # fails loudly instead of passing green, so a release can't silently + # publish nothing to GHCR. + - name: Check repository readiness id: guard - run: | - # {{ZENODO_DOI}} is documented to remain unsubstituted until the first - # GitHub release mints the concept DOI — so don't treat it as a blocker. - # See docs/fair4rs-checklist.md. - placeholder_files=$(grep -rln '{{[A-Z_]\+}}' . --include='*.md' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.cff' --include='*.py' --include='*.toml' 2>/dev/null \ - | grep -v 'claude/skills/init-template/' \ - | while read f; do - if grep -oE '\{\{[A-Z_]+\}\}' "$f" | grep -qv '{{ZENODO_DOI}}'; then - echo "$f" - fi - done) - if [ -n "$placeholder_files" ]; then - echo "::notice::Template placeholders detected ({{...}} tokens). Run /init-template before releasing. Skipping Docker build." - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi + uses: ./.github/actions/check-ready - name: Log in to GHCR - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -49,7 +37,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' id: meta uses: docker/metadata-action@v5 with: @@ -61,7 +49,7 @@ jobs: type=raw,value=latest - name: Build and push - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' uses: docker/build-push-action@v6 with: context: . @@ -74,7 +62,7 @@ jobs: # Zenodo as a separate deposit with its own DOI (per FAIR4RS F1.2). - name: Export Docker image - if: ${{ env.ZENODO_TOKEN != '' && steps.guard.outputs.skip != 'true' }} + if: ${{ env.ZENODO_TOKEN != '' && steps.guard.outputs.ready == 'true' }} run: | TAG="${{ github.event.release.tag_name || 'latest' }}" docker save ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG#v} \ @@ -84,7 +72,7 @@ jobs: ls -lh docker-image.tar.gz - name: Upload Docker image to Zenodo - if: ${{ env.ZENODO_TOKEN != '' && steps.guard.outputs.skip != 'true' }} + if: ${{ env.ZENODO_TOKEN != '' && steps.guard.outputs.ready == 'true' }} run: | set -euo pipefail diff --git a/.github/workflows/jupyter-book.yml b/.github/workflows/jupyter-book.yml index d16742d..a037a16 100644 --- a/.github/workflows/jupyter-book.yml +++ b/.github/workflows/jupyter-book.yml @@ -18,34 +18,19 @@ jobs: build: runs-on: ubuntu-latest outputs: - skip: ${{ steps.guard.outputs.skip }} + ready: ${{ steps.guard.outputs.ready }} pages_enabled: ${{ steps.pages_check.outputs.pages_enabled }} steps: - uses: actions/checkout@v5 - - name: Skip if template has not been initialised + # Single source of truth — see .github/actions/check-ready. + - name: Check repository readiness id: guard - run: | - # {{ZENODO_DOI}} is documented to remain unsubstituted until the first - # GitHub release mints the concept DOI — so don't treat it as a blocker. - # See docs/fair4rs-checklist.md. - placeholder_files=$(grep -rln '{{[A-Z_]\+}}' . --include='*.md' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.cff' --include='*.py' --include='*.toml' 2>/dev/null \ - | grep -v 'claude/skills/init-template/' \ - | while read f; do - if grep -oE '\{\{[A-Z_]+\}\}' "$f" | grep -qv '{{ZENODO_DOI}}'; then - echo "$f" - fi - done) - if [ -n "$placeholder_files" ]; then - echo "::notice::Template placeholders detected ({{...}} tokens). Run /init-template inside Claude Code (or substitute manually) before the Jupyter Book builds meaningfully. Skipping." - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi + uses: ./.github/actions/check-ready - name: Check whether GitHub Pages is enabled id: pages_check - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -58,7 +43,7 @@ jobs: fi - name: Set up pixi - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' uses: prefix-dev/setup-pixi@v0.9.6 with: pixi-version: v0.68.1 @@ -67,7 +52,7 @@ jobs: environments: docs - name: Convert .py notebooks to .ipynb - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' run: | for nb in notebooks/*.py; do pixi run jupytext --to notebook "$nb" @@ -75,7 +60,7 @@ jobs: # Glob, not a hard-coded list — new notebooks are picked up automatically. - name: Execute notebooks - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' run: | for nb in notebooks/*.ipynb; do echo "::group::Executing $nb" @@ -84,7 +69,7 @@ jobs: done - name: Build MyST site - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' env: # MyST silently ignores `base_url` in myst.yml — only this env var # works. See docs/cicd-conventions.md. @@ -92,14 +77,14 @@ jobs: run: pixi run -e docs myst build --html - name: Upload pages artifact - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.ready == 'true' uses: actions/upload-pages-artifact@v5 with: path: _build/html deploy: needs: build - if: needs.build.outputs.skip != 'true' && needs.build.outputs.pages_enabled == 'true' + if: needs.build.outputs.ready == 'true' && needs.build.outputs.pages_enabled == 'true' runs-on: ubuntu-latest environment: name: github-pages diff --git a/.template-uninitialised b/.template-uninitialised new file mode 100644 index 0000000..8c0b73e --- /dev/null +++ b/.template-uninitialised @@ -0,0 +1,6 @@ +This file marks an uninitialised forrt-replication-template clone. + +While it exists, CI / Docker / Jupyter Book skip their pipelines (there is +nothing real to run yet) and Claude's first-run guard tells the user to run +/init-template. The /init-template skill deletes this file as its final step, +which activates all the workflows. Do not delete it by hand — run /init-template. diff --git a/CLAUDE.md b/CLAUDE.md index 0481489..e3b7b1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,21 +35,19 @@ The FORRT nanopublication chain itself is what makes **R1.2 (provenance)** machi Before doing any other work in this repository, run this check: ```bash -grep -r \ - --include='*.md' --include='*.yml' --include='*.json' --include='*.yaml' \ - --include='*.cff' --include='*.toml' \ - --include='Dockerfile' --include='LICENSE' \ - '{{[A-Z_]\+}}' . 2>/dev/null | grep -v '^./.claude/' | grep -v '^./CLAUDE.md' \ - | while read f; do - if grep -oE '\{\{[A-Z_]+\}\}' "$f" | grep -qv '{{ZENODO_DOI}}'; then - echo "$f" - fi - done | head +test -f .template-uninitialised && echo "UNINITIALISED" ``` -`{{ZENODO_DOI}}` is documented to remain unsubstituted until Phase 4 mints the concept DOI (see `docs/fair4rs-checklist.md`), so the guard above ignores it. +The presence of the `.template-uninitialised` sentinel file is the single +signal that the template has not been bootstrapped — the same signal CI, Docker, +and the Jupyter Book workflow use (via `.github/actions/check-ready`). Do **not** +grep the repo for `{{...}}` tokens to decide this: the template legitimately +ships literal token examples in `docs/` and `.claude/` (they document the token +system), and grepping for them is exactly what used to cause false-positive +skips and silent-green CI. `/init-template` deletes the sentinel as its final +step. -If the output contains any unsubstituted `{{...}}` token, the template has not been initialised. **Stop**, tell the user: +If the sentinel file is present, the template has not been initialised. **Stop**, tell the user: > "This repository still has unsubstituted placeholder tokens from the template. Run `/init-template` to bootstrap it (you'll be asked for author identity, paper DOI, etc.), then we can proceed." diff --git a/README.md b/README.md index df68d42..8c4bff5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ If you are reading this in a fresh fork, run [`/init-template`](.claude/skills/i After `/init-template`, do these one-time setup steps to enable the full CI/CD path: - **Enable GitHub Pages** at *Settings → Pages → Source: GitHub Actions*. Until enabled, the Jupyter Book build runs but the deploy step is skipped (CI stays green). -- The CI workflows ship with **scaffold-detection guards** — they run end-to-end only after you implement Phase 2 (the `notebooks/*.py` files). Until then they exit early with an informative `::notice::` and the badges stay green. +- All three workflows share one **readiness guard** (`.github/actions/check-ready`). Before `/init-template` runs, the `.template-uninitialised` sentinel makes them skip with an informative `::notice::` (badges stay green); `/init-template` deletes the sentinel, which activates them. They also skip while `notebooks/*.py` are still scaffolds (Phase 2). **Once you've published a nanopub chain** (real URIs in `nanopubs/PUBLISHED.md`), a skip is treated as a bug and **fails the run loudly** — so a finished replication can't sit on silently-green-but-empty CI. ## Repository structure