diff --git a/.github/workflows/gitnexus-skill-evolution.yml b/.github/workflows/gitnexus-skill-evolution.yml index ce61c9586..d4d930674 100644 --- a/.github/workflows/gitnexus-skill-evolution.yml +++ b/.github/workflows/gitnexus-skill-evolution.yml @@ -41,7 +41,8 @@ # most weekly. Revisit if run frequency increases or the threat model # changes; stopping already bounds the exposure window to the job's own # runtime on 1 day out of 7. -# [ ] Stop the runner box from restarting services under a running job. A run +# [ ] Install and verify the runner survival policy below before enabling +# scheduled runs. A run # spans ~15h and apt-daily-upgrade.timer fires daily (~06:34), so every # scheduled run crosses it. On 2026-08-02 unattended-upgrades upgraded # openssl at 07:54:02 and needrestart restarted the Actions runner five @@ -58,14 +59,13 @@ # uploads, and a promotion (if any) opens a well-formed PR. Run # 29907431284 (2026-07-22) went green end to end in 14h45m and reached a # gate decision (`insufficient_evidence`, no promotion). -# [x] Set the repository variable GITNEXUS_EVOLUTION_ENABLED=true. Roll back -# the scheduled lane by setting it to false. workflow_dispatch always runs -# the full benchmark loop regardless and bills real API usage on -# GITNEXUS_BENCH_AUTH_TOKEN. # [ ] After resizing the runner, prove a manual workers=3 run has zero excluded # runs and does not stretch the 48-minute serial mean toward the session -# ceiling; then set GITNEXUS_EVOLUTION_WORKERS=3 for scheduled runs. Leave -# it unset or set it to 1 for the serial fallback and immediate rollback. +# ceiling; then set GITNEXUS_EVOLUTION_WORKERS=3 and +# GITNEXUS_EVOLUTION_ENABLED=true for scheduled runs. Scheduled runs +# require both values, so leaving workers unset/1 is an immediate rollback; +# workflow_dispatch remains available for the proof and bills real API +# usage on GITNEXUS_BENCH_AUTH_TOKEN. name: GitNexus skill evolution on: @@ -121,7 +121,10 @@ jobs: github.repository == 'abhigyanpatwari/GitNexus' && ( github.event_name == 'workflow_dispatch' || - vars.GITNEXUS_EVOLUTION_ENABLED == 'true' + ( + vars.GITNEXUS_EVOLUTION_ENABLED == 'true' && + vars.GITNEXUS_EVOLUTION_WORKERS == '3' + ) ) runs-on: [self-hosted, linux, x64, gitnexus-evolution] # Gate promotion runs on a protected Environment. An admin must attach a @@ -164,6 +167,20 @@ jobs: exit 1 fi + - name: Verify runner survival policy + run: | + set -euo pipefail + needrestart_policy=/etc/needrestart/conf.d/90-gitnexus-evolution.conf + if [[ ! -r "${needrestart_policy}" ]] || ! grep -Fqx '$nrconf{restart} = '\''l'\'';' "${needrestart_policy}"; then + echo "::error::${needrestart_policy} must contain: \$nrconf{restart} = 'l';" + exit 1 + fi + oom_score_adjustment="$( -900 )); then + echo "::error::Runner.Worker descendants require OOMScoreAdjust=-900 or stronger; effective value is ${oom_score_adjustment}." + exit 1 + fi + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/gitnexus/test/unit/skill-evolution-workflow.test.ts b/gitnexus/test/unit/skill-evolution-workflow.test.ts index 983dd6b14..c1263e34b 100644 --- a/gitnexus/test/unit/skill-evolution-workflow.test.ts +++ b/gitnexus/test/unit/skill-evolution-workflow.test.ts @@ -29,6 +29,7 @@ const workflowDocument = load(workflow) as { { environment?: unknown; env?: Record; + if?: unknown; 'timeout-minutes'?: unknown; steps?: Array<{ name?: string; @@ -407,6 +408,21 @@ exit 1`); expect(mint?.with).not.toHaveProperty('app-id'); }); + it('keeps scheduled runs off until the three-worker proof is explicitly enabled', () => { + const condition = String(evolveJob?.if); + expect(condition).toContain("github.event_name == 'workflow_dispatch'"); + expect(condition).toContain("vars.GITNEXUS_EVOLUTION_ENABLED == 'true'"); + expect(condition).toContain("vars.GITNEXUS_EVOLUTION_WORKERS == '3'"); + }); + + it('fails before paid work when runner survival protections are ineffective', () => { + const preflight = stepRun('Verify runner survival policy'); + expect(preflight).toContain('/etc/needrestart/conf.d/90-gitnexus-evolution.conf'); + expect(preflight).toContain("$nrconf{restart} = 'l';"); + expect(preflight).toContain('/proc/self/oom_score_adj'); + expect(preflight).toContain('oom_score_adjustment > -900'); + }); + it('labels the upload-artifact pin with its real version', () => { expect(workflow).toContain( 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1', @@ -456,7 +472,7 @@ exit 1`); expect(workflow).toContain('RELEASE_APP_ID'); expect(workflow).toContain('RELEASE_APP_PRIVATE_KEY'); expect(workflow).toContain('gitnexus-evolution'); - expect(workflow).toContain('[x] Set the repository variable GITNEXUS_EVOLUTION_ENABLED=true'); + expect(workflow).toContain('GITNEXUS_EVOLUTION_ENABLED=true for scheduled runs'); expect(workflow).toContain('GITNEXUS_EVOLUTION_WORKERS'); }); });