Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .claude/skills/init-template/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
54 changes: 54 additions & 0 deletions .github/actions/check-ready/action.yml
Original file line number Diff line number Diff line change
@@ -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|"<dataset-name>"|# 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"
34 changes: 9 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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|"<dataset-name>"|# 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
Expand Down Expand Up @@ -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
Expand Down
34 changes: 11 additions & 23 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,24 @@ 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 }}
username: ${{ github.actor }}
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:
Expand All @@ -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: .
Expand All @@ -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} \
Expand All @@ -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

Expand Down
37 changes: 11 additions & 26 deletions .github/workflows/jupyter-book.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
Expand All @@ -67,15 +52,15 @@ 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"
done

# 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"
Expand All @@ -84,22 +69,22 @@ 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.
BASE_URL: /${{ github.event.repository.name }}
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
Expand Down
6 changes: 6 additions & 0 deletions .template-uninitialised
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 10 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading