Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions .github/actions/setup-bubblewrap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# setup-bubblewrap

Installs [bubblewrap](https://github.com/containers/bubblewrap) so melange can
use its default Linux runner, and nothing else.

## Why this exists separately

melange resolves an empty `--runner` to bubblewrap on Linux, and then requires
`bwrap` on `PATH` plus a working `bwrap --unshare-user` before it will build.
Neither holds on a stock `ubuntu-24.04` runner: bubblewrap is not in the image,
and 24.04 blocks unprivileged user namespaces through apparmor
([melange#1508](https://github.com/chainguard-dev/melange/issues/1508)).

`chainguard-dev/actions/setup-melange` fixes both, but also installs a Go
toolchain, `build-essential` and `qemu-user-static`, and finishes by
`curl`-installing an unverified melange tarball into `/usr/local/bin`. In this
repo melange is installed by [`setup-chainguard-tool`](../setup-chainguard-tool),
which verifies the release checksums against their Sigstore signature and asserts
the binary reports the pinned commit — so the only piece worth keeping is the
sandbox setup. The Go toolchain only matters for `version: tip` (building melange
from source) and qemu only for emulating a foreign arch, which the `apks` jobs
never do: each runs natively on a runner of its own arch.

## Usage

```yaml
- name: Install melange
uses: ./.github/actions/setup-chainguard-tool
with:
tool: melange
version: v0.41.1
# sudo's secure_path is what `sudo melange` searches.
install-dir: /usr/local/bin

- name: Setup bubblewrap
uses: ./.github/actions/setup-bubblewrap
```

No inputs, no outputs. The two steps are lifted from `setup-melange`, with one
change: `apt-get install` runs against the image's existing apt lists and only
falls back to `apt-get update` if they are too stale. Upstream needs no fallback
because its qemu step already ran `apt update` — the step that stalled this job
for 20+ minutes when the runner's Azure mirror was degraded.

Verification is upstream's `bwrap --unshare-user --bind / / true`, unprivileged.
That is stricter than our builds need — `melange-build-pkg` runs
`sudo melange build`, and as root melange never asks for a uid map, so apparmor's
userns restriction does not apply — but it fails at setup rather than mid-build.
13 changes: 13 additions & 0 deletions .github/actions/setup-bubblewrap/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: "Setup bubblewrap"
description: >
Install bubblewrap and the apparmor profile melange's default Linux runner
needs on a GitHub-hosted Ubuntu runner, without the Go toolchain, qemu and
unverified melange download that chainguard-dev/actions/setup-melange also
brings. Pair it with setup-chainguard-tool, which installs melange itself.

runs:
using: composite
steps:
- name: Install bubblewrap
shell: bash
run: "${{ github.action_path }}/scripts/setup-bubblewrap.sh"
37 changes: 37 additions & 0 deletions .github/actions/setup-bubblewrap/scripts/setup-bubblewrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Install bubblewrap, melange's default Linux runner, which the runner image does
# not ship. Adapted from chainguard-dev/actions/setup-melange, minus the Go
# toolchain, qemu and the unverified melange download — see the README.
set -euo pipefail

# Only refresh the index if the image's apt lists turn out to be too stale: that
# update is the mirror round trip that has stalled this job for 20+ minutes.
sudo apt-get install --assume-yes bubblewrap \
|| { sudo apt-get update && sudo apt-get install --assume-yes bubblewrap; }

# https://github.com/chainguard-dev/melange/issues/1508
sudo tee /etc/apparmor.d/local-bwrap >/dev/null <<"EOF"
abi <abi/4.0>,
include <tunables/global>

profile local-bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/bwrap>
}
EOF

sudo systemctl reload apparmor

if ! bwrap --unshare-user --bind / / true; then
echo "::error::failed to verify 'bwrap --unshare-user'"
command -v bwrap || :
ls /proc/self/ns || echo "no /proc/self/ns"
kver="$(uname -r)" || echo "uname -r failed"
if [ -f "/boot/config-${kver}" ]; then
grep CONFIG_USER_NS "/boot/config-${kver}" || echo "no CONFIG_USER_NS in /boot/config-${kver}"
fi
exit 1
fi

echo "bubblewrap (bwrap) installed successfully."
14 changes: 8 additions & 6 deletions .github/actions/setup-chainguard-tool/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# setup-chainguard-tool

Local composite action that installs a Chainguard Go CLI — [apko](https://github.com/chainguard-dev/apko)
or [melange](https://github.com/chainguard-dev/melange) — on a `linux/amd64`
GitHub-hosted runner.
or [melange](https://github.com/chainguard-dev/melange) — on a `linux/amd64` or
`linux/arm64` GitHub-hosted runner. Both are needed: the `apks` jobs run
natively on a runner of the arch they build.

There is no first-party `chainguard-dev/actions/setup-apko` upstream, and
`curl | sh` is not a supply-chain story we want to tell. So we ship our own
installer that:

1. Resolves the requested version (or `latest`) to an immutable release tag
and dereferences it to the commit SHA the tag points at.
2. Downloads `<tool>_<version>_linux_amd64.tar.gz` + `checksums.txt` from the
release.
2. Downloads `<tool>_<version>_linux_<runner arch>.tar.gz` + `checksums.txt`
from the release.
3. Verifies `checksums.txt` was signed by that project's release workflow using
Sigstore keyless (cosign `verify-blob` against Fulcio + Rekor).
4. Verifies the SHA-256 of the archive matches the entry in `checksums.txt`.
Expand All @@ -23,7 +24,7 @@ installer that:
## Why one action for both

Chainguard's CLIs are goreleaser-built to the same shape — the archive is
`<tool>_<version>_linux_amd64.tar.gz`, it sits beside a cosign-signed
`<tool>_<version>_linux_<arch>.tar.gz`, it sits beside a cosign-signed
`checksums.txt`, and `<tool> version` prints `GitVersion:` / `GitCommit:`. Only
the name varies, so the tool is an input rather than a second copy of the
scripts.
Expand Down Expand Up @@ -61,7 +62,7 @@ This downloads one verified binary instead.
| `verify-signature` | `"true"` | Verify the Sigstore signature of `checksums.txt`. |
| `cosign-certificate-identity-regexp` | the tool's own release workflows | Cosign identity regex for the release signer. |
| `cosign-oidc-issuer` | `https://token.actions.githubusercontent.com` | Cosign OIDC issuer. |
| `install-dir` | `<runner-tool-cache>/<tool>/<tag>/amd64` | Where to install the binary. |
| `install-dir` | `<runner-tool-cache>/<tool>/<tag>/<arch>` | Where to install the binary. |

## Outputs

Expand All @@ -70,6 +71,7 @@ This downloads one verified binary instead.
| `version` | Installed version without the leading `v` (e.g. `1.2.30`). |
| `tag` | Release tag installed (e.g. `v1.2.30`). |
| `commit-sha` | Commit SHA the release tag dereferences to (also verified against the binary). |
| `arch` | Release architecture installed for this runner (`amd64` or `arm64`). |
| `sha256` | SHA-256 of the downloaded archive. |
| `path` | Full path to the installed binary. |
| `install-dir` | Directory added to `PATH`. |
Expand Down
11 changes: 7 additions & 4 deletions .github/actions/setup-chainguard-tool/action.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: "Setup Chainguard tool"
description: >
Install a Chainguard Go CLI (apko, melange, ...) on a linux/amd64 runner.
Resolves the requested version (or the latest release) to an immutable
release tag + commit sha1, verifies the downloaded archive against the
release SHA-256 checksums and their Sigstore (cosign keyless) signature,
Install a Chainguard Go CLI (apko, melange, ...) on a linux/amd64 or
linux/arm64 runner. Resolves the requested version (or the latest release) to
an immutable release tag + commit sha1, verifies the downloaded archive against
the release SHA-256 checksums and their Sigstore (cosign keyless) signature,
then verifies the installed binary reports the expected tag and commit.

# One action for every Chainguard CLI: their releases are goreleaser-built to
Expand Down Expand Up @@ -68,6 +68,9 @@ outputs:
commit-sha:
description: Commit sha1 the release tag points at, also verified against the installed binary.
value: ${{ steps.resolve.outputs.commit-sha }}
arch:
description: Release architecture installed for this runner, amd64 or arm64.
value: ${{ steps.resolve.outputs.arch }}
sha256:
description: SHA-256 checksum of the downloaded release archive.
value: ${{ steps.install.outputs.sha256 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ if [ -z "${BINARY}" ]; then
exit 1
fi

mkdir -p "${INSTALL_DIR}"
install -m 0755 "${BINARY}" "${INSTALL_DIR}/${TOOL}"
# A root-owned install-dir is deliberate: `sudo melange` (what melange-build-pkg
# runs) searches sudo's secure_path, which the tool cache is not in.
SUDO=()
mkdir -p "${INSTALL_DIR}" 2>/dev/null || true
if [ ! -w "${INSTALL_DIR}" ]; then
SUDO=(sudo)
"${SUDO[@]}" mkdir -p "${INSTALL_DIR}"
fi
"${SUDO[@]}" install -m 0755 "${BINARY}" "${INSTALL_DIR}/${TOOL}"
echo "${INSTALL_DIR}" >> "${GITHUB_PATH}"

{
Expand Down
26 changes: 19 additions & 7 deletions .github/actions/setup-chainguard-tool/scripts/resolve-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
# installed.
#
# Inputs (env): TOOL, REPOSITORY, VERSION, EXPECTED_COMMIT_SHA, INSTALL_DIR_INPUT, GH_TOKEN
# Outputs (GITHUB_OUTPUT): tag, version, commit-sha, archive, install-dir
# Outputs (GITHUB_OUTPUT): tag, version, commit-sha, arch, archive, install-dir
set -euo pipefail

# This action only ships the linux/amd64 release archive.
if [ "$(uname -s)" != "Linux" ] || [ "$(uname -m)" != "x86_64" ]; then
echo "::error::setup-chainguard-tool only supports linux/amd64 runners (got $(uname -s)/$(uname -m))"
# The runner's own architecture, not the image's: this picks which release
# archive to download. `apks` jobs run natively on an arm64 runner, so hardcoding
# amd64 here fails half the matrix. RUNNER_ARCH is what GitHub sets; uname is the
# fallback for local runs, and both spellings of each arch are accepted.
if [ "$(uname -s)" != "Linux" ]; then
echo "::error::setup-chainguard-tool only supports Linux runners (got $(uname -s))"
exit 1
fi
case "${RUNNER_ARCH:-$(uname -m)}" in
X64 | x86_64 | amd64) ARCH="amd64" ;;
ARM64 | aarch64 | arm64) ARCH="arm64" ;;
*)
echo "::error::setup-chainguard-tool has no release archive for ${RUNNER_ARCH:-$(uname -m)}"
exit 1
;;
esac

# Resolve the requested version to an immutable release tag.
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then
Expand Down Expand Up @@ -51,15 +62,16 @@ fi

INSTALL_DIR="${INSTALL_DIR_INPUT}"
if [ -z "${INSTALL_DIR}" ]; then
INSTALL_DIR="${RUNNER_TOOL_CACHE:-${HOME}/.cache}/${TOOL}/${TAG}/amd64"
INSTALL_DIR="${RUNNER_TOOL_CACHE:-${HOME}/.cache}/${TOOL}/${TAG}/${ARCH}"
fi

echo "${TOOL} ${TAG} (${COMMIT_SHA}) for linux/amd64 -> ${INSTALL_DIR}"
echo "${TOOL} ${TAG} (${COMMIT_SHA}) for linux/${ARCH} -> ${INSTALL_DIR}"

{
echo "tag=${TAG}"
echo "version=${VERSION_NUMBER}"
echo "commit-sha=${COMMIT_SHA}"
echo "archive=${TOOL}_${VERSION_NUMBER}_linux_amd64.tar.gz"
echo "arch=${ARCH}"
echo "archive=${TOOL}_${VERSION_NUMBER}_linux_${ARCH}.tar.gz"
echo "install-dir=${INSTALL_DIR}"
} >> "${GITHUB_OUTPUT}"
127 changes: 127 additions & 0 deletions .github/actions/setup-chainguard-tool/tests/test-resolve-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
#
# Tests for setup-chainguard-tool's resolve-release.sh.
# No network: `gh` is stubbed on PATH.
#
# The arch mapping is the point: it only runs on the runner it resolves for, so an
# amd64-only assumption passes every local check and every x86_64 job, and fails
# exactly one thing — the aarch64 `apks` job in the nightly.
# Silent on success; prints only failures.
set -euo pipefail

SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/resolve-release.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "${TMP}"' EXIT

checks=0
fails=0
fail() { fails=$((fails + 1)); echo " FAIL $1" >&2; }
assert_eq() {
checks=$((checks + 1))
if [ "$2" != "$3" ]; then
echo " FAIL $1: want '$2', got '$3'" >&2
fails=$((fails + 1))
fi
}

mkdir -p "${TMP}/bin"
cat > "${TMP}/bin/gh" <<'STUB'
#!/usr/bin/env bash
case "$*" in
*"git/ref/tags/"*) echo "${STUB_OBJECT_TYPE:-commit} ${STUB_SHA:-c0ffee1111111111111111111111111111111111}" ;;
*"git/tags/"*) echo "${STUB_DEREF_SHA:-deadbeef2222222222222222222222222222222}" ;;
*"release view"*) echo "${STUB_LATEST_TAG:-v9.9.9}" ;;
*) echo "unexpected gh call: $*" >&2; exit 1 ;;
esac
STUB
chmod +x "${TMP}/bin/gh"
export PATH="${TMP}/bin:${PATH}"

# Echoes the script's `key=value` outputs. The status is returned explicitly:
# errexit is suppressed inside an `if` condition, so the trailing `cat` would
# otherwise report success for a failed script.
resolve() {
local out="${TMP}/out" rc=0
: > "${out}"
env GITHUB_OUTPUT="${out}" \
RUNNER_TOOL_CACHE="${TMP}/tool-cache" \
TOOL="${TOOL:-melange}" \
REPOSITORY="${REPOSITORY:-chainguard-dev/melange}" \
VERSION="${VERSION:-v0.41.1}" \
EXPECTED_COMMIT_SHA="${EXPECTED_COMMIT_SHA:-}" \
INSTALL_DIR_INPUT="${INSTALL_DIR_INPUT:-}" \
"$@" "${SCRIPT}" >/dev/null || rc=$?
cat "${out}"
return "${rc}"
}
value() { sed -n "s/^$2=//p" <<<"$1"; }

# --- arch mapping -----------------------------------------------------------
# Both spellings reach the script: GitHub sets RUNNER_ARCH=X64/ARM64, a local run
# falls back to uname's x86_64/aarch64.
for arch in X64 x86_64 amd64; do
out="$(resolve RUNNER_ARCH="${arch}")"
assert_eq "${arch} resolves to amd64" "amd64" "$(value "${out}" arch)"
assert_eq "${arch} picks the amd64 archive" \
"melange_0.41.1_linux_amd64.tar.gz" "$(value "${out}" archive)"
done

for arch in ARM64 aarch64 arm64; do
out="$(resolve RUNNER_ARCH="${arch}")"
assert_eq "${arch} resolves to arm64" "arm64" "$(value "${out}" arch)"
assert_eq "${arch} picks the arm64 archive" \
"melange_0.41.1_linux_arm64.tar.gz" "$(value "${out}" archive)"
done

# Per-arch, so a cached amd64 binary can never be served to an arm64 job.
out="$(resolve RUNNER_ARCH=ARM64)"
assert_eq "install dir is arch-scoped" \
"${TMP}/tool-cache/melange/v0.41.1/arm64" "$(value "${out}" install-dir)"

checks=$((checks + 1))
if resolve RUNNER_ARCH=ARMV7 >/dev/null 2>&1; then
fail "an arch with no release archive should exit non-zero"
fi

# --- version + tag resolution -----------------------------------------------
out="$(TOOL=apko REPOSITORY=chainguard-dev/apko VERSION=1.2.30 resolve RUNNER_ARCH=X64)"
assert_eq "a version without the v still tags" "v1.2.30" "$(value "${out}" tag)"
assert_eq "version output drops the v" "1.2.30" "$(value "${out}" version)"
assert_eq "tool name drives the archive prefix" \
"apko_1.2.30_linux_amd64.tar.gz" "$(value "${out}" archive)"

out="$(VERSION=latest resolve RUNNER_ARCH=X64 STUB_LATEST_TAG=v1.2.31)"
assert_eq "latest resolves through gh release view" "v1.2.31" "$(value "${out}" tag)"

out="$(resolve RUNNER_ARCH=X64 STUB_OBJECT_TYPE=commit STUB_SHA=abc1234567890def1234567890abcdef12345678)"
assert_eq "a lightweight tag is used as-is" \
"abc1234567890def1234567890abcdef12345678" "$(value "${out}" commit-sha)"

# An annotated tag's ref points at the tag object, not the commit, so the pin
# would otherwise be compared against a sha `uses:` never sees.
out="$(resolve RUNNER_ARCH=X64 STUB_OBJECT_TYPE=tag \
STUB_DEREF_SHA=99991234567890def1234567890abcdef1234567)"
assert_eq "an annotated tag is dereferenced" \
"99991234567890def1234567890abcdef1234567" "$(value "${out}" commit-sha)"

# --- pins -------------------------------------------------------------------
checks=$((checks + 1))
if ! EXPECTED_COMMIT_SHA=abc1234 resolve RUNNER_ARCH=X64 \
STUB_SHA=abc1234567890def1234567890abcdef12345678 >/dev/null 2>&1; then
fail "an abbreviated expected-commit-sha should match its full sha"
fi

checks=$((checks + 1))
if EXPECTED_COMMIT_SHA=0000000 resolve RUNNER_ARCH=X64 >/dev/null 2>&1; then
fail "a mismatched expected-commit-sha should exit non-zero"
fi

# --- explicit install dir ---------------------------------------------------
out="$(INSTALL_DIR_INPUT=/usr/local/bin resolve RUNNER_ARCH=X64)"
assert_eq "an explicit install-dir wins" "/usr/local/bin" "$(value "${out}" install-dir)"

if [ "${fails}" -ne 0 ]; then
echo "resolve-release: ${fails}/${checks} failed" >&2
exit 1
fi
echo "resolve-release: ${checks} checks passed"
Loading
Loading