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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Preserve raw terminal bytes and replay timing; do not normalize the capture.
docs/demos/output.log -text -diff
docs/demos/installer/output.log -text whitespace=-blank-at-eol
62 changes: 62 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release wheel installer

on:
pull_request:
paths: [install.sh, tests/test_installer.py, .github/workflows/install.yml]
push:
branches: [main]
paths: [install.sh, tests/test_installer.py, .github/workflows/install.yml]

permissions:
contents: read

jobs:
install:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12'
- uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6
with:
version: '0.12.0'
enable-cache: false
- name: Actual installer controls
run: python3 tests/test_installer.py
- name: Published release private install and retained-destination refusal
shell: bash
run: |
set -euo pipefail
export PREFIX="$RUNNER_TEMP/willay-release"
export VERSION=0.1.0
sh install.sh --dry-run
test ! -e "$PREFIX"
sh install.sh
test "$("$PREFIX/bin/willay" --version)" = 'willay 0.1.0'
if sh install.sh; then
echo 'existing install was overwritten' >&2
exit 1
fi
test "$("$PREFIX/bin/willay" --version)" = 'willay 0.1.0'
- name: Default command discovery without shell-profile changes
shell: bash
run: |
set -euo pipefail
export HOME="$RUNNER_TEMP/willay-default-home"
mkdir -p "$HOME"
unset PREFIX
export VERSION=0.1.0
sh install.sh --dry-run
test ! -e "$HOME/.local"
sh install.sh
test -L "$HOME/.local/bin/willay"
test "$("$HOME/.local/bin/willay" --version)" = 'willay 0.1.0'
test ! -e "$HOME/.profile"
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ and can publish it to an already-private podcast feed.

Willay does not call a language model. The agent writing the script already is one.

## Install

