Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fc6d7e0
security: add locked mise runtime config
eXPerience83 Jul 28, 2026
ecc8f22
security: commit mise runtime artifacts
eXPerience83 Jul 28, 2026
01fa193
security: validate mise runtime lock
eXPerience83 Jul 28, 2026
d6fae66
security: add mise lock regeneration helper
eXPerience83 Jul 28, 2026
76d33ee
security: install mise runtimes from lockfile
eXPerience83 Jul 28, 2026
1eb6c0c
security: validate locked mise build inputs
eXPerience83 Jul 28, 2026
ed21ab9
security: regenerate mise lock in upstream automation
eXPerience83 Jul 28, 2026
0a7a0f4
docs: explain locked runtime maintenance
eXPerience83 Jul 28, 2026
d9a78d2
docs: link runtime lock maintenance guide
eXPerience83 Jul 28, 2026
3e83778
docs: record locked mise runtime security
eXPerience83 Jul 28, 2026
6cda75a
security: reverify locked runtime provenance
eXPerience83 Jul 28, 2026
ca310fe
security: validate provenance re-verification
eXPerience83 Jul 28, 2026
662ab23
docs: explain locked provenance verification
eXPerience83 Jul 28, 2026
69832a0
security: disable unused mise experimental features
eXPerience83 Jul 28, 2026
2c5b3a5
ci: bound mise lock regeneration time
eXPerience83 Jul 28, 2026
63c2e49
security: guard mise cache cleanup path
eXPerience83 Jul 28, 2026
a0f66bb
refactor: split mise lock validation helpers
eXPerience83 Jul 28, 2026
15f929e
chore: include mise lock in CodeRabbit review
eXPerience83 Jul 28, 2026
d0bc259
fix: preserve full CodeRabbit review scope
eXPerience83 Jul 28, 2026
332c55e
security: harden mise lock schema validation
eXPerience83 Jul 28, 2026
c75e87d
test: cover adversarial mise lock inputs
eXPerience83 Jul 28, 2026
32d761e
test: run mise lock validation cases in CI
eXPerience83 Jul 28, 2026
850a560
chore: narrow CodeRabbit lockfile review override
eXPerience83 Jul 28, 2026
6a7e156
test: make mise lock mutations version-independent
eXPerience83 Jul 28, 2026
aad84e7
chore: keep review policy changes separate
eXPerience83 Jul 28, 2026
ab3c8a9
security: isolate mise lock regeneration inputs
eXPerience83 Jul 28, 2026
ceef08f
test: exercise isolated mise lock regeneration
eXPerience83 Jul 28, 2026
8efe75e
test: run isolated lock regeneration cases in CI
eXPerience83 Jul 28, 2026
93ab5c6
fix: validate mise regeneration timeouts
eXPerience83 Jul 28, 2026
f1b44eb
test: cover regeneration timeout validation
eXPerience83 Jul 28, 2026
db84742
security: require cross-platform lock coherence
eXPerience83 Jul 28, 2026
4087e71
test: cover cross-platform lock coherence
eXPerience83 Jul 28, 2026
42b70b8
security: serialize upstream update writes
eXPerience83 Jul 28, 2026
25e445f
docs: describe fail-closed lock validation
eXPerience83 Jul 28, 2026
c7da89f
fix: replace regenerated mise lock atomically
eXPerience83 Jul 28, 2026
a09af10
fix: publish edge for mise lock changes
eXPerience83 Jul 28, 2026
3537e4c
test: cover atomic mise lock replacement
eXPerience83 Jul 28, 2026
50b4269
test: require edge publication for lock inputs
eXPerience83 Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 63 additions & 10 deletions .github/workflows/check-upstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ permissions:
contents: write
pull-requests: write

concurrency:
group: check-upstream
cancel-in-progress: false

jobs:
check:
runs-on: ubuntu-latest
Expand All @@ -21,6 +25,13 @@ jobs:
with:
fetch-depth: 0

- name: Require the main branch
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "ERROR: upstream maintenance may only run from main; got $GITHUB_REF" >&2
exit 1
fi

- name: Check stable upstream releases
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -158,7 +169,23 @@ jobs:
--json number \
--jq '.[0].number // empty')"

