diff --git a/docs/assessments/release_manifest_proposal.md b/docs/assessments/release_manifest_proposal.md new file mode 100644 index 0000000..1bb3893 --- /dev/null +++ b/docs/assessments/release_manifest_proposal.md @@ -0,0 +1,64 @@ +# Proposal: Generative Bootstrap Asset Manifest and Project-level Release Manifest + +## 1. Background +Currently, bootstrap assets (such as `uv` and `opencode` installer) are managed via hardcoded environment variables in `scripts/init_system.sh` and `scripts/init_system_uv_release_manifest.sh`. This approach makes it difficult to: +- Synchronize versions across multiple scripts. +- Validate asset integrity automatically during CI/CD. +- Support air-gapped environments where a local mirror of assets is required. + +## 2. Proposed Solution +Implement a generative manifest system that decouples asset definitions from deployment logic. + +### 2.1 Release Manifest Structure +A JSON-based manifest (`release_manifest.json`) that explicitly defines all external dependencies required for bootstrapping. + +```json +{ + "version": "1.0", + "timestamp": "2026-03-15T03:23:01Z", + "assets": { + "uv": { + "version": "0.10.7", + "variants": { + "x86_64-unknown-linux-gnu": { + "name": "uv-x86_64-unknown-linux-gnu.tar.gz", + "url": "...", + "sha256": "..." + } + } + }, + "opencode": { + "version": "1.2.5", + "url": "...", + "sha256": "..." + } + } +} +``` + +### 2.2 Generative Tooling +A Python script `scripts/gen_release_manifest.py` to automate the generation of this manifest. This script: +1. Fetches asset metadata from upstream sources. +2. Calculates SHA256 checksums. +3. Produces a machine-readable JSON file. + +### 2.3 Consumption in Shell Scripts +Deployment scripts will be updated to consume the JSON manifest using `jq` (or a simple Python wrapper if `jq` is not available, though `jq` is standard in most ops environments). + +Example usage in `init_system.sh`: +```bash +UV_VERSION=$(jq -r '.assets.uv.version' release_manifest.json) +UV_SHA256=$(jq -r '.assets.uv.variants["x86_64-unknown-linux-gnu"].sha256' release_manifest.json) +``` + +## 3. Implementation Plan +1. [x] Draft `scripts/gen_release_manifest.py`. +2. [x] Generate initial `release_manifest.json`. +3. [ ] Integrate manifest reading into `scripts/init_system.sh`. +4. [ ] Deprecate `scripts/init_system_uv_release_manifest.sh`. +5. [ ] Add a CI check to ensure `release_manifest.json` matches the generated output from the definitions in the script. + +## 4. Architectural Reflection +- **Pros**: Single source of truth for dependencies; better security via verified checksums in a centralized place; easier to support private mirrors by rewriting URLs in the JSON. +- **Cons**: Adds a dependency on a JSON parser or a temporary Python invocation in shell scripts. +- **Decision**: The benefit of having a verifiable, project-level manifest outweighs the minor complexity of parsing JSON, especially as the project grows in scale. diff --git a/release_manifest.json b/release_manifest.json new file mode 100644 index 0000000..6926b49 --- /dev/null +++ b/release_manifest.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "timestamp": "2026-03-15T03:23:01Z", + "assets": { + "uv": { + "version": "0.10.7", + "variants": { + "x86_64-unknown-linux-gnu": { + "name": "uv-x86_64-unknown-linux-gnu.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "9ac6cee4e379a5abfca06e78a777b26b7ba1f81cb7935b97054d80d85ac00774" + }, + "x86_64-unknown-linux-musl": { + "name": "uv-x86_64-unknown-linux-musl.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-x86_64-unknown-linux-musl.tar.gz", + "sha256": "992529add6024e67135b1c80617abd2eca7be2cf0b99b3911f923de815bd8dc1" + }, + "aarch64-unknown-linux-gnu": { + "name": "uv-aarch64-unknown-linux-gnu.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-aarch64-unknown-linux-gnu.tar.gz", + "sha256": "20efc27d946860093650bcf26096a016b10fdaf03b13c33b75fbde02962beea9" + }, + "aarch64-unknown-linux-musl": { + "name": "uv-aarch64-unknown-linux-musl.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-aarch64-unknown-linux-musl.tar.gz", + "sha256": "115291f9943531a3b63db3a2eabda8b74b8da4831551679382cb309c9debd9f7" + } + } + }, + "opencode": { + "version": "1.2.5", + "url": "https://opencode.ai/install", + "sha256": "fc3c1b2123f49b6df545a7622e5127d21cd794b15134fc3b66e1ca49f7fb297e" + } + } +} diff --git a/scripts/gen_release_manifest.py b/scripts/gen_release_manifest.py new file mode 100644 index 0000000..b646a42 --- /dev/null +++ b/scripts/gen_release_manifest.py @@ -0,0 +1,72 @@ +import hashlib +import json +import os +import sys +import urllib.request +from typing import Dict, Any + +# Current bootstrap versions (as found in scripts/init_system.sh) +MANIFEST_DEFAULTS = { + "uv": { + "version": "0.10.7", + "url_template": "https://github.com/astral-sh/uv/releases/download/{version}/{asset}", + "assets": { + "x86_64-unknown-linux-gnu": "uv-x86_64-unknown-linux-gnu.tar.gz", + "x86_64-unknown-linux-musl": "uv-x86_64-unknown-linux-musl.tar.gz", + "aarch64-unknown-linux-gnu": "uv-aarch64-unknown-linux-gnu.tar.gz", + "aarch64-unknown-linux-musl": "uv-aarch64-unknown-linux-musl.tar.gz", + } + }, + "opencode": { + "version": "1.2.5", + "url": "https://opencode.ai/install", + } +} + +def get_sha256(url: str) -> str: + print(f"Fetching {url} for hashing...", file=sys.stderr) + try: + with urllib.request.urlopen(url) as response: + data = response.read() + return hashlib.sha256(data).hexdigest() + except Exception as e: + print(f"Error fetching {url}: {e}", file=sys.stderr) + return "" + +def generate_manifest() -> Dict[str, Any]: + manifest = { + "version": "1.0", + "timestamp": os.popen("date -u +'%Y-%m-%dT%H:%M:%SZ'").read().strip(), + "assets": {} + } + + # UV Assets + uv_conf = MANIFEST_DEFAULTS["uv"] + uv_assets = {} + for arch_libc, asset_name in uv_conf["assets"].items(): + url = uv_conf["url_template"].format(version=uv_conf["version"], asset=asset_name) + sha = get_sha256(url) + uv_assets[arch_libc] = { + "name": asset_name, + "url": url, + "sha256": sha + } + manifest["assets"]["uv"] = { + "version": uv_conf["version"], + "variants": uv_assets + } + + # OpenCode Installer + oc_conf = MANIFEST_DEFAULTS["opencode"] + oc_sha = get_sha256(oc_conf["url"]) + manifest["assets"]["opencode"] = { + "version": oc_conf["version"], + "url": oc_conf["url"], + "sha256": oc_sha + } + + return manifest + +if __name__ == "__main__": + manifest_data = generate_manifest() + print(json.dumps(manifest_data, indent=2))