Skip to content
Draft
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
86 changes: 86 additions & 0 deletions .github/actions/build-target/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: build-target
description: >-
Build the runner+run binaries for a single target triple, package them into a
flat tar.gz matching the existing binstall layout, and compute a SHA256
sidecar. action.yml is wiring only — pack logic lives in pack.sh.

inputs:
triple:
description: Rust target triple (e.g. x86_64-unknown-linux-gnu)
required: true
kind:
description: Build kind — cargo | cross | cargo-build-std | cargo-cross-toolchain
required: true
version:
description: Crate version without leading v (e.g. 0.8.0)
required: true
binaries:
description: Space-separated list of [[bin]] names to ship
required: false
default: runner run

outputs:
archive:
description: Path to the produced .tar.gz
value: ${{ steps.pack.outputs.archive }}
sha256:
description: Path to the SHA256 sidecar
value: ${{ steps.pack.outputs.sha256 }}
bin-dir:
description: Directory containing the unpacked binaries (for further packaging)
value: ${{ steps.pack.outputs.bin-dir }}

runs:
using: composite
steps:
- name: Bootstrap rustup on windows-11-arm
if: runner.os == 'Windows' && runner.arch == 'ARM64'
shell: pwsh
run: |
if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) {
Invoke-WebRequest -Uri 'https://static.rust-lang.org/rustup/dist/aarch64-pc-windows-msvc/rustup-init.exe' -OutFile rustup-init.exe
.\rustup-init.exe --default-toolchain none -y --profile minimal
"$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding ascii $env:GITHUB_PATH
}

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ inputs.kind == 'cargo-build-std' && 'nightly' || 'stable' }}
target: ${{ inputs.triple }}
components: ${{ inputs.kind == 'cargo-build-std' && 'rust-src' || '' }}
cache-key: ${{ inputs.triple }}
cache-on-failure: "true"

- name: Install cross
if: inputs.kind == 'cross' || inputs.kind == 'cargo-cross-toolchain'
uses: taiki-e/install-action@v2
with:
tool: cross@0.2.5

- name: cargo build
if: inputs.kind == 'cargo'
shell: bash
run: cargo build --release --locked --target "${{ inputs.triple }}" --bins

- name: cross build
if: inputs.kind == 'cross' || inputs.kind == 'cargo-cross-toolchain'
shell: bash
run: cross build --release --locked --target "${{ inputs.triple }}" --bins

- name: cargo +nightly -Z build-std
if: inputs.kind == 'cargo-build-std'
shell: bash
run: |
cargo +nightly build --release --locked \
-Z build-std=std,panic_abort \
--target "${{ inputs.triple }}" --bins

- name: Pack archive
id: pack
shell: bash
env:
TRIPLE: ${{ inputs.triple }}
VERSION: ${{ inputs.version }}
BINARIES: ${{ inputs.binaries }}
run: bash "$GITHUB_ACTION_PATH/pack.sh"
48 changes: 48 additions & 0 deletions .github/actions/build-target/pack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Stage the just-built binaries into a flat tar.gz matching the binstall
# `pkg-fmt = "tgz"` + `bin-dir = "{bin}{binary-ext}"` contract in Cargo.toml.
# Always emits .tar.gz — install.sh and action.yml are the only consumers
# and both fetch .tar.gz only. Git Bash on Windows runners has tar(1).
#
# Inputs (env): TRIPLE, VERSION, BINARIES (space-separated)
# Outputs: archive, sha256, bin-dir -> $GITHUB_OUTPUT

set -euo pipefail

: "${TRIPLE:?missing}"
: "${VERSION:?missing}"
: "${BINARIES:?missing}"

ARCHIVE_NAME="runner-v${VERSION}-${TRIPLE}"
OUT_DIR="${GITHUB_WORKSPACE}/dist/bin-${TRIPLE}"
STAGE="$(mktemp -d)"
mkdir -p "${OUT_DIR}"

for bin in ${BINARIES}; do
if [[ -f "target/${TRIPLE}/release/${bin}.exe" ]]; then
cp "target/${TRIPLE}/release/${bin}.exe" "${STAGE}/"
elif [[ -f "target/${TRIPLE}/release/${bin}" ]]; then
cp "target/${TRIPLE}/release/${bin}" "${STAGE}/"
else
echo "::error::missing target/${TRIPLE}/release/${bin}[.exe]"
exit 1
fi
done
cp README.md LICENSE "${STAGE}/"

ARCHIVE="${ARCHIVE_NAME}.tar.gz"
tar -C "${STAGE}" -czf "${OUT_DIR}/${ARCHIVE}" .

# Portable SHA256: sha256sum on Linux + Git Bash, shasum on macOS.
cd "${OUT_DIR}"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${ARCHIVE}" >"${ARCHIVE}.sha256"
else
shasum -a 256 "${ARCHIVE}" >"${ARCHIVE}.sha256"
fi

