Establish public verified release boundary
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
`ink-releases` is the public, source-free distribution boundary for private Ink repositories.
|
||||||
|
|
||||||
|
## Authority
|
||||||
|
|
||||||
|
- Read `REQUIREMENTS.md` before durable changes.
|
||||||
|
- `REQUIREMENTS.md` is authoritative; generated artifacts and private source repositories are evidence.
|
||||||
|
- Update requirements before changing durable installer, publication, integrity, or platform behavior.
|
||||||
|
- Keep strict Redgate proof current. `req:` is traceability; accepted local `test` edges are proof.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Never copy private source, credentials, deploy keys, policy, profiles, or configuration here.
|
||||||
|
- `ink` publication owns only `install.sh`, `channels/*/ink`, and `releases/*/ink`.
|
||||||
|
- `ink-toolset` publication owns only `channels/*/toolset` and `releases/*/toolset`.
|
||||||
|
- Published executables are individual static x86_64 Linux-musl assets; do not add bundles.
|
||||||
|
- Installation writes executable files only. Never edit PATH, shell configuration, policy, or credentials.
|
||||||
|
|
||||||
|
## Work tracking
|
||||||
|
|
||||||
|
Work is tracked in Gitea Issues for `tmk241/ink-releases`. Do not mirror issues into local files. Do not create, close, or relabel issues without explicit user authority.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash -n install.sh
|
||||||
|
python3 test/installer_e2e.py
|
||||||
|
redgate lint < requirements.tsv
|
||||||
|
```
|
||||||
|
|
||||||
|
Final reports must include `REQUIREMENT IMPACT: IDs` or `REQUIREMENT IMPACT: none`.
|
||||||
@@ -1,3 +1,37 @@
|
|||||||
# ink-releases
|
# Ink Releases
|
||||||
|
|
||||||
Public verified static release assets for Ink and Ink Toolset
|
Public, source-free distribution for private `ink` and `ink-toolset` repositories.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Inspect the installer, then run it. A noninteractive invocation installs Ink
|
||||||
|
only. With a terminal it offers a compact native set menu.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | less
|
||||||
|
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | sh
|
||||||
|
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | sh -s -- --set text --set web -y
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer verifies SHA-256 before installing. It writes executable files
|
||||||
|
under `~/.ink/bin` only and never edits PATH, shell configuration, Ink policy,
|
||||||
|
or credentials.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
install.sh
|
||||||
|
channels/main/ink/
|
||||||
|
channels/main/toolset/
|
||||||
|
releases/VERSION/ink/
|
||||||
|
releases/VERSION/toolset/
|
||||||
|
```
|
||||||
|
|
||||||
|
Each executable is an individual static x86_64 Linux-musl asset. There are no
|
||||||
|
bundles: selecting one tool downloads one tool.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
The private source workflows build, test, inspect, run on Void Linux musl, and
|
||||||
|
then publish only artifacts, checksums, and manifests. Each source owns its own
|
||||||
|
subtree. This repository contains no private source or publisher credential.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Requirements
|
||||||
|
|
||||||
|
## distribution
|
||||||
|
|
||||||
|
001 The repository must remain public and contain only installers, manifests, checksums, and built release artifacts derived from private source repositories.
|
||||||
|
002 The stable `channels/main/ink` path must contain the latest verified Ink core asset and checksum published from the private Ink main branch.
|
||||||
|
003 The stable toolset channel must contain verified individual assets, a set manifest, and checksums from the private main branch.
|
||||||
|
004 A source release tag must publish the same source-owned files under `releases/TAG/ink` or `releases/TAG/toolset` without modifying the other source subtree.
|
||||||
|
005 Every executable artifact must be a static x86_64 Linux-musl executable and must pass `--help` inside Void Linux musl before publication.
|
||||||
|
006 Every published manifest must identify its source repository, source commit, target, executable, set, byte count, digest, and asset path.
|
||||||
|
007 Publication must fail closed before changing this repository when build, test, static-artifact inspection, checksum, or Void Linux runtime proof fails.
|
||||||
|
008 This repository must never contain source-repository credentials, deploy keys, private source, Ink policy, shell configuration, or user credentials.
|
||||||
|
|
||||||
|
## installer
|
||||||
|
|
||||||
|
001 The root `install.sh` must install Ink and only explicitly selected tool assets from public repository paths after SHA-256 verification.
|
||||||
|
002 Without a terminal or selection flags the installer must install Ink only; with a terminal it may present a compact native text menu.
|
||||||
|
003 The installer must support exact tool and named set selection without downloading unselected executable assets.
|
||||||
|
004 The installer must write only selected-prefix executables and must not edit PATH, shell configuration, policy, or credentials.
|
||||||
|
005 Installer help must completely document options, environment, effects, defaults, examples, and platform support.
|
||||||
|
006 The installer must support the stable main channel and immutable matching source-release tags.
|
||||||
|
|
||||||
|
## governance
|
||||||
|
|
||||||
|
001 `REQUIREMENTS.md` must remain authoritative; code, workflows, tests, runtime, and source repositories are evidence.
|
||||||
|
002 Durable behavior changes must update requirements before implementation and proof.
|
||||||
|
003 `req:` references must provide traceability; only accepted local executable `test` edges satisfy proof coverage.
|
||||||
|
004 Work is tracked in Gitea Issues for `tmk241/ink-releases`; agents must not mirror issues locally or mutate tracker state without explicit authority.
|
||||||
Executable
+175
@@ -0,0 +1,175 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
INK_VERSION=${INK_VERSION:-main}
|
||||||
|
TOOLSET_VERSION=${TOOLSET_VERSION:-main}
|
||||||
|
PREFIX=${INK_PREFIX:-$HOME/.ink}
|
||||||
|
RELEASE_BASE=${INK_RELEASE_BASE:-https://git.tmk241.com/tmk241/ink-releases/raw/branch/main}
|
||||||
|
TARGET=x86_64-linux-musl
|
||||||
|
SETS=
|
||||||
|
TOOLS=
|
||||||
|
ASSUME_YES=0
|
||||||
|
LIST_ONLY=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'HELP'
|
||||||
|
Usage: install.sh [OPTIONS]
|
||||||
|
|
||||||
|
Install the static Ink core and optionally selected permission-sized tools.
|
||||||
|
With a terminal and no selection flags, a small set menu is shown. Without a
|
||||||
|
terminal, the safe default installs only Ink.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--set NAME Add text, filesystem, web, git, or all (repeatable)
|
||||||
|
--tool NAME Add one exact executable (repeatable)
|
||||||
|
--version VERSION Use immutable VERSION for Ink and Ink Toolset
|
||||||
|
--ink-version VERSION Select main or an immutable Ink release
|
||||||
|
--toolset-version VER Select main or an immutable Toolset release
|
||||||
|
--prefix DIR Install executable files under DIR/bin
|
||||||
|
--list List available tool assets; do not install
|
||||||
|
-y, --yes Do not ask for confirmation
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
INK_VERSION, TOOLSET_VERSION, INK_PREFIX (versions default to main)
|
||||||
|
INK_RELEASE_BASE (public ink-releases raw root)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
curl -fsSL URL/install.sh | sh
|
||||||
|
curl -fsSL URL/install.sh | sh -s -- --set text --set web -y
|
||||||
|
curl -fsSL URL/install.sh | sh -s -- --tool lines --tool httpget -y
|
||||||
|
|
||||||
|
Effects:
|
||||||
|
Writes executable files only under PREFIX/bin. It never edits PATH, shell
|
||||||
|
configuration, Ink policy, credentials, or unrelated state.
|
||||||
|
HELP
|
||||||
|
}
|
||||||
|
|
||||||
|
append_word() {
|
||||||
|
if [ -z "$1" ]; then printf '%s' "$2"; else printf '%s %s' "$1" "$2"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case $1 in
|
||||||
|
--set) [ $# -ge 2 ] || { echo 'install: --set needs a value' >&2; exit 2; }; SETS=$(append_word "$SETS" "$2"); shift 2 ;;
|
||||||
|
--tool) [ $# -ge 2 ] || { echo 'install: --tool needs a value' >&2; exit 2; }; TOOLS=$(append_word "$TOOLS" "$2"); shift 2 ;;
|
||||||
|
--version) [ $# -ge 2 ] || { echo 'install: --version needs a value' >&2; exit 2; }; INK_VERSION=$2; TOOLSET_VERSION=$2; shift 2 ;;
|
||||||
|
--ink-version) [ $# -ge 2 ] || { echo 'install: --ink-version needs a value' >&2; exit 2; }; INK_VERSION=$2; shift 2 ;;
|
||||||
|
--toolset-version) [ $# -ge 2 ] || { echo 'install: --toolset-version needs a value' >&2; exit 2; }; TOOLSET_VERSION=$2; shift 2 ;;
|
||||||
|
--prefix) [ $# -ge 2 ] || { echo 'install: --prefix needs a value' >&2; exit 2; }; PREFIX=$2; shift 2 ;;
|
||||||
|
--list) LIST_ONLY=1; shift ;;
|
||||||
|
-y|--yes) ASSUME_YES=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
*) echo "install: unknown option: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ $# -eq 0 ] || { echo 'install: trailing arguments' >&2; exit 2; }
|
||||||
|
|
||||||
|
case $(uname -m) in x86_64|amd64) ;; *) echo 'install: this release supports x86_64 Linux only' >&2; exit 1 ;; esac
|
||||||
|
case $PREFIX in ''|/) echo 'install: unsafe empty/root prefix' >&2; exit 2 ;; esac
|
||||||
|
|
||||||
|
TTY=0
|
||||||
|
if [ -r /dev/tty ] && [ -w /dev/tty ]; then TTY=1; fi
|
||||||
|
if [ "$TTY" -eq 1 ]; then
|
||||||
|
cyan='\033[36m'; bold='\033[1m'; dim='\033[2m'; reset='\033[0m'
|
||||||
|
else
|
||||||
|
cyan= bold= dim= reset=
|
||||||
|
fi
|
||||||
|
|
||||||
|
say() { printf '%s\n' "$*" >&2; }
|
||||||
|
fetch() {
|
||||||
|
url=$1 destination=$2
|
||||||
|
if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$destination"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then wget -qO "$destination" "$url"
|
||||||
|
else echo 'install: curl or wget is required' >&2; exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'
|
||||||
|
else echo 'install: sha256sum or shasum is required' >&2; exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "$SETS$TOOLS" ] && [ "$TTY" -eq 1 ] && [ "$LIST_ONLY" -eq 0 ]; then
|
||||||
|
printf '%bInk%b %bsmall tools, explicit authority%b\n\n' "$bold$cyan" "$reset" "$dim" "$reset" >/dev/tty
|
||||||
|
printf ' 0 Ink only\n 1 text\n 2 filesystem\n 3 web\n 4 git\n 5 all tools\n\nSelect sets [0]: ' >/dev/tty
|
||||||
|
IFS= read -r answer </dev/tty || answer=0
|
||||||
|
for choice in $answer; do
|
||||||
|
case $choice in 0|'') ;; 1) SETS=$(append_word "$SETS" text) ;; 2) SETS=$(append_word "$SETS" filesystem) ;; 3) SETS=$(append_word "$SETS" web) ;; 4) SETS=$(append_word "$SETS" git) ;; 5) SETS=all ;; *) echo "install: invalid selection: $choice" >&2; exit 2 ;; esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
work=${TMPDIR:-/tmp}/ink-install.$$
|
||||||
|
trap 'rm -rf "$work"' EXIT HUP INT TERM
|
||||||
|
mkdir -p "$work"
|
||||||
|
|
||||||
|
release_path() {
|
||||||
|
version=$1 kind=$2
|
||||||
|
if [ "$version" = main ]; then printf 'channels/main/%s' "$kind"
|
||||||
|
else printf 'releases/%s/%s' "$version" "$kind"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
toolset_url=$RELEASE_BASE/$(release_path "$TOOLSET_VERSION" toolset)
|
||||||
|
manifest_name=ink-toolset-$TOOLSET_VERSION-$TARGET.manifest.tsv
|
||||||
|
if [ -n "$SETS$TOOLS" ] || [ "$LIST_ONLY" -eq 1 ]; then
|
||||||
|
fetch "$toolset_url/$manifest_name" "$work/$manifest_name"
|
||||||
|
fetch "$toolset_url/ink-toolset-SHA256SUMS" "$work/toolset-SHA256SUMS"
|
||||||
|
expected=$(awk -v file="$manifest_name" '$2 == file || $2 == "*" file {print $1; exit}' "$work/toolset-SHA256SUMS")
|
||||||
|
[ -n "$expected" ] && [ "$(digest "$work/$manifest_name")" = "$expected" ] || { echo 'install: toolset manifest checksum mismatch' >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LIST_ONLY" -eq 1 ]; then
|
||||||
|
cat "$work/$manifest_name"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
selected=$work/selected.tsv
|
||||||
|
: > "$selected"
|
||||||
|
if [ -n "$SETS" ]; then
|
||||||
|
awk -F '\t' -v sets="$SETS" 'NR > 1 { n=split(sets,a," "); for(i=1;i<=n;i++) if(a[i]=="all" || $4==a[i]) {print; break} }' "$work/$manifest_name" >> "$selected"
|
||||||
|
fi
|
||||||
|
for tool in $TOOLS; do
|
||||||
|
row=$(awk -F '\t' -v tool="$tool" 'NR > 1 && $3==tool {print; exit}' "$work/$manifest_name")
|
||||||
|
[ -n "$row" ] || { echo "install: unknown tool: $tool" >&2; exit 2; }
|
||||||
|
printf '%s\n' "$row" >> "$selected"
|
||||||
|
done
|
||||||
|
sort -u "$selected" -o "$selected"
|
||||||
|
|
||||||
|
say "${cyan}Ink installer${reset}"
|
||||||
|
say " core $INK_VERSION"
|
||||||
|
say " tools $TOOLSET_VERSION ($(wc -l < "$selected" | tr -d ' ') selected)"
|
||||||
|
say " target $TARGET"
|
||||||
|
say " location $PREFIX/bin"
|
||||||
|
if [ "$ASSUME_YES" -eq 0 ] && [ "$TTY" -eq 1 ]; then
|
||||||
|
printf 'Install? [y/N] ' >/dev/tty
|
||||||
|
IFS= read -r answer </dev/tty || answer=n
|
||||||
|
case $answer in y|Y|yes|YES) ;; *) say 'install: cancelled'; exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
ink_asset=ink-$TARGET
|
||||||
|
ink_url=$RELEASE_BASE/$(release_path "$INK_VERSION" ink)
|
||||||
|
fetch "$ink_url/$ink_asset" "$work/$ink_asset"
|
||||||
|
fetch "$ink_url/ink-SHA256SUMS" "$work/ink-SHA256SUMS"
|
||||||
|
expected=$(awk -v file="$ink_asset" '$2 == file || $2 == "*" file {print $1; exit}' "$work/ink-SHA256SUMS")
|
||||||
|
[ -n "$expected" ] && [ "$(digest "$work/$ink_asset")" = "$expected" ] || { echo 'install: Ink checksum mismatch' >&2; exit 1; }
|
||||||
|
|
||||||
|
while IFS="$(printf '\t')" read -r source_repo source_commit name set target bytes expected asset; do
|
||||||
|
[ -n "$name" ] || continue
|
||||||
|
fetch "$toolset_url/$asset" "$work/$asset"
|
||||||
|
[ "$(digest "$work/$asset")" = "$expected" ] || { echo "install: checksum mismatch: $asset" >&2; exit 1; }
|
||||||
|
done < "$selected"
|
||||||
|
|
||||||
|
mkdir -p "$PREFIX/bin"
|
||||||
|
chmod 755 "$work/$ink_asset"
|
||||||
|
mv "$work/$ink_asset" "$PREFIX/bin/ink"
|
||||||
|
while IFS="$(printf '\t')" read -r source_repo source_commit name set target bytes expected asset; do
|
||||||
|
[ -n "$name" ] || continue
|
||||||
|
chmod 755 "$work/$asset"
|
||||||
|
mv "$work/$asset" "$PREFIX/bin/$name"
|
||||||
|
done < "$selected"
|
||||||
|
|
||||||
|
say "${bold}Installed.${reset} Executables only; policy and PATH were not changed."
|
||||||
|
say "Run: $PREFIX/bin/ink --help"
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# req: installer/001, installer/002, installer/003, installer/004, installer/006 test
|
||||||
|
import hashlib
|
||||||
|
import http.server
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import socketserver
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
TARGET = "x86_64-linux-musl"
|
||||||
|
|
||||||
|
|
||||||
|
def sha(path: Path) -> str:
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as raw:
|
||||||
|
tmp = Path(raw)
|
||||||
|
server_root = tmp / "server"
|
||||||
|
ink_release = server_root / "releases" / "v-test" / "ink"
|
||||||
|
tools_release = server_root / "releases" / "v-tools" / "toolset"
|
||||||
|
ink_release.mkdir(parents=True)
|
||||||
|
tools_release.mkdir(parents=True)
|
||||||
|
|
||||||
|
ink_asset = ink_release / f"ink-{TARGET}"
|
||||||
|
ink_asset.write_text("#!/bin/sh\necho ink\n")
|
||||||
|
os.chmod(ink_asset, 0o755)
|
||||||
|
(ink_release / "ink-SHA256SUMS").write_text(f"{sha(ink_asset)} {ink_asset.name}\n")
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for name, group in (("facts", "text"), ("lines", "text"), ("httpget", "web"), ("gitstatus", "git")):
|
||||||
|
asset = tools_release / f"{name}-{TARGET}"
|
||||||
|
asset.write_text(f"#!/bin/sh\necho {name}\n")
|
||||||
|
os.chmod(asset, 0o755)
|
||||||
|
rows.append(("tmk241/ink-toolset", "fixture-commit", name, group, TARGET, asset.stat().st_size, sha(asset), asset.name))
|
||||||
|
|
||||||
|
manifest_name = f"ink-toolset-v-tools-{TARGET}.manifest.tsv"
|
||||||
|
manifest = tools_release / manifest_name
|
||||||
|
manifest.write_text("source_repository\tsource_commit\texecutable\tset\ttarget\tbytes\tsha256\tasset\n" + "".join("\t".join(map(str, row)) + "\n" for row in rows))
|
||||||
|
sums = [f"{sha(tools_release / row[7])} {row[7]}\n" for row in rows]
|
||||||
|
sums.append(f"{sha(manifest)} {manifest.name}\n")
|
||||||
|
(tools_release / "ink-toolset-SHA256SUMS").write_text("".join(sums))
|
||||||
|
|
||||||
|
class Quiet(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def log_message(self, *_args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
handler = lambda *args, **kwargs: Quiet(*args, directory=server_root, **kwargs)
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 0), handler) as server:
|
||||||
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
port = server.server_address[1]
|
||||||
|
prefix = tmp / "prefix"
|
||||||
|
home = tmp / "home"
|
||||||
|
home.mkdir()
|
||||||
|
policy = home / "policy.tsv"
|
||||||
|
policy.write_text("sentinel\n")
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"HOME": str(home),
|
||||||
|
"INK_RELEASE_BASE": f"http://127.0.0.1:{port}",
|
||||||
|
}
|
||||||
|
result = subprocess.run(
|
||||||
|
["/bin/sh", str(ROOT / "install.sh"), "--ink-version", "v-test", "--toolset-version", "v-tools", "--set", "text", "--tool", "httpget", "--prefix", str(prefix), "--yes"],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
server.shutdown()
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert sorted(path.name for path in (prefix / "bin").iterdir()) == ["facts", "httpget", "ink", "lines"]
|
||||||
|
assert subprocess.check_output([prefix / "bin" / "ink"], text=True).strip() == "ink"
|
||||||
|
assert not (prefix / "bin" / "gitstatus").exists()
|
||||||
|
assert policy.read_text() == "sentinel\n"
|
||||||
|
assert not (home / ".profile").exists()
|
||||||
Reference in New Issue
Block a user