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
21 changes: 11 additions & 10 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ name: release
description: >
Cut a release of methods, the public MTHDS method library at
github.com/Pipelex/methods: the release/vX.Y.Z worktree, the lockstep bump of
every methods/*/METHODS.toml, the changelog entry, the format, lint and
validation gates, one commit, and a pull request to main whose merge creates
the annotated vX.Y.Z tag every pinned address resolves against. Use when the
user says "release", "cut a release", "bump version", "prepare a release",
"new version", "make a release", "ship it", "create release branch",
"promote dev to main", "tag the library", "snapshot the library", or wants a
change reachable at a new @vX.Y.Z address. Changelog content passed inline
("/release Added a contract review method") becomes the entry. The merge is
landed by /ledger-land, never by this skill.
every methods/*/METHODS.toml, the changelog entry, the format, lint,
validation and sample gates, one commit, and a pull request to main whose
merge creates the annotated vX.Y.Z tag every pinned address resolves against.
Use when the user says "release", "cut a release", "bump version", "prepare a
release", "new version", "make a release", "ship it", "create release
branch", "promote dev to main", "tag the library", "snapshot the library", or
wants a change reachable at a new @vX.Y.Z address. Changelog content passed
inline ("/release Added a contract review method") becomes the entry. The
merge is landed by /ledger-land, never by this skill.
---

# Releasing the method library

The procedure is the workspace release play, [`docs/workspace/releasing.md`](../../../../docs/workspace/releasing.md) at the workspace root — `../docs/workspace/releasing.md` from this repo's own root, which resolves the same from the main checkout and from any worktree. Read it first, then run it with what follows. The repo key is `methods`, the base is `dev`, and the pull request targets `main`. The release worktree is `_methods--release`, made with `wt add methods release --branch release/vX.Y.Z`. The repo declares no `.worktree.toml`, no `.worktreeinclude` and no Makefile, so `wt` resolves the base from `origin/dev` and provisions nothing: the gates run with the `pipelex` and `plxt` installed on the machine. The repo's own account of the scheme, the workflows and why they are built as they are is [`docs/releasing.md`](../../../docs/releasing.md).
The procedure is the workspace release play, [`docs/workspace/releasing.md`](../../../../docs/workspace/releasing.md) at the workspace root — `../docs/workspace/releasing.md` from this repo's own root, which resolves the same from the main checkout and from any worktree. Read it first, then run it with what follows. The repo key is `methods`, the base is `dev`, and the pull request targets `main`. The release worktree is `_methods--release`, made with `wt add methods release --branch release/vX.Y.Z`. The repo declares no `.worktree.toml`, no `.worktreeinclude` and no Makefile, so `wt` resolves the base from `origin/dev` and provisions nothing: the gates run with the `pipelex`, `plxt`, `curl` and `jq` installed on the machine. The repo's own account of the scheme, the workflows and why they are built as they are is [`docs/releasing.md`](../../../docs/releasing.md).

## What ships

Expand Down Expand Up @@ -52,6 +52,7 @@ Run at the root of the worktree, in this order. Every one is blocking.
1. **Lockstep** — `grep -h '^version = ' methods/*/METHODS.toml | sort -u` must print exactly one line. Before the bump that line is the previous release's version, and a second line means a package reached `dev` declaring another one; the bump cures it, since it rewrites every manifest. **After the bump** it runs again and must print exactly `version = "X.Y.Z"`.
2. **Format and lint** — `plxt fmt --check`, then `plxt lint`, over every `.mthds` bundle and `METHODS.toml`. The repo carries no `plxt` configuration, so `plxt` reads the machine's own, `~/.pipelex/plxt.toml` when there is one. A red format check is cured by `plxt fmt`, which rewrites, and whatever it touched joins the release commit. A red lint is a bundle to fix on `dev` through an ordinary branch before the release is cut again.
3. **Every package validates** — `for d in methods/*/; do pipelex validate bundle "$d" >/dev/null || echo "✗ $d"; done` prints nothing when every package passes; re-run a failing one without the redirect to read why. It is the static validation and dry run the README asks of every contribution, and it spends no inference. A red package is fixed on `dev` through an ordinary branch, never inside the release commit.
4. **Every sample resolves** — `.claude/skills/release/scripts/check-samples.sh` reads every `url` in every `methods/*/inputs.json` and must print `✓` for each one. It refuses by name a host under a reserved domain (`.invalid`, `.test`, `.example`, `.localhost`, `example.com` and its siblings), which is what a generated inputs template leaves behind, and a `raw.githubusercontent.com` link whose ref is not a `v*` tag of its repository, which it asks GitHub with `git ls-remote` because a branch can be named like a version; it asks every other URL for its first byte and wants a `2xx`, and holds a URL that redirects, such as a `github.com/…/raw/…` link, to the same rules where it lands. It needs `curl`, `git` and `jq`, no API key, and spends no inference. Validation never fetches a sample, which is how `doc_summarizer` shipped a placeholder from `v0.1.0` to `v0.1.3`, and a tag freezes its samples for good. A red sample is fixed on `dev` through an ordinary branch, never inside the release commit.

