Skip to content
Open
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
18 changes: 18 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# actionlint configuration.
#
# Declares the self-hosted runner labels served by the GKE ARC scale sets
# (see the hermes-agent-ci-infra repo). Without this, actionlint only knows
# GitHub-hosted labels and reports every `runs-on:` in the repo as unknown —
# 40 warnings that bury real findings.
#
# Keep in sync with the scale sets deployed in hermes-agent-ci-infra.
self-hosted-runner:
labels:
# Default set: general-purpose jobs.
- arc-runner-set
# Short gate jobs (detect, lint, small checks) — no docker sidecar.
- arc-runner-small
# Jobs that need a docker daemon (image build/test).
- arc-runner-docker
# arm64 image builds.
- arc-runner-arm64
86 changes: 86 additions & 0 deletions .github/actions/merge-base/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Ensure merge base is present
description: >-
Guarantee that the local (shallow) clone contains the merge base of two
commits, so `git diff base...head` is meaningful. Three-dot diff is defined
as "since the merge base", so having both endpoint commits is NOT enough —
git reports "fatal: no merge base" until the histories actually connect.
A caller that swallows that error silently diffs nothing, which for a
security scanner means reporting clean without having looked at anything.

inputs:
base:
description: Base commit SHA (e.g. github.event.pull_request.base.sha).
required: true
head:
description: Head commit SHA (e.g. github.event.pull_request.head.sha).
required: true
fail-on-missing:
description: >-
Fail the step when no merge base exists. Default true: a caller about to
run a three-dot diff must never continue, or it silently diffs nothing.
Set false when absence is the thing you are measuring (history-check),
and branch on the `found` output instead.
default: 'true'

outputs:
sha:
description: The resolved merge-base commit SHA (empty when none exists).
value: ${{ steps.resolve.outputs.sha }}
found:
description: '"true" when a merge base exists, "false" otherwise.'
value: ${{ steps.resolve.outputs.found }}

runs:
using: composite
steps:
- id: resolve
shell: bash
env:
BASE: ${{ inputs.base }}
HEAD: ${{ inputs.head }}
FAIL_ON_MISSING: ${{ inputs.fail-on-missing }}
run: |
set -euo pipefail

# Make sure both endpoints exist locally before deepening.
git cat-file -e "${BASE}^{commit}" 2>/dev/null || git fetch --depth=1 origin "$BASE" -q
git cat-file -e "${HEAD}^{commit}" 2>/dev/null || git fetch --depth=1 origin "$HEAD" -q

# Escalating deepen. A fixed budget is a correctness bug, not just a
# slow path: main lands ~270 commits/day, so a flat --deepen=100 stops
# connecting histories for any branch more than ~9 hours old.
for depth in 200 1000 5000; do
if git merge-base "$BASE" "$HEAD" >/dev/null 2>&1; then break; fi
git fetch --deepen="$depth" origin "$BASE" -q 2>/dev/null || true
git fetch --deepen="$depth" -q 2>/dev/null || true
done

# Full history is the only way to PROVE absence, so a genuinely
# unrelated branch always lands here. Rare (that PR is rejected
# anyway) and still cheaper than every job unshallowing by default.
if ! git merge-base "$BASE" "$HEAD" >/dev/null 2>&1; then
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
echo "::warning::deepen did not connect the histories; unshallowing (slow path)"
git fetch --unshallow -q 2>/dev/null || true
git fetch --depth=2147483647 origin "$BASE" -q 2>/dev/null || true
fi
fi

if MB=$(git merge-base "$BASE" "$HEAD" 2>/dev/null) && [ -n "$MB" ]; then
echo "sha=$MB" >> "$GITHUB_OUTPUT"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "::notice::merge base: $MB"
exit 0
fi

# Never fail open. Callers running a three-dot diff must stop here;
# history-check opts out to report the failure in its own words.
echo "sha=" >> "$GITHUB_OUTPUT"
echo "found=false" >> "$GITHUB_OUTPUT"
if [ "$FAIL_ON_MISSING" = "true" ]; then
echo "::error::No merge base between $BASE and $HEAD after deepening." \
"Refusing to continue: a three-dot diff would silently produce" \
"an empty result and report a vacuous pass."
exit 1
fi
echo "::notice::no merge base between $BASE and $HEAD"
91 changes: 91 additions & 0 deletions .github/actions/profile/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Profile a command (CPU/RAM/Disk)
description: >-
Run a shell command while sampling CPU, RAM, and disk IO every second.
Produces a resource-profile.json artifact per job so the CI timing
report can show per-job resource usage and identify bottlenecks.
inputs:
command:
description: Shell command to run (and profile).
required: true
label:
description: Label for this profile (e.g. "tests slice 1/8").
required: true
working-directory:
description: Directory to run in.
default: '.'

runs:
using: composite
steps:
- name: Start resource profiler
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
# Start profiler in background. It writes to resource-profile.json
# on SIGTERM (or when the command finishes and we signal it).
python3 scripts/ci/resource_profile.py \
--output resource-profile.json \
--label "$PROFILE_LABEL" &
echo $! > "$RUNNER_TEMP/profiler.pid"
env:
PROFILE_LABEL: ${{ inputs.label }}

- name: Run command
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
_CMD: ${{ inputs.command }}
run: |
# -e / pipefail: match the semantics of a normal `run:` step
# (bash -e {0}) so a failing early line (e.g. `source .venv/...`)
# fails the step instead of silently running the rest.
bash -eo pipefail -c "$_CMD"
- name: Stop profiler and collect results
id: stop-profiler
if: always()
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
if [ -f "$RUNNER_TEMP/profiler.pid" ]; then
PID=$(cat "$RUNNER_TEMP/profiler.pid")
if kill -0 "$PID" 2>/dev/null; then
kill -TERM "$PID"
# Give it a moment to write the JSON
for i in 1 2 3 4 5; do
if kill -0 "$PID" 2>/dev/null; then
sleep 0.2
else
break
fi
done
kill -KILL "$PID" 2>/dev/null || true
fi
fi
# hashFiles() only matches inside the workspace, so surface file
# existence as a step output instead for the upload condition.
if [ -s resource-profile.json ]; then
echo "profile_written=true" >> "$GITHUB_OUTPUT"
else
echo "profile_written=false" >> "$GITHUB_OUTPUT"
fi
- name: Sanitize resource profile label
id: sanitize
if: always()
shell: bash
env:
PROFILE_LABEL: ${{ inputs.label }}
run: |
SAFE=$(printf '%s' "$PROFILE_LABEL" | sed -E 's/[^a-zA-Z0-9]+/-/g')
echo "safe_label=$SAFE" >> "$GITHUB_OUTPUT"
- name: Upload resource profile
if: always() && steps.stop-profiler.outputs.profile_written == 'true'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: resource-profile-${{ steps.sanitize.outputs.safe_label }}
path: ${{ inputs.working-directory }}/resource-profile.json
retention-days: 14
24 changes: 24 additions & 0 deletions .github/actions/uv-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Cache uv downloads
description: >-
Persist uv's download/wheel cache (~/.cache/uv) across runs, keyed on the
dependency manifests. This is the half of astral-sh/setup-uv we still need:
uv itself and CPython 3.11 are baked into the nousresearch/nous-gke-runner
image (see hermes-agent-ci-infra runner/Dockerfile), but the wheel cache is
per-workspace and must still be restored. Without it `uv sync` re-downloads
and re-builds every wheel on every job — the toolchain would be faster to
set up and the sync dramatically slower, a net loss.

runs:
using: composite
steps:
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/uv
# runner.arch in the key: the cache holds built wheels, which are
# arch-specific — the docker workflow runs this on arm64 too.
key: uv-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}
# Fall back to any older cache for this arch: a stale wheel set still
# saves most of the download, and `uv sync --locked` re-resolves from
# uv.lock regardless, so a partial hit can't produce a wrong env.
restore-keys: |
uv-cache-${{ runner.os }}-${{ runner.arch }}-
Loading
Loading