Skip to content

feat(release): cut a release on every merge, with no release PR to approve - #174

Merged
guilyx merged 17 commits into
mainfrom
feat/zen-gates-g3lolk
Nov 3, 2026
Merged

guilyx merged 17 commits into
mainfrom
feat/zen-gates-g3lolk

Conversation

@guilyx

@guilyx guilyx commented Nov 3, 2026

Copy link
Copy Markdown
Member

What and why

release-please needs a release PR, and opening one from Actions needs the "Allow GitHub Actions to create and approve pull requests" setting. Every run since #169 failed on exactly that.

guilyx/grip avoids it entirely. It has no release PR: the workflow commits and tags main itself, which the workflow token is allowed to do. This PR copies that shape, and keeps the rule that the commits in the merged PR decide the bump.

On every push to main, release.yml now runs these jobs:

  1. plan finds the merged PR and takes the largest bump requested by any of:

    • its title;
    • every commit in it;
    • every commit on main since the last v* tag. GitHub drops all but one pending run per concurrency group, so quick successive merges can lose a run; counting these commits means its bump still lands with the next release.
    Commit/title Release
    fix: / perf: patch
    feat: minor
    feat!: / fix!: / BREAKING CHANGE: footer major
    docs:, chore:, ci:, test:, refactor:… none

    A release:major|minor|patch|none label overrides the result, as in grip.

  2. bump runs tools/release.py apply, which:

    • updates the version in pyproject.toml, jupyddl.__version__ and CITATION.cff;
    • turns [Unreleased] into ## [X.Y.Z] - date, falling back to the PR title if nobody wrote an entry;
    • rebuilds web/dist.

    It then commits chore(release): X.Y.Z, tags vX.Y.Z, pushes both to main, and starts pages. Its own pushes trigger no workflows, so nothing loops.

  3. build → pypi → github-release are the existing steps, unchanged: the version/tag/__version__ checks, the clean-wheel smoke test, PyPI trusted publishing, and release notes from the changelog.

Hand-pushed v* tags and the Actions-tab dispatch (testpypi dry run, or pypi) still work, including the guard against releasing a version twice.

Removed: release-please.yml, its config and manifest, tools/release_pr_fixup.py, and the version marker.

⚠️ Merging this PR publishes 2.5.0

Its title is feat(release): …, and the commits since v2.4.0 include feat: commits, so the new workflow will immediately release 2.5.0 to PyPI. That release carries all the Unreleased notes: plugins, the extending guide, and every fix from the review.

What it needs to work:

  • main must accept pushes from Actions (the format workflow already relies on this).
  • The pypi environment must have no required reviewers, otherwise the publish waits for one.

How it was checked

  • tests/test_release.py, rewritten (21 cases):
    • every bump rule;
    • label overrides;
    • non-conventional commits ask for nothing;
    • a "BREAKING CHANGE" outside a conventional commit is ignored;
    • the version bump arithmetic;
    • dating the changelog, including the PR-title fallback and the refusal when there's nothing at all;
    • apply edits all three files;
    • catching up a dropped merge from git history, using a real temporary repo;
    • every version in the repo agrees.
  • Both workflow files parse. Full suite: 641 passed, 1 skipped. flake8 and black are clean, and web/dist is rebuilt.
  • Not exercised: the workflow itself has not run yet. It first runs when this PR merges.

The stale release-please--branches--main--components--jupyddl branch that release-please left behind can be deleted.

The history has a merge -s ours of the already-merged #172 tip so the push fast-forwards. Squash-merge with the PR title.


Generated by Claude Code

claude added 17 commits November 2, 2026 21:26
Write down where and why we mean to publish jupyddl as citable research
software (.docs/PUBLISHING.md): a Zenodo DOI per release, a JOSS paper,
ICAPS workshops for the research results, with a readiness checklist, the
paper outline, the AI-usage disclosure JOSS requires, and how contributors
can be considered for authorship. The README states the project's aim and
its Cite section now says which version the 2021 preprint describes.

Add the community files an outside contributor expects: a security policy,
a pull-request template, a "Propose an algorithm" issue template, and an
"Adding an algorithm" section in CONTRIBUTING.md.

Run tests and build on pull requests. They were `on: [push]`, which never
fires for a fork, so a contributor's PR showed no checks at all.

Fix AGENTS.md drift: the Python floor is 3.10 and Mergify is retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
A package can now add planners, heuristics and generators by declaring
`jupyddl.planners`, `jupyddl.heuristics` or `jupyddl.generators` entry
points. Once installed, they work from the CLI, the benchmark harness and
`jupyddl.solve` by name, without anyone importing the package.

Discovery is lazy and defensive (jupyddl/plugins.py): the CLI runs it
before building its parser, the library only on a registry miss, so
importing jupyddl never scans installed packages. A built-in name always
wins, a plugin that fails to import is skipped with a warning, and
JUPYDDL_NO_PLUGINS=1 turns it off. `jupyddl plugins` lists what was found.
The CLI now reads `requires_heuristic` from the registry at call time,
since INFORMED_PLANNERS is fixed at import and cannot know about plugins.

.docs/EXTENDING.md walks through a heuristic, a planner from a priority
function, a planner from scratch with the budget/observer contract, a
generator, testing the guarantees you claim, and shipping as a plugin.
tests/test_extending.py runs every Python block in it, so it cannot drift.

best_first now accepts a tuple priority for tie-breaking; observers get
its first element as `f`, so traces stay numeric.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
# Conflicts:
#	.docs/PUBLISHING.md
#	AGENTS.md
#	CHANGELOG.md
Each of these returned a plan, a cost or a value without complaint, and
validate_plan accepted the plans because it replays them through the same
Task.apply. tests/test_semantics.py now pins them on tiny hand-written
tasks; 16 of its cases fail on the previous code.

- Derived facts were never retracted, and (not <derived>) held from the
  start. close() now recomputes axioms from scratch and maintains negated
  derived facts; negation inside a rule body (needs stratification) is
  refused. The relaxation gets the complement for free, so it never calls
  a solvable state a dead end.
- An atom both added and deleted left its complement true too; a `when`
  re-adding a deleted atom did the same. Complements: the delete wins.
- Under a (total-cost) metric an action without a cost effect is free, as
  in IPC planners (pegsol/sokoban optimised the wrong objective). Cost
  inside `when` was charged unconditionally: refused, as are negative costs.
- A never-defined numeric fluent read as 0; conditions reading one fail.
- blind skipped zero-cost actions and overestimated.
- h1/h2/hm crashed on numeric states; LM-cut crashed on an empty goal.
- lmcut and h2 clear `admissible` on tasks with conditional effects.
- Optimal planners default to lmcut, not hff; `-H none` means blind.
- make_heuristic builds a heuristic class or partial instead of passing it
  through unbuilt.
- Fact ids and id sets are built in sorted order, so LM-cut tie-breaks,
  expansions and plans no longer change with PYTHONHASHSEED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
- iddfs, idastar and ehc never read their budget, so --time-limit was
  ignored. They poll per expansion now; the recursive ones unwind with
  BudgetExhausted. beam polls per node, not per layer, and bnb stops
  evaluating siblings once the budget fires.
- Hitting a depth cap (iddfs max_depth, bnb max_depth) or Python's
  recursion limit sets stats.truncated: it is not a proof of
  unsolvability, nor of optimality.
- bfs and iddfs leave OPTIMAL_PLANNERS: they minimise plan length, and on
  errands return cost 16 where 10 is optimal.
- Live dashboard: a budget stop no longer reads "no plan found"; lines are
  clipped to the stream's terminal width so repaints do not smear; the
  non-tty log writes at most every two seconds.
- idastar/ehc/hc/beam without a heuristic raise a clear ValueError;
  MultiObserver forwards positional arguments; the SearchObserver and
  best_first docstrings say what actually happens.

tests/test_search_contract.py runs every planner where the budget is the
only thing that stops it, checks OPTIMAL_PLANNERS against dijkstra under
action costs, and pins the dashboard behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
- Exit codes say what happened: 0 solved, 1 no plan, 2 bad input, 3
  stopped by a budget, 4 a plan that failed validation. Unsolvable, out of
  budget and a crash all used to exit 1.
- Bad input (missing file, parse error, unknown planner or heuristic, a
  malformed --sizes) is one line on stderr, not a traceback;
  JUPYDDL_DEBUG=1 restores the traceback. --version exists.
- `demo` and `benchmark demos` never finished. Both stop each search after
  10 s by default (--time-limit 0 for none); animate and demo take the
  same budget flags as solve.
- benchmark validates --planners/--heuristic/--metric up front, and chart
  options check for the viz extra before searching rather than after.
- Dead ends (h = inf) crashed the progress chart and coloured the
  wavefront NaN; in the workbench they made the progress message invalid
  JSON and aborted the run.
- A trace's plan is the visible plan, without compiler bookkeeping.
- Packaging: the sdist's own suite failed because .github/ was dropped
  wholesale; only workflows are excluded now. build runs twine --strict.
  black is pinned to one major in dev and format.yml; Renovate pins
  third-party actions to digests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
PR titles become Conventional Commits (pr-title check, plain shell, no
third-party action). On every push to main release-please keeps one release
PR whose version follows from what was merged: fix -> patch, feat -> minor,
feat!/BREAKING CHANGE -> major. tools/release_pr_fixup.py then files the
hand-written [Unreleased] notes under the new section and rebuilds web/dist,
which records the version.

Merging the release PR tags the commit; release-please.yml then dispatches
release.yml on the tag, because a tag made with the workflow token triggers
no workflows. release.yml now also refuses a dispatch that would re-release
an existing version. Renovate opens semantic titles so its PRs pass the check.

tests/test_release.py pins the manifest, pyproject and __version__ together
and checks the changelog fix-up, including the section release.yml extracts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
…ct benchmark configs

- Ranking ties counted as wins, so a constant model scored top-1 = 1.0 and
  checkpoint selection preferred collapsed epochs. Identical siblings are
  dropped and a tie with k siblings earns 1/(k+1).
- "Held out" top-1 used training groups; split now keeps the held-out
  groups and selection/reporting use them.
- gripper and workshop ignored the seed, so tuning, validation and held-out
  sets were duplicates. The seed now varies the instance.
- blocksworld and random-strips could generate instances already solved
  at the start; those seeds are re-rolled, every other seed is unchanged.
- Malformed heuristic bundles are refused on load; set_flat is atomic;
  negated facts key on not-<predicate>; DAgger/bootstrap samples split
  with their base instance; pure ranking without groups raises;
  learn_heuristic accepts generators for its size arguments.
- Benchmark rows carry a config label including planner arguments and
  record the heuristic that actually ran (api.resolve_heuristic).
- summarise_transfer charges unsolved runs instead of averaging over
  solved ones only.

Published figures are left as measured; .docs/learned-heuristics.md and
rl-for-search.md carry dated corrections saying what each one measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
… cannot be honoured

- Durative actions: at-start effects apply before at-end effects (at-end
  wins), and over-all/at-end conditions are checked in the state after the
  at-start effects. Forms the sequential compilation cannot express are
  refused by name.
- sometime-after: an obligation set by the last action can be discharged.
- forall around a trajectory constraint or a goal preference is expanded
  per object instead of being read as one conjunction (or crashing).
- Timed initial literals: forced monitors ride on their firing actions, and
  closing a plan blocks them, so a preference cannot be credited and undone.
- Object fluents are rewritten inside constraints; fluent-valued assigns
  are refused.
- Metric: an unmentioned preference costs 0; a metric without total-cost
  charges nothing for domain actions; total-time charges durations;
  maximize and other shapes are refused.
- ground() no longer mutates the parsed Domain/Problem.
- Invalid PDDL (unknown keys and sections, missing :goal) and malformed
  input are PDDLErrors with line numbers, never a bare IndexError; objects
  named nan/inf parse as objects.
- (increase (total-cost) (road-length ?a ?b)) over static functions is
  supported, so the IPC transport domains parse; :fluents implies
  :object-fluents; :ucpop, :domain-axioms and :safety-constraints are
  refused by name.

tests/test_frontend_semantics.py: 56 cases, 55 of which fail on the
previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
…prove

Replaces release-please with the shape guilyx/grip uses. On each push to
main, release.yml finds the merged pull request and takes the largest bump
its title and commits ask for (tools/release.py: fix/perf patch, feat
minor, !/BREAKING CHANGE major; a release:* label overrides), plus every
commit since the last v* tag so a dropped run's bump is not lost.

It then bumps pyproject.toml, jupyddl.__version__ and CITATION.cff, dates
[Unreleased] in the changelog, rebuilds web/dist, commits and tags main
with the workflow token, starts pages, and builds, verifies and publishes
as before. The workflow token may push but may not open pull requests, so
with no release PR there is no setting to change and nothing to approve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
@guilyx
guilyx marked this pull request as ready for review November 3, 2026 11:37
@guilyx
guilyx merged commit 64858d6 into main Nov 3, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants