Skip to content

busybox: recover the 1.8/1.10..1.18 ash+hush gap band #141

busybox: recover the 1.8/1.10..1.18 ash+hush gap band

busybox: recover the 1.8/1.10..1.18 ash+hush gap band #141

Workflow file for this run

# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
# SPDX-License-Identifier: ISC
name: Build shell versions
permissions:
contents: read
packages: write
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
workflow_dispatch:
inputs:
force_all:
description: "Rebuild every target, ignoring the content-addressed skip"
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Pinned base image source digests. The `mirror` job copies these exact
# digests from docker.io into a run-scoped GHCR tag and exports
# ghcr.io/...@<digest> refs for the toolchain/build/assemble jobs, so
# every job in a single run uses immutable, race-free base images.
# Bump these (and regenerate build checksums) when refreshing the base.
# Keep in sync with Dockerfile ARG defaults.
MIRROR_TOOLCHAIN_SOURCE: debian:trixie-slim@sha256:cedb1ef40439206b673ee8b33a46a03a0c9fa90bf3732f54704f99cb061d2c5a
MIRROR_RUNTIME_SOURCE: busybox:stable-musl@sha256:3c6ae8008e2c2eedd141725c30b20d9c36b026eb796688f88205845ef17aa213
# BuildKit builder image for the docker-container driver. Mirrored to
# GHCR like the bases so booting a builder never pulls from Docker Hub
# (the recurring "Booting builder ... context deadline exceeded"). Pin
# matches docker/setup-buildx-action@v4's default tag, buildx-stable-1.
MIRROR_BUILDKIT_SOURCE: moby/buildkit:buildx-stable-1@sha256:0168606be2315b7c807a03b3d8aa79beefdb31c98740cebdffdfeebf31190c9f
jobs:
# Note: there is no bulk `download` job. Each build job fetches its own
# target's source (single-download), and a separate scheduled workflow
# (warm-sources.yml) keeps every source cache warm so skipped targets stay
# rebuildable even if upstream later removes a tarball.
mirror:
runs-on: ubuntu-24.04
outputs:
toolchain_ref: ${{ steps.resolve.outputs.toolchain_ref }}
runtime_ref: ${{ steps.resolve.outputs.runtime_ref }}
buildkit_ref: ${{ steps.resolve.outputs.buildkit_ref }}
env:
# Run-scoped GHCR tags. The `run-` prefix matches the existing
# ephemeral cleanup filter in ghcr-cleanup.yml, so these mirror
# tags are reaped automatically; PR and push runs never overwrite
# each other or any long-lived shared tag.
TOOLCHAIN_TAG: ghcr.io/${{ github.repository }}:run-${{ github.run_id }}-mirror-toolchain
RUNTIME_TAG: ghcr.io/${{ github.repository }}:run-${{ github.run_id }}-mirror-runtime
BUILDKIT_TAG: ghcr.io/${{ github.repository }}:run-${{ github.run_id }}-mirror-buildkit
steps:
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# No setup-buildx-action here on purpose: `imagetools` is a
# registry-side operation that uses the preinstalled default builder,
# so it never boots a buildkit container. That breaks the
# chicken-and-egg of mirroring the buildkit image itself.
- name: Mirror base images to run-scoped GHCR tags
run: |
set -eu
retry() {
for attempt in 1 2 3; do
if "$@"; then return 0; fi
echo "attempt ${attempt} failed: $*" >&2
sleep 5
done
return 1
}
retry docker buildx imagetools create \
--tag "${TOOLCHAIN_TAG}" "${MIRROR_TOOLCHAIN_SOURCE}"
retry docker buildx imagetools create \
--tag "${RUNTIME_TAG}" "${MIRROR_RUNTIME_SOURCE}"
retry docker buildx imagetools create \
--tag "${BUILDKIT_TAG}" "${MIRROR_BUILDKIT_SOURCE}"
- name: Resolve digest-pinned refs
id: resolve
run: |
toolchain_digest=$(docker buildx imagetools inspect \
--format '{{.Manifest.Digest}}' "${TOOLCHAIN_TAG}")
runtime_digest=$(docker buildx imagetools inspect \
--format '{{.Manifest.Digest}}' "${RUNTIME_TAG}")
buildkit_digest=$(docker buildx imagetools inspect \
--format '{{.Manifest.Digest}}' "${BUILDKIT_TAG}")
{
echo "toolchain_ref=ghcr.io/${{ github.repository }}@${toolchain_digest}"
echo "runtime_ref=ghcr.io/${{ github.repository }}@${runtime_digest}"
echo "buildkit_ref=ghcr.io/${{ github.repository }}@${buildkit_digest}"
} >> "$GITHUB_OUTPUT"
toolchain:
needs: [mirror]
# One toolchain per arch, each on a native runner so its musl-cross-make and
# Rust target build natively. Cache refs/scopes are arch-suffixed so the two
# arches never cross-pollute each other's layers.
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/toolchain-downloads
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=${{ needs.mirror.outputs.buildkit_ref }}
- name: Build toolchain image
uses: docker/build-push-action@v7
with:
context: .
target: toolchain
push: false
platforms: linux/${{ matrix.arch }}
build-args: |
TOOLCHAIN_BASE=${{ needs.mirror.outputs.toolchain_ref }}
RUNTIME_BASE=${{ needs.mirror.outputs.runtime_ref }}
# Publish the toolchain build cache to the registry as well as GHA. A
# registry cache is content-addressed and reliably shared across the
# whole build matrix; the GHA cache is not dependable for a multi-GB
# toolchain read concurrently by every build job. Without this, when the
# toolchain layer changes (e.g. a shvr.sh edit) the per-target
# buildcaches go stale and every build job recompiles musl-cross-make.
cache-to: |
type=gha,scope=toolchain-${{ matrix.arch }}-v3,mode=max
${{ github.event_name == 'push' && format('type=registry,ref=ghcr.io/{0}:cache-toolchain-{1},mode=max', github.repository, matrix.arch) || '' }}
cache-from: |
type=registry,ref=ghcr.io/${{ github.repository }}:cache-toolchain-${{ matrix.arch }}
type=gha,scope=toolchain-${{ matrix.arch }}-v3
# Decide which targets actually need building. Each pushed per-target image
# carries an org.shvr.oid annotation (its content-addressed build identity);
# a target whose published OID still matches the freshly computed one cannot
# have changed and is skipped. Emits a dynamic build matrix consumed below.
plan:
runs-on: ubuntu-24.04
outputs:
# Per-arch matrices: GitHub caps a job's matrix at 256 configurations, and a
# full rebuild is ~250 targets PER ARCH, so the two arches must drive two
# separate build jobs rather than one merged 2x matrix.
matrix_amd64: ${{ steps.plan.outputs.matrix_amd64 }}
matrix_arm64: ${{ steps.plan.outputs.matrix_arm64 }}
count_amd64: ${{ steps.plan.outputs.count_amd64 }}
count_arm64: ${{ steps.plan.outputs.count_arm64 }}
built: ${{ steps.plan.outputs.built }}
steps:
- uses: actions/checkout@v7
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Plan targets to build
id: plan
env:
REPO: ${{ github.repository }}
FORCE_ALL: ${{ github.event.inputs.force_all }}
run: |
set -eu
# Kill-switch: rebuild everything on manual force_all or a
# [rebuild-all] commit-message flag.
force=0
if [ "${FORCE_ALL:-}" = "true" ]; then force=1; fi
if git log -1 --pretty=%B | grep -qiF '[rebuild-all]'; then force=1; fi
# Read each target+arch's published OID from the arch-suffixed
# :<target>-<arch> tag's manifest annotation, without pulling layers.
# Any read failure or a missing image yields MISSING -> (re)build. The
# skip is safe-by-default: it can over-build but never wrongly skip.
# The OID is arch-specific (arch is folded into the fingerprint), so the
# two arches are planned independently.
: > registry_oids.txt
for arch in amd64 arm64; do
for t in $(sh shvr.sh targets); do
if [ "$force" = 1 ]; then
printf '%s %s FORCE\n' "$t" "$arch" >> registry_oids.txt
continue
fi
oid=$(docker buildx imagetools inspect --raw "ghcr.io/${REPO}:${t}-${arch}" 2>/dev/null \
| jq -r '.annotations["org.shvr.oid"] // empty' 2>/dev/null || true)
printf '%s %s %s\n' "$t" "$arch" "${oid:-MISSING}" >> registry_oids.txt
done
done
# plan_matrix runs once per arch (SHVR_ARCH selects the fingerprint and
# checksum dir); each emits a separate {include:[...]} consumed by that
# arch's build job. Kept separate (not merged) to stay under the 256
# configurations-per-matrix limit on a full rebuild.
amd=$(SHVR_ARCH=amd64 sh shvr.sh plan_matrix registry_oids.txt)
arm=$(SHVR_ARCH=arm64 sh shvr.sh plan_matrix registry_oids.txt)
count_amd64=$(printf '%s' "$amd" | jq '.include | length')
count_arm64=$(printf '%s' "$arm" | jq '.include | length')
# "<target>-<arch>" list of rows built this run, for the assemble job to
# tell apart freshly-built (run-scoped) from reused (stable) images.
built=$(printf '%s\n%s' "$amd" "$arm" | jq -r -s '[.[].include[] | (.target + "-" + .arch)] | join(" ")')
printf 'matrix_amd64=%s\n' "$amd" >> "$GITHUB_OUTPUT"
printf 'matrix_arm64=%s\n' "$arm" >> "$GITHUB_OUTPUT"
printf 'count_amd64=%s\n' "$count_amd64" >> "$GITHUB_OUTPUT"
printf 'count_arm64=%s\n' "$count_arm64" >> "$GITHUB_OUTPUT"
printf 'built=%s\n' "$built" >> "$GITHUB_OUTPUT"
printf '%s amd64 + %s arm64 of %s target(s) need building\n' \
"$count_amd64" "$count_arm64" "$(sh shvr.sh targets | wc -l)" >> "$GITHUB_STEP_SUMMARY"
# build is split per arch because GitHub caps a job's matrix at 256
# configurations and a full rebuild is ~250 targets per arch. Both jobs are
# identical except for their native runner, their per-arch matrix, and the
# per-arch skip count; the steps use matrix.arch (constant within each job).
build-amd64:
needs: [plan, toolchain, mirror]
if: needs.plan.outputs.count_amd64 != '0'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/single-download
with:
shvr_shell: ${{ matrix.shell }}
shvr_version: "${{ matrix.version }}"
cache_path: "${{ matrix.cache_path }}"
- uses: ./.github/actions/toolchain-downloads
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# No Docker Hub login here: the per-target :<target>-<arch> images are
# internal building blocks and are pushed to GHCR only. Docker Hub receives
# just the user-facing multi-arch :<target> lists, assembled from the GHCR
# arch images by the target-manifest job.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=${{ needs.mirror.outputs.buildkit_ref }}
- name: Build and push shell image
uses: ./.github/actions/build-push-retry
with:
context: .
target: artifacts
push: true
platforms: linux/${{ matrix.arch }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.event_name == 'push' && format('{0}-{1}', matrix.target, matrix.arch) || format('run-{0}-{1}-{2}', github.run_id, matrix.target, matrix.arch) }}
build-args: |
TARGETS=${{ matrix.target }}
TOOLCHAIN_BASE=${{ needs.mirror.outputs.toolchain_ref }}
RUNTIME_BASE=${{ needs.mirror.outputs.runtime_ref }}
cache-from: |
type=registry,ref=ghcr.io/${{ github.repository }}:cache-toolchain-${{ matrix.arch }}
type=gha,scope=toolchain-${{ matrix.arch }}-v3
type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-${{ matrix.target }}-${{ matrix.arch }}
cache-to: >-
${{ github.event_name == 'push'
&& format('type=registry,ref=ghcr.io/{0}:buildcache-{1}-{2},mode=max', github.repository, matrix.target, matrix.arch)
|| '' }}
# Single-manifest image so the OID annotation sits at the manifest top
# level, readable via `imagetools inspect --raw` without pulling layers.
# The OID comes from the plan matrix entry (shvr_plan_matrix) and is
# arch-specific.
provenance: false
annotations: |
manifest:org.shvr.oid=${{ matrix.oid }}
labels: |
org.shvr.oid=${{ matrix.oid }}
- name: Verify build checksums
uses: ./.github/actions/verify-build-checksums
with:
arch: ${{ matrix.arch }}
image: >-
${{ github.event_name == 'push'
&& format('ghcr.io/{0}:{1}-{2}', github.repository, matrix.target, matrix.arch)
|| format('ghcr.io/{0}:run-{1}-{2}-{3}', github.repository, github.run_id, matrix.target, matrix.arch) }}
targets: ${{ matrix.target }}
strategy:
fail-fast: ${{ github.event_name == 'push' }}
matrix: ${{ fromJSON(needs.plan.outputs.matrix_amd64) }}
build-arm64:
needs: [plan, toolchain, mirror]
if: needs.plan.outputs.count_arm64 != '0'
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/single-download
with:
shvr_shell: ${{ matrix.shell }}
shvr_version: "${{ matrix.version }}"
cache_path: "${{ matrix.cache_path }}"
- uses: ./.github/actions/toolchain-downloads
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# No Docker Hub login here: the per-target :<target>-<arch> images are
# internal building blocks and are pushed to GHCR only. Docker Hub receives
# just the user-facing multi-arch :<target> lists, assembled from the GHCR
# arch images by the target-manifest job.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=${{ needs.mirror.outputs.buildkit_ref }}
- name: Build and push shell image
uses: ./.github/actions/build-push-retry
with:
context: .
target: artifacts
push: true
platforms: linux/${{ matrix.arch }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.event_name == 'push' && format('{0}-{1}', matrix.target, matrix.arch) || format('run-{0}-{1}-{2}', github.run_id, matrix.target, matrix.arch) }}
build-args: |
TARGETS=${{ matrix.target }}
TOOLCHAIN_BASE=${{ needs.mirror.outputs.toolchain_ref }}
RUNTIME_BASE=${{ needs.mirror.outputs.runtime_ref }}
cache-from: |
type=registry,ref=ghcr.io/${{ github.repository }}:cache-toolchain-${{ matrix.arch }}
type=gha,scope=toolchain-${{ matrix.arch }}-v3
type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-${{ matrix.target }}-${{ matrix.arch }}
cache-to: >-
${{ github.event_name == 'push'
&& format('type=registry,ref=ghcr.io/{0}:buildcache-{1}-{2},mode=max', github.repository, matrix.target, matrix.arch)
|| '' }}
provenance: false
annotations: |
manifest:org.shvr.oid=${{ matrix.oid }}
labels: |
org.shvr.oid=${{ matrix.oid }}
- name: Verify build checksums
uses: ./.github/actions/verify-build-checksums
with:
arch: ${{ matrix.arch }}
image: >-
${{ github.event_name == 'push'
&& format('ghcr.io/{0}:{1}-{2}', github.repository, matrix.target, matrix.arch)
|| format('ghcr.io/{0}:run-{1}-{2}-{3}', github.repository, github.run_id, matrix.target, matrix.arch) }}
targets: ${{ matrix.target }}
strategy:
fail-fast: ${{ github.event_name == 'push' }}
matrix: ${{ fromJSON(needs.plan.outputs.matrix_arm64) }}
assemble:
needs: [plan, build-amd64, build-arm64, mirror]
# Run even when build was skipped (0 changed targets): flavors are
# reassembled from the per-target images (rebuilt this run or reused from a
# prior push) and the full-set checksum verify still guards every binary.
# Only bail if a build actually failed or was cancelled.
if: >-
always()
&& needs.mirror.result == 'success'
&& needs.plan.result == 'success'
&& needs['build-amd64'].result != 'failure'
&& needs['build-amd64'].result != 'cancelled'
&& needs['build-arm64'].result != 'failure'
&& needs['build-arm64'].result != 'cancelled'
# Each (flavor, arch) is assembled on its native runner: the final stage's
# RUN (manifest.txt generation) and the COPY --from of per-arch images both
# execute under the matching arch with no emulation.
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
env:
ALL_TARGETS: ${{ matrix.targets }}
ARCH: ${{ matrix.arch }}
RUNTIME_REF: ${{ needs.mirror.outputs.runtime_ref }}
# Rows actually built this run (from the plan job), keyed "<target>-<arch>".
# On a PR these carry run-scoped tags; every other target+arch is reused
# from its stable tag.
BUILT_TARGETS: ${{ needs.plan.outputs.built }}
# Per-arch flavor image, pushed to GHCR only (internal building block); the
# assemble-manifest job fuses the two arches into the user-facing multi-arch
# :<flavor> manifest list on Docker Hub.
ASSEMBLY_TAG: >-
${{ github.event_name == 'push'
&& format('ghcr.io/{0}:{1}-{2}', github.repository, matrix.flavor, matrix.arch)
|| format('ghcr.io/{0}:run-{1}-{2}-{3}', github.repository, github.run_id, matrix.flavor, matrix.arch) }}
strategy:
fail-fast: false
matrix:
include:
# AUTO-GENERATED ASSEMBLIES. DO NOT EDIT MANUALLY.
- flavor: latest
arch: amd64
targets: >
ash_1.38.0
ash_1.37.0
bash_5.3.15
bash_5.2.37
brush_0.4.0
brush_0.3.0
dash_0.5.13.4
dash_0.5.12
hush_1.38.0
hush_1.37.0
ksh_shvrA93uplusm-v1.0.10
ksh_shvrA93uplusm-v1.0.9
loksh_7.9
loksh_7.8
mksh_R59c
mksh_R58
oksh_7.9
oksh_7.8
osh_0.37.0
osh_0.36.0
posh_0.14.5
posh_0.13.2
yash_2.61
yash_2.60
yashrs_3.2.1
yashrs_3.1.0
zsh_5.9.1
zsh_5.8.1
- flavor: all
arch: amd64
targets: >
ash_1.38.0
ash_1.37.0
ash_1.36.1
ash_1.35.0
ash_1.34.1
ash_1.33.2
ash_1.32.1
ash_1.31.1
ash_1.30.1
ash_1.29.3
ash_1.28.4
ash_1.27.2
ash_1.26.2
ash_1.25.1
ash_1.24.2
ash_1.23.2
ash_1.22.1
ash_1.21.1
ash_1.20.2
ash_1.19.4
ash_1.18.5
ash_1.17.4
ash_1.16.2
ash_1.15.3
ash_1.14.4
ash_1.13.4
ash_1.12.4
ash_1.11.3
ash_1.10.4
ash_1.8.3
ash_1.2.2.1
ash_1.2.2
bash_5.3.15
bash_5.2.37
bash_5.1.16
bash_5.0.18
bash_4.4.23
bash_4.3.48
bash_4.2.53
bash_4.1.17
bash_4.0.44
bash_3.2.57
bash_3.1.23
bash_3.0.22
bash_2.05.0
bash_2.05b.13
bash_2.05a.0
bash_2.04.0
bash_2.03.0
bash_2.02.0
bash_2.01.0
brush_0.4.0
brush_0.3.0
brush_0.2.23
dash_0.5.13.4
dash_0.5.12
dash_0.5.11.5
dash_0.5.10.2
dash_0.5.9.1
dash_0.5.8
dash_0.5.7
dash_0.5.6.1
dash_0.5.5.1
dash_0.5.4
dash_0.5.3
hush_1.38.0
hush_1.37.0
hush_1.36.1
hush_1.35.0
hush_1.34.1
hush_1.33.2
hush_1.32.1
hush_1.31.1
hush_1.30.1
hush_1.29.3
hush_1.28.4
hush_1.27.2
hush_1.26.2
hush_1.25.1
hush_1.24.2
hush_1.23.2
hush_1.22.1
hush_1.21.1
hush_1.20.2
hush_1.19.4
hush_1.18.5
hush_1.17.4
hush_1.16.2
hush_1.15.3
hush_1.14.4
hush_1.13.4
hush_1.12.4
hush_1.11.3
hush_1.10.4
hush_1.8.3
hush_1.2.2.1
hush_1.2.2
ksh_shvrChistory-b_2016-01-10
ksh_shvrChistory-b_2012-08-01
ksh_shvrChistory-b_2011-03-10
ksh_shvrChistory-b_2010-10-26
ksh_shvrChistory-b_2010-06-21
ksh_shvrChistory-b_2008-11-04
ksh_shvrChistory-b_2008-06-08
ksh_shvrChistory-b_2008-02-02
ksh_shvrChistory-b_2007-01-11
ksh_shvrB2020-2020.0.0
ksh_shvrA93uplusm-v1.0.10
ksh_shvrA93uplusm-v1.0.9
ksh_shvrA93uplusm-v1.0.8
ksh_shvrA93uplusm-v1.0.7
ksh_shvrA93uplusm-v1.0.6
ksh_shvrA93uplusm-v1.0.4
ksh_shvrA93uplusm-v1.0.3
ksh_shvrA93uplusm-v1.0.2
ksh_shvrA93uplusm-v1.0.1
loksh_7.9
loksh_7.8
loksh_7.7
loksh_7.6
loksh_7.5
loksh_7.4
loksh_7.3
loksh_7.2
loksh_7.1
loksh_7.0
loksh_6.9
loksh_6.8.1
loksh_6.7.5
mksh_R59c
mksh_R58
mksh_R57
mksh_R56c
mksh_R55
mksh_R54
mksh_R53a
mksh_R52c
mksh_R51
mksh_R50f
mksh_R49
mksh_R48b
mksh_R47
mksh_R46
mksh_R45
mksh_R44
mksh_R43
mksh_R42b
mksh_R41c
mksh_R40f
mksh_R39c
mksh_R38c
mksh_R37c
mksh_R36b
mksh_R35b
mksh_R33d
mksh_R32
mksh_R31d
mksh_R30
oksh_7.9
oksh_7.8
oksh_7.7
oksh_7.6
oksh_7.5
oksh_7.4
oksh_7.3
oksh_7.2
oksh_7.1
oksh_7.0
oksh_6.9
oksh_6.8.1
oksh_6.7.1
oksh_6.6
oksh_6.5
osh_0.37.0
osh_0.36.0
osh_0.35.0
osh_0.34.0
osh_0.33.0
osh_0.32.0
osh_0.31.0
osh_0.30.0
osh_0.29.0
osh_0.28.0
osh_0.27.0
osh_0.26.0
osh_0.25.0
posh_0.14.5
posh_0.13.2
posh_0.12.6
yash_2.61
yash_2.60
yash_2.59
yash_2.58.1
yash_2.57
yash_2.56.1
yash_2.55
yash_2.54
yash_2.53
yash_2.52
yash_2.51
yash_2.50
yash_2.49
yash_2.48
yash_2.47
yash_2.46
yash_2.45
yash_2.44
yash_2.43
yash_2.42
yash_2.41
yash_2.40
yash_2.39
yash_2.38
yash_2.37
yash_2.36
yash_2.35
yash_2.34
yash_2.33.1
yash_2.32.1
yash_2.31
yash_2.30
yash_2.29
yash_2.28
yash_2.27
yash_2.26.1
yash_2.25
yash_2.24
yash_2.23
yash_2.22
yash_2.21
yash_2.20
yash_2.19
yash_2.18
yash_2.17
yash_2.16
yash_2.15
yash_2.14
yash_2.13
yash_2.12
yash_2.11
yash_2.10
yashrs_3.2.1
yashrs_3.2.0
yashrs_3.1.0
yashrs_3.0.8
yashrs_3.0.7
yashrs_3.0.5
yashrs_3.0.4
yashrs_3.0.3
yashrs_3.0.2
yashrs_3.0.1
yashrs_3.0.0
yashrs_0.4.5
yashrs_0.4.4
yashrs_0.4.3
yashrs_0.4.2
yashrs_0.4.1
yashrs_0.4.0
yashrs_0.3.0
zsh_5.9.1
zsh_5.8.1
zsh_5.7.1
zsh_5.6.2
zsh_5.5.1
zsh_5.4.2
zsh_5.3.1
zsh_5.2
zsh_5.1.1
zsh_5.0.8
zsh_4.2.7
zsh_4.0.9
- flavor: latest
arch: arm64
targets: >
ash_1.38.0
ash_1.37.0
bash_5.3.15
bash_5.2.37
brush_0.4.0
brush_0.3.0
dash_0.5.13.4
dash_0.5.12
hush_1.38.0
hush_1.37.0
ksh_shvrA93uplusm-v1.0.10
ksh_shvrA93uplusm-v1.0.9
loksh_7.9
loksh_7.8
mksh_R59c
mksh_R58
oksh_7.9
oksh_7.8
osh_0.37.0
osh_0.36.0
posh_0.14.5
posh_0.13.2
yash_2.61
yash_2.60
yashrs_3.2.1
yashrs_3.1.0
zsh_5.9.1
zsh_5.8.1
- flavor: all
arch: arm64
targets: >
ash_1.38.0
ash_1.37.0
ash_1.36.1
ash_1.35.0
ash_1.34.1
ash_1.33.2
ash_1.32.1
ash_1.31.1
ash_1.30.1
ash_1.29.3
ash_1.28.4
ash_1.27.2
ash_1.26.2
ash_1.25.1
ash_1.24.2
ash_1.23.2
ash_1.22.1
ash_1.21.1
ash_1.20.2
ash_1.19.4
ash_1.18.5
ash_1.17.4
ash_1.16.2
ash_1.15.3
ash_1.14.4
ash_1.13.4
ash_1.12.4
ash_1.11.3
ash_1.10.4
ash_1.8.3
ash_1.2.2.1
ash_1.2.2
bash_5.3.15
bash_5.2.37
bash_5.1.16
bash_5.0.18
bash_4.4.23
bash_4.3.48
bash_4.2.53
bash_4.1.17
bash_4.0.44
bash_3.2.57
bash_3.1.23
bash_3.0.22
bash_2.05.0
bash_2.05b.13
bash_2.05a.0
bash_2.04.0
bash_2.03.0
bash_2.02.0
bash_2.01.0
brush_0.4.0
brush_0.3.0
brush_0.2.23
dash_0.5.13.4
dash_0.5.12
dash_0.5.11.5
dash_0.5.10.2
dash_0.5.9.1
dash_0.5.8
dash_0.5.7
dash_0.5.6.1
dash_0.5.5.1
dash_0.5.4
dash_0.5.3
hush_1.38.0
hush_1.37.0
hush_1.36.1
hush_1.35.0
hush_1.34.1
hush_1.33.2
hush_1.32.1
hush_1.31.1
hush_1.30.1
hush_1.29.3
hush_1.28.4
hush_1.27.2
hush_1.26.2
hush_1.25.1
hush_1.24.2
hush_1.23.2
hush_1.22.1
hush_1.21.1
hush_1.20.2
hush_1.19.4
hush_1.18.5
hush_1.17.4
hush_1.16.2
hush_1.15.3
hush_1.14.4
hush_1.13.4
hush_1.12.4
hush_1.11.3
hush_1.10.4
hush_1.8.3
hush_1.2.2.1
hush_1.2.2
ksh_shvrChistory-b_2016-01-10
ksh_shvrChistory-b_2012-08-01
ksh_shvrChistory-b_2011-03-10
ksh_shvrChistory-b_2010-10-26
ksh_shvrChistory-b_2010-06-21
ksh_shvrChistory-b_2008-11-04
ksh_shvrChistory-b_2008-06-08
ksh_shvrChistory-b_2008-02-02
ksh_shvrChistory-b_2007-01-11
ksh_shvrB2020-2020.0.0
ksh_shvrA93uplusm-v1.0.10
ksh_shvrA93uplusm-v1.0.9
ksh_shvrA93uplusm-v1.0.8
ksh_shvrA93uplusm-v1.0.7
ksh_shvrA93uplusm-v1.0.6
ksh_shvrA93uplusm-v1.0.4
ksh_shvrA93uplusm-v1.0.3
ksh_shvrA93uplusm-v1.0.2
ksh_shvrA93uplusm-v1.0.1
loksh_7.9
loksh_7.8
loksh_7.7
loksh_7.6
loksh_7.5
loksh_7.4
loksh_7.3
loksh_7.2
loksh_7.1
loksh_7.0
loksh_6.9
loksh_6.8.1
loksh_6.7.5
mksh_R59c
mksh_R58
mksh_R57
mksh_R56c
mksh_R55
mksh_R54
mksh_R53a
mksh_R52c
mksh_R51
mksh_R50f
mksh_R49
mksh_R48b
mksh_R47
mksh_R46
mksh_R45
mksh_R44
mksh_R43
mksh_R42b
mksh_R41c
mksh_R40f
mksh_R39c
mksh_R38c
mksh_R37c
mksh_R36b
mksh_R35b
mksh_R33d
mksh_R32
mksh_R31d
mksh_R30
oksh_7.9
oksh_7.8
oksh_7.7
oksh_7.6
oksh_7.5
oksh_7.4
oksh_7.3
oksh_7.2
oksh_7.1
oksh_7.0
oksh_6.9
oksh_6.8.1
oksh_6.7.1
oksh_6.6
oksh_6.5
osh_0.37.0
osh_0.36.0
osh_0.35.0
osh_0.34.0
osh_0.33.0
osh_0.32.0
osh_0.31.0
osh_0.30.0
osh_0.29.0
osh_0.28.0
osh_0.27.0
osh_0.26.0
osh_0.25.0
posh_0.14.5
posh_0.13.2
posh_0.12.6
yash_2.61
yash_2.60
yash_2.59
yash_2.58.1
yash_2.57
yash_2.56.1
yash_2.55
yash_2.54
yash_2.53
yash_2.52
yash_2.51
yash_2.50
yash_2.49
yash_2.48
yash_2.47
yash_2.46
yash_2.45
yash_2.44
yash_2.43
yash_2.42
yash_2.41
yash_2.40
yash_2.39
yash_2.38
yash_2.37
yash_2.36
yash_2.35
yash_2.34
yash_2.33.1
yash_2.32.1
yash_2.31
yash_2.30
yash_2.29
yash_2.28
yash_2.27
yash_2.26.1
yash_2.25
yash_2.24
yash_2.23
yash_2.22
yash_2.21
yash_2.20
yash_2.19
yash_2.18
yash_2.17
yash_2.16
yash_2.15
yash_2.14
yash_2.13
yash_2.12
yash_2.11
yash_2.10
yashrs_3.2.1
yashrs_3.2.0
yashrs_3.1.0
yashrs_3.0.8
yashrs_3.0.7
yashrs_3.0.5
yashrs_3.0.4
yashrs_3.0.3
yashrs_3.0.2
yashrs_3.0.1
yashrs_3.0.0
yashrs_0.4.5
yashrs_0.4.4
yashrs_0.4.3
yashrs_0.4.2
yashrs_0.4.1
yashrs_0.4.0
yashrs_0.3.0
zsh_5.9.1
zsh_5.8.1
zsh_5.7.1
zsh_5.6.2
zsh_5.5.1
zsh_5.4.2
zsh_5.3.1
zsh_5.2
zsh_5.1.1
zsh_5.0.8
zsh_4.2.7
zsh_4.0.9
steps:
- uses: actions/checkout@v7
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# No Docker Hub login: the per-arch flavor image is pushed to GHCR only.
# Docker Hub receives just the fused multi-arch :<flavor> list, built from
# the GHCR arch images by the assemble-manifest job.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=${{ needs.mirror.outputs.buildkit_ref }}
- name: Generate assembly Dockerfile
run: |
# Overlayfs caps mount options at one page (~4KB) and lower
# layers at 500; a single stage with one COPY layer per target
# (~500 for "all") exceeds both. Chunk targets into shallow
# stages, then merge the chunks.
set -eu
# Per-target images are arch-suffixed (:<target>-<arch>). On a PR, a
# target+arch built this run is at run-<id>-<target>-<arch>; one skipped
# this run is reused from its stable :<target>-<arch> image (a prior push
# to main). On push everything resolves to the stable :<target>-<arch>.
is_built () { case " ${BUILT_TARGETS} " in *" $1-${ARCH} "*) return 0 ;; *) return 1 ;; esac ; }
chunk_size=8
i=0
last_chunk=0
{
for target in $ALL_TARGETS; do
if [ $((i % chunk_size)) -eq 0 ]; then
last_chunk=$((i / chunk_size))
echo "FROM scratch AS chunk_${last_chunk}"
fi
if [ "${{ github.event_name }}" = "pull_request" ] && is_built "$target"; then
src="ghcr.io/${{ github.repository }}:run-${{ github.run_id }}-${target}-${ARCH}"
else
src="ghcr.io/${{ github.repository }}:${target}-${ARCH}"
fi
echo "COPY --from=${src} --chown=0:0 /opt /opt"
echo "COPY --from=${src} --chown=0:0 /deps /deps"
echo "COPY --from=${src} --chown=0:0 /shvr/checksums/build /shvr/checksums/build"
i=$((i + 1))
done
echo 'FROM scratch AS collected'
c=0
while [ "$c" -le "$last_chunk" ]; do
echo "COPY --from=chunk_${c} --chown=0:0 / /"
c=$((c + 1))
done
echo "FROM ${RUNTIME_REF}"
echo 'COPY "entrypoint.sh" "/opt/shvr/entrypoint.sh"'
echo 'ENV SHVR_DIR_OUT=/opt'
echo 'WORKDIR /'
echo 'COPY --from=collected --chown=0:0 /opt /opt'
echo 'COPY --from=collected --chown=0:0 /deps /'
echo 'COPY --from=collected --chown=0:0 /shvr/checksums/build /opt/shvr/checksums/build'
echo "RUN find /opt \( -type l -o -type f \) -not -path '*/shvr/*' | sort > /opt/shvr/manifest.txt"
echo 'ENTRYPOINT [ "/bin/sh", "/opt/shvr/entrypoint.sh" ]'
} > Dockerfile.assembly
- name: Build and push assembled image
uses: ./.github/actions/build-push-retry
with:
context: .
file: Dockerfile.assembly
platforms: linux/${{ matrix.arch }}
push: "true"
tags: ${{ env.ASSEMBLY_TAG }}
- name: Verify build checksums
uses: ./.github/actions/verify-build-checksums
with:
arch: ${{ matrix.arch }}
image: ${{ env.ASSEMBLY_TAG }}
targets: ${{ env.ALL_TARGETS }}
- name: Generate and upload build checksums
if: github.event_name == 'push' && matrix.flavor == 'all'
uses: ./.github/actions/generate-build-checksums
with:
arch: ${{ matrix.arch }}
image: ${{ env.ASSEMBLY_TAG }}
artifact_name: shvr-build-checksums-${{ matrix.arch }}
# Fuse the two per-arch flavor images (:<flavor>-amd64 + :<flavor>-arm64) into
# the published multi-arch manifest list :<flavor>. This is the only point the
# user-facing :latest / :all become multi-arch; `docker pull` then selects the
# native arch per platform (arm64 on Apple Silicon, no --platform, no Rosetta).
# imagetools create is a registry-side op on the preinstalled default builder,
# so no buildx container is booted (same pattern as the mirror job).
assemble-manifest:
needs: [assemble]
if: always() && needs.assemble.result == 'success'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
flavor: [latest, all]
steps:
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
if: github.event_name == 'push'
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Fuse per-arch flavors into a multi-arch manifest list
env:
FLAVOR: ${{ matrix.flavor }}
RUN_ID: ${{ github.run_id }}
REPO: ${{ github.repository }}
EVENT: ${{ github.event_name }}
run: |
set -eu
retry() { for a in 1 2 3; do "$@" && return 0; echo "retry $a: $*" >&2; sleep 5; done; return 1; }
# Sources are always the GHCR per-arch flavor images (the only place
# they are pushed). On push the destination is the user-facing Docker
# Hub :<flavor> list — imagetools create copies the source arch
# manifests into the Hub repo by digest. On a PR everything stays in a
# run-scoped GHCR tag.
if [ "$EVENT" = "push" ]; then
target="alganet/shell-versions:${FLAVOR}"
amd="ghcr.io/${REPO}:${FLAVOR}-amd64"
arm="ghcr.io/${REPO}:${FLAVOR}-arm64"
else
target="ghcr.io/${REPO}:run-${RUN_ID}-${FLAVOR}"
amd="ghcr.io/${REPO}:run-${RUN_ID}-${FLAVOR}-amd64"
arm="ghcr.io/${REPO}:run-${RUN_ID}-${FLAVOR}-arm64"
fi
retry docker buildx imagetools create --tag "$target" "$amd" "$arm"
# Sanity: the list must advertise both platforms.
docker buildx imagetools inspect "$target"
# Publish a multi-arch manifest list for EVERY per-target image so users can
# `docker pull .../<shell>_<version>` and get their native arch. The per-arch
# :<target>-<arch> images stay (the plan job reads their arch-specific OID and
# the assembly copies from them); this only adds the fused :<target> list on
# top, for both registries. Push only — on a PR the per-target images are
# run-scoped and ephemeral, so there is nothing stable to fuse. Runs even when
# build was skipped (0 changed): unchanged targets keep both arch images from a
# prior push and re-fusing is idempotent (identical source digests).
target-manifest:
needs: [plan, build-amd64, build-arm64]
if: >-
always()
&& github.event_name == 'push'
&& needs.plan.result == 'success'
&& needs['build-amd64'].result != 'failure'
&& needs['build-amd64'].result != 'cancelled'
&& needs['build-arm64'].result != 'failure'
&& needs['build-arm64'].result != 'cancelled'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Fuse per-target arch images into multi-arch manifest lists
env:
REPO: ${{ github.repository }}
run: |
set -eu
# One job per (destination, target); fuse in parallel with retry. The
# SOURCES are always the GHCR :<target>-<arch> images (the only place
# the per-arch images are pushed); the destination is either the GHCR
# or the Docker Hub :<target> list. imagetools create copies the source
# arch manifests into the Hub repo (by digest, untagged) when building
# the Hub list, so the multi-arch pull works without ever tagging the
# arch images on Hub. A missing source fails the target (and the job),
# which is correct — a per-arch image always exists after a successful
# build (or is reused from a prior push).
: > /tmp/fuse_jobs
for t in $(sh shvr.sh targets); do
printf 'ghcr.io/%s|%s\n' "$REPO" "$t" >> /tmp/fuse_jobs
printf 'alganet/shell-versions|%s\n' "$t" >> /tmp/fuse_jobs
done
fuse_one() {
dest="${1%%|*}"; t="${1##*|}"
amd="ghcr.io/${REPO}:${t}-amd64"
arm="ghcr.io/${REPO}:${t}-arm64"
for a in 1 2 3; do
if docker buildx imagetools create --tag "${dest}:${t}" "$amd" "$arm"; then return 0; fi
echo "retry $a: ${dest}:${t}" >&2; sleep 5
done
echo "FAILED ${dest}:${t}" >&2; return 1
}
export -f fuse_one
xargs -P 8 -a /tmp/fuse_jobs -I {} bash -c 'fuse_one "$@"' _ {}
printf 'fused %s per-target manifest list(s) across 2 registries (sources: GHCR)\n' \
"$(sh shvr.sh targets | wc -l)" >> "$GITHUB_STEP_SUMMARY"