git fetch origin "refs/heads/$branch:refs/remotes/origin/$branch" 2>/dev/null || true
remote_ref="refs/remotes/origin/$branch"
set +e
git ls-remote --exit-code --heads origin "$branch" >/dev/null
remote_status=$?
set -e
case "$remote_status" in
0)
git fetch origin "refs/heads/$branch:$remote_ref"
;;
2)
;;
*)
echo "ERROR: could not determine whether remote automation branch exists" >&2
exit "$remote_status"
;;
esac

git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git checkout -B "$branch"
Expand All @@ -180,6 +207,14 @@ jobs:
sed -i "s|^ARG ${name}=.*|ARG ${name}=${value}|" "$file"
}

replace_mise_tool() {
local name="$1"
local value="$2"
grep -q "^${name} = \"" mise.toml \
|| { echo "ERROR: mise.toml has no ${name} tool pin" >&2; exit 1; }
sed -i "s|^${name} = .*|${name} = \"${value}\"|" mise.toml
}

replace_env CODEX_RELEASE_TAG "$latest_codex"
replace_env CODEX_AMD64_SHA256 "$codex_amd64_sha256"
replace_env CODEX_ARM64_SHA256 "$codex_arm64_sha256"
Expand Down Expand Up @@ -214,21 +249,39 @@ jobs:
replace_arg images/base/Dockerfile NPM_VERSION "$latest_npm"
replace_arg images/base/Dockerfile UV_VERSION "$latest_uv"

replace_mise_tool python "$latest_python"
replace_mise_tool node "$latest_node"
replace_mise_tool uv "$latest_uv"

mise_bin="$workdir/mise"
curl "${curl_args[@]}" \
"https://github.com/jdx/mise/releases/download/v${latest_mise}/mise-v${latest_mise}-linux-x64" \
-o "$mise_bin"
printf '%s %s\n' "$mise_amd64_sha256" "$mise_bin" | sha256sum -c -
chmod 0755 "$mise_bin"
MISE_BIN="$mise_bin" bash scripts/regenerate-mise-lock.sh

bash scripts/validate-version-pins.sh

if git diff --quiet -- versions.env images/base/Dockerfile images/codex/Dockerfile; then
echo "No upstream release or digest changes."
tracked_files=(
versions.env
mise.toml
mise.lock
Comment thread
eXPerience83 marked this conversation as resolved.
images/base/Dockerfile
images/codex/Dockerfile
)
if git diff --quiet -- "${tracked_files[@]}"; then
echo "No upstream release, digest or runtime artifact changes."
if [[ -n "$existing_pr" ]]; then
gh pr close "$existing_pr" \
--comment "Closing because main already contains the latest tracked stable releases and asset digests."
--comment "Closing because main already contains the latest tracked stable releases, digests and locked runtime artifacts."
fi
exit 0
fi

git add versions.env images/base/Dockerfile images/codex/Dockerfile
git commit -m "chore: update upstream versions and digests"
git add "${tracked_files[@]}"
git commit -m "chore: update upstream versions, digests, and runtime lock"

remote_ref="refs/remotes/origin/$branch"
if git rev-parse --verify "$remote_ref" >/dev/null 2>&1 &&
[[ "$(git rev-parse 'HEAD^{tree}')" == "$(git rev-parse "$remote_ref^{tree}")" ]]; then
echo "The automation branch already contains the desired update."
Expand All @@ -245,15 +298,15 @@ jobs:
--json number \
--jq '.[0].number // empty')"

body="Automated stable upstream release and digest update. Tracks final Codex, GitHub CLI, ttyd, mise and uv releases plus maintenance updates within Python 3.14, Node 24 LTS and npm 12. Merge only after the required AMD64 build, image vulnerability scans, runtime smoke tests and review pass. Build AMD64 is dispatched explicitly because pull-request runs created with GITHUB_TOKEN otherwise require manual approval. Merging publishes a new public edge image; stable image tags are not changed."
body="Automated stable upstream release, digest and mise runtime-lock update. Tracks final Codex, GitHub CLI, ttyd, mise and uv releases plus maintenance updates within Python 3.14, Node 24 LTS and npm 12. The same PR regenerates exact AMD64 and ARM64 runtime artifact URLs, SHA-256 values and provenance with the verified pinned mise binary. Merge only after the required AMD64 build, image vulnerability scans, runtime smoke tests and review pass. Build AMD64 is dispatched explicitly because pull-request runs created with GITHUB_TOKEN otherwise require manual approval. Merging publishes a new public edge image; stable image tags are not changed."

