Skip to content

Commit 6933186

Browse files
feat(toolchain): make opsm.toml [runtime] the single source of truth (#67)
## Problem OPSM had **no single source of truth for its own toolchain versions — five sources disagreed**: | Source | Erlang | Elixir | Notes | |---|---|---|---| | `.tool-versions` (what actually builds it) | 28.3.1 | 1.19.5-otp-28 | + vestigial nodejs 25.6.1 | | `opsm.toml [runtime]` (read by `opsm runtime install`) | 26.2.0 | 1.16.0 | stale | | `.gitlab-ci.yml` | `elixir:latest` | `rust:latest` | non-reproducible | | `e2e.yml` + `nif-build.yml` | 27.0 | 1.17 | hardcoded | | `trust-pipeline-e2e.yml` | 26.2 | 1.16.0 | hardcoded | Plus: **Nickel** heavily used (`runtime/**/*.ncl`) but pinned nowhere, **Idris2** (a real build dep — `src/abi/*.idr`) unpinned, **Rust** floating on `stable`. ## Fix `opsm.toml [runtime]` is now canonical (dogfooding — `opsm runtime install` already consumes it). Everything else derives from it or is drift-checked against it: ``` opsm.toml [runtime] ── just toolchain-sync ──► .tool-versions ──► asdf/mise + setup-beam (version-file) └─ just toolchain-check (Mustfile-gated) ──► verifies .tool-versions + GitLab image pins ``` - **opsm.toml**: corrected stale pins (erlang → 28.3.1, elixir → 1.19.5-otp-28); completed the set (rust 1.97.0, nickel 1.16.0, idris2 0.8.0); dropped the vestigial nodejs pin (no package.json, nothing invokes node — Deno is the sole JS runtime) - **scripts/toolchain.sh** (new, POSIX sh): `sync` regenerates `.tool-versions`; `check` fails loudly on any drift, incl. GitLab image tags. Wired as `just toolchain-sync` / `just toolchain-check` + Mustfile check - **`.tool-versions`**: now generated, with idris2 (no asdf plugin — pack/Chez route) and rust (asdf-global, cargo-audit conflict) excluded via documented skip-list. zig/deno/nickel provision via the estate's [`asdf-tool-plugins`](https://github.com/hyperpolymath/asdf-tool-plugins) - **GitLab CI**: `elixir:1.19.5-otp-28` + `rust:1.97` (tags verified on Docker Hub); also fixed the invalid `//` comment on line 2 that broke YAML parsing. Scanner images (trivy/semgrep/trufflehog) deliberately float — fresh detection DBs beat cosmetic pinning - **GitHub workflows**: `setup-beam` now reads `.tool-versions` (`version-file` + `version-type: strict`) in e2e, nif-build, and trust-pipeline-e2e; rust steps pin `toolchain: 1.97.0` (nif-build, echidna-validation) - **`parse_runtime_section/1`** (`manager.ex`): hardened — comment lines containing `=` were mis-parsed as pins and inline comments corrupted versions; tests added - **docs/TOOLCHAIN.adoc** (new): derivation chain, per-tool provisioner table, the honest bootstrap story (OPSM is an Elixir escript — it cannot provision the BEAM for itself; asdf/mise/Guix sit underneath), and the Idris2 route (pack + Chez + GMP today; `guix.scm` is still a stub, documented as TODO rather than overclaimed) ## Verified - `sh scripts/toolchain.sh sync && sh scripts/toolchain.sh check` → green - Drift injections (`.tool-versions` version mutated; GitLab image reverted to `:latest`) → both exit 1 - All 5 edited YAML files parse cleanly - Elixir parser tests **not run locally** (no BEAM in this container) — CI runs them; watching this PR for fallout, especially `trust-pipeline-e2e` moving 26.2 → 28.3.1 (accepted risk per owner decision) ## Known follow-ups (out of scope here) - `guix.scm` is a stub (`source #f`) — growing it into a real manifest is recorded in STATE.a2ml next-actions - Handoff sections B/C (service deps wiring, solver stack) — separate work 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Kq24sZCEohSrNFXSuEuz6C --- _Generated by [Claude Code](https://claude.ai/code/session_01Kq24sZCEohSrNFXSuEuz6C)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f91eb34 commit 6933186

20 files changed

Lines changed: 712 additions & 347 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ jobs:
3838
- name: Setup Erlang/Elixir
3939
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
4040
with:
41-
otp-version: '27.0'
42-
elixir-version: '1.17'
41+
# Versions derive from opsm.toml [runtime] via .tool-versions
42+
# (generated by `just toolchain-sync` — see docs/TOOLCHAIN.adoc)
43+
version-file: '.tool-versions'
44+
version-type: 'strict'
4345
- name: Restore Mix cache
4446
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4
4547
with:
@@ -65,8 +67,10 @@ jobs:
6567
- name: Setup Erlang/Elixir
6668
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
6769
with:
68-
otp-version: '27.0'
69-
elixir-version: '1.17'
70+
# Versions derive from opsm.toml [runtime] via .tool-versions
71+
# (generated by `just toolchain-sync` — see docs/TOOLCHAIN.adoc)
72+
version-file: '.tool-versions'
73+
version-type: 'strict'
7074
- name: Restore Mix cache
7175
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4
7276
with:

.github/workflows/echidna-validation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ jobs:
4949
if: steps.detect.outputs.relevant == 'true'
5050
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
5151
with:
52+
# Pinned to the canonical rust version in opsm.toml [runtime]
53+
toolchain: '1.97.0'
5254
components: clippy
5355
- name: Cache Rust dependencies
5456
if: steps.detect.outputs.relevant == 'true'

.github/workflows/nif-build.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,20 @@ jobs:
2626
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2727
- name: Install Rust toolchain
2828
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
29+
with:
30+
# Pinned to the canonical rust version in opsm.toml [runtime]
31+
toolchain: '1.97.0'
2932
- name: Cache Rust build
3033
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
3134
with:
3235
workspaces: opsm_ex/native/opsm_pq_nif -> target
3336
- name: Setup Erlang/Elixir
3437
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
3538
with:
36-
otp-version: "27.0"
37-
elixir-version: "1.17"
39+
# Versions derive from opsm.toml [runtime] via .tool-versions
40+
# (generated by `just toolchain-sync` — see docs/TOOLCHAIN.adoc)
41+
version-file: '.tool-versions'
42+
version-type: 'strict'
3843
- name: Restore Mix cache
3944
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4
4045
with:

.github/workflows/trust-pipeline-e2e.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ jobs:
4242
- name: Install Elixir and OTP
4343
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
4444
with:
45-
otp-version: '26.2'
46-
elixir-version: '1.16.0'
45+
# Versions derive from opsm.toml [runtime] via .tool-versions
46+
# (generated by `just toolchain-sync` — see docs/TOOLCHAIN.adoc)
47+
version-file: '.tool-versions'
48+
version-type: 'strict'
4749
- name: Cache Mix dependencies
4850
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4
4951
with:

.gitlab-ci.yml

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# SPDX-License-Identifier: MPL-2.0
2-
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
2+
# Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
# Primary CI/CD - GitLab is the source of truth
4+
#
5+
# Toolchain images (elixir/rust) are pinned to the canonical versions in
6+
# opsm.toml [runtime] — drift is gated by `just toolchain-check`.
7+
# Scanner images (trivy/semgrep/trufflehog) deliberately float: their
8+
# detection databases need to stay fresh, so pinning them is worse.
49

510
stages:
611
- security
@@ -32,7 +37,7 @@ semgrep:
3237
allow_failure: true
3338
cargo-audit:
3439
stage: security
35-
image: rust:latest
40+
image: rust:1.97
3641
script:
3742
- cargo install cargo-audit
3843
- cargo audit
@@ -41,7 +46,7 @@ cargo-audit:
4146
- Cargo.toml
4247
cargo-deny:
4348
stage: security
44-
image: rust:latest
49+
image: rust:1.97
4550
script:
4651
- cargo install cargo-deny
4752
- cargo deny check
@@ -51,7 +56,7 @@ cargo-deny:
5156
allow_failure: true
5257
mix-audit:
5358
stage: security
54-
image: elixir:latest
59+
image: elixir:1.19.5-otp-28
5560
script:
5661
- mix local.hex --force
5762
- mix archive.install hex mix_audit --force
@@ -66,7 +71,7 @@ mix-audit:
6671
# ==================
6772
rustfmt:
6873
stage: lint
69-
image: rust:latest
74+
image: rust:1.97
7075
script:
7176
- rustup component add rustfmt
7277
- cargo fmt -- --check
@@ -75,7 +80,7 @@ rustfmt:
7580
- Cargo.toml
7681
clippy:
7782
stage: lint
78-
image: rust:latest
83+
image: rust:1.97
7984
script:
8085
- rustup component add clippy
8186
- cargo clippy -- -D warnings
@@ -85,15 +90,15 @@ clippy:
8590
allow_failure: true
8691
mix-format:
8792
stage: lint
88-
image: elixir:latest
93+
image: elixir:1.19.5-otp-28
8994
script:
9095
- mix format --check-formatted
9196
rules:
9297
- exists:
9398
- mix.exs
9499
credo:
95100
stage: lint
96-
image: elixir:latest
101+
image: elixir:1.19.5-otp-28
97102
script:
98103
- mix local.hex --force
99104
- mix deps.get
@@ -107,15 +112,15 @@ credo:
107112
# ==================
108113
cargo-test:
109114
stage: test
110-
image: rust:latest
115+
image: rust:1.97
111116
script:
112117
- cargo test --all-features
113118
rules:
114119
- exists:
115120
- Cargo.toml
116121
mix-test:
117122
stage: test
118-
image: elixir:latest
123+
image: elixir:1.19.5-otp-28
119124
script:
120125
- mix local.hex --force
121126
- mix deps.get
@@ -128,7 +133,7 @@ mix-test:
128133
# ==================
129134
cargo-build:
130135
stage: build
131-
image: rust:latest
136+
image: rust:1.97
132137
script:
133138
- cargo build --release
134139
artifacts:
@@ -140,7 +145,7 @@ cargo-build:
140145
- Cargo.toml
141146
mix-build:
142147
stage: build
143-
image: elixir:latest
148+
image: elixir:1.19.5-otp-28
144149
script:
145150
- mix local.hex --force
146151
- mix deps.get

.machine_readable/6a2/STATE.a2ml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# SPDX-License-Identifier: MPL-2.0
22
# STATE.a2ml — Project state checkpoint
3-
# Updated 2026-04-25f: CRG-B achieved — 6 external targets documented
3+
# Updated 2026-07-09: toolchain single source of truth (opsm.toml [runtime])
44

55
[metadata]
66
project = "odds-and-sods-package-manager"
77
version = "2.0.0"
8-
last-updated = "2026-04-25"
8+
last-updated = "2026-07-09"
99
status = "first-class system active"
1010

1111
[project-context]
@@ -169,5 +169,19 @@ evidence-file = "READINESS.md"
169169
- Health poll loop (180s deadline, 3s interval) before running tests
170170
- Full diagnostics (service logs) on failure; docker compose down always
171171

172+
[completed-2026-07-09]
173+
- Toolchain single source of truth established: opsm.toml [runtime] canonical ✅
174+
- Stale pins corrected (erlang 26.2.0→28.3.1, elixir 1.16.0→1.19.5-otp-28)
175+
- Pin set completed: rust 1.97.0, nickel 1.16.0 (was unpinned), idris2 0.8.0
176+
- Vestigial nodejs pin dropped — Deno is the sole JS runtime
177+
- .tool-versions now GENERATED (scripts/toolchain.sh; just toolchain-sync)
178+
- Drift gate: just toolchain-check, wired into Mustfile
179+
- GitLab CI images pinned off :latest; GitHub workflows read .tool-versions
180+
via setup-beam version-file (previously 3 disagreeing hardcoded sets)
181+
- [runtime] parser hardened: comment lines + inline comments (manager.ex)
182+
- Full story: docs/TOOLCHAIN.adoc (incl. bootstrap layering + Idris2/pack route)
183+
172184
[next-actions]
173185
- Grade A: requires external user confirmation outside hyperpolymath
186+
- guix.scm is a stub (source #f) — grow into real manifest (erlang, elixir,
187+
zig, just, nickel, idris2, chez-scheme, gmp) for reproducible bootstrap

.tool-versions

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
# SPDX-License-Identifier: MPL-2.0
2-
# Pinned runtime versions for OPSM
3-
# Managed via asdf (https://asdf-vm.com)
4-
elixir 1.19.5-otp-28
2+
# GENERATED FILE — do not edit by hand.
3+
# Source of truth: opsm.toml [runtime]. Regenerate with: just toolchain-sync
4+
# Drift is CI-gated by: just toolchain-check
5+
#
6+
# Plugin sources: erlang/elixir via the standard asdf plugins;
7+
# zig, deno, nickel via https://github.com/hyperpolymath/asdf-tool-plugins
8+
#
9+
# Canonical pins deliberately NOT listed here (see docs/TOOLCHAIN.adoc):
10+
# idris2 0.8.0 — no asdf plugin; provision via pack + Chez Scheme + GMP
11+
# rust 1.97.0 — asdf-global-managed to avoid cargo-audit conflicts; CI pins it
512
erlang 28.3.1
6-
# Note: Rust managed via asdf global (nightly). Not pinned here to avoid
7-
# conflicting with cargo-audit and other cargo subcommands.
8-
nodejs 25.6.1
13+
elixir 1.19.5-otp-28
14+
zig 0.14.0
15+
just 1.46.0
16+
deno 2.6.10
17+
nickel 1.16.0

Justfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ fmt-check:
107107
# Format all code
108108
fmt: fmt-ex fmt-deno
109109

110+
# ═══════════════════════════════════════════════════════════════════════════════
111+
# TOOLCHAIN (single source of truth: opsm.toml [runtime] — docs/TOOLCHAIN.adoc)
112+
# ═══════════════════════════════════════════════════════════════════════════════
113+
114+
# Regenerate .tool-versions from opsm.toml [runtime]
115+
toolchain-sync:
116+
sh scripts/toolchain.sh sync
117+
118+
# Verify .tool-versions and CI image pins match opsm.toml [runtime]
119+
toolchain-check:
120+
sh scripts/toolchain.sh check
121+
110122
# ═══════════════════════════════════════════════════════════════════════════════
111123
# RUN
112124
# ═══════════════════════════════════════════════════════════════════════════════

Mustfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ checks:
1111
run: just test
1212
- name: format
1313
run: just fmt
14+
- name: toolchain
15+
run: just toolchain-check
1416

0 commit comments

Comments
 (0)