{
echo "archive=${OUT_DIR}/${ARCHIVE}"
echo "sha256=${OUT_DIR}/${ARCHIVE}.sha256"
echo "bin-dir=${STAGE}"
} >>"$GITHUB_OUTPUT"
40 changes: 40 additions & 0 deletions .github/actions/pack-npm-facade/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: pack-npm-facade
description: >-
Stamp the facade package (reusing npm/facade/* and its bin/lib JS shims),
inject optionalDependencies from npm/targets.json, and run `npm pack`.

inputs:
facade-dir:
description: "Path to npm/facade/ (template)."
required: false
default: npm/facade
targets-json:
description: "Path to npm/targets.json."
required: false
default: npm/targets.json
version:
description: "Version without leading v."
required: true
out-dir:
description: "Where to write the resulting .tgz."
required: true

outputs:
tarball:
description: "Path to the produced .tgz"
value: ${{ steps.pack.outputs.tarball }}
name:
description: "Facade package name"
value: ${{ steps.pack.outputs.name }}

runs:
using: composite
steps:
- id: pack
shell: bash
env:
FACADE_DIR: ${{ inputs.facade-dir }}
TARGETS_JSON: ${{ inputs.targets-json }}
VERSION: ${{ inputs.version }}
OUT_DIR: ${{ inputs.out-dir }}
run: bash "$GITHUB_ACTION_PATH/pack.sh"
39 changes: 39 additions & 0 deletions .github/actions/pack-npm-facade/pack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Stage the facade package, inject optionalDependencies for tier-1/tier-2
# targets only (tier-3 may have failed to publish), stamp the version, pack.
#
# Inputs (env): FACADE_DIR, TARGETS_JSON, VERSION, OUT_DIR
# Outputs: tarball, name -> $GITHUB_OUTPUT

set -euo pipefail

: "${FACADE_DIR:?}" "${TARGETS_JSON:?}" "${VERSION:?}" "${OUT_DIR:?}"

mkdir -p "${OUT_DIR}"
STAGE="$(mktemp -d)"
cp -R "${FACADE_DIR}/." "${STAGE}/"
cp README.md LICENSE "${STAGE}/" 2>/dev/null || true

SCOPE=$(jq -r '.scope' "${TARGETS_JSON}")
FACADE_NAME=$(jq -r '.facade' "${TARGETS_JSON}")

# optionalDependencies: { "<scope>/<pkg>": "<version>" } for tier <= 2 only.
OPT_DEPS=$(jq --arg s "${SCOPE}" --arg v "${VERSION}" \
'[.targets[] | select(.tier <= 2) | { key: ($s + "/" + .pkg), value: $v }] | from_entries' \
"${TARGETS_JSON}")

TMP=$(mktemp)
jq --arg v "${VERSION}" --argjson od "${OPT_DEPS}" \
'.version = $v | .optionalDependencies = $od' \
"${STAGE}/package.json" >"${TMP}"
mv "${TMP}" "${STAGE}/package.json"

TGZ_NAME=$(cd "${STAGE}" && npm pack --json --pack-destination "${OUT_DIR}" | jq -r '.[0].filename')
TGZ_PATH="${OUT_DIR}/${TGZ_NAME}"

{
echo "tarball=${TGZ_PATH}"
echo "name=${FACADE_NAME}"
} >>"$GITHUB_OUTPUT"

echo "Packed facade ${FACADE_NAME}@${VERSION} -> ${TGZ_PATH}"
65 changes: 65 additions & 0 deletions .github/actions/pack-npm-platform/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: pack-npm-platform
description: >-
Stamp the per-target npm sub-package (`<scope>/<pkg>`), copy the prebuilt
binaries into its `bin/` directory, and run `npm pack`. Wiring only — logic
in pack.sh.

inputs:
pkg:
description: "Short target name (e.g. linux-x64-gnu); becomes the package suffix."
required: true
scope:
description: "npm scope (e.g. @runner-run)."
required: true
facade-name:
description: "Facade package name (e.g. runner-run). Used in description text."
required: true
version:
description: "Version without leading v."
required: true
os:
description: 'JSON array as a compact string, e.g. ["linux"].'
required: true
cpu:
description: 'JSON array as a compact string, e.g. ["x64"].'
required: true
libc:
description: "JSON array as a compact string. Empty string when not applicable."
required: false
default: ""
bin-dir:
description: "Directory containing the unpacked binaries to embed."
required: true
binaries:
description: 'Space-separated [[bin]] names to include (e.g. "runner run").'
required: false
default: runner run
out-dir:
description: "Where to write the resulting .tgz."
required: true

outputs:
tarball:
description: "Path to the produced .tgz"
value: ${{ steps.pack.outputs.tarball }}
name:
description: "Fully-qualified package name"
value: ${{ steps.pack.outputs.name }}

runs:
using: composite
steps:
- id: pack
shell: bash
env:
PKG: ${{ inputs.pkg }}
SCOPE: ${{ inputs.scope }}
FACADE: ${{ inputs.facade-name }}
VERSION: ${{ inputs.version }}
OS: ${{ inputs.os }}
CPU: ${{ inputs.cpu }}
LIBC: ${{ inputs.libc }}
BIN_DIR: ${{ inputs.bin-dir }}
BINARIES: ${{ inputs.binaries }}
OUT_DIR: ${{ inputs.out-dir }}
run: bash "$GITHUB_ACTION_PATH/pack.sh"
70 changes: 70 additions & 0 deletions .github/actions/pack-npm-platform/pack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Build a per-target npm sub-package from already-unpacked binaries.
#
# Inputs (env): PKG, SCOPE, FACADE, VERSION, OS (JSON), CPU (JSON),
# LIBC (JSON or empty), BIN_DIR, BINARIES, OUT_DIR
# Outputs: tarball, name -> $GITHUB_OUTPUT

set -euo pipefail

: "${PKG:?}" "${SCOPE:?}" "${FACADE:?}" "${VERSION:?}"
: "${OS:?}" "${CPU:?}" "${BIN_DIR:?}" "${BINARIES:?}" "${OUT_DIR:?}"

FULL_NAME="${SCOPE}/${PKG}"
STAGE="$(mktemp -d)"
mkdir -p "${STAGE}/bin" "${OUT_DIR}"

# Copy binaries; preserve .exe on Windows. resolve.cjs re-adds the extension
# based on process.platform, so the file just needs to be present.
for bin in ${BINARIES}; do
if [[ -f "${BIN_DIR}/${bin}.exe" ]]; then
cp "${BIN_DIR}/${bin}.exe" "${STAGE}/bin/${bin}.exe"
elif [[ -f "${BIN_DIR}/${bin}" ]]; then
cp "${BIN_DIR}/${bin}" "${STAGE}/bin/${bin}"
chmod +x "${STAGE}/bin/${bin}"
else
echo "::error::missing binary ${bin} in ${BIN_DIR}"
exit 1
fi
done

cp README.md LICENSE "${STAGE}/" 2>/dev/null || true

# Build package.json. libc field is omitted entirely on darwin/win32 so npm's
# selector doesn't reject those hosts.
LIBC_FRAG='{}'
if [[ -n "${LIBC}" ]]; then
LIBC_FRAG="$(jq -n --argjson v "${LIBC}" '{libc: $v}')"
fi

jq -n \
--arg name "${FULL_NAME}" \
--arg version "${VERSION}" \
--arg description "Prebuilt ${FACADE} binary for ${PKG}" \
--argjson os "${OS}" \
--argjson cpu "${CPU}" \
--argjson libc_obj "${LIBC_FRAG}" \
'{
name: $name,
version: $version,
description: $description,
homepage: "https://runner.kjanat.dev",
bugs: { url: "https://github.com/kjanat/runner/issues" },
repository: { type: "git", url: "git+https://github.com/kjanat/runner.git" },
license: "MIT",
author: "Kaj Kowalski <info+runner@kajkowalski.nl>",
os: $os,
cpu: $cpu,
files: ["bin/", "README.md", "LICENSE"],
preferUnplugged: true
} + $libc_obj' >"${STAGE}/package.json"

TGZ_NAME=$(cd "${STAGE}" && npm pack --json --pack-destination "${OUT_DIR}" | jq -r '.[0].filename')
TGZ_PATH="${OUT_DIR}/${TGZ_NAME}"

{
echo "tarball=${TGZ_PATH}"
echo "name=${FULL_NAME}"
} >>"$GITHUB_OUTPUT"

echo "Packed ${FULL_NAME}@${VERSION} -> ${TGZ_PATH}"
32 changes: 32 additions & 0 deletions .github/actions/pack-wheel/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: pack-wheel
description: >-
Thin wrapper around PyO3/maturin-action with `--bindings bin` to ship both
Rust binaries (`runner`, `run`) as a single wheel where each is installed as
a wheel script (i.e. ends up on PATH after `pip install runner-run`).

inputs:
triple:
description: Rust target triple
required: true
manylinux:
description: "manylinux/musllinux tag (e.g. 2_28, musllinux_1_2, auto). Ignored on non-Linux."
required: false
default: auto
out-dir:
description: Where to drop the resulting .whl
required: true

outputs:
out-dir:
description: "Directory containing the .whl"
value: ${{ inputs.out-dir }}

runs:
using: composite
steps:
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: ${{ inputs.triple }}
manylinux: ${{ inputs.manylinux }}
args: --release --locked --bindings bin --out ${{ inputs.out-dir }} --compatibility pypi
Loading