if [[ -n "$existing_pr" ]]; then
gh pr edit "$existing_pr" \
--title "chore: update stable upstream versions and digests" \
--title "chore: update stable upstream versions and runtime lock" \
--body "$body"
else
gh pr create \
--title "chore: update stable upstream versions and digests" \
--title "chore: update stable upstream versions and runtime lock" \
--body "$body" \
--base main \
--head "$branch"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish-edge-amd64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
paths:
- ".dockerignore"
- "versions.env"
- "mise.toml"
- "mise.lock"
- "images/**"
- "scripts/**"
- "config/**"
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Persistent credential permission hardening for Codex, GitHub CLI, Git and SSH state.
- Embedded image channel and source revision metadata exposed in the menu, diagnostics and `remote-dev-version`, together with the installed Codex CLI version reported at runtime.
- Trivy JSON reports for all critical findings in locally built images and exact publication candidates; only findings with a known fixed version fail the gate.
- Committed mise runtime configuration and lock data for Linux AMD64 and ARM64, plus validation and a documented regeneration helper.

### Changed

Expand All @@ -45,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Assigned npm updates exclusively to the grouped upstream workflow to avoid competing Renovate pull requests.
- Added an official `SHA256SUMS` fallback for upstream releases such as ttyd that do not expose GitHub asset digest metadata.
- Centralized the fixable-critical Trivy gate so build, edge and stable workflows share the same enforcement logic.
- Extended upstream automation to regenerate and review the mise lock whenever runtime versions or resolved artifacts change.

### Security

Expand All @@ -60,4 +62,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Third-party GitHub Actions are pinned to immutable commit SHAs.
- The Ubuntu base image is pinned to an immutable OCI digest.
- Downloaded Codex, GitHub CLI, ttyd and mise assets are verified against repository-controlled architecture-specific SHA-256 values.
- Python, Node.js and uv install from committed artifact URLs and SHA-256 values in strict mise locked mode, with GitHub artifact attestations required where supported.
- Publication workflows scan exact pushed digests before promoting public tags and use only the permissions required to read source and write packages.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Read `CONTRIBUTING.md` before proposing changes. Pull requests use the repositor
- `docs/security.md`
- `docs/decisions.md`
- `docs/releases.md`
- `docs/runtime-locks.md`
- `docs/roadmap.md`

## Upstream references
Expand Down
47 changes: 47 additions & 0 deletions docs/runtime-locks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Locked mise runtimes

Python, Node.js and uv are installed by mise, but their build inputs are committed rather than resolved dynamically during the image build.

## Source of truth

The runtime pins are represented in three places for different purposes:

- `versions.env` supplies reviewed repository and build arguments.
- `mise.toml` declares the exact mise-managed runtime versions and enables provenance re-verification for locked installs.
- `mise.lock` records the resolved Linux AMD64 and ARM64 artifact URLs, SHA-256 checksums and available provenance requirements.

`scripts/validate-version-pins.sh` fails when these files or the base Dockerfile disagree. The lock validator treats both TOML files as security-sensitive schemas: unknown sections or fields, malformed platform values, unexpected backends or URLs, invalid checksums, missing provenance, mixed Python build dates and reused uv asset IDs are rejected. Adversarial fixtures exercise these rejection paths on every validation run.

The Dockerfile copies `mise.toml` and `mise.lock` as read-only inputs and runs `mise install --locked`; a missing artifact entry, dynamic-resolution requirement, provenance failure or checksum mismatch stops the build. `locked_verify_provenance = true` ensures that Python and uv GitHub artifact attestations are checked during installation instead of trusting only the provenance marker already stored in the lockfile.

The current CI builds Linux AMD64, so it downloads, checksums, installs and re-verifies provenance for the AMD64 artifacts. ARM64 entries are checked for exact schema, platform, backend, URL, checksum and provenance metadata coherence, but are not executed by the current AMD64 job. A future ARM64 image build will use the same locked installation and re-verification path before ARM64 publication.