With Python 3.12+ and [uv](https://docs.astral.sh/uv/) already installed:

```sh
curl -fsSL https://raw.githubusercontent.com/intigraph/willay/main/install.sh | sh
```

The installer selects the latest stable release, verifies its wheel against the
release's SHA256SUMS, and installs it with uv into a new private version directory
under `~/.local/share/willay`, then links the verified command at
`~/.local/bin/willay`. Add `~/.local/bin` to PATH if needed. It refuses an existing
destination or command, including a dangling command symlink; `VERSION=0.1.0` pins a release, `PREFIX` or `--prefix`
chooses a new destination, and `--dry-run` checks metadata without installing or
verifying payload bytes. To pass options through a pipe use `sh -s -- --dry-run`;
set VERSION/PREFIX on the `sh` process, not on curl.

The release checksum trusts the publisher and HTTPS; it is not an independent
signature. Runtime dependencies are resolved from PyPI by uv and are not covered
by the release wheel checksum. No Python runtime is downloaded. No API key,
rendering, feed, service or shell profile is configured. To remove this install,
remove only its printed version directory and its command symlink. A custom
prefix keeps the command in that prefix’s `bin` directory without creating the
default command link.

## See it in action

```sh
Expand Down
9 changes: 9 additions & 0 deletions docs/demos/installer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The recording runs the actual embedded installer through the retained fixture
controls. It demonstrates successful private installation, retained-destination
refusal, checksum refusal before destination creation, and wrong-version cleanup.
HTTP and uv are mocked. It does not prove a real wheel installs on any OS.

Reproduce with `python3 docs/demos/installer/demo.py`. Replay the recorded terminal
with `scriptreplay --log-timing docs/demos/installer/timing.log --log-out
docs/demos/installer/output.log`. The hosted installer workflow separately tests
the real published v0.1.0 wheel; those results remain pending.
24 changes: 24 additions & 0 deletions docs/demos/installer/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Exercise the actual installer through the retained HTTP/uv fixture controls."""
import contextlib
import importlib.util
import io
from pathlib import Path

root = Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location('installer_controls', root / 'tests/test_installer.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for method, caption in [
('test_install_and_refuse_overwrite', 'Private install succeeds; existing destination refuses overwrite.'),
('test_corrupt_payload_refuses_before_destination', 'Corrupt wheel refuses before creating the destination.'),
('test_wrong_installed_version_cleanup', 'Wrong installed version refuses and cleans the new directory.'),
]:
control = module.InstallerTests(method)
control.setUp()
try:
with contextlib.redirect_stdout(io.StringIO()):
getattr(control, method)()
print('PASS:', caption)
finally:
control.doCleanups()
print('HTTP and uv are mocked; installer code is real. No real release or platform acceptance claimed.')
7 changes: 7 additions & 0 deletions docs/demos/installer/output.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Script started on 2026-11-04 17:45:32-05:00 [COMMAND="python3 docs/demos/installer/demo.py" <not executed on terminal>]
PASS: Private install succeeds; existing destination refuses overwrite.
PASS: Corrupt wheel refuses before creating the destination.
PASS: Wrong installed version refuses and cleans the new directory.
HTTP and uv are mocked; installer code is real. No real release or platform acceptance claimed.

Script done on 2026-11-04 17:45:32-05:00 [COMMAND_EXIT_CODE="0"]
4 changes: 4 additions & 0 deletions docs/demos/installer/timing.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0.052855 73
0.001400 62
0.000882 69
0.000023 97
113 changes: 113 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/sh
# Install a published wheel with an existing uv and Python 3.12+.
set -eu
exec python3 - "$@" <<'PY'
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request


def download(url, limit):
with urllib.request.urlopen(url, timeout=30) as response:
data = response.read(limit + 1)
if len(data) > limit:
raise ValueError('download exceeds size limit')
return data


def install(argv=None):
parser = argparse.ArgumentParser(description='Install a checksummed Willay release wheel')
parser.add_argument('--prefix', default=os.environ.get('PREFIX'))
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args(argv)
if sys.version_info < (3, 12):
raise ValueError('Python 3.12+ is required; no interpreter will be downloaded')
version = os.environ.get('VERSION', '')
if version and not re.fullmatch(r'\d+\.\d+\.\d+', version):
raise ValueError('VERSION must be a stable version such as 0.1.0')
api = 'https://api.github.com/repos/intigraph/willay/releases/'
release = json.loads(download(api + ('tags/v' + version if version else 'latest'), 2**20))
tag = release['tag_name']
if release.get('draft') or release.get('prerelease') or not re.fullmatch(
r'v\d+\.\d+\.\d+', tag
) or (version and tag != 'v' + version):
raise ValueError('release is not the requested stable version')
version = tag[1:]
wheel = f'willay-{version}-py3-none-any.whl'
base = f'https://github.com/intigraph/willay/releases/download/{tag}/'
assets = {asset['name']: asset['browser_download_url'] for asset in release['assets']}
for name in ('SHA256SUMS', wheel):
if assets.get(name) != base + name:
raise ValueError('missing or noncanonical release asset: ' + name)
prefix = Path(args.prefix).expanduser().absolute() if args.prefix else (
Path.home() / '.local/share/willay' / tag
)
if prefix.exists() or prefix.is_symlink():
raise ValueError('destination already exists; retained unchanged: ' + str(prefix))
entrypoint = None if args.prefix else Path.home() / '.local/bin/willay'
if entrypoint is not None and (entrypoint.exists() or entrypoint.is_symlink()):
raise ValueError('command already exists; retained unchanged: ' + str(entrypoint))
uv = shutil.which('uv')
if not uv:
raise ValueError('uv is required; install it from https://docs.astral.sh/uv/')
if args.dry_run:
print(f'Would install Willay {version} into {prefix}; metadata only, payload not verified')
return
sums = download(assets['SHA256SUMS'], 65536).decode('ascii')
matches = [line.split()[0] for line in sums.splitlines()
if len(line.split()) == 2 and line.split()[1].lstrip('*') == wheel]
if len(matches) != 1 or not re.fullmatch(r'[a-fA-F0-9]{64}', matches[0]):
raise ValueError('missing or ambiguous wheel checksum')
data = download(assets[wheel], 16 * 2**20)
if hashlib.sha256(data).hexdigest() != matches[0].lower():
raise ValueError('wheel checksum mismatch; nothing installed')
# uv environments contain absolute paths: create at the final private location.
# Refuse existing destinations and remove only the new directory on failure.
prefix.mkdir(parents=True, exist_ok=False)
try:
with tempfile.TemporaryDirectory(prefix='willay-wheel-') as directory:
artifact = Path(directory) / wheel
artifact.write_bytes(data)
environment = {key: os.environ[key] for key in ('HOME', 'PATH', 'TMPDIR')
if key in os.environ}
environment.update(UV_TOOL_DIR=str(prefix / 'tools'),
UV_TOOL_BIN_DIR=str(prefix / 'bin'),
UV_PYTHON_DOWNLOADS='never', UV_NO_CONFIG='1',
UV_NO_CACHE='1')
subprocess.run([uv, 'tool', 'install', '--python', sys.executable,
'--no-managed-python', '--index-url', 'https://pypi.org/simple',
str(artifact)], env=environment, check=True, timeout=240)
result = subprocess.run([str(prefix / 'bin/willay'), '--version'],
env=environment, check=True, capture_output=True,
text=True, timeout=30)
if result.stdout.strip() != 'willay ' + version:
raise ValueError('installed command version does not match the release')
if entrypoint is not None:
entrypoint.parent.mkdir(parents=True, exist_ok=True)
entrypoint.symlink_to(prefix / 'bin/willay')
except BaseException:
shutil.rmtree(prefix)
raise
print(f'Installed Willay {version}: {prefix}/bin/willay')
if entrypoint is not None:
print(f'Command: {entrypoint}; add {entrypoint.parent} to PATH if needed.')
else:
print(f'Add {prefix}/bin to PATH.')
print('No credentials, feed, service or shell profile were configured.')


if __name__ == '__main__':
try:
install()
except Exception as error:
print('Willay install refused: ' + str(error), file=sys.stderr)
raise SystemExit(1)
PY
Loading
Loading