xim index

URL Templates: Legacy Version Auto-Update

Rendered from docs/specs/url-template.md in the index repository — one source of truth, no second copy to drift.

Deprecated: new packages should use xpm.source (a string or a GLOBAL/CN map). Existing url_template packages remain supported for compatibility and will be migrated opportunistically.

A small contract between an xpkg description and the in-repo version checker (.github/scripts/version-check.py). The package must explicitly opt in with ci = { update = true }; url_template only describes the asset URL. Without the CI opt-in, the package is maintained by hand. The scan interval is centralized in .github/xpkg-ci.yml and is not repeated in individual package files.

The contract

A package opts in with ci = { update = true } and places a single string field xpm.<platform>.url_template next to that platform's version table:

xpm = {
    linux = {
        url_template = "https://github.com/astral-sh/uv/releases/download/{version}/uv-x86_64-unknown-linux-gnu.tar.gz",

        ["latest"] = { ref = "0.11.7" },
        ["0.11.7"] = { url = "...", sha256 = "..." },
    },
    macosx = {
        url_template = "https://github.com/astral-sh/uv/releases/download/{version}/uv-aarch64-apple-darwin.tar.gz",
        ...
    },
    windows = {
        url_template = "https://github.com/astral-sh/uv/releases/download/{version}/uv-x86_64-pc-windows-msvc.zip",
        ...
    },
}

The placeholder {version} is the only token recognised today.

Resolution rules (v1)

  1. Updater scans every pkgs/**/*.lua.
  2. Only packages with ci = { update = true } and at least one platform with a url_template field are considered opt-in.
  3. The updater treats package.repo as a GitHub URL and extracts <owner>/<name> from it.
  4. It calls GET https://api.github.com/repos/<owner>/<name>/releases/latest and reads the tag_name. A leading v (if present) is stripped to produce <version>.
  5. The current "latest" is read from xpm.linux["latest"].ref (whichever platform's latest is checked first; they are required to agree).
  6. If the upstream <version> is not the same as the current one, the updater computes per-platform URLs by substituting {version} in each url_template, downloads each artifact, and records the sha256 sum.

In v1 only source = github-release is supported (implicitly — there is no source field; the contract assumes GitHub). Packages whose upstream is not a GitHub Release simply leave url_template unset and continue to be maintained by hand.

XLINGS_RES mode (res_versioned)

Some packages — notably xlings itself — record their version entries as ["<ver>"] = "XLINGS_RES" (resolved through the multi-mirror resource service) instead of a hardcoded url + sha256. They cannot use url_template (there is no single URL, and the resource service handles integrity), so they previously had no auto-update path and went stale silently (xlings latest was stuck 5 releases behind).

Such a platform opts in with res_versioned = true next to its version table:

xpm = {
    linux = {
        res_versioned = true,
        ["latest"] = { ref = "0.4.60" },
        ["0.4.60"] = "XLINGS_RES",
    },
    -- macosx / windows likewise
}

Resolution is identical to url_template (rules 1–5 above: scan, read package.repo, query releases/latest, compare against ["latest"].ref). The only difference is the bump shape: on an update the updater appends a bare ["<upstream>"] = "XLINGS_RES" entry and bumps ["latest"].ref — no artifact download, no sha256 (the resource service resolves and verifies the binary, and provides mirror fallback). A platform may set url_template or res_versioned, not both; url_template wins if both are present.

What is NOT in scope (v1)

  • The xlings package manager does not consume url_template. It is a private convention between the package description and the in-repo updater. xpm version entries continue to record an explicit url + sha256 so xlings install behaves exactly as before.
  • Any tag/version syntax beyond "leading-v stripped". Date-based tags, monorepo tags, prefixed tags, and similar variants are out; packages that need them stay on manual maintenance for now.
  • Pre-releases. The updater always pulls releases/latest (which excludes drafts and pre-releases by GitHub's own definition). No knob to opt in.
  • Multi-arch. The current xpm shape is one platform → one URL, so this spec inherits the same shape. If a package needs distinct URLs per arch within the same platform, it stays on manual maintenance.

Behaviour when fields are missing or inconsistent

  • No url_template and no res_versioned on any platform → package is skipped (manual mode).
  • res_versioned = true on a platform → that platform auto-bumps as a bare ["<ver>"] = "XLINGS_RES" entry (no url/sha256). Requires package.repo like the url_template path.
  • url_template on some platforms only → only those platforms are refreshed; other platforms are left alone.
  • package.repo missing or not a GitHub URL → the package is skipped and a warning is emitted.
  • The string {version} not present in the template → the template is rejected (lint failure) and the package is skipped.

Phase 1 vs Phase 2

Both phases now ship in the same script (.github/scripts/version-check.py) behind the --apply flag and as separate scheduled workflows.

Phase 1 — version-check.yml (default, dry-run): wakes daily according to the central policy. Reads opted-in url_templates, queries upstream, prints a JSON report of ": → ", uploads it as a workflow artifact, and does not modify any file or open any PR.

Phase 2 — version-bump.yml (--apply): runs from the central scheduler or on manual dispatch. For every opted-in package the dry-run flags as update-available, the script:

  1. Downloads the new artifact for each opted-in platform and computes its sha256.
  2. Bumps ["latest"].ref to the upstream version on every opted-in platform.
  3. Appends a new ["<upstream>"] = { url, sha256 } block right after ["latest"] on the same platform. Existing version blocks are left untouched, so consumers pinned to an older version keep working unchanged.

The workflow then commits the modified lua to a per-package branch named auto/bump-<pkg>-<upstream> and opens a PR via gh. If that branch already exists on origin (i.e. an open PR is already pending for the same target version) the package is skipped — the run is idempotent.

Manual one-shot:

# preview only — no file changes
python3 .github/scripts/version-check.py --workspace .

# apply (modifies pkgs/**/*.lua in place)
python3 .github/scripts/version-check.py --workspace . --apply [--only <pkg>]

Example: pkgs/u/uv.lua

uv is the reference implementation of this spec. See its xpm block for the canonical shape; copy from there when adding url_template to other packages.