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
82 changes: 82 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,85 @@ jobs:

- name: Run unit tests
run: python -m unittest discover -s tests -v

install-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
version: '0.10.7'

- name: Test editable install mode
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
expected_version="$(cat VERSION)"

uv tool install --editable .
test "$(syncwheel --version)" = "syncwheel ${expected_version}"

git remote remove origin || true
state_dir="$(mktemp -d)"
SYNCWHEEL_UPDATE_STATE_PATH="${state_dir}/state.json" \
SYNCWHEEL_UPDATE_SETTINGS_PATH="${state_dir}/settings.json" \
syncwheel self status --json > "${state_dir}/editable-status.json"

python - "${state_dir}/editable-status.json" <<'PY'
import json
import sys

data = json.load(open(sys.argv[1]))
assert data["status"]["install_kind"] == "git-clone", data
PY

- name: Test uv-tool install mode
run: |
set -euo pipefail
expected_version="$(cat VERSION)"
tool_root="$(mktemp -d)"
export UV_TOOL_DIR="${tool_root}/tools"
export UV_TOOL_BIN_DIR="${tool_root}/bin"
mkdir -p "$UV_TOOL_BIN_DIR"

uv tool install "git+file://$PWD"
test "$("$UV_TOOL_BIN_DIR/syncwheel" --version)" = "syncwheel ${expected_version}"

status_env=(
"SYNCWHEEL_UPDATE_STATE_PATH=${tool_root}/state.json"
"SYNCWHEEL_UPDATE_SETTINGS_PATH=${tool_root}/settings.json"
"SYNCWHEEL_REMOTE_VERSION_URL=file://$PWD/VERSION"
"SYNCWHEEL_UV_TOOL_SOURCE=git+file://$PWD"
)

env "${status_env[@]}" "$UV_TOOL_BIN_DIR/syncwheel" self status --json > "${tool_root}/uv-status.json"
python - "${tool_root}/uv-status.json" <<'PY'
import json
import sys

data = json.load(open(sys.argv[1]))
assert data["status"]["install_kind"] == "uv-tool", data
PY

env "${status_env[@]}" "$UV_TOOL_BIN_DIR/syncwheel" self check-update --fetch --json > "${tool_root}/check-update.json"
python - "${tool_root}/check-update.json" "$expected_version" <<'PY'
import json
import sys

data = json.load(open(sys.argv[1]))
expected = sys.argv[2]
assert data["current_version"] == expected, data
assert data["latest_version"] == expected, data
assert data["update_available"] is False, data
assert data["remote_version_url"].startswith("file://"), data
PY
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__pycache__/
*.pyc
*.egg-info/
.syncwheel/
.tmp/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.18.0 - 2026-06-10

- Add uv packaging with a `syncwheel` console script while preserving direct
`python3 scripts/syncwheel.py ...` execution.
- Add an idempotent `scripts/install.sh` for production uv installs and
editable development installs.
- Extend `self status`, `self check-update`, and `self update` to distinguish
git checkouts, uv tool installs, and plain script execution.
- Teach uv tool installs to check the upstream `VERSION` file directly and
update with uv.
- Add CI coverage for editable and git-sourced uv tool install modes.

## 0.17.0 - 2026-05-13

- Add a segmented append-only ledger under `.syncwheel/ledger/` with a replayed
Expand Down
81 changes: 66 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Keep many long-lived pull requests clean, rebuildable, and publishable from one
manifest.

Current version: `0.14.0`
Current version: `0.18.0`

`syncwheel` is a small CLI and workflow model for maintainers who carry several
PR branches against an upstream repository and need those branches to stay
Expand Down Expand Up @@ -193,31 +193,63 @@ Practical meaning:

## Install

No package install is required. The tool is a single Python script.

Requirements:
- Python 3.11+
- Git
- uv 0.10+ for PATH-based installs

Recommended production install:

```bash
uv tool install "git+https://github.com/NestDevLab/syncwheel"
```

Development editable install from a local checkout:

```bash
uv tool install --editable .
```

Installer script:

```bash
scripts/install.sh
scripts/install.sh --editable /path/to/syncwheel
```

If `uv` is not installed, `scripts/install.sh` exits with instructions by
default. To explicitly let the installer bootstrap uv with the official
astral.sh installer, pass `--with-uv`.

Legacy checkout execution remains supported for pinned submodules, vendored
checkouts, and existing scripts:

```bash
python3 scripts/syncwheel.py --help
```

## Self update, notifications, and AI-safe visibility

Syncwheel now includes a built-in install/update channel so humans and AI agents
can notice new releases instead of silently drifting.

- default mode: `notify`
- automatic notice is emitted on normal syncwheel usage when the local install is
behind its upstream branch
- automatic notice is emitted on normal syncwheel usage when the local install
is behind the configured update source
- git-checkout installs update with the existing `git fetch` plus fast-forward
merge flow
- uv tool installs update with `uv tool upgrade syncwheel`
- manual inspection:

```bash
python3 scripts/syncwheel.py self status
python3 scripts/syncwheel.py self check-update --fetch
syncwheel self status
syncwheel self check-update --fetch
```

- manual update:

```bash
python3 scripts/syncwheel.py self update
syncwheel self update
```

