Skip to content

feat(runtime): implement instant launch fast paths #6037

feat(runtime): implement instant launch fast paths

feat(runtime): implement instant launch fast paths #6037

Workflow file for this run

name: Construct Image
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: construct-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Single source of truth for the digest directory — `build` writes
# into it, `publish-manifest` reads from it.
DIGEST_DIR: /tmp/jackin-construct-digests
# Single source of truth for the registry coordinate — keeps the
# two buildcache refs in sync.
REGISTRY_IMAGE: projectjackin/construct
# NOTE: BUILDX_BUILDER is set at the `build` job level only, not
# workflow-wide. Setting it workflow-wide leaks into `publish-manifest`,
# which has no buildx builder and would then fail with
# `ERROR: no builder "jackin-construct" found` because docker buildx
# treats BUILDX_BUILDER as the default-builder selection.
jobs:
changes:
runs-on: ubuntu-latest
outputs:
construct: ${{ steps.dispatch.outputs.construct || steps.filter.outputs.construct }}
construct_image: ${{ steps.dispatch.outputs.construct_image || steps.filter.outputs.construct_image }}
# Single source of truth for "should this run produce / publish
# registry artifacts" — true on push-to-main and on
# workflow_dispatch from main, false everywhere else (PRs,
# dispatch from feature branches). Downstream steps gate on
# `needs.changes.outputs.is_publish` instead of restating the
# same boolean five times.
is_publish: ${{ steps.flags.outputs.is_publish }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Force construct=true on workflow_dispatch
id: dispatch
if: github.event_name == 'workflow_dispatch'
shell: bash
run: |
echo "construct=true" >> "$GITHUB_OUTPUT"
echo "construct_image=true" >> "$GITHUB_OUTPUT"
- name: Classify changed paths
id: filter
if: github.event_name != 'workflow_dispatch'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
filters: |
# Build the construct image when build plumbing changes, even if the
# change should not publish a new immutable construct tag.
construct:
- '.github/workflows/construct.yml'
- 'docker-bake.hcl'
- 'docker/construct/**'
- 'crates/jackin-xtask/**'
# Require an unpublished docker/construct/VERSION only when inputs
# baked into the construct image changed.
construct_image:
- 'docker-bake.hcl'
- 'docker/construct/Dockerfile'
- 'docker/construct/fish-config.fish'
- 'docker/construct/versions.env'
- 'docker/construct/zshrc'
- name: Compute publish flag
id: flags
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
run: |
if [ "$EVENT_NAME" = "push" ] || { [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$REF" = "refs/heads/main" ]; }; then
echo "is_publish=true" >> "$GITHUB_OUTPUT"
else
echo "is_publish=false" >> "$GITHUB_OUTPUT"
fi
# amd64 and arm64 share every step except runner label and platform
# string. Matrix collapse keeps the per-platform fail-fast=false
# behavior (one arch failing must not cancel the other) while
# eliminating ~60 lines of cross-job copy-paste.
build:
name: ${{ matrix.platform }}
needs: changes
if: needs.changes.outputs.construct == 'true'
runs-on: ${{ matrix.runner }}
permissions:
contents: read
env:
# Scoped to the build job only. Setting it workflow-wide leaks
# into `publish-manifest`, which has no buildx builder and would
# then fail with `ERROR: no builder "jackin-construct" found`.
BUILDX_BUILDER: jackin-construct
# `mise run` activates the whole mise.toml tool set and, by default,
# installs every missing one before the task. This job only needs the
# rust toolchain mise-action already installed to compile `cargo xtask`;
# without this, an unrelated tool's install (e.g. a flaky rustup
# component download for cargo-zigbuild) reds the construct build.
MISE_TASK_RUN_AUTO_INSTALL: "false"
strategy:
fail-fast: false
matrix:
include:
- platform: amd64
runner: ubuntu-24.04
- platform: arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Cache Rust toolchain
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
key: rustup-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}
restore-keys: |
rustup-${{ runner.os }}-${{ runner.arch }}-
- name: Cache Cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry/cache
~/.cargo/registry/index
~/.cargo/registry/src
~/.cargo/git/db
key: cargo-registry-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-registry-${{ runner.os }}-${{ runner.arch }}-
- id: sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
continue-on-error: true
env:
SCCACHE_GHA_ENABLED: "true"
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Setup mise
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4
with:
install_args: "rust"
github_token: ${{ secrets.GH_READONLY_TOKEN }}
- name: Cache Cargo build artifacts
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target
key: cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-${{ matrix.platform }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-${{ matrix.platform }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-${{ matrix.platform }}-${{ hashFiles('Cargo.lock') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-${{ matrix.platform }}-
- name: Install Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
name: ${{ env.BUILDX_BUILDER }}
driver: docker-container
- name: Log in to Docker Hub
if: needs.changes.outputs.is_publish == 'true' && needs.changes.outputs.construct_image == 'true'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Bootstrap buildx
run: mise run construct-init-buildx
- name: Expose GitHub Actions cache runtime
if: needs.changes.outputs.is_publish != 'true' || needs.changes.outputs.construct_image != 'true'
uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0
- name: Build ${{ matrix.platform }} without push
if: needs.changes.outputs.is_publish != 'true' || needs.changes.outputs.construct_image != 'true'
env:
# Registry buildcache (written by main) is the primary warm source — avoids a cold
# GHA start when the scope is empty or evicted. GHA scope is secondary: persists
# layers written by earlier runs of this PR so mid-PR iterations stay warm too.
CACHE_FROM: |
type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }}
type=gha,scope=construct-pr-${{ matrix.platform }}
CACHE_TO: type=gha,scope=construct-pr-${{ matrix.platform }},mode=max
run: mise run construct-build-platform ${{ matrix.platform }}
- name: Push ${{ matrix.platform }} image by digest
if: needs.changes.outputs.is_publish == 'true' && needs.changes.outputs.construct_image == 'true'
env:
CACHE_FROM: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }}
CACHE_TO: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }},mode=max
run: mise run construct-push-platform ${{ matrix.platform }}
- name: Upload ${{ matrix.platform }} digest
if: needs.changes.outputs.is_publish == 'true' && needs.changes.outputs.construct_image == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: construct-digest-${{ matrix.platform }}
path: ${{ env.DIGEST_DIR }}/${{ matrix.platform }}.digest
if-no-files-found: error
retention-days: 1
publish-manifest:
name: publish manifest
if: needs.changes.outputs.is_publish == 'true' && needs.changes.outputs.construct_image == 'true'
needs: [changes, build]
runs-on: ubuntu-24.04
permissions:
contents: read
env:
# Keep `mise run` on the rust toolchain mise-action installed instead of
# installing the whole mise.toml tool set for the rust-only xtask task.
MISE_TASK_RUN_AUTO_INSTALL: "false"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Cache Rust toolchain
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
key: rustup-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}
restore-keys: |
rustup-${{ runner.os }}-${{ runner.arch }}-
- name: Cache Cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry/cache
~/.cargo/registry/index
~/.cargo/registry/src
~/.cargo/git/db
key: cargo-registry-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-registry-${{ runner.os }}-${{ runner.arch }}-
- id: sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
continue-on-error: true
env:
SCCACHE_GHA_ENABLED: "true"
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Setup mise
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4
with:
install_args: "rust"
github_token: ${{ secrets.GH_READONLY_TOKEN }}
- name: Cache Cargo build artifacts
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target
key: cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-${{ hashFiles('Cargo.lock') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-
# publish-manifest only runs `docker buildx imagetools create`
# and `inspect`, both registry-side ops. They use the buildx
# CLI plugin (bundled with Docker on ubuntu-24.04) and need no
# local builder container, so no setup-buildx-action /
# construct-init-buildx here.
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Download platform digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: construct-digest-*
path: ${{ env.DIGEST_DIR }}
merge-multiple: true
- name: Publish multi-platform manifest
run: mise run construct-publish-manifest
# PR-time rehearsal of `publish-manifest`'s setup. Runs the same
# docker buildx CLI plumbing publish-manifest depends on, without
# logging in to Docker Hub or writing a manifest to the registry,
# so a workflow- or job-level env-var leak that breaks
# `docker buildx` resolution surfaces in PR rather than
# post-merge on main. Catches the exact failure class from
# jackin-project/jackin#266 — `docker buildx ls` evaluates
# BUILDX_BUILDER at startup and exits non-zero on a missing-builder
# reference, regardless of whether the rest of the command would
# have hit the network. The rehearsal runs on the events
# publish-manifest itself skips: PR and feature-branch dispatch
# that touch construct paths.
publish-manifest-rehearsal:
name: publish manifest (rehearsal)
if: needs.changes.outputs.construct == 'true' && needs.changes.outputs.is_publish != 'true'
needs: [changes, build]
runs-on: ubuntu-24.04
permissions:
contents: read
env:
# Keep `mise run` on the rust toolchain mise-action installed instead of
# installing the whole mise.toml tool set for the rust-only xtask task.
MISE_TASK_RUN_AUTO_INSTALL: "false"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Cache Rust toolchain
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
key: rustup-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}
restore-keys: |
rustup-${{ runner.os }}-${{ runner.arch }}-
- name: Cache Cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry/cache
~/.cargo/registry/index
~/.cargo/registry/src
~/.cargo/git/db
key: cargo-registry-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-registry-${{ runner.os }}-${{ runner.arch }}-
- id: sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
continue-on-error: true
env:
SCCACHE_GHA_ENABLED: "true"
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Setup mise
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4
with:
install_args: "rust"
github_token: ${{ secrets.GH_READONLY_TOKEN }}
- name: Cache Cargo build artifacts
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target
key: cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-rehearsal-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-rehearsal-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-rehearsal-${{ hashFiles('Cargo.lock') }}-
cargo-target-${{ runner.os }}-${{ runner.arch }}-construct-manifest-rehearsal-
- name: Verify docker buildx CLI resolves under workflow env
# `docker buildx ls` reads BUILDX_BUILDER (and the rest of
# buildx's env-driven config) and fails fast on a missing
# builder reference. This is the exact error shape from
# jackin-project/jackin#266 — running it on PR + feature-branch
# dispatch surfaces a leak that publish-manifest would
# otherwise only hit post-merge on main.
run: docker buildx ls
- name: Verify imagetools CLI plugin loads
# Exercises the same plugin path publish-manifest's
# `docker buildx imagetools create` / `inspect` commands rely
# on. Cheap, network-free smoke that the plugin is bundled
# and loadable on the runner.
run: docker buildx imagetools --help >/dev/null
- name: Verify construct VERSION is not already published
if: needs.changes.outputs.construct_image == 'true'
# PR-time mirror of publish-manifest's version-bump guard. The
# build job already ran, so reaching here means docker/construct/**
# content changed; an anonymous public-registry inspect needs no
# Docker Hub login, so the "changed the image but didn't bump
# docker/construct/VERSION" failure surfaces on the PR instead of
# on main, where publish-manifest is the first job to evaluate it.
run: mise run construct-assert-version-unpublished
construct-required:
if: always()
needs: [changes, build, publish-manifest, publish-manifest-rehearsal]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: ./.github/actions/aggregate-needs
with:
needs-json: ${{ toJSON(needs) }}
workflow-label: construct