Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
797e7af
fix(ci): pin drills-nightly to nself-ci 1.2.11, same as nself-ci.yml …
acamarata Oct 23, 2026
d3be313
fix(ci): make the drills pipeline and nself-ci/source runnable (D-109…
acamarata Oct 23, 2026
ff3f927
test(ci): refresh regression patch 23 against the current deliver.go …
acamarata Oct 23, 2026
6f5d9d3
fix(ci): --trust owner for a head in this repository in ci-scenarios …
acamarata Oct 23, 2026
f3a9fbb
test(ci): resolve the macOS tempdir symlink in the node prepare tests…
acamarata Oct 23, 2026
d0d0629
fix(ci): drills-nightly pins nself-ci 1.2.13 (policy protect --branch…
acamarata Oct 23, 2026
57e63ad
fix(ci): a manual drills-nightly run is the owner's own run (--trust …
acamarata Oct 23, 2026
0c06b0f
fix(ci): give drill-security-suite a 60 minute timeout; hand gh a rea…
acamarata Oct 23, 2026
9ea9cb4
test(ci): resolve the executor home tempdir too (E612 on macOS)
acamarata Oct 23, 2026
b4f6776
fix(ci): isolation drill excerpt carries the lines before a failing t…
acamarata Oct 23, 2026
5173de5
fix(ci): main-status-audit audits one day (the seven day window excee…
acamarata Oct 23, 2026
7ce387b
fix(ci): isolation drill tests run with TMPDIR=/tmp (the executor's p…
acamarata Oct 23, 2026
11020f5
ci: keep the isolation drill's full test output as an artifact
acamarata Oct 23, 2026
4242eba
ci(scenarios): remove leftover containers before scenario 1 on the th…
acamarata Oct 23, 2026
79539f9
fix(ci): the hygiene leg tests a world-readable copy of free/ci (job …
acamarata Oct 23, 2026
acb763e
test(ci): role grants are not lease grants for the admit chokepoint s…
acamarata Oct 23, 2026
b7c982a
fix(ci): a cancelled job's orphans no longer hold the cancel for the …
acamarata Oct 23, 2026
84b59ca
ci(drill): name the running containers when the network cut refuses
acamarata Oct 23, 2026
93c82a6
fix(ci): log tail window covers Go 1.26's six-byte invalid-byte escap…
acamarata Oct 23, 2026
b818294
fix(ci): TMPDIR=/tmp only for the hygiene leg of the isolation drill …
acamarata Oct 23, 2026
6aa84f5
fix(ci): pairing withdraw audits before it wakes the window; TestQuot…
acamarata Oct 23, 2026
1d0f432
ci(isolation): remove leftover nself-ci containers and networks befor…
acamarata Oct 23, 2026
fa98bf5
fix(exec): podman listings use index .Labels, not the docker-only .La…
acamarata Oct 23, 2026
1718b97
ci(scenarios): allow the network cut on the throwaway Linux runner wh…
acamarata Oct 23, 2026
c61420b
fix(drill): the network-cut preflight waits up to 60s for the engine'…
acamarata Oct 23, 2026
01ab0a4
fix(exec): podman network listings keep the label map under .Network.…
acamarata Oct 23, 2026
f1740e8
ci(isolation): the drill job's own pass line decides the step (a sing…
acamarata Oct 23, 2026
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
22 changes: 21 additions & 1 deletion .github/workflows/ci-isolation-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ jobs:
PKGS: ${{ inputs.packages }}
TAGS: ${{ inputs.tags }}
INSTALL_RUNSC: ${{ inputs.install_runsc }}
# A pull request checkout is a merge ref no branch contains: the built binary classes it untrusted (E625).
# A head in this repository is the owner's own code on a disposable hosted runner; a fork head leaves this empty.
NSELF_CI_TRUST: ${{ (github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'owner' || '' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

Expand Down Expand Up @@ -164,6 +167,12 @@ jobs:
- name: Isolation levels drill
run: |
set -euo pipefail
# The integration step above ran the same netscope tests; a rootless podman job network it leaves
# behind (nself-ci-t08-hp-podman-net, run 40016512358) makes the drill's own run of that test fail.
for b in docker podman; do
"$b" ps -aq --filter name=nself-ci | xargs -r "$b" rm -f >/dev/null 2>&1 || true
"$b" network ls --format '{{.Name}}' | grep '^nself-ci-' | xargs -r "$b" network rm >/dev/null 2>&1 || true
done
drill=""
for d in scripts/drill/isolation-levels/run free/ci/scripts/drill/isolation-levels/run; do
if [[ -x "$d" ]]; then drill="$d"; break; fi
Expand All @@ -174,9 +183,20 @@ jobs:
fi
work="$(mktemp -d)"
(cd free/ci && CGO_ENABLED=0 go build -mod=vendor -o "$work/nself-ci" ./cmd)
NSELF_CI_BIN="$work/nself-ci" NSELF_CI_HOME="$work/home" "$drill"
# A single-job run is a partial selection, so nself-ci exits 1 even when the job passes: the job's own line decides.
NSELF_CI_BIN="$work/nself-ci" NSELF_CI_HOME="$work/home" "$drill" 2>&1 | tee "$work/drill.txt" || true
grep -Eq '^ +pass +drill-isolation-levels( |$)' "$work/drill.txt" || { echo "drill-isolation-levels did not pass" >&2; exit 1; }
# doctor exits 12 when any unrelated check warns; the jq checks below decide the step.
NSELF_V15=1 NSELF_CI_HOME="$work/home" "$work/nself-ci" doctor --json | tee "$work/doctor.json" || [[ ${PIPESTATUS[0]} -eq 12 ]]
for check in isolation.container isolation.sandboxed-container isolation.network; do
jq -e --arg c "$check" '.. | objects | select(.id? == $c or .name? == $c) | .verified_at | select(. != null)' "$work/doctor.json" >/dev/null
done

- name: Keep the isolation drill's test output
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: isolation-drill-logs-${{ github.run_id }}
path: /tmp/drill-logs/
if-no-files-found: ignore
retention-days: 14
22 changes: 19 additions & 3 deletions .github/workflows/ci-scenarios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14]
env:
# A pull request checkout is a merge ref no branch contains: the built binary classes it untrusted (E625).
# A head in this repository is the owner's own code on a disposable hosted runner; a fork head leaves this empty.
NSELF_CI_TRUST: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'owner' || '' }}
steps:
- name: Check out the repository (the pull request head, not the merge commit)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand All @@ -58,7 +62,8 @@ jobs:
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: free/ci/go.mod
go-version: "1.26" # newest 1.26: the cli checkout below needs more than free/ci/go.mod names
check-latest: true
cache: false

- name: Build the ci plugin from this commit and the cli from main
Expand All @@ -80,17 +85,28 @@ jobs:
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/evidence"
.drill-bin/nself-ci --no-status --pipeline drills --job drill-scenario-12-external-consumer --json > "$RUNNER_TEMP/evidence/scenario-12.json" || true # exit 1 is the partial selection; the job check below decides
.drill-bin/nself-ci --no-status ${NSELF_CI_TRUST:+--trust "$NSELF_CI_TRUST"} --pipeline drills --job drill-scenario-12-external-consumer --json > "$RUNNER_TEMP/evidence/scenario-12.json" || true # exit 1 is the partial selection; the job check below decides
# A single-job run is a partial selection, so its overall result is never pass: read the job's own check.
jq -r '"scenario-12: " + ([.data.checks[] | select(.id == "drill-scenario-12-external-consumer") | .result][0] // "no result")' "$RUNNER_TEMP/evidence/scenario-12.json" | tee -a "$GITHUB_STEP_SUMMARY"
jq -e '[.data.checks[] | select(.id == "drill-scenario-12-external-consumer") | .result] == ["pass"]' "$RUNNER_TEMP/evidence/scenario-12.json" >/dev/null

# Scenario 12 can leave containers behind; scenario 1 refuses to cut the network of running containers. The runner is
# a throwaway VM that holds nothing else, so remove them.
- name: Remove leftover containers (Linux)
if: runner.os == 'Linux' && !cancelled()
run: |
ids=$(docker ps -aq)
if [ -n "$ids" ]; then docker ps -a --format '{{.ID}} {{.Image}} {{.Status}}'; docker rm -f $ids; fi

- name: Scenario 1, local only (network cut)
if: ${{ !cancelled() }}
env:
# the runner is a throwaway VM; the engine's own network probe container can still be winding down when the cut preflight looks (run 40022254401)
DRILL_CUT_ALLOW_SHARED_HOST: ${{ runner.os == 'Linux' && '1' || '' }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/evidence"
.drill-bin/nself-ci --no-status --pipeline drills --job drill-scenario-01-local-only --json > "$RUNNER_TEMP/evidence/scenario-01.json" || true # exit 1 is the partial selection; the job check below decides
.drill-bin/nself-ci --no-status ${NSELF_CI_TRUST:+--trust "$NSELF_CI_TRUST"} --pipeline drills --job drill-scenario-01-local-only --json > "$RUNNER_TEMP/evidence/scenario-01.json" || true # exit 1 is the partial selection; the job check below decides
# A single-job run is a partial selection, so its overall result is never pass: read the job's own check.
jq -r '"scenario-01: " + ([.data.checks[] | select(.id == "drill-scenario-01-local-only") | .result][0] // "no result")' "$RUNNER_TEMP/evidence/scenario-01.json" | tee -a "$GITHUB_STEP_SUMMARY"
jq -e '[.data.checks[] | select(.id == "drill-scenario-01-local-only") | .result] == ["pass"]' "$RUNNER_TEMP/evidence/scenario-01.json" >/dev/null
Expand Down
84 changes: 78 additions & 6 deletions .github/workflows/drills-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
# stops firing shows as a missing run here, not as an `overdue` report.
# The pin: CI_VERSION, CI_RELEASE_URL and CI_RELEASE_SHA256 take the `ci-release` handoff (tag plus sha256 of each
# platform tarball) of P7-PLUG-69, the first plugins release that holds `--pipeline` and drill jobs (Epic P7-CI D34).
# Until that release exists the first step fails with a clear message; filling the three values is the whole change.
# This pin moves on its own: the drill scripts come from the checkout, so they need a binary that has the flags they call
# (1.2.13 for main-status-audit); nself-ci.yml and nself-ci-source.yml keep their own pin. If a value is ever empty again
# the first step fails with a clear message instead of running an unpinned binary.
# See also scripts/drill/README.md and .github/wiki/Catalog.md (Freshness drill).

name: drills-nightly
Expand All @@ -21,17 +23,22 @@ on:

permissions:
contents: read
checks: read # main-status-audit reads the check runs of main's commits
statuses: read # ... and their commit statuses

concurrency:
group: drills-nightly-${{ github.ref }}
cancel-in-progress: false

env:
PIPELINE: drills # the pipeline name in .nself/ci.yaml
CI_VERSION: "" # the pinned release, for example 1.2.7 (shown in the summary)
CI_RELEASE_URL: "" # https URL of the release tarball for this runner's platform
CI_RELEASE_SHA256: "" # sha256 of that tarball, 64 hex digits
CI_VERSION: "1.2.13" # the pinned release (shown in the summary); main-status-audit needs the policy protect --branch flag, new in 1.2.13
CI_RELEASE_URL: "https://github.com/nself-org/plugins/releases/download/v1.2.13/ci-1.2.13-linux-amd64.tar.gz"
CI_RELEASE_SHA256: "2b2d24a17937cecbe3c1d4ee2e59f1c488fe2a7ea6e3e60bd5c814c56454272e"
NSELF_V15: "1" # the plugin binary does not follow the cli flip; kept until v1.6
GVISOR_RELEASE: "20261012.0" # same pin and checksum as ci-isolation-linux.yml
GVISOR_SHA512: "9606445eed5aedfcf05310e7ccef61d994089914c6622fc82624c24d2b18db1b2210f40e40a7be27b8b7772859de7d95c501b4eba66423053c960bf671377c5e"
TRUST04_TEST_IMAGE: "alpine:3.19"

jobs:
pipeline:
Expand All @@ -54,6 +61,39 @@ jobs:
with:
fetch-depth: 0

# The drills run Go (security-suite, truth-sweep, scenarios), docker and podman (isolation-levels) and
# gVisor's runsc registered with docker; the steps are the ones ci-isolation-linux.yml runs.
# The scenario drills build nself-org/cli main, whose go.mod needs a newer Go than free/ci's (GOTOOLCHAIN=local in the
# engine's job environment), so take the newest 1.26.
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: "1.26"
check-latest: true
cache: false

- name: Container runtimes
run: |
set -euo pipefail
docker info --format '{{.ServerVersion}} rootless={{.SecurityOptions}}'
if ! command -v podman >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq podman
fi
podman info --format '{{.Version.Version}} rootless={{.Host.Security.Rootless}}'
docker pull "$TRUST04_TEST_IMAGE"

- name: Install gVisor runsc
run: |
set -euo pipefail
base="https://storage.googleapis.com/gvisor/releases/release/${GVISOR_RELEASE}/x86_64"
curl -fsSL -o "$RUNNER_TEMP/gvisor.tar.bz2" "$base/gvisor.tar.bz2"
echo "${GVISOR_SHA512} $RUNNER_TEMP/gvisor.tar.bz2" | sha512sum -c -
sudo tar -xjf "$RUNNER_TEMP/gvisor.tar.bz2" -C /usr/local/bin
sudo runsc install
sudo systemctl restart docker
docker info --format '{{json .Runtimes}}' | tee "$RUNNER_TEMP/runtimes.json"
grep -q runsc "$RUNNER_TEMP/runtimes.json"

- name: Install the pinned nself-ci
run: |
set -euo pipefail
Expand All @@ -69,27 +109,59 @@ jobs:
fi
chmod +x "$bin"
echo "NSELF_CI_BIN=$bin" >> "$GITHUB_ENV"
# The drills look the binary up by name (main-status-audit), and the engine's job environment does not carry NSELF_CI_BIN.
echo "$(dirname "$bin")" >> "$GITHUB_PATH"

- name: Install nft (scenario 1 cuts the network)
run: command -v nft >/dev/null || (sudo -n apt-get update -qq && sudo -n apt-get install -y -qq nftables)

# The executor hands a job HOME and XDG_* but no token variable, so the drills that call gh (main-status-audit,
# truth-sweep) read their credential from gh's own config file. The token is the job's read-only github.token.
- name: Give gh a read-only token
env:
GH_READ_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir -p "$HOME/.config/gh"
chmod 700 "$HOME/.config/gh"
( umask 077; printf 'github.com:\n oauth_token: %s\n git_protocol: https\n' "$GH_READ_TOKEN" > "$HOME/.config/gh/hosts.yml" )

- name: Run the pipeline
env:
NSELF_CI_HOME: ${{ runner.temp }}/ci-home # a known run store, so the job logs can be kept below
# The schedule runs on the default branch, which the binary classes owner. A manual run from another branch is a
# merge ref no default-branch commit contains and would be refused (E625); whoever can dispatch this workflow can
# also edit it, so a dispatch is the owner's own run.
NSELF_CI_TRUST: ${{ github.event_name == 'workflow_dispatch' && 'owner' || '' }}
run: |
set -o pipefail
mkdir -p "$RUNNER_TEMP/evidence"
status=0
"$NSELF_CI_BIN" --no-status --pipeline "$PIPELINE" --json > "$RUNNER_TEMP/evidence/$PIPELINE.json" || status=$?
args=(--no-status --pipeline "$PIPELINE" --json)
if [ -n "$NSELF_CI_TRUST" ]; then args+=(--trust "$NSELF_CI_TRUST"); fi
"$NSELF_CI_BIN" "${args[@]}" > "$RUNNER_TEMP/evidence/$PIPELINE.json" || status=$?
{
echo "### $PIPELINE (nself-ci $CI_VERSION)"
echo "exit code: $status"
} >> "$GITHUB_STEP_SUMMARY"
exit "$status"

- name: Keep the drill logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: drill-logs-${{ github.run_id }}
path: /tmp/drill-logs/
if-no-files-found: ignore
retention-days: 14

- name: Keep the evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: evidence-${{ github.run_id }}
path: ${{ runner.temp }}/evidence/
path: |
${{ runner.temp }}/evidence/
${{ runner.temp }}/ci-home/logs/
if-no-files-found: warn
retention-days: 30
9 changes: 8 additions & 1 deletion .github/workflows/nself-ci-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@ jobs:
- name: Run the gate
env:
NSELF_CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
# A pull request checkout is a merge ref no branch contains, so the built binary's source
# trust (P7-TRUST-13) classes it untrusted and refuses to run (E625). A head in this very
# repository is the owner's own code on a disposable hosted runner: say so with the audited
# --trust owner. A fork head leaves this empty, stays untrusted and is refused, as intended.
NSELF_CI_TRUST: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'owner' || '' }}
run: |
set -o pipefail
mkdir -p "$RUNNER_TEMP/evidence"
status=0
"$NSELF_CI_BIN" --no-status --json > "$RUNNER_TEMP/evidence/nself-ci-source.json" || status=$?
args=(--no-status --json)
if [ -n "$NSELF_CI_TRUST" ]; then args+=(--trust "$NSELF_CI_TRUST"); fi
"$NSELF_CI_BIN" "${args[@]}" > "$RUNNER_TEMP/evidence/nself-ci-source.json" || status=$?
{
echo "### nself-ci/source (built from $GITHUB_SHA)"
echo "exit code: $status"
Expand Down
14 changes: 14 additions & 0 deletions .nself/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ jobs:
kind: drill
run: [./scripts/drill/scenario-12-external-consumer/run]
timeout: 30m
# The default seven day window holds more than the 200 commits the audit reads on this busy repository (it then
# refuses to answer); the drill runs daily, so one day covers every commit since the last run.
drill-main-status-audit:
kind: drill
run: [./scripts/drill/main-status-audit/run]
env:
AUDIT_DAYS: "1"
timeout: 20m
# The security suite runs the whole acceptance suite and then plants each regression in a copy of the module (about
# 40 s each, three copies at a time): it needs far more than the 10 minute default.
drill-security-suite:
kind: drill
run: [./scripts/drill/security-suite/run]
timeout: 60m

# Per-check configuration. `truth.rules` lists the rule files of the truth check; its
# presence is what plans the check as a job when none declares it (ImpliedBy).
Expand Down
1 change: 1 addition & 0 deletions free/ci/cmd/build_v15_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const requiredUnit = "version: 1\njobs:\n unit:\n kind: test\n run: [sh,
func TestBuildV15Synthesized(t *testing.T) {
repo := androidRepo(t, requiredUnit)
t.Setenv("ANDROID_HOME", "/opt/sdk-fixture")
t.Setenv("ANDROID_SDK_ROOT", "") // GitHub runners set it; the job passes it through, so the count below would be 2
t.Setenv("JAVA_HOME", "")
lane, err := planBuildLane(context.Background(), repo, "android", 15*time.Minute)
if err != nil {
Expand Down
20 changes: 20 additions & 0 deletions free/ci/cmd/runs_wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,37 @@ import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"time"

cirun "github.com/nself-org/plugins/free/ci/internal/run"
"github.com/nself-org/plugins/free/ci/internal/store"
)

// noContainerRuntime puts a docker and a podman that refuse every command first on PATH. Reading a run
// recovers after dead coordinators, which asks each installed container runtime for orphans (`info`, `ps`);
// a host with a live daemon (a CI runner) spends about a second on that, and these tests are about the
// run's stream, not the host's runtimes.
func noContainerRuntime(t *testing.T) {
t.Helper()
if runtime.GOOS == "windows" {
return
}
dir := t.TempDir()
for _, name := range []string{"docker", "podman"} {
if err := os.WriteFile(filepath.Join(dir, name), []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
t.Fatal(err)
}
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
}

// runsHome points NSELF_CI_HOME at a fresh directory and speeds the poll up.
func runsHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
noContainerRuntime(t)
t.Setenv("NSELF_CI_HOME", home)
t.Setenv("NSELF_V15", "1")
t.Setenv("NSELF_INVOKE_DEADLINE_MS", "")
Expand Down
11 changes: 9 additions & 2 deletions free/ci/internal/dispatch/chokepoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ var (
"internal/storetest.Admit": true, // test fixtures only; TestStoretestIsTestOnly keeps it out of shipped code
}
localFiles = map[string]bool{"internal/run/attempt.go": true, "internal/run/dispatch_hook.go": true}
// roleGrantFiles name a TEAM role grant (P7-TRUST-12): "Grant" there is a permission row of a person, not a lease.
// The exemption is for the identifier Grant only; every other admission function stays banned in these files.
roleGrantFiles = map[string]bool{
"internal/roles/grants.go": true,
"internal/store/trust_roles.go": true,
"cmd/serve_admin_roles.go": true,
}
)

// banned are the admission functions only the chokepoint may name.
Expand Down Expand Up @@ -108,11 +115,11 @@ func scanFile(f *ast.File, rel string, add func(string)) {
if !localFiles[rel] {
report(name)
}
case banned[name] && !chokeFuncs[key]:
case banned[name] && !chokeFuncs[key] && !(name == "Grant" && roleGrantFiles[rel]):
report(name)
}
case *ast.Ident:
if banned[x.Name] && x != declName && !(x.Name == "LocalAdmit" && localFiles[rel]) && (x.Name == "LocalAdmit" || !chokeFuncs[key]) {
if banned[x.Name] && x != declName && !(x.Name == "Grant" && roleGrantFiles[rel]) && !(x.Name == "LocalAdmit" && localFiles[rel]) && (x.Name == "LocalAdmit" || !chokeFuncs[key]) {
report(x.Name)
}
case *ast.BasicLit:
Expand Down
Loading
Loading