## The release commit

Expand Down
153 changes: 153 additions & 0 deletions .claude/skills/release/scripts/check-samples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Check that every URL a sample inputs.json links answers.
#
# check-samples.sh [inputs.json ...]
#
# With no argument it reads every methods/*/inputs.json of the checkout the
# script lives in, wherever it is run from; name files to check those instead.
# It takes every `url` value in each file, at any depth, and refuses one that
# is not an http(s) URL, one whose host is under a domain reserved for
# documentation and testing (.invalid, .test, .example, .localhost, and
# example.com, example.net, example.org), which is what a generated inputs
# template's placeholder looks like, and a raw.githubusercontent.com URL whose
# ref is not a v* tag, since a sample must not change under a library tag. A
# bare ref (<owner>/<repo>/<ref>/<file>) is looked up with git ls-remote, since
# a branch can be named like a version: it must be a tag of that repository
# and not also a branch. Every other URL must answer a request for its first
# byte with a 2xx, and a URL that redirects is held to the same rules where it
# lands, which is how github.com's /raw/ and ?raw=true links are checked. No
# call spends inference. Prints one line per URL, and one per file jq cannot
# read, and exits non-zero when any line is a failure.
#
# Needs curl, git and jq.

set -euo pipefail

for arg in "$@"; do
if [[ "$arg" == -* ]]; then
echo "usage: $0 [inputs.json ...]" >&2
exit 2
fi
[ -f "$arg" ] || { echo "$arg: no such file" >&2; exit 2; }
done
for tool in curl git jq; do
command -v "$tool" >/dev/null || { echo "$tool is required" >&2; exit 2; }
done

if [ "$#" -eq 0 ]; then
cd "$(dirname "$0")/../../../.."
shopt -s nullglob
set -- methods/*/inputs.json
shopt -u nullglob
fi

errors=$(mktemp)
trap 'rm -f "$errors"' EXIT
failures=0

# Why a URL is refused before anything fetches it, or nothing when it is not.
refusal() {
local url=$1 rest host owner repo ref kind name spelled_as_tag refs
if [[ ! "$url" =~ ^[Hh][Tt][Tt][Pp][Ss]?:// ]]; then
echo "not an http(s) URL, so the hosted API cannot fetch it"
return
fi
rest=${url#*://}
host=${rest%%[/?#]*}
host=${host##*@}
host=${host%:*}
host=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
host=${host%.}
case "$host" in
invalid | *.invalid | test | *.test | example | *.example)
echo "$host is under a reserved top-level domain that never resolves: a placeholder, not a sample"
return
;;
localhost | *.localhost)
echo "$host is the machine making the request, never a public host: a placeholder, not a sample"
return
;;
example.com | *.example.com | example.net | *.example.net | example.org | *.example.org)
echo "$host is a reserved example domain: a placeholder, not a sample"
return
;;
esac
if [ "$host" = raw.githubusercontent.com ]; then
# The path is /<owner>/<repo>/<ref>/<file>, the ref possibly spelled refs/tags/<tag>.
IFS=/ read -r owner repo ref kind name _ <<<"${rest#*/}"
spelled_as_tag=false
if [ "$ref" = refs ]; then
if [ "$kind" != tags ]; then
echo "names refs/$kind/$name, which is not a tag, so the file can change under a library tag: link it at a v* tag of $owner/$repo"
return
fi
ref=$name
spelled_as_tag=true
fi
if [[ ! "$ref" =~ ^v[0-9] ]]; then
echo "names the ref '$ref', which is not a v* tag, so the file can change under a library tag: link it at a v* tag of $owner/$repo"
return
fi
# A bare ref is whatever GitHub resolves the name to, and a branch can be
# named like a version, so ask GitHub which one it is.
if [ "$spelled_as_tag" = false ]; then
if ! refs=$(GIT_TERMINAL_PROMPT=0 git ls-remote "https://github.com/$owner/$repo.git" "refs/tags/$ref" "refs/heads/$ref" 2>"$errors"); then
echo "cannot list the refs of $owner/$repo on GitHub: $(head -n 1 "$errors")"
return
fi
if [ -z "$(awk -v want="refs/tags/$ref" '$2 == want' <<<"$refs")" ]; then
echo "names '$ref', which is not a tag of $owner/$repo, so the file can change under a library tag: link it at a v* tag"
return
fi
if [ -n "$(awk -v want="refs/heads/$ref" '$2 == want' <<<"$refs")" ]; then
echo "names '$ref', which is both a tag and a branch of $owner/$repo: spell it refs/tags/$ref so the tag is what is served"
return
fi
fi
fi
}

