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
19 changes: 11 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ permissions:
contents: read

jobs:
xh:
name: xh / ${{ matrix.arch }}
image:
name: ${{ matrix.image }} / ${{ matrix.arch }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
image:
- age
- xh
arch:
- amd64
- arm64
Expand All @@ -28,7 +31,7 @@ jobs:

- name: Read metadata
id: meta
run: python3 scripts/meta.py xh
run: python3 scripts/meta.py "${{ matrix.image }}"

- name: Set up QEMU
uses: docker/setup-qemu-action@v4
Expand All @@ -40,17 +43,17 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: images/xh/Dockerfile
file: images/${{ matrix.image }}/Dockerfile
platforms: linux/${{ matrix.arch }}
load: true
tags: tiny/xh:test
tags: tiny/${{ matrix.image }}:test
build-args: |
VERSION=${{ steps.meta.outputs.version }}
REVISION=${{ github.sha }}
SHA256_AMD64=${{ steps.meta.outputs.sha_amd64 }}
SHA256_ARM64=${{ steps.meta.outputs.sha_arm64 }}
cache-from: type=gha,scope=xh-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=xh-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}-${{ matrix.arch }}

- name: Test
run: images/xh/test.sh tiny/xh:test
run: images/${{ matrix.image }}/test.sh tiny/${{ matrix.image }}:test
52 changes: 34 additions & 18 deletions .github/workflows/upstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@ on:
permissions:
contents: read

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

