From 0d2ab547a3c0a5b59bca89e806127ee7d83aec32 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 10 Jul 2026 12:28:55 -0400 Subject: [PATCH 1/3] VersionCheck: reject non-portable [sources] entries in Project.toml Fail any PR whose tracked `Project.toml` declares a `[sources]` entry that must not reach `main`: an absolute `path` (a machine-specific `Pkg.develop` leak that stays green on CI, where the path is absent) or a `url` (a cross-repo branch pin that resolves the dep from an unregistered branch once merged). A relative `path` such as `".."` for a subproject is allowed. Runs as an ungated step in `VersionCheck`, handling both the `[sources.Name]` and inline-table forms. --- .github/workflows/VersionCheck.yml | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/VersionCheck.yml b/.github/workflows/VersionCheck.yml index 63396b0..ecedc3d 100644 --- a/.github/workflows/VersionCheck.yml +++ b/.github/workflows/VersionCheck.yml @@ -49,6 +49,45 @@ jobs: steps: - uses: actions/checkout@v7 + # `[sources]` entries that must never reach `main`: an absolute `path` is a + # machine-specific `Pkg.develop` leak (it resolves the dep from a path that + # exists only on the author's machine, staying green on CI where it is + # absent), and a `url` is a cross-repo branch pin that is fine mid-review but + # resolves the dep from an unregistered branch once merged. A relative `path` + # (e.g. a subproject pointing at "..") is legitimate and allowed. Failing in + # draft PRs is intended: the point is to block these from reaching `main`. + - name: "Reject non-portable [sources] entries in Project.toml" + run: | + python3 - <<'PY' + import os, subprocess, sys, tomllib + + files = subprocess.check_output( + ["git", "ls-files", "*Project.toml"], text=True + ).split() + bad = [] + for f in files: + with open(f, "rb") as fh: + data = tomllib.load(fh) + for name, spec in data.get("sources", {}).items(): + if not isinstance(spec, dict): + continue + path = spec.get("path") + if path is not None and os.path.isabs(path): + bad.append(f'{f}: [sources.{name}] has an absolute path = "{path}"') + if "url" in spec: + bad.append(f'{f}: [sources.{name}] has a url = "{spec["url"]}"') + if bad: + print( + "::error::These [sources] entries must not be merged to main: an " + "absolute path is machine-specific, and a url is a cross-repo " + 'branch pin. Use a relative path (e.g. "..") or drop the source.' + ) + for b in bad: + print(b) + sys.exit(1) + print(f"Checked {len(files)} Project.toml file(s); no non-portable [sources] entries.") + PY + - uses: ITensor/ITensorActions/.github/actions/classify-pr@main id: classify with: From 5ac687e0a342d1ccb0927a334bdcebd97cd03204 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 10 Jul 2026 12:41:51 -0400 Subject: [PATCH 2/3] Add CLAUDE.md documenting the release convention Callers consume these workflows at the mutable `v2` tag, which does not follow `main`, so a behavior-changing merge reaches no caller until a release moves `v2`. Capture that in a repo-local `CLAUDE.md`: a behavior-changing merge must be followed by `.scripts/release.sh`, with the bump chosen per the README's SemVer rule. --- CLAUDE.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0970e9a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# ITensorActions + +Reusable GitHub Actions workflows shared across the ITensor Julia packages. Caller +repos pin these workflows at the major-version tag (e.g. +`ITensor/ITensorActions/.github/workflows/Tests.yml@v2`). + +## Releasing (required after any behavior change) + +Callers consume these workflows at the mutable `v2` tag, which does **not** follow +`main`. A change merged to `main` therefore reaches no caller until a release moves +`v2`. So after merging a PR that changes workflow behavior, cut a release: + +- Run `.scripts/release.sh vX.Y.Z` from a clean `main` checkout. It tags the new + version and moves the `v2` pointer to it (and pins the released commit's internal + `@main` action references to a fixed SHA so the release is a stable bundle). +- Choose the bump per the SemVer rule in the README ("Versioning" section): patch + for fixes or internal hardening, minor for additive behavior or new inputs, major + for breaking caller-facing changes. +- Docs-only PRs (including edits to this file) need no release and must not move + `v2`. From 57c2fffa984554596c41460b7caff082e2a32548 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 10 Jul 2026 12:44:59 -0400 Subject: [PATCH 3/3] Document the [sources] check in the README Describe the non-portable `[sources]` check under the Version Check section: the entries it rejects (an absolute `path`, a `url`), what it allows (a relative `path`), and that it runs on every PR regardless of draft state. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 8c97204..f7ab1f0 100644 --- a/README.md +++ b/README.md @@ -673,6 +673,15 @@ The suffix is freeform; any non-empty prerelease tag triggers the skip behavior. `VersionCheck` also validates the bump shape pre-merge (patch+1 for non-breaking, minor+1 with patch=0 for pre-1.0 breaking, major+1 with minor=patch=0 for post-1.0 breaking) so malformed bumps fail the PR check directly rather than only after merge in `Registrator`. +### Project.toml `[sources]` check + +Independently of the version-bump check (it runs on every PR, regardless of scope or draft state), `VersionCheck` rejects `[sources]` entries in any tracked `Project.toml` that must not reach `main`: + +- an absolute `path`, which is a machine-specific `Pkg.develop` artifact: it resolves the dependency from a location that exists only on the author's machine, and stays green on CI, where the path is absent, so nothing else catches it; +- a `url`, which is a cross-repo branch pin: legitimate while a dependent PR is in review, but once merged it resolves the dependency from an unregistered branch. + +A relative `path` (for example a subproject pointing at `".."`) is legitimate and allowed. Both the `[sources.Name]` table and the inline `Name = {path = "..."}` forms are checked. Because it runs regardless of draft state, a dependent PR that pins an upstream branch shows a failing check until it drops the pin before merge. + ## Check Compat Bounds The `CheckCompatBounds` workflow instantiates the package and fails if any