npm is intentionally excluded from `mise.lock` because the image installs it separately from the npm registry.

## Regenerate the lockfile

Use the exact mise release pinned by `MISE_VERSION` in `versions.env`. The helper rejects any other mise version. It copies only `versions.env`, `mise.toml` and the existing `mise.lock` into a temporary workspace, clears inherited `MISE_*` settings, uses isolated config/data/cache/system/tmp directories, bounds network and command time, validates the generated lock and replaces the repository lock only after validation succeeds. A failed or malformed regeneration leaves the previous lock untouched.

```bash
source versions.env
mise --version
bash scripts/regenerate-mise-lock.sh
bash scripts/validate-version-pins.sh
```

When changing Python, Node.js or uv:

1. Update the version in `versions.env`.
2. Update the matching `ARG` default in `images/base/Dockerfile`.
3. Update the matching tool in `mise.toml`.
4. Run `scripts/regenerate-mise-lock.sh` with the pinned mise release.
5. Review every changed URL, SHA-256 and provenance field for both `linux-x64` and `linux-arm64`.
6. Run `make validate` and build the AMD64 images so mise verifies the current-platform downloaded artifacts and supported provenance.

The daily upstream workflow follows the same procedure with a freshly downloaded mise binary whose SHA-256 is verified before it regenerates the lock. A plain `mise lock` refreshes artifact metadata for the already pinned versions, so the workflow may propose a lock-only change when an upstream provider publishes a newer artifact for an unchanged runtime version. The workflow is restricted to `main`, serialized to prevent competing writers and uses a force-with-lease update for its dedicated automation branch.

## Recovery

Do not remove `--locked`, disable `locked_verify_provenance`, delete `mise.lock` or fall back to `mise use` to work around a stale lock. Regenerate the lock with the exact pinned mise version, review the artifact changes, and keep the version/config/lock updates in one pull request.

If a checksum has changed unexpectedly for an artifact URL that should be immutable, stop the update and investigate upstream before merging.
14 changes: 7 additions & 7 deletions images/base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
MISE_DATA_DIR=/opt/remote-dev/mise \
MISE_CACHE_DIR=/opt/remote-dev/mise-cache \
MISE_CONFIG_DIR=/etc/mise \
MISE_GLOBAL_CONFIG_FILE=/etc/mise/config.toml \
MISE_GLOBAL_CONFIG_FILE=/etc/mise/mise.toml \
PATH=/opt/remote-dev/mise/shims:/opt/remote-dev/mise/bin:/root/.local/bin:${PATH} \
GH_CONFIG_DIR=/root/.config/gh \
GH_HOST=github.com \
Expand Down Expand Up @@ -148,19 +148,19 @@ RUN case "${TARGETARCH}" in \
&& cd / \
&& rm -rf "$workdir"

# Runtime config and resolved artifacts are immutable build inputs. Strict locked
# installation refuses missing URLs and verifies every committed checksum.
COPY --chmod=0444 mise.toml mise.lock /etc/mise/

# One current runtime per language. Other runtimes are deliberately not bundled.
RUN mkdir -p "$MISE_DATA_DIR" "$MISE_CACHE_DIR" "$MISE_CONFIG_DIR" \
&& mise settings set experimental true \
&& mise use --global \
"python@${PYTHON_VERSION}" \
"node@${NODE_VERSION}" \
"uv@${UV_VERSION}" \
&& mise install --locked \
&& npm install --global --ignore-scripts --no-audit --no-fund "npm@${NPM_VERSION}" \
&& python --version \
&& node --version \
&& test "$(npm --version)" = "$NPM_VERSION" \
&& uv --version \
&& rm -rf "$MISE_CACHE_DIR"/*
&& rm -rf "${MISE_CACHE_DIR:?}"/*

COPY config/tmux.conf /etc/tmux.conf
COPY scripts/base-verify.sh /usr/local/bin/remote-dev-base-verify
Expand Down
43 changes: 43 additions & 0 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[settings]
lockfile = true
locked_verify_provenance = true
lockfile_platforms = ["linux-x64", "linux-arm64"]

[tools]
python = "3.14.6"
node = "24.18.0"
uv = "0.11.32"
Loading