jobs:
xh:
image:
name: ${{ matrix.image }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
image:
- age
- xh

concurrency:
group: upstream-${{ matrix.image }}
cancel-in-progress: false

steps:
- name: Checkout
uses: actions/checkout@v7
Expand All @@ -25,23 +33,27 @@ jobs:

- name: Current version
id: meta
run: python3 scripts/meta.py xh
run: python3 scripts/meta.py "${{ matrix.image }}"

- name: Check upstream
id: upstream
uses: actions/github-script@v9
env:
CURRENT_VERSION: ${{ steps.meta.outputs.version }}
IMAGE: ${{ matrix.image }}
UPSTREAM_REPOSITORY: ${{ steps.meta.outputs.upstream }}
with:
script: |
const [owner, repo] = process.env.UPSTREAM_REPOSITORY.split("/");
const { data: release } =
await github.rest.repos.getLatestRelease({
owner: "ducaale",
repo: "xh"
owner,
repo
});

const upstream = release.tag_name.replace(/^v/, "");
const current = process.env.CURRENT_VERSION;
const image = process.env.IMAGE;

if (!/^\d+\.\d+\.\d+$/.test(upstream)) {
throw new Error(`Unexpected upstream version: ${upstream}`);
Expand All @@ -51,25 +63,28 @@ jobs:
core.setOutput("update", String(upstream !== current));

if (upstream === current) {
console.log(`xh ${current} is current`);
console.log(`${image} ${current} is current`);
return;
}

console.log(`xh ${upstream} is available; current version is ${current}`);
console.log(
`${image} ${upstream} is available; current version is ${current}`
);

- name: Update manifest
if: steps.upstream.outputs.update == 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
IMAGE: ${{ matrix.image }}
VERSION: ${{ steps.upstream.outputs.version }}
run: |
set -euo pipefail

python3 scripts/update.py xh "$VERSION"
python3 scripts/meta.py xh
python3 scripts/update.py "$IMAGE" "$VERSION"
python3 scripts/meta.py "$IMAGE"

changes="$(git status --short)"
if [[ "$changes" != " M images/xh/image.toml" ]]; then
if [[ "$changes" != " M images/${IMAGE}/image.toml" ]]; then
echo "Unexpected update result:"
printf '%s\n' "$changes"
exit 1
Expand All @@ -94,11 +109,12 @@ jobs:
BASE_BRANCH: ${{ github.event.repository.default_branch }}
CURRENT_VERSION: ${{ steps.meta.outputs.version }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
IMAGE: ${{ matrix.image }}
VERSION: ${{ steps.upstream.outputs.version }}
run: |
set -euo pipefail

branch="automation/xh-v${VERSION}"
branch="automation/${IMAGE}-v${VERSION}"
existing_pr="$(
gh pr list \
--repo "$GITHUB_REPOSITORY" \
Expand All @@ -120,8 +136,8 @@ jobs:
git config user.email "${bot_id}+${bot}@users.noreply.github.com"
gh auth setup-git
git switch -c "$branch"
git add -- images/xh/image.toml
git commit -m "Update xh to ${VERSION}"
git add -- "images/${IMAGE}/image.toml"
git commit -m "Update ${IMAGE} to ${VERSION}"

remote_sha="$(
git ls-remote --heads origin "refs/heads/${branch}" | cut -f1
Expand All @@ -138,7 +154,7 @@ jobs:

body="$(
printf '%s\n\n' \
"Updates xh from ${CURRENT_VERSION} to upstream release v${VERSION}."
"Updates ${IMAGE} from ${CURRENT_VERSION} to upstream release v${VERSION}."
printf '%s\n' \
"The release asset digests were obtained and verified by scripts/update.py."
)"
Expand All @@ -147,5 +163,5 @@ jobs:
--repo "$GITHUB_REPOSITORY" \
--base "$BASE_BRANCH" \
--head "$branch" \
--title "Update xh to ${VERSION}" \
--title "Update ${IMAGE} to ${VERSION}" \
--body "$body"
11 changes: 6 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ image-specific behavior under `images/<tool>/` and reusable orchestration under
Do not add another CLI image without prior discussion. The project deliberately
avoids tools that already have a strong official or community image.

## Updating xh
## Updating an image

Use the update script with a stable upstream release version:

```sh
python3 scripts/update.py age 1.3.0
python3 scripts/update.py xh 0.26.2
```

Review the resulting `images/xh/image.toml` diff. Never use placeholder or
unverified checksums.
Review the resulting `images/<tool>/image.toml` diff. Never use placeholder
or unverified checksums.

## Validation

Before submitting a pull request:

1. Run `python3 scripts/meta.py xh`.
1. Run `python3 scripts/meta.py <tool>`.
2. Build both `linux/amd64` and `linux/arm64` images.
3. Run `images/xh/test.sh <local-image>` for each architecture.
3. Run `images/<tool>/test.sh <local-image>` for each architecture.
4. Confirm that no unrelated files or generated artifacts are included.

Pull requests must not publish images. Production publication occurs only from
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ Minimal OCI images for useful command-line tools.

| Image | amd64 | arm64 | Runtime |
|---|---|---|---|
| age | ✓ | ✓ | scratch |
| xh | ✓ | ✓ | scratch |

## age

```sh
docker run --rm ghcr.io/unitmatrix/age:1.3.0 --version
```

See [the age image documentation](images/age/README.md) for usage and release
details.

## xh

```sh
Expand Down
51 changes: 32 additions & 19 deletions docs/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
Tiny Images publishes small, secure OCI images for useful command-line tools
that do not already have a strong official or established community image.

The first supported tool is `xh`. Do not add another tool without an explicit
project decision. Common tools whose OCI distribution is already well covered,
including jq, yq, crane, oras, cosign, kubectl, Helm, and Git, are intentionally
out of scope.
The supported tools are `age` and `xh`. Do not add another tool without an
explicit project decision. Common tools whose OCI distribution is already well
covered, including jq, yq, crane, oras, cosign, kubectl, Helm, and Git, are
intentionally out of scope.

## Repository model

Expand Down Expand Up @@ -36,6 +36,11 @@ duplication.
├── docs/
│ └── PROJECT.md
├── images/
│ ├── age/
│ │ ├── image.toml
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── test.sh
│ └── xh/
│ ├── image.toml
│ ├── Dockerfile
Expand Down Expand Up @@ -92,29 +97,37 @@ ENTRYPOINT=["/xh"]

No shell or package manager belongs in the final image.

## age image

Upstream is [FiloSottile/age](https://github.com/FiloSottile/age). The image
consumes the upstream static Linux release archives for `amd64` and `arm64`.
It includes the `age`, `age-keygen`, `age-inspect`, and
`age-plugin-batchpass` binaries shipped in those archives, with `/age` as the
entrypoint. The final image uses `scratch` and runs as UID/GID `65532:65532`.

## Metadata and updates

`scripts/meta.py` reads `images/<tool>/image.toml` and exposes the name,
version, upstream repository, platform targets, and checksums to GitHub
Actions.

`scripts/update.py xh <version>` queries the GitHub Releases API, rejects
missing, draft, or prerelease releases, requires both expected musl artifacts
and valid SHA-256 digests, and updates only the relevant values in
`images/xh/image.toml`.

The initial updater may contain xh-specific release knowledge. A complex asset
template system is intentionally deferred until more images reveal common
requirements.
`scripts/update.py <tool> <version>` queries the GitHub Releases API, rejects
missing, draft, or prerelease releases, requires both expected platform
artifacts and valid SHA-256 digests, and updates only the relevant values in
`images/<tool>/image.toml`. Both current upstreams name release archives as
`<tool>-v<version>-<target>.tar.gz`, so no more general asset-template system is
needed yet.

## Continuous integration

Pull requests that affect images, scripts, or workflows build and exercise both
`linux/amd64` and `linux/arm64`, using QEMU where necessary. Builds verify
upstream checksums and run deterministic smoke tests without pushing images.
Pull requests that affect images, scripts, or workflows build and exercise each
image on both `linux/amd64` and `linux/arm64`, using QEMU where necessary.
Builds verify upstream checksums and run deterministic smoke tests without
pushing images.

Smoke tests cover at least `xh --version` and `xh --help`. Network integration
tests should remain separate where practical.
Smoke tests cover at least `<tool> --version` and `<tool> --help`, plus
deterministic image-specific behavior. Network integration tests should remain
separate where practical.

## Releases

Expand Down Expand Up @@ -150,8 +163,8 @@ should ultimately be pinned to full commit SHAs.

## Upstream detection

The scheduled upstream workflow detects new stable xh releases but never
publishes them directly. The intended flow is:
The scheduled upstream workflow detects new stable releases for every supported
image but never publishes them directly. The intended flow is:

```text
upstream release
Expand Down
72 changes: 72 additions & 0 deletions images/age/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# syntax=docker/dockerfile:1

FROM alpine:3.24.1 AS fetch

ARG VERSION
ARG TARGETARCH
ARG SHA256_AMD64
ARG SHA256_ARM64

RUN apk add --no-cache \
ca-certificates \
curl

RUN set -eux; \
case "${TARGETARCH}" in \
amd64) \
target="linux-amd64"; \
expected="${SHA256_AMD64}" \
;; \
arm64) \
target="linux-arm64"; \
expected="${SHA256_ARM64}" \
;; \
*) \
echo "Unsupported architecture: ${TARGETARCH}" >&2; \
exit 1 \
;; \
esac; \
archive="age-v${VERSION}-${target}.tar.gz"; \
url="https://github.com/FiloSottile/age/releases/download/v${VERSION}/${archive}"; \
curl --fail --silent --show-error --location \
"${url}" \
--output /tmp/age.tar.gz; \
echo "${expected} /tmp/age.tar.gz" | sha256sum -c -; \
mkdir -p /tmp/extract; \
tar -xzf /tmp/age.tar.gz -C /tmp/extract; \
root="/tmp/extract/age"; \
mkdir -p \
/rootfs/licenses/age \
/rootfs/tmp \
/rootfs/work; \
for binary in age age-inspect age-keygen age-plugin-batchpass; do \
cp "${root}/${binary}" "/rootfs/${binary}"; \
chmod 0555 "/rootfs/${binary}"; \
done; \
cp "${root}/LICENSE" /rootfs/licenses/age/LICENSE; \
chmod 1777 /rootfs/tmp; \
chown 65532:65532 /rootfs/work


FROM scratch

ARG VERSION
ARG REVISION

COPY --from=fetch /rootfs /

LABEL org.opencontainers.image.title="age"
LABEL org.opencontainers.image.description="Minimal OCI image for age"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.revision="${REVISION}"
LABEL org.opencontainers.image.source="https://github.com/FiloSottile/age"
LABEL org.opencontainers.image.licenses="BSD-3-Clause"

USER 65532:65532

ENV HOME=/tmp
ENV PATH=/

WORKDIR /work

ENTRYPOINT ["/age"]
Loading