for file in "$@"; do
if ! urls=$(jq -r '.. | objects | .url? | strings' "$file" 2>"$errors"); then
echo "✗ $file — not valid JSON: $(head -n 1 "$errors")"
failures=$((failures + 1))
continue
fi
while IFS= read -r url; do
[ -n "$url" ] || continue
reason=$(refusal "$url")
if [ -n "$reason" ]; then
echo "✗ $file: $url — $reason"
failures=$((failures + 1))
continue
fi
if ! fetched=$(curl -sSL -r 0-0 --max-time 30 --retry 2 -o /dev/null \
-w '%{http_code} %{num_redirects} %{url_effective}' "$url" 2>"$errors"); then
echo "✗ $file: $url — $(head -n 1 "$errors")"
failures=$((failures + 1))
continue
fi
read -r code redirects effective <<<"$fetched"
if [[ "$code" != 2?? ]]; then
echo "✗ $file: $url — HTTP $code"
failures=$((failures + 1))
continue
fi
# A link can reach raw GitHub through a redirect, as github.com's /raw/ and
# ?raw=true links do, so where it lands is held to the same rules.
if [ "$redirects" -gt 0 ]; then
reason=$(refusal "$effective")
if [ -n "$reason" ]; then
echo "✗ $file: $url — redirects to $effective, which fails: $reason"
failures=$((failures + 1))
continue
fi
fi
echo "✓ $file: $url"
done <<<"$urls"
done

if [ "$failures" -ne 0 ]; then
echo "$failures sample check(s) failed" >&2
exit 1
fi
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [v0.1.4] - 2026-09-25

### Fixed

- **`doc_summarizer`'s sample document**: its `inputs.json` links a real document, the cookbook's CatOps pitch deck at the cookbook's `v0.18.0` tag, where it named a placeholder host that never resolves. At `v0.1.3` and earlier the sample does not run: run it at `@v0.1.4` or later.

## [v0.1.3] - 2026-09-25

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Beyond that rule:
- **Exports**: list the pipes you mean callers to use in `[exports.<domain>]`; unlisted pipes stay private to the package.
- **It must validate**: `pipelex validate bundle methods/<your_method>/` must pass before you open a PR.
- **Quality over quantity**: a method that actually runs, with a sample `inputs.json` where practical, beats a pile of stubs. Keep binary assets out of the package — link to hosted samples instead.
- **Samples must answer**: every URL in a sample `inputs.json` must resolve, and no release is cut while one does not. A file hosted on GitHub is linked at a tag, never at a branch, so the sample cannot change under a library tag; the library's own samples use the [Pipelex cookbook](https://github.com/Pipelex/pipelex-cookbook)'s tags.

## License

Expand Down
7 changes: 4 additions & 3 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ A release is a `release/vX.Y.Z` branch cut from `dev` and merged into `main` by
1. On the release branch, set the version in every manifest: `perl -pi -e 's/^version = ".*"/version = "X.Y.Z"/' methods/*/METHODS.toml`. Then `grep -h '^version = ' methods/*/METHODS.toml | sort -u` must print exactly one line, the version being released.
2. In the same commit, turn the `## [Unreleased]` section of `CHANGELOG.md` into the release's entry, `## [vX.Y.Z] - YYYY-MM-DD`, leaving no `[Unreleased]` heading behind.
3. Before committing, check the bundles: `plxt fmt --check` and `plxt lint` over every `.mthds` and `METHODS.toml`, and `pipelex validate bundle methods/<name>/` for every package.
4. Open the pull request into `main`. Its checks refuse a manifest that does not declare the branch's version, and a changelog without the entry.
5. The merge creates the tag and the GitHub Release. Once the tag exists, check that every package runs by its address at it: `.claude/skills/release/scripts/check-addresses.sh vX.Y.Z`, with `PIPELEX_API_KEY` set, asks the hosted API to fetch and validate each package at the tag, spends no inference, and must print `✓` for every one.
4. Check that every sample resolves: `.claude/skills/release/scripts/check-samples.sh` reads every `url` in every `methods/*/inputs.json`, refuses a placeholder on a reserved domain such as `.invalid` and a `raw.githubusercontent.com` link whose ref is not a `v*` tag of its repository, whether linked directly or reached by a redirect such as a `github.com/…/raw/…` link, asks every other URL for its first byte, spends no inference, and must print `✓` for every one. Validation never fetches a sample, and a tag freezes its samples for good, so this is the one moment a dead sample link can be caught before it ships. A sample file hosted on GitHub is a house-owned asset linked at one of the cookbook's tags, never at a branch and never at a tag of this repository, so it cannot change under a library tag.
5. Open the pull request into `main`. Its checks refuse a manifest that does not declare the branch's version, and a changelog without the entry.
6. The merge creates the tag and the GitHub Release. Once the tag exists, check that every package runs by its address at it: `.claude/skills/release/scripts/check-addresses.sh vX.Y.Z`, with `PIPELEX_API_KEY` set, asks the hosted API to fetch and validate each package at the tag, spends no inference, and must print `✓` for every one.