- update policy:
Expand All @@ -229,23 +261,42 @@ python3 scripts/syncwheel.py self mode off
```

`auto` tries a safe fast-forward self-update when a newer upstream version is
detected. If the syncwheel checkout is dirty or detached, syncwheel falls back
to a visible notice instead of mutating it unsafely.
detected for git-checkout installs and runs the uv tool updater for uv installs.
If a git checkout is dirty or detached, syncwheel falls back to a visible notice
instead of mutating it unsafely.

For uv installs, `self check-update` reads the upstream `VERSION` file directly
instead of requiring a local git checkout. Advanced wrappers can override the
version source with `SYNCWHEEL_REMOTE_VERSION_URL` and the installer/update
source label with `SYNCWHEEL_UV_TOOL_SOURCE`.

## Installation and adoption modes

1. **Global toolkit (recommended)**
1. **uv production tool (recommended for normal hosts)**
- Run `uv tool install "git+https://github.com/NestDevLab/syncwheel"`.
- The `syncwheel` executable is placed on PATH when uv's tool bin directory
is configured in the shell.
- `syncwheel self update` uses uv to upgrade the installed tool.

2. **uv editable development tool (recommended for syncwheel development)**
- Clone `syncwheel` once in a stable location.
- Run it against target repos via `-r/--repo` using either paths or aliases.
- Best when you want one central install to keep updated.
- Run `uv tool install --editable /path/to/syncwheel`.
- The `syncwheel` executable reflects local source edits immediately.
- `syncwheel self update` treats the checkout as a git install and uses the
existing fast-forward flow against the clone's upstream.

2. **Git submodule**
3. **Git submodule**
- Add `syncwheel` as a submodule inside each target repo.
- Good when each project must pin an explicit syncwheel version.
- Invoke it with `python3 path/to/syncwheel/scripts/syncwheel.py ...`.
- Self-update status works for detached submodule-style checkouts; updating
remains controlled by the parent repository's submodule policy.

3. **Vendored script**
4. **Vendored checkout or script**
- Copy `scripts/syncwheel.py` into a project.
- Fastest for experiments, but updates are manual.
- `self status` reports `install_kind: script` when no git checkout or uv
tool environment is detected.

## Repo aliases

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.0
0.18.0
9 changes: 9 additions & 0 deletions docs/agent-procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ Given one of those prompts, the agent should:
is used.
- If the repo uses GitHub, validate publication state after branch rebuilds.
- If the manifest and Git disagree, fix the manifest or name the conflict explicitly.
- **A rebuild reconstructs a branch from the manifest's commit projection, NOT from the
branch's current remote tip.** If the manifest points at a pre-cleanup commit (or a
range that misses a later fix), `stack rebuild` / `int rebuild` will silently **revert
that work** — the rebuilt branch force-pushes back to the older state and the cleanup
disappears. This is a real regression mode, not hypothetical. **Guard against it:**
after every rebuild/sync/publish, diff the rebuilt branch against the expected
post-cleanup state and confirm earlier fixes did not regress; keep the manifest current
with `stack set <id> <rev-or-range>` pointing at the post-cleanup commit BEFORE rebuilding,
so the projection includes the latest work.

## Suggested human/AI split

Expand Down
25 changes: 25 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "syncwheel"
dynamic = ["version"]
description = "Deterministic helper for maintaining long-lived PR stacks."
readme = "README.md"
requires-python = ">=3.11"
license = {file = "LICENSE"}
authors = [
{name = "NestDevLab"},
]
dependencies = []

[project.scripts]
syncwheel = "syncwheel:main"

[tool.setuptools]
py-modules = ["syncwheel"]
package-dir = {"" = "scripts"}

[tool.setuptools.dynamic]
version = {file = ["VERSION"]}
89 changes: 89 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/sh
set -eu

usage() {
cat <<'EOF'
Usage: scripts/install.sh [--with-uv] [--editable PATH]

Installs syncwheel as a uv tool.

Options:
--with-uv Install uv with the official astral.sh installer if uv is missing.
--editable PATH Install a local checkout in editable mode for development.
-h, --help Show this help.
EOF
}

with_uv=0
editable_path=

while [ "$#" -gt 0 ]; do
case "$1" in
--with-uv)
with_uv=1
shift
;;
--editable)
if [ "$#" -lt 2 ]; then
echo "error: --editable requires a path" >&2
exit 2
fi
editable_path=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done

if ! command -v uv >/dev/null 2>&1; then
if [ "$with_uv" -ne 1 ]; then
echo "error: uv is not installed. Install uv first or rerun with --with-uv." >&2
exit 1
fi
if command -v curl >/dev/null 2>&1; then
curl -LsSf https://astral.sh/uv/install.sh | sh
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://astral.sh/uv/install.sh | sh
else
echo "error: --with-uv requires curl or wget" >&2
exit 1
fi
Comment on lines +51 to +58
fi

if ! command -v uv >/dev/null 2>&1; then
echo "error: uv was installed but is not on PATH yet" >&2
echo "Run: uv tool update-shell" >&2
exit 1
fi

if [ -n "$editable_path" ]; then
uv tool install --force --editable "$editable_path"
else
uv tool install --force "git+https://github.com/NestDevLab/syncwheel"
fi

tool_bin_dir=${UV_TOOL_BIN_DIR:-"$HOME/.local/bin"}
case ":$PATH:" in
*":$tool_bin_dir:"*) ;;
*)
echo "warning: uv tool bin directory is not on PATH: $tool_bin_dir" >&2
echo "Run: uv tool update-shell" >&2
;;
esac

if command -v syncwheel >/dev/null 2>&1; then
syncwheel --version
elif [ -x "$tool_bin_dir/syncwheel" ]; then
"$tool_bin_dir/syncwheel" --version
else
echo "warning: syncwheel was installed but the executable was not found on PATH" >&2
echo "Run: uv tool update-shell" >&2
fi
Loading
Loading