Jak se Majordomus balí, publikuje a instaluje a proč je odpovědí jeden soubor.
Uživatelská polovina je INSTALL.md, která je generovaná. Tento dokument je pro toho, kdo mění, jak se nástroj dodává: co je kanonický model, co z něj vzniká, jaká je cesta důvěry, jak probíhá release a jak přidat platformu.
Pravidlo
Distribuce má jeden kanonický strojově čitelný model. Podporované cíle, artefakty release, rozřešení instalátoru, dokumentace, automatizace release a vystavená metadata UI/API se z tohoto modelu odvozují nebo se proti němu mechanicky validují.
Model je share/distribution.yaml, kontrakt majordomus-distribution/v1, schéma share/schemas/majordomus/distribution/distribution.v1.schema.json. Deklaruje binárku, ze které se publikují release repozitáře, kanonickou URL instalátoru a výchozí hodnoty, jak se pojmenovává archiv a každý cíl, o kterém má projekt názor — podporovaný, experimentální, nebo nedostupný s důvodem.
Je v share/ a ne pod .ai/ ze stejného důvodu jako share/commands.yaml: je to tvrzení o tom, jak se nástroj chová, ne kontext pro pracovníka, a cestuje s nainstalovanou kopií, aby nainstalovaný spustitelný soubor mohl odpovídat na stejné otázky jako checkout.
Co z toho vychází
share/distribution.yaml
|
|-- majordomus generate --target distribution
| |-- docs/generated/distribution-matrix.json the release build matrix
| |-- site/static/install.sh the installer, with its platform table
| |-- docs/INSTALL.md the guide, with its platform table
| |-- site/data/registry/distribution.json what the website renders
| `-- site/static/releases/*.json from .ai/repo/releases/*.yaml
|
|-- the `distribution` capability module CLI, HTTP, OpenAPI, MCP, cockpit
`-- scripts/release-* packaging, verification, recording
Nic z toho seznamu neuvádí vlastní platformu, název souboru ani URL. Skripty se ptají majordomus distribution artifact na jméno, místo aby ho skládaly; workflow releasu čte generovanou matici, místo aby vypisoval runnery; tabulka platforem instalátoru je generovaná oblast uvnitř ručně psaného skriptu; tabulka platforem v dokumentaci je generována do ručně psané příručky.
majordomus generate --check — vyvolané příkazem just derive-check, úlohou site a první fází release pipeline — odmítne strom, v němž se kterékoli z nich rozchází.
Přidání platformy
Jedna úprava, pak jeden příkaz:
$EDITOR share/distribution.yaml # add a target: id, os, arch, libc, rust_target, status, build
just derive # regenerate every projection
bash test/run.sh 84_distribution_model
Instalátor to nabízí, release to sestaví na runneru, který model pojmenoval, průvodce to vypisuje, web to vypisuje, matice to nese a invariant úplnosti nyní vyžaduje artefakt pro to v každém budoucím release. Nic dalšího se needituje.
Skutečně nový koncept — jiný operační systém, jiný formát archivu, jiná libc — je jediný případ, který mění také Rust: slovníky jsou enumy v apps/majordomus-cli/src/distribution/mod.rs, aby nemohl být zapsán nemožný cíl. Přidání hodnoty tam je záměrné rozšíření modelu, nikoli synchronizační povinnost.
Jedna verze a jedna její projekce
Release má jednu verzi a je napsána na jednom místě: [package] version manifestu crate. Shellový nástroj tento soubor tam, kde je nainstalován, přečíst nemůže — nainstalovaný strom nemá Cargo.toml — proto čte jeho projekci, která se dodává vedle něj:
apps/majordomus-cli/Cargo.toml the version — authored here, and only here
share/version.txt its projection, generated by `majordomus generate`;
`majordomus version` prints its `version=` line
Každý archiv nese share/ celý, takže checkout, nainstalovaný strom i přípravný adresář, který instalátor kontroluje, čtou verzi stejně. generate --check odmítne projekci, která zaostala za manifestem; scripts/release-version --check prokáže, že nástroj vypisuje autoritu, v CI a znovu jako první věc, kterou release dělá — spolu s tagem, který musí být v následované touto verzí. Překlep v tagu stojí třicet sekund místo publikovaného releasu. ADR 0085 je rozhodnutí a project.release-is-a-projection pravidlo.
Cesta důvěry
share/distribution.yaml the model: platforms, names, the installer's URL
|
v
release build (one runner per target, the model names which)
|
v
release-verify the archive is what the model says it is
|
v
release-record digests and sizes read off the files that exist
|
v
.ai/repo/releases/<tag>.yaml the repository's evidence of what was published
|
v
majordomus generate site/static/releases/<tag>.json and latest.json
|
v
GitHub Pages the metadata the installer reads, over HTTPS
|
v
install.sh verifies the digest before unpacking anything
|
v
~/.local/share/majordomus/versions/<version>/, then the launcher, by rename
Každou šipku kontroluje krok po ní. Instalátor nikdy nečte API ani HTML GitHubu; čte dva dokumenty JSON, které tento repozitář generuje, takže změna stránek GitHubu instalaci nerozbije.
Co je chráněno a čemu se důvěřuje
Protected:
- transport — jen HTTPS, přesměrování omezená na HTTPS; základ metadat bez HTTPS se odmítne, pokud to dvě explicitní proměnné prostředí neřeknou jinak, což existuje, aby vlastní testy instalátoru mohly servírovat fixtures;
- integrity — každý artefakt nese v metadatech SHA-256 a velikost, obojí zkontrolované před rozbalením; neshoda stažení zahodí a zastaví, bez „varovat a pokračovat“;
- původ URL — URL artefaktu musí být předpona stahování release tohoto projektu, odvozená z modelu, a kontroluje se dříve, než se z ní cokoli stáhne;
- tvar každé hodnoty — každé pole přečtené z metadat se před použitím porovná s literální znakovou třídou, takže nic přečtené po síti se nemůže dostat do příkazové řádky, cesty ani shellu jinak než jako data;
- extrakce — absolutní cesty, traversal přes
.., symbolické a pevné odkazy, zařízení a jakákoli položka mimo vlastní adresář archivu se odmítají jménem a nic se nerozbalí; - samotný spustitelný soubor — rozbalený nástroj se jednou spustí z dočasného umístění a musí ohlásit verzi, která byla vyřešena; to zachytí release sestavený ze špatného stromu;
- stávající instalace — nový strom se rozbalí vedle starého a launcher se nahradí přejmenováním, takže jakékoli selhání před tím ponechá předchozí instalaci přesně tak, jak byla;
- privilege —
sudose nevyskytuje nikde; nezapisovatelný cíl se pojmenuje, nezvyšují se oprávnění.
Důvěryhodné: GitHub Pages obsluhující metadata, GitHub Releases obsluhující artefakty a pipeline, která vytvořila obojí. Kontrolní součty váží artefakt k jeho metadatům; samy o sobě neprokazují, kdo metadata napsal.
Provenience, kde ji lze ověřit. Workflow release atestuje každý archiv, který publikuje (atestace artefaktů GitHub, podepsané identitou běhu workflow), takže archiv lze vysledovat k workflow release tohoto repozitáře na jeho tagu. Atestace se hledá podle digestu archivu, což znamená, že GitHub stejně odpoví na release publikovaný před zavedením atestací i na archiv, který není tím, který workflow sestavil: žádná atestace pro ten digest. Co která cesta ověřuje:
| path | kontrolní součet (SHA-256 a velikost) | provenance (atestace) |
|---|---|---|
curl … | sh, nainstalované a přihlášené GitHub CLI | vždy, při neshodě fatální | dotázáno; odmítnutí se ohlásí a instalace pokračuje na kontrolním součtu |
totéž, s MAJORDOMUS_REQUIRE_PROVENANCE=1 | vždy, při neshodě fatální | vyžadováno; cokoli kromě ověřeného atestu nenainstaluje nic |
curl … | sh bez GitHub CLI, nebo odhlášen | vždy, při neshodě fatální | nezkontrolováno a závěrečná zpráva to říká |
| testovací origin v prostém HTTP (vlastní testy instalátoru) | vždy, při neshodě fatální | nezkontrolováno: žádná atestace nepopisuje fixture |
ručně: gh attestation verify <archive> --repo korczis/prismatic-majordomus | SHA256SUMS vedle releasu | ano |
Závěrečný report instalátoru jmenuje, co ověřil pod Verified: — provenance (…) and checksum, nebo checksum only — provenance not verified: <why>. Provenience ve výchozím stavu není fatální, protože odmítnutí nelze odlišit od staršího release; na požádání se stává fatální. Netvrdí se: podpis nad samotnými metadaty ani jakákoli kontrola release starších než jejich atestace.
Vydávání
# 1. raise the version in the one place it is authored — the measured minimum unless told
# otherwise — and derive everything that follows from it
bin/majordomus-cli release bump
scripts/derive # builds, then projects share/version.txt and every stamp
scripts/release-version --check --tag v0.3.0
# 2. commit, open the pull request, and merge it once `ci` is green
# 3. the commit to be tagged passed ci — the same question the pipeline asks first
scripts/ci/release-verdict --commit HEAD# 4. tag the merge on the default branch, and push the tag
git tag v0.3.0 && git push origin v0.3.0
# 5. the pipeline publishes, puts releases/v0.3.0.json and releases/latest.json on the
# site at once (ADR 0131), then proposes the record as release/record-v0.3.0; confirm
# with curl -fsSL https://majordomus.dev/releases/latest.json
# 6. start the record pull request's own run, which the pipeline cannot: approve its
# waiting validate run, or close and reopen it (majordomus prs drain reopens it)
# 7. merge the record's pull request once that run's ci is green; Pages then deploys master
# with the record, and scripts/pages verify --commit <merge sha> confirms it
Tag je první a záznam druhý a okno mezi nimi je čekající, ne červené. Záznam nelze zapsat před tagem: vzniká z toho, co bylo publikováno — názvů a SHA-256 digestů archivů, stránky release — a nic z toho neexistuje, dokud neproběhne pipeline, kterou tag spouští. Záznam se tedy navrhuje po publikaci, na release/record-<tag>, a jeho pull request potřebuje před sloučením vlastní plný běh, zhruba dvě hodiny. scripts/ci/release-check odmítne publikovaný tag, pro který vrstva nedrží záznam, a do I2054 jej odmítal na každém headu v tom okně: 2026-10-08 zčervenaly čtyři pull requesty o jiných věcech kvůli tomuto jedinému nálezu, zatímco běžel záznam v0.17.0, a 2026-10-14 zčervenala každá větev mezi tagem v0.25.0 a sloučením jeho záznamu. Nyní čte návrh z gitu, jak jej už má úloha structure (fetch-depth: 0 stahuje každou větev jako refs/remotes/origin/*): tag, jehož záznam nese špička větve release/record-<tag> mladší než MJ_RELEASE_RECORD_GRACE_HOURS (ve výchozím stavu 24), se hlásí jako pending s uvedením té větve a projde. Co to stojí: kontrola důvěřuje větvi, ne sloučenému záznamu, až den — větev záznamu, jejíž pull request byl zavřen, omlouvá svůj tag, dokud neuplyne lhůta — a checkout, který nestáhl žádné větve, žádný návrh nevidí a odmítne jako dřív. Tag bez záznamu a bez větve, která by jej navrhovala, stav v0.7.0, se odmítne okamžitě na každém headu. Navrhnout záznam před tagem byla druhá možnost a nezvolila se: záznam by popisoval artefakty, které ještě nikdo nesestavil. test/cases/3021_a_tag_does_not_freeze_the_queue.sh drží každý stav.
Okno před existencí větve záznamu je také čekající (I2712). Mezi pushem tagu a pushem release/record-<tag> publikačním jobem není žádný záznam ani větev a zatím se nic ani nepublikuje: 2026-10-14 structure job z #1146 zčervenal na v0.26.0, zatímco jeho release běh ještě čekal na build pro macOS. Tento stav vidí jen forge, takže pro tag bez záznamu i návrhu se kontrola ptá gh na běhy tagu v release.yml: běh ve frontě nebo běžící učiní tag pending se jmenováním běhu; žádný uvedený běh a zatím žádný release na GitHubu v téže odkladné lhůtě je také čekající. Ukončený běh, existující release na GitHubu, forge, který nelze číst, a repozitář, který nelze pojmenovat (MJ_RELEASE_CHECK_REPO, jinak remote origin na github.com) nechají odmítnutí v platnosti, s poznámkou, který z nich — offline je kontrola stejně přísná jako dřív. test/cases/3029_a_tag_in_flight_is_pending.sh tyto stavy drží.
Pipeline je .github/workflows/release.yml, adaptér nad .ai/repo/ci/release.yaml a generovanou maticí:
plan the tag equals the version the crate manifest declares and the shell tool prints
it; the tagged commit's `ci` check concluded success (scripts/ci/release-verdict:
failure refuses with 10, a run still in progress after the wait or a verdict that
cannot be read with 12); the model and every recorded release hold their
invariants; the matrix is emitted
build one archive per supported target, on the runner the model names, verified where
it was built (fail-fast: a release missing a platform is not a release)
publish digests, the GitHub release, the record written from what was uploaded and staged,
every projection derived from it and judged by scripts/derive-check, then both
proposed to the default branch as one pull request, release/record-<tag>
smoke scripts/ci/install-check --expect <tag>: the published installer, from its published
URL, installs the release that was just published, and the installed tool
initialises and serves a repository that is not this one, on every runner whose
target it was built for; no step of it swallows a failure
Jen publish má contents: write a s ním zápisy id-token a attestations, které atestují jeho archivy. Nic jiného v běhu nemůže nic zapsat; plan čte check-runs (checks: read) a nic víc.
Release následuje verdikt. v0.3.1, v0.5.0, v0.6.0 a v0.7.0 byly publikovány z commitů, jejichž check-run ci skončil selháním, protože pipeline se ptala jen, zda tag souhlasí s verzí. Nejnovější check-run ci otagovaného commitu je teď první věc, kterou plan po verzi čte, a nic se nesestaví z commitu, o kterém nevidělo, že prošel. test/cases/362_a_release_follows_the_ci_verdict.sh zavazuje skript k úspěchu, selhání, běžícímu běhu, chybějící kontrole a nečitelné odpovědi a workflow k tomu, aby se ho ptalo.
Štítek runneru, který model jmenuje, musí být standardní, aktuálně nabízený štítek hostovaný GitHubem. Není to stylové pravidlo. Vyřazený štítek nehlásí selhání: úloha se přijme a čeká ve frontě na runner, který nikdy nedorazí, takže běh ani nepublikuje, ani nezčervená — prostě nikdy neskončí a fail-fast ho zruší, když selže sourozenec, takže vypadá jako vedlejší oběť, ne jako příčina. macos-13 tak vydržel dva pokusy o release. Seznam je GitHubu, na actions/runner-images; přípona -large nebo -xlarge znamená placený větší runner místo standardního.
Běh, který selže v build, nic nepublikuje, což je zamýšlené chování a zároveň to, od kterého se snadno odejde: tag stále existuje, ukazuje na commit, na kterém build selhal, a za ním není žádný release. Oprava příčiny na výchozí větvi tag neopraví. Dokončete release — nová verze, navýšená a otagovaná, je běžná cesta; přesunutí tagu, za nímž není release, je ta druhá, a jen dokud ho nikdo nemohl připnout. Dokud se jedno z toho nestane, inzerovaný instalační příkaz je pro všechny rozbitý a installer-live níže to říká.
Předběžná verze není nikdy stabilní release
Tag s pre-release částí (v1.0.0-alpha.1) se publikuje jako GitHub prerelease a nikdy jako „latest“ (--prerelease --latest=false v kroku publish v release.yml) a scripts/release-record to zaznamená na channel: prerelease: kanál se odvodí z tagu, pokud není zadáno --channel, a --channel stable na tagu s příponou skončí exit kódem 2. Rozhoduje i verze, nejen záznam: Release::is_stable_candidate je nepravda pro každou verzi s pre-release částí, takže ani chybně označený záznam se nemůže stát releases/latest.json a Release::findings chybné označení ohlásí. Pre-release se instaluje jeho pojmenováním (install.sh --version 1.0.0-alpha.1); nepřipnutý instalátor a uzel, který sleduje stable, dál rozhodují nejvyšší stabilní release. Verze se řadí podle precedence sémantického verzování (alpha.2 před alpha.10, pre-release před vlastním finálem, build metadata se ignorují). Toto pořadí je pořadí release::version::Version, jediné čtení verze v crate: stabilní ukazatel, zapisovač releasu a kontrola kompatibility ho sdílejí, takže nemohou řadit dvojici verzí různě, a záznam, jehož verze vůbec není sémantická verze, také nikdy není stabilním ukazatelem. release bump --exact 1.0.0-alpha.1 zapíše pre-release a kontrola přijme strom, který nějaký deklaruje; docs/RELEASE.md říká, jak se pre-release potkává s dolní mezí. Testy crate distribution::tests a release::version dokládají rozlišení a pořadí; test/cases/3179_a_prerelease_is_never_published_as_stable.sh dokládá dva zapisovače na straně shellu a test/cases/3212_a_prerelease_version_is_bumped_and_ordered.sh zapisovač, kontrolu a pořadí proti skutečnému repozitáři.
Když release selže v půlce
Release mění věci, které svět vidí, a může selhat poté, co se některé změnily. Opakované spuštění workflow pro stejný tag je podporované zotavení a je bezpečné v každé fázi. Co „bezpečné“ znamená, je deklarováno v .ai/repo/ci/release.yaml pod rerun: a implementováno v úloze publikace:
- Assety existujícího releasu jsou ten release. Pokud tag už GitHub release má, opakovaný běh stáhne jeho publikované assety a zahodí vlastní přestavbu. Záznam je důkazem toho, co uživatel stahuje, a přestavby nejsou bit po bitu totožné (viz Reprodukovatelnost níže), takže zaznamenat bajty tohoto běhu by uvedlo digesty, které nic neobsluhuje.
- commit metadat je idempotentní. Opakovaný běh, který najde záznam a jeho projekce už commitnuté, nic nepushne a uspěje.
- smoke se nikdy nepřeskakuje. Obě cesty končí stejným veřejným instalačním testem, takže opakovaný běh dokazuje stejný vnější kontrakt jako první.
Fáze po fázi:
| Selhalo v | Viditelné zvenčí? | Znovu spustit | Úklid |
|---|---|---|---|
plan | ne | ano | žádné — nebyl sestaven žádný artefakt |
build | ne | ano | žádné — nic nebylo nahráno |
publish, než release existuje | ne | ano | žádné |
publish, poté co release existuje | ano — release a jeho assety | ano; opakovaný běh tyto assety převezme | žádné |
publish, po commitu s metadaty | ano — záznam je na výchozí větvi | ano; krok commit nenajde nic k přidání | žádné |
pages | ano — záznam je potvrzen, ale neservírován | gh workflow run pages.yml --ref master | žádné |
smoke, dokud je záznam návrhem | ano — release, nikoli jeho metadata | spusťte vlastní běh pull requestu se záznamem — majordomus prs drain ho zavře a znovu otevře; ručně schvalte jeho čekající běh validate, nebo gh pr close <n> a pak gh pr reopen <n> — mergněte ho, jakmile je ci onoho běhu zelené, pak znovu spusťte smoke; ci dispatchnutého běhu mu merge nedovolí | žádné |
smoke | ano — vše je publikováno | opravte příčinu a spusťte znovu | žádný; release platí, nebo je níže stažen |
Kdo zahajuje verdikt záznamu
Záznam se navrhuje jako pull request a ten se otevírá pomocí GITHUB_TOKEN. GitHub nespouští žádný workflow pro událost, kterou způsobil tento token, takže pull request záznamu nemá vlastní běh, dokud ho někdo nespustí. Běh, který zaznamenává jeho událost pull_request, měl dva tvary: přes v0.26.0 (2026-10-14) skončil neúspěchem bez jediné úlohy; na v0.27.0 (2026-10-16, běh 38865305333) byl vytvořen action_required, čekající na schválení. V obou případech vyžadovaná kontrola ci nikdy nehlásí a návrh nelze sloučit. Po v0.15.0 a po v0.16.0 tak zůstal, s prázdným souhrnem kontrol, dokud ho někdo nezavřel a znovu neotevřel, a smoke po celou tu dobu selhávala záměrně.
Pipeline ten běh nemůže spustit a netvrdí, že může. Spouští jej člověk, jedním ze dvou způsobů: schválením čekajícího běhu validate, nebo zavřením a znovuotevřením pull requestu. To druhé dělá integrační executor: majordomus prs drain zavře a znovu otevře pull request release/record-<tag>, který otevřel token workflow a který nemá vlastní kontrolu, jednou na head, a běh, který jeho znovuotevření spustí, je ten, na který merge čeká (INTEGRATION.md, „The record pull request it reopens“; test/cases/1036_the_executor_reopens_a_record_pull_request.sh). Bez běžícího drainu se obojí dělá ručně: schvalte běh na jeho stránce, nebo gh pr close <n> a pak gh pr reopen <n>. Záznam se pak sloučí na ci toho běhu, bez admin merge.
Po navržení záznamu pipeline také odešle dispatch validate na větev záznamu se vstupem plan=change a validate ten běh plánuje tak, jak bude plánován vlastní běh pull requestu: kontroly změny head commitu, bez runnerů na vyžádání, které dispatch jinak žádá. Ten běh není verdiktem záznamu. Jeho check-runy dopadají na head commit záznamu, ale ochrana větve a souhrn kontrol pull requestu nečtou žádnou check suite, kterou dispatch vytvořil. v0.19.1 (#866) a v0.27.0 (#1273) byly odmítnuty s base branch policy prohibits, zatímco odeslaný ci stál na jejich headech zeleně, a souhrny v0.25.0 (#1142) a v0.26.0 (#1147) nevypisují žádnou kontrolu, ačkoli jejich odeslaný ci prošel (I2282). Dispatch dává včasné čtení stromu záznamu: červený záznam je znám dřív, než někdo spustí běh, který se počítá, a cesta releasu pro něj drží runnery (CI.md). Dispatch, který nelze spustit, je varování jmenující nápravu a úlohu neshodí, protože release je publikován tak či tak a smoke musí stále běžet, aby to řekl. test/cases/996_the_record_pull_request_gets_its_verdict.sh drží oba workflow u tohoto a workflow i tuto sekci u toho, že dispatch nenazývají ničím víc.
Dvě nápravy by krok člověka odstranily a žádná se nepoužívá. Stav nebo kontrola, kterou spuštěný běh zapsal pod jménem ci, by splnila povinný kontext, aniž by ho vytvořil běh samotného pull requestu, což je padělaný verdikt, ne náprava. Otevření pull requestu, nebo jeho zavření a znovuotevření, pomocí GitHub App nebo jemně granulárního tokenu, jehož události spouštějí workflow, by vlastní běh spustilo okamžitě; to vyžaduje přihlašovací údaj, který může vytvořit a držet jen vlastník repozitáře, a je to rozhodnutí vlastníka. Dokud nepadne, stojí každý release mezi publikací a dosažením jeho záznamu do masteru jeden akt jednoho člověka.
Web servíruje release dříve, než přistane jeho záznam
Instalátor čte releases/latest.json a releases/<tag>.json, obě projekce záznamu release. Záznam se dostane do masteru pull requestem, jehož ci běžel 106 minut pro v0.19.1, a 2026-10-10 byl v0.20.0 publikován na GitHubu v 16:38Z a web ho servíroval v 17:42Z, poté co byl záznam ručně slit. Publikační job proto, jakmile je záznam a jeho projekce odvozen a posouzen, spustí scripts/site-deploy --release-metadata <tag>: ty dva soubory se dostanou na to, co servíruje gh-pages, a nic jiného. build.json dál jmenuje commit masteru, z něhož byl web sestaven, takže kontrola pages-live stále čte master (ADR 0131).
Dokud záznam nepřistane, nasazení z masteru nesou latest.json předchozího releasu. Běžné nasazení proto zachová latest.json, které servíruje gh-pages, a soubor tagu, který jmenuje, když jmenuje novější verzi, než je sestavení; řekne to ve svém výstupu a v commitu gh-pages. --release-metadata odmítne tag, který metadata nejmenují, latest.json, které se liší od souboru tagu, a release starší, než je servírovaný: web se nikdy nevrací o release zpět. test/cases/1041_a_release_publishes_its_metadata.sh to všechno drží.
Stránky sledují metadata, ne záznam. Do 2026-10-12 tomu tak nebylo: latest.json pojmenovávala v0.21.0 od 10:23Z, zatímco úvodní stránka vedle instalačního příkazu nabízela v0.20.0, protože každá stránka četla záznam release a ten byl stále pull requestem. Release, který stránka nabízí, se nyní odvozuje jednou, pomocí scripts/site-build, jako novější z nejnovějšího záznamu a releases/latest.json, který servíruje gh-pages (čteno z git ref, pojmenovaného v /build.json jako release.published_from), a každá stránka čte tuto jedinou hodnotu. Takže build, o který publikační úloha níže žádá, je ten, který posune úvodní stránku, minuty po archivech místo hodin. scripts/pages release-live drží úvodní stránku, latest.json a nejnovější release na GitHubu na jedné verzi po deploy.release_window, po každém nasazení a v kontrole pages-live (project.the-site-offers-the-release-it-published, test/cases/1087_the_site_offers_the_release_it_published.sh).
Proč release žádá Pages o publikaci
Úloha publikace končí spuštěním gh workflow run pages.yml a tento krok není pojistka navíc: bez něj se release strukturálně nemůže dostat k uživateli.
GitHub nespustí workflow z pushe provedeného s GITHUB_TOKEN. Je to záměrný přerušovač smyčky a pro token jej nelze vypnout. Commit, který vytvoří úloha publish — jediná věc, která nese site/static/releases/latest.json — proto nespustí žádný běh pages, takže web dál poskytuje sestavení z doby před existencí release a smoke fáze čeká patnáct minut na soubor, který nikdo nezapíše.
Release v0.3.1 strávil celou svou fázi smoke přesně v tomto stavu: šest zelených buildů, publikovaný GitHub Release, commitnutý záznam a latest.json vracející 404 každému, kdo spustil inzerovaný příkaz. Každé pozorovatelné ukazovalo na packer a packer byl v pořádku; poslední článek řetězu prostě neexistoval. Dispatch je ten článek. Běží s if: always(), protože opakovaný běh po neúspěšném deployi je přesně případ, kdy není co commitovat a je co publikovat.
workflow_dispatch je vstupní bod, který člověk používá, takže toto spouští jedinou cestu deploye, a nepřidává druhou (viz záruka site-deploy-one-path).
Jediný případ, který opakované spuštění nenapraví, je release, který existuje a nepublikuje žádný archiv — assety nelze z tagu rekonstruovat. Běh se zastaví a jmenuje příkaz, který uvolní cestu:
gh release delete v0.3.0 --yes # then rerun the workflow
Nic z toho nevyžaduje přesun tagu. Tag ukazující na špatný commit je jiný problém a odpovědí na něj je nová verze, ne přesunutý tag.
Vyzkoušení bez publikace
scripts/release-fixture --out /tmp/rel --base-url http://127.0.0.1:8099
(cd /tmp/rel && python3 -m http.server 8099 --bind 127.0.0.1) &
MAJORDOMUS_RELEASE_BASE_URL=http://127.0.0.1:8099 MAJORDOMUS_INSECURE_BASE_URL=1 \
sh site/static/install.sh --dry-run
To je to, co dělá test/cases/85_installer.sh, a je to celý stack: skutečný archiv sestavený ze stromu, ve kterém jste, skutečná metadata vykreslená rendererem, který web publikuje, skutečné HTTP stažení, skutečná kontrola digestu, skutečná atomická instalace.
Prokázání, že inzerovaný příkaz stále funguje
Release pipeline prokazuje, že inzerovaný příkaz funguje, jednou, ve své smoke fázi, v okamžiku publikace. To není stejný slib jako ten, který dává titulní stránka, ten je v přítomném čase, a rozdíl byl skutečný: build, který selhal na jedné platformě, nepublikuje nic, značka zůstane, kde je, oprava přistane na výchozí větvi a už se nikdy neoznačí — a URL, na kterou odkazuje každý dokument, dál servíruje instalátor, který nerozřeší žádný release. Každá kontrola nad stromem zůstala po celou dobu zelená, protože žádná z nich publikovaný web nevidí.
scripts/ci/install-check je kontrola, která to dokáže. Čte adresy z site/data/registry/distribution.json — nemá žádné vlastní stavy — a pak z publikovaného webu:
1 the metadata the installer resolves is served, and names a release
2 the installer is served, and this machine's /bin/sh parses it
3 the advertised line, run as it is written, pipe included, into a home of its own
4 the installed tool reports the version the metadata resolved
5 the installed MCP launcher runs with MAJORDOMUS_NO_BUILD=1 — an archive that left a
launcher out passes every check that only reads the archive's file list, and fails here
6 the installed tool initialises a repository that has none
7 and serves it: the installed MCP launcher, started there without --standalone, answers
initialize and tools/list over stdio and elects itself that repository's shared server,
which reports itself ready and names that repository — its identity is the digest of
the repository's root, so a server that answered for this checkout instead is refused
Release smoke spouští stejnou kontrolu nad tagem, který publikoval (--expect <tag> odmítne metadata jmenující jakýkoli jiný release), takže slib měří jedna kontrola při publikaci, každou noc a na požádání. Případ 1024 (test/cases/1024_an_installed_release_serves_a_foreign_repository.sh) dokazuje, že selže na archivu, jehož launcher MCP nemůže nastartovat.
Mimo svůj dočasný strom se nezapisuje nic: instalace jde do HOME vlastního pro tento běh, takže prefix, launchery i nápověda PATH skončí uvnitř.
CI to spouští jako --wait 300. Publikace release a nasazení webu jsou dva workflow a druhý není okamžitý, takže push, který přistane mezi nimi, by se dozvěděl, že slib je porušen, ačkoli je jen několik minut starý. Spuštěno ručně je čekání nulové, protože člověk, který se ptá, zda příkaz funguje, chce odpověď hned. Web, který nedokáže obsloužit metadata v rámci okna, je rozbitý tak či onak a zjištění platí.
Je to kontrola installer-live v .ai/repo/ci/gates.yaml, job install, na Linuxu a macOS. Záměrně ji nevybírá žádná třída cest: žádná změna stromu ji nemůže učinit pravdivou ani nepravdivou — jen nasazení. Je také jednou z kontrol modelu na požádání, protože polovina její matice je runner macOS a tento repozitář na něj čeká hodiny: noční rozvrh, dispatch a pull request s labelem ci:full ji plánují, běžný push ne. Rozbitý inzerovaný příkaz tak zčervená noční běh do dne od nasazení, které jej rozbilo, což je jediná podmínka, za níž to kdo zjistí. Chcete-li se zeptat hned — třeba po release — spusťte workflow dispatchem, nebo zde spusťte scripts/ci/install-check.
Proti lokální fixture místo publikovaného webu:
scripts/release-fixture --out /tmp/rel --base-url http://127.0.0.1:8099
(cd /tmp/rel && python3 -m http.server 8099 --bind 127.0.0.1) &
scripts/ci/install-check --base http://127.0.0.1:8099Zotavení ze špatného release
Publikovaný release je neměnný: jeho artefakty a jeho verzovaná metadata zůstávají přesně tak, jak byly, protože je někdo mohl připnout. Stažení jednoho je úprava jeho záznamu:
# .ai/repo/releases/v0.3.0.yaml
yanked: true
poté just derive a commit. latest.json se odvozuje při každém generování jako nejvyšší verze mezi stabilními, nestaženými záznamy, takže ukazatel na stabilní se vrátí sám a --version v0.3.0 se stále rozřeší pro každého, kdo chce přesně tu verzi. Nic se nemaže a žádné URL nepřestane fungovat. Web následuje stejné odvození: čtení release, který záznam nabízí, je latest v site/data/registry/distribution.json, nikdy nejvyšší název souboru záznamu, takže stažený nebo prerelease záznam není tím, co stránky představují, a scripts/site-check odmítne sestavení, jehož odznaky se liší od onoho jednoho nabízeného release.
Reprodukovatelnost
Vstupy artefaktu se zaznamenávají, nikoli tvrdí: tag, cílový triple, commit a čas buildu jdou do RELEASE.json uvnitř každého archivu a commit je do spustitelného souboru zkompilován pomocí build.rs. Samotný archiv je zabalen se záznamy v seřazeném pořadí.
Každý archiv obsahuje každou cestu právě jednou a nic kromě běžných souborů a adresářů: žádný pevný odkaz, žádný symlink, žádný souborový uzel zařízení. scripts/release-package to ověří u právě zapsaného archivu a raději jej smaže, než by vrátil porušující, a scripts/release-verify to ověří znovu u předaného archivu. Obojí existuje, protože release v0.2.0 nebyl nikdy publikován: packer předal úplný seznam souborů nástroji tar, který navíc rekurzivně procházel, každá cesta se archivovala dvakrát, GNU tar zapsal druhou kopii každé jako pevný odkaz a verifikátor — správně — odmítl všech 931. V packeru záměrně není žádný fallback: archiv zabalený jinak, než bylo požadováno, není archiv, který cokoli ověřilo. test/cases/87b_release_archive_shape.sh reprodukuje tento packer pomocí shimu tar a dokazuje, že ho kontrola zastaví.
Bitově shodné rebuildy se nenárokují: to vyžaduje reprodukovatelné vyvolání kompilátoru (SOURCE_DATE_EPOCH, připnutou verzi toolchainu, žádné absolutní cesty v ladicích informacích) a toolchain je zde stable, ne pin. Zaručeno je, že dva lidé, kteří sestaví stejný tag, dostanou funkčně shodné archivy a že archiv říká, z jakého commitu a cíle pochází.
Samoaktualizace
Vlastník rozhodl 2026-10-14, že uzel meshe tohoto repozitáře sleduje publikovaný release sám (ADR 0161). Dokud nebudou I2720-I2722 hotové, relace držící roli release ručně upgraduje každý zapsaný uzel, jakmile releases/latest.json pojmenuje novou verzi, a zpětně si přečte verzi každého uzlu. Níže následuje návrh, který tyto úkoly staví.
Jedna implementace rizikové části. majordomus self update znovu spustí publikovaný instalátor s --version <tag> a vlastním prefixem instalace; neimplementuje znovu stahování, ověření ani výměnu. --check hlásí běžící verzi, verzi, kterou jmenuje stabilní ukazatel, a zda se upgrade uplatní; --apply ji nainstaluje a přečte verzi zpět z nového launcheru. Vše, co řeší, pochází z toho, co je už typované:
Model::load(share) // this installation's own model, shipped in share/
Model::release_url(tag) // where the metadata for a tag or `latest` lives
Releases::latest_stable() // which release an unpinned update resolves to
Release::artifact(target) // the artifact, its digest and its size
crate::TARGET // the triple this build was made for, compiled in
Kdo sleduje. Každý stroj, který deklarace meshe zapisuje, včetně hubů, s follow: stable (výchozí pro zapsaný uzel meshe vlastníka), follow: pinned <version> nebo follow: off. Repozitář, který přijme Majordomus bez zapsání strojů do tohoto meshe, se netýká a při vstupu do repozitáře nic nestahuje.
Čemu uzel důvěřuje. Pouze releases/latest.json a releases/<tag>.json na webu a jen archivu, jehož digest a velikost jim odpovídají. Oznámení release v mesh znamená „zkontroluj teď“ a nic víc. Uzel bez cesty k webu (jetson) může převzít archiv od peera a ověří jej proti metadatům, která může zkontrolovat před rozbalením (I2724).
Jen dopředu, jen stabilní. Žádný downgrade, žádný pre-release, žádná verze, kterou metadata nevypisují. Připnutý uzel zůstává připnutý. Návrat zpět je install.sh --version operátora.
Instalace vedle, aktivace v pořadí, vrácení samo. Nový release jde do vlastního verzovaného stromu, zatímco starý server obsluhuje. Hub se pod správcem služeb restartuje na něj, když není připojena žádná relace nebo po omezené odkladné lhůtě (I2722). Sdílený server checkoutu předá své relace dál a skončí (I2127, I2163) a do té doby se nahradí při svém příštím nečinném konci. Kontrolou zdraví je route připravenosti plus živé kolo mesh verify. Pokud selže, launcher se přejmenuje zpět, předchozí verze dál obsluhuje a nástěnka jmenuje uzel, verzi a selhavší kontrolu.
Kdy kontroluje. Při startu serveru, každých 15 minut s jitterem a při oznámení release v mesh: jeden HTTPS požadavek na releases/latest.json, nikdy v cestě požadavku.
Kde se to ukazuje. Každý runtime hlásí na meshi svou verzi a nastavení sledování; majordomus mesh peers je vypíše a mesh doctor jmenuje uzel o více než jedno vydání pozadu (I2723). Release je nasazen, když jej hlásí každý zapsaný, sledující uzel.
test/cases/85_installer.sh dnes pokrývá rizikovou část a self update překládá releasy přes tytéž záznamy a týž stabilní ukazatel jako instalátor, až bude existovat: self update je výše uvedený návrh a zatím není implementován.
Správci balíčků
Model release je primární kontrakt a je neutrální vůči správcům balíčků: archiv na cíl s digestem a strojově čitelný index. Formule Homebrew, repozitář APT nebo derivace Nix by každá četly tento index, místo aby jej nahrazovaly, a žádná z nich není implementována. Nic v modelu to nebrání. Kontejnerový image je jediný existující spotřebitel: úloha image release workflow jej sestaví z linuxových archivů téhož release a pushne do kontejnerového registru GitHubu pod tagem release.
Windows je vyjádřitelný — schéma nese operační systém zip, binary_suffix a windows — a je deklarován unavailable s důvodem: příkaz, který člověk spouští, je bash, a dokud tato polovina nemá odpověď pro Windows, publikace archivu pro Windows by dodala exekutor, který nic nedokáže ovládat.
Kde co je
| Co | Kde |
|---|---|
| model | share/distribution.yaml |
| jeho kontrakt | share/schemas/majordomus/distribution/distribution.v1.schema.json |
| smlouva záznamu releasu | share/schemas/majordomus/release/release.v1.schema.json |
| záznamy | .ai/repo/releases/, jeden na publikovaný release |
| sémantika | apps/majordomus-cli/src/distribution/ |
| modul schopnosti | apps/majordomus-cli/src/capability/builtin/distribution.rs |
| chování instalátoru | share/install/install.sh.in |
| próza průvodce | share/install/INSTALL.md.in |
| balení a ověření | scripts/release-{version,package,verify,record,fixture} |
| model pipeline | .ai/repo/ci/release.yaml |
| pipeline | .github/workflows/release.yml |
| behaviorální případy | test/cases/84_distribution_model.sh, 85_installer.sh, 86_installer_platform.sh, 87_release_pipeline.sh |
| pravidlo | .ai/repo/rules/project/distribution-canonical.v1.md |