The Pipelex team runs these steps with the repository's `/release` skill for Claude Code, [`.claude/skills/release/SKILL.md`](../.claude/skills/release/SKILL.md), inside the workspace release play that every Pipelex repository shares.

Expand All @@ -35,7 +36,7 @@ The Pipelex team runs these steps with the repository's `/release` skill for Cla
| `changelog-check.yml` | pull request → `main` | `CHANGELOG.md` has `## [vX.Y.Z] - …` for that version, and no `[Unreleased]` heading survives |
| `github-release.yml` | push to `main`, and `workflow_dispatch` from `main` | creates the annotated tag on the release pull request's merge commit, and the GitHub Release |

The pull-request checks act only on a head matching `release/vX.Y.Z` exactly and pass trivially on anything else. They read the branch name from an environment variable rather than splicing it into their script, since in a public repository anyone can open a pull request from a branch named with shell syntax. Nothing in CI formats, lints or validates a bundle: step 3 above runs on the releaser's machine and nowhere else.
The pull-request checks act only on a head matching `release/vX.Y.Z` exactly and pass trivially on anything else. They read the branch name from an environment variable rather than splicing it into their script, since in a public repository anyone can open a pull request from a branch named with shell syntax. Nothing in CI formats, lints or validates a bundle, or checks a sample link: steps 3 and 4 above run on the releaser's machine and nowhere else. A check of the sample links in CI would also catch a dead link on an ordinary pull request, but it would make every pull request depend on third-party hosts answering, and the release is the moment a sample becomes immutable.

### Why the tagger creates the tag itself

Expand Down
2 changes: 1 addition & 1 deletion methods/cv_analyzer/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "cv_analyzer"
display_name = "CV Analyzer"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "End-to-end candidate screening: extract a CV and a job offer, analyze the match, then either generate tailored interview questions or draft a courteous refusal email."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion methods/doc_summarizer/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "doc_summarizer"
display_name = "Document Summarizer"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Deep document summarization: profile the document and extract importance-ranked key points in parallel, then synthesize a structured summary with themes and open questions."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
5 changes: 3 additions & 2 deletions methods/doc_summarizer/inputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"document": {
"concept": "native.Document",
"content": {
"url": "https://mock-d6ae6957.invalid/ee7a682e-4944-4e1e-81b4-178fdd2a6788"
"url": "https://raw.githubusercontent.com/Pipelex/pipelex-cookbook/v0.18.0/assets/presentations/CatOps.pdf",
"mime_type": "application/pdf"
}
}
}
}
2 changes: 1 addition & 1 deletion methods/documents/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "documents"
display_name = "Documents"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Document extraction methods for text, images, and page views."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion methods/image_generation/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "image_generation"
display_name = "Image Generation"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Image generation methods: render a description directly, or refine it into an optimized image prompt first."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion methods/invoice_extraction/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "invoice_extraction"
display_name = "Invoice Extraction"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Extract structured invoice data from a document: classify each page as bill or receipt, then extract amounts, VAT, vendor and buyer details using both the OCR text and the page view."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion methods/slide_designer/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "slide_designer"
display_name = "Slide Designer"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Turn a rough slide-deck brief into design proposals: polish the brief, generate multiple visual themes, render a mockup image for each, and compose an HTML report presenting them all."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion methods/table_extraction/METHODS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "table_extraction"
display_name = "Table Extraction"
address = "github.com/Pipelex/methods"
version = "0.1.3"
version = "0.1.4"
description = "Extract a data table from a screenshot into faithful HTML, then review the result against the image to correct text and formatting."
authors = ["Evotis S.A.S"]
license = "MIT"
Expand Down
Loading
Loading