Two stable-release failures on rc.3-v0.21.6 (run 37753841932): - nix: the stamped release source is a `path:` flake with no git metadata, so self.rev is null, install-stamp.json carries no commit and the packaged `hermes --version` reports "unknown". Pass the commit as `?rev=$GITHUB_SHA` (verified on Nix 2.35: path:<tree>?rev=<sha> sets self.rev). - docker: the versioned images were pushed under the final vX.Y.Z, which does not exist until publish. rc.1 created `:v0.21.6`, and every later attempt hit "versioned Docker manifest differs" against it. scripts/releases/docker.py (published_digest, promote_stable) already reads the attempt-ref tag, so pass claim-tag to both docker.yml calls.
178 lines
8.1 KiB
YAML
178 lines
8.1 KiB
YAML
name: Nix flake check
|
|
|
|
# Builds every output of the flake: the package, the devShell, and the 21
|
|
# checks under nix/checks.nix — module evaluation, option parity, .env
|
|
# assembly, service argv, and the rest.
|
|
#
|
|
# This workflow owns its triggers and ci.yml does not call it, for the reason
|
|
# docker.yml gives: a reusable-workflow call holds the caller run in progress
|
|
# for the full build, and GitHub refuses `gh run rerun` on a run that is still
|
|
# in progress. One slow advisory job in the CI lane blocks every rerun of the
|
|
# fast required jobs beside it. A separate run reruns and cancels on its own.
|
|
|
|
on:
|
|
pull_request:
|
|
push:
|
|
branches: [main]
|
|
workflow_call:
|
|
inputs:
|
|
release:
|
|
description: 'Stable-release candidate run: force the flake-check lane regardless of the classifier.'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
version:
|
|
description: 'Release version stamped into an isolated flake source tree.'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# A workflow_call run (stable release) is never cancelled: its group uses
|
|
# github.run_id so a parent rerun cannot kill this child mid-flight, and
|
|
# cancel-in-progress is false there — each run saves the store cache that
|
|
# later PRs restore from.
|
|
concurrency:
|
|
group: nix-${{ inputs.release == true && github.run_id || github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ inputs.release != true && github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
# A `paths:` filter cannot gate this workflow: a push to main must run it
|
|
# whatever changed, because the flake packages the product and nine of the
|
|
# checks run the built binary. The `nix` lane is a slow lane
|
|
# (classify_changes.py): on a pull request it runs for the flake files and
|
|
# the dependency manifests, or the run-e2e label. On push the classifier
|
|
# fails open and every lane is true, so a product change that breaks the
|
|
# flake turns main red after the merge.
|
|
detect:
|
|
name: Detect affected areas
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
outputs:
|
|
nix: ${{ steps.classify.outputs.nix }}
|
|
steps:
|
|
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
# github.sha is the exact candidate commit on a stable tag-dispatched
|
|
# caller and the head SHA on every other event.
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Detect affected areas
|
|
id: classify
|
|
uses: ./.github/actions/detect-changes
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
|
|
flake-check:
|
|
name: nix flake check
|
|
needs: [detect]
|
|
# Release runs force the lane: a stable candidate must build the flake
|
|
# no matter what its diff touches.
|
|
if: needs.detect.outputs.nix == 'true' || inputs.release == true
|
|
# The build compiles the package and its whole dependency closure, so this
|
|
# takes minutes and not seconds when the cache misses. `nix flake check`
|
|
# builds 21 checks, and --max-jobs defaults to the core count. It uses the
|
|
# wider runner with no more configuration.
|
|
runs-on: ubuntu-latest-32-core
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
# github.sha is the exact candidate commit on a stable tag-dispatched
|
|
# caller and the head SHA on every other event.
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Install Nix
|
|
uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
|
with:
|
|
extra_nix_config: |
|
|
experimental-features = nix-command flakes
|
|
# A store path that does not substitute is a cache miss and not a
|
|
# build failure. Build it here instead.
|
|
fallback = true
|
|
# Each source archive is fetched one time in a run, and not one
|
|
# time for each evaluation.
|
|
tarball-ttl = 3600
|
|
|
|
# Restores /nix/store from the GitHub Actions cache. The store holds the
|
|
# whole dependency closure, so a hit turns a build of several minutes
|
|
# into a short evaluation.
|
|
#
|
|
# The Magic Nix Cache is not an option here. Its free tier ended in
|
|
# February 2025 with the GitHub cache API that it was built on. This
|
|
# action uses the current API and needs no account and no secret.
|
|
- name: Restore and save the Nix store
|
|
uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7
|
|
with:
|
|
# The closure changes when the flake inputs change or when the
|
|
# dependencies of the project change. The key hashes both, so an
|
|
# edit to the source alone keeps the hit.
|
|
primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', 'nix/**', 'pyproject.toml', 'uv.lock') }}
|
|
# On a miss, restore the newest store for this runner. Most of the
|
|
# closure — Python, node, each transitive library — survives a bump
|
|
# of the lockfile, so an old store still removes most of the work.
|
|
restore-prefixes-first-match: nix-${{ runner.os }}-
|
|
|
|
# Save from main only. A cache that a PR writes is visible to that
|
|
# PR alone and never to another branch, so a save there spends the
|
|
# 10 GB quota of the repository and helps no later run. A PR still
|
|
# restores: it reads the cache that the merge to main wrote. This is
|
|
# the same rule that docker.yml applies to `cache-to`.
|
|
save: ${{ github.event_name != 'pull_request' }}
|
|
|
|
# Collect garbage before the save, so the store stays inside the
|
|
# 10 GB quota of the repository. Without a limit the store grows at
|
|
# each merge until GitHub removes the entry, and the next PR then
|
|
# gets nothing. This number is the size of the store and not the
|
|
# size of the compressed archive.
|
|
gc-max-store-size-linux: 5G
|
|
|
|
# Delete the caches that this key replaces. GitHub removes caches by
|
|
# least recent use across the whole repository, so a Nix store that
|
|
# is never purged pushes out the caches of the other workflows.
|
|
purge: true
|
|
purge-prefixes: nix-${{ runner.os }}-
|
|
purge-created: 0
|
|
purge-primary-key: never
|
|
|
|
- name: Prepare isolated release source
|
|
if: inputs.release == true
|
|
env:
|
|
RELEASE_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
mkdir -p "$RUNNER_TEMP/release-source"
|
|
git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/release-source"
|
|
python3 scripts/releases/stamping.py --tree "$RUNNER_TEMP/release-source" \
|
|
--version "$RELEASE_VERSION"
|
|
|
|
- name: nix flake check
|
|
# --print-build-logs: a check that fails then prints the assertion
|
|
# that failed, and not only the derivation that failed to build.
|
|
# A path: flake has no git metadata, so the release source names its
|
|
# commit with ?rev=: without it self.rev is null, the install stamp
|
|
# carries no commit, and the packaged CLI reports "unknown".
|
|
run: nix flake check "${{ inputs.release == true && format('path:{0}/release-source?rev={1}', runner.temp, github.sha) || '.' }}" --print-build-logs
|
|
|
|
- name: Verify release package runtime identity
|
|
if: inputs.release == true
|
|
env:
|
|
RELEASE_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
source="path:$RUNNER_TEMP/release-source?rev=$GITHUB_SHA"
|
|
package="$(nix build "$source#default" --no-link --print-out-paths)"
|
|
actual="$("$package/bin/hermes" --version)"
|
|
ACTUAL_VERSION="$actual" python3 - <<'PY'
|
|
import os
|
|
|
|
expected = os.environ["RELEASE_VERSION"]
|
|
# Line one is "Hermes Agent v<version> (...)"; later lines (Python, OpenAI SDK) carry other versions.
|
|
first = os.environ["ACTUAL_VERSION"].splitlines()[0].split()
|
|
actual = first[2].removeprefix("v") if first[:2] == ["Hermes", "Agent"] and len(first) > 2 else ""
|
|
if actual != expected:
|
|
raise SystemExit(f"Nix package version mismatch: expected {expected}, got {actual}")
|
|
PY
|