Upgrading PormG
PormG is pre-1.0. In Julia's 0.y.z convention the y slot is the breaking slot, so the API still changes when a change makes it better — and every such change has to be rolled out by hand in each app that depends on it. That stays true for the whole 0.x era.
Breaking changes are a pain. The point of everything below is to make them mechanical rather than archaeological: each one ships with the search that finds your affected call sites and the exact edit to make, so you — or an agent working for you — can apply it without reverse-engineering what changed. This page covers the versioning model, how to scope an upgrade to your app, and how to hand the work to an AI agent.
This page is about migrating your application's source code to keep up with the PormG API. It is unrelated to the makemigrations / migrate engine that manages your database tables.
The change log itself lives in upgrading/ in the PormG repository — one file per breaking change, named YYYY-MM-DD-<slug>.md so the directory sorts chronologically. You rarely need to open it: upgrade_guide renders exactly the slice that applies to you. The rules for writing an entry are in UPGRADING.md, which is the contract, not the log.
How PormG versions: release trains
PormG bumps per release train, not per pull request.
yis the breaking slot. Any release that forces an edit in your app bumpsyand carries a matching change-log entry. PinPormG = "0.3"and Pkg will hold you there until you choose to move.zis safe. A purely additive train, or a hotfix that changes no value a call returns. Nothing to port.- During a train, every breaking or behavior-changing PR adds one new file to
upgrading/marked- **Version**: Unreleased, without bumpingProject.toml— that still holds the last cut train's version. A change is behavior-changing when any call returns a different value, or raises where the last release did not, bug fixes included: an app may depend on the old value. - Cutting a train bumps
yonce and stamps everyUnreleasedentry with that number, then dates and tags the release.
So one y bump can carry several entries. They are applied together, newest-first, as one rollout.
If you dev or path-depend on PormG rather than installing a released version, you are running the last cut train plus every entry still marked Unreleased since. upgrade_guide includes that uncut work by default, because it is what your app is actually running — pass to = pkgversion(PormG) to scope to the released surface only.
Upgrading an app
Your app's PormG dependency pin is its rollout state. There is nothing else to track — no per-app checklist, no status table.
1. Scope the work to your version
using PormG
PormG.upgrade_guide(from = v"0.3.0") # the version your app currently pinsThis prints every entry newer than your pin, newest first — each with its "How to find the calls to migrate" recipe and its concrete before → after. If nothing applies, you get a single line:
# PormG: nothing to port between 0.3.0 and Unreleased.To scope to a released version instead of HEAD, pass to:
PormG.upgrade_guide(from = v"0.2.0", to = v"0.3.0")Without a Julia session handy you can do the same by eye: open upgrading/ and work up from the bottom — GitHub sorts the listing oldest-first, so the newest entry is the last one — skipping any whose - **Version**: is ≤ your pin. Everything you did not skip is what changed since you pinned.
Fourteen of the oldest entries carry no - **Version**: bullet at all. Those predate the versioning policy and shipped before 0.2.0; if your pin is 0.2.0 or later they do not apply to you, and upgrade_guide filters them out for you.
2. Find the call sites
Each entry carries a grep tuned to that specific change. Run it inside your app rather than reading your whole codebase — for example, the #200 entry's:
rg -n '\|>\s*first\(' <app>/src3. Apply the before → after
Every entry shows the ✗ old form and the ✓ new one. Edit each call site to the ✓ form.
4. Verify against your own tests
Run your app's test or integration suite against the upgraded PormG. An entry is done for your app only when the code is updated and the tests pass — not when the edit compiles.
An offline golden-SQL suite makes this step cheap: it pins the SQL your app issues, needs no database, and shows each change as a diff to read against the entries. The recipe is on Testing Your App.
5. Bump the pin
Once green, bump your app's PormG dependency to the version you upgraded to. That pin is the app's rollout state, so the next upgrade_guide call scopes correctly from there.
Driving the upgrade with an AI agent
The workflow above is mechanical — scope, grep, apply, verify — which makes it a good fit for a coding agent. PormG ships three things to support that.
Install the consumer skill
PormG.install_ai_skills() # writes .github/skills/pormg-usage/ into the current projectThis copies PormG's usage skill bundle into your app, so an agent working in that repo has accurate context on models, queries, writes, error handling and async — rather than guessing from Django-shaped intuition. Claude Code only registers a skill found under .claude/skills/, so it also writes .claude/skills/pormg-usage/SKILL.md, a short stub that points at the bundle, which makes /pormg-usage work there. It prints what it did and returns the same facts as a named tuple:
- the files it wrote (
written), and the stub's path (stub). Run it again after every PormG bump: overwriting the bundle and the stub is how they get refreshed. When.claude,.claude/skillsor the stub's own path is a link, as in a skills tree shared between tools, no stub is written. - stale files — files in that directory the installed PormG no longer ships, usually a topic file an older version installed. They are left in place; delete them if they came from PormG.
- whether anything points at the bundle. It checks
AGENTS.md,CLAUDE.md,.github/copilot-instructions.mdand.github/instructions/*.md, and if none mentionspormg-usageit prints a one-line pointer for you to add. It never edits those files itself.
Make the rollout self-triggering
Add one line to your app's AGENTS.md or CLAUDE.md:
Before bumping the PormG dependency, run
PormG.upgrade_guide(from = v"<current pin>")and apply what it lists.
That single line is what makes the whole thing work unattended: an agent asked to bump PormG in that repo picks up the rollout on its own, instead of bumping the pin and leaving the code behind.
Consume the entries as data
For a scripted or agent-driven rollout, structured = true returns the entries as data instead of printing them:
entries = PormG.upgrade_guide(from = v"0.3.0", structured = true)
for e in entries
println(e.version, " — ", e.title)
# e.body holds the full markdown: severity, grep recipe, before → after
endEach element is a (; version, title, body) named tuple, newest-first — enough to drive a per-entry loop, or to decide up front whether a bump needs a human at all.
Entries can supersede each other. #231, for example, retyped the write-disabled error that #205 had described one entry earlier — so applying them bottom-up produces code that matches the older contract. The printed output is ordered newest-first for exactly this reason; keep that order, and run the tests before moving to the next entry.
API Reference
PormG.upgrade_guide — Function
upgrade_guide([io::IO = stdout]; from, to = <current code>, structured = false)Print the change-log entries a consuming app must work through to move from PormG version from up to to. The default to covers the current code — every released entry plus the uncut entries still marked Unreleased that the install is running (release-train model), so a consumer dev'ing PormG at HEAD sees work that has not been stamped with a release number yet. Pass to = pkgversion(PormG) to scope to the installed release only. Reads the upgrading/ log shipped with the resolved PormG install — one file per entry — so the scope is accurate against the version your app actually depends on, not a latest-on-GitHub copy that may not match.
Entries print newest-first; each keeps its "How to find the calls to migrate" grep and its before → after, with the PormG-internal per-app rollout table trimmed off.
from is required — pass the PormG version your app currently depends on. Both from and to accept a VersionNumber or a version string (v"0.2" or "0.2").
Pass structured = true to get the entries back as data instead of printing — a Vector of (; version, title, body) named tuples, newest-first — for programmatic consumers.
Throws
ArgumentErrorwhenfromis omitted — a mistake in the call itself.InvalidConfigurationError(<: ConfigurationError <: PormGError) when the bundledupgrading/log is missing or holds no entries — a broken install, which would otherwise print "nothing to port" and read as being up to date.
Examples
julia> using PormG
julia> PormG.upgrade_guide(from = v"0.1") # everything up to the installed version
julia> PormG.upgrade_guide(from = "0.2", to = "0.3")
julia> entries = PormG.upgrade_guide(from = v"0.1", structured = true);PormG.install_ai_skills — Function
install_ai_skills(target_dir::String = pwd(); io::IO = stdout)
-> (; installed_dir, written, stale, referenced_in, stub)Copy PormG's pormg-usage skill bundle into <target_dir>/.github/skills/pormg-usage/, the file set an AI coding assistant (GitHub Copilot, Claude, Cursor, …) reads to learn the PormG API. Re-running it is the update mechanism: every shipped file is overwritten with the copy from the PormG version your project resolves, so run it again after each PormG bump.
The bundle is flat — SKILL.md, a router, plus the topic files it links to — and the whole directory is copied so none of its relative links dangle after install.
Claude Code registers a skill only from .claude/skills/<name>/SKILL.md, so it also writes <target_dir>/.claude/skills/pormg-usage/SKILL.md: a discovery stub holding the bundle's front-matter and a pointer to the bundle's SKILL.md, rewritten on every run. When any step of that path is a link (.claude, .claude/skills, the skill directory or the file), it writes no stub.
It writes only those two places, and reports what it found rather than acting on it:
written— the files it copied into the bundle directory, by name.stub— the path of the Claude Code stub, ornothingwhen it was not written (a link is on its path, or the write failed; both are logged as a warning).stale— files already in the target directory that this PormG version does not ship. Usually a topic file an older PormG installed and a later one renamed; possibly a file you added yourself. They are left in place — delete them if they came from PormG, because an assistant browsing the directory would still read their outdated content.referenced_in— the project's agent-instruction files (AGENTS.md,CLAUDE.md,.github/copilot-instructions.md,.github/instructions/*.md) that mentionpormg-usage. Copying the files does not make an assistant read them: when this is empty the function prints a one-line pointer to add to one of those files. It never edits them itself — many projects keep a hand-curated skills table there.
Returns nothing when the bundle could not be found or copied (the failure is logged).