Skip to content

feat(k8s): surface container errors and fast-fail on unrecoverable pod errors (runner 2.337.0) - #51

Open
Longwt123 wants to merge 66 commits into
release/2.337from
release-2.337.0-4e9113c
Open

feat(k8s): surface container errors and fast-fail on unrecoverable pod errors (runner 2.337.0)#51
Longwt123 wants to merge 66 commits into
release/2.337from
release-2.337.0-4e9113c

Conversation

@Longwt123

Copy link
Copy Markdown
Collaborator

Summary

  • Merge error feedback and fast-fail requirements from release-2.336.0-6d63663 into release/2.337
  • Base image kept at actions-runner 2.337.0 (Dockerfile conflict resolved in favor of 2.337.0)

Included requirements

  • Surface container/pod error messages with actionable hints (!26, !30)
  • Fast-fail on unrecoverable scheduling errors with whitelist (!26, fix/scheduling-fast-fail)
  • Grace period for image pull failures instead of instant fail (!37)
  • Null check for jobPod in runScriptStep (!29)
  • Sync after file copy for data durability (!28)
  • Retry ErrImagePull caused by network/TLS timeouts instead of failing (!27)
  • Unify error format and surface terminated container hints (!30)
  • Script vs container error classification for run steps (!24)

Validation

  • k8s package: 126 tests passed, 29 pre-existing failures unchanged vs source branch (env-related, not introduced by this PR)
  • tsc --noEmit passes

Longwt123 and others added 30 commits June 26, 2026 17:13
When a CI job references a non-existent image or one it lacks permission
to pull, the pod stays in Pending and waitForPodPhases previously timed
out with only a generic phase-status message. GitHub Actions users had
no indication of the real cause.

Detect unrecoverable container waiting reasons (ImagePullBackOff,
ErrImagePull, InvalidImageName, CreateContainerConfigError,
CreateContainerError) on both init and regular containers, and fail fast
with the container name, reason, and Kubernetes message so the error is
visible in the Actions log.

Refactor getPodPhase into readPod + parsePodPhase so the pod object can
be inspected for container errors, and add unit tests covering
parsePodPhase, getContainerErrors, and waitForPodPhases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r reasons

Extend the error feedback mechanism introduced in the previous commit with:

- describePodFailure(): aggregates pod phase, conditions, container statuses
  and Warning events into a single human-readable diagnostic string.
  Never throws — safe to call from any error path.

- describePodWarningEvents(): best-effort retrieval of recent Warning K8s
  events (requires optional "events" RBAC permission). Degrades gracefully
  when the permission is missing.

- getUnrecoverableWaitingReasons(): allows operators to extend the built-in
  fast-fail whitelist via the ACTIONS_RUNNER_K8S_UNRECOVERABLE_WAITING_REASONS
  environment variable without a code change. Built-in defaults cannot be
  removed.

- All three failure paths in waitForPodPhases now attach full diagnostics:
  1. Non-backoff phase (e.g. Failed) — includes pod details + events
  2. Unrecoverable container error (e.g. ImagePullBackOff) — fail-fast with
     diagnostics instead of waiting for timeout
  3. Timeout — includes pod details so the user can see WHY the pod never
     became ready

- README: document the optional "events" permission and the new env var.

- Tests: 8 new test cases (19 total) covering describePodFailure,
  describePodWarningEvents, getUnrecoverableWaitingReasons, and edge cases
  such as forbidden events API and unreadable pods.
The project pins prettier@2.6.2 in package-lock.json, but the previous
commit was formatted with prettier 3.x which has different line-wrapping
rules for template literals. Re-format with prettier 2.6.2 to pass CI.
v2.329.0 was deprecated by GitHub and rejected at the broker level with
"Runner version v2.329.0 is deprecated and cannot receive messages.",
causing runner pods to crash-loop immediately after connecting.

Also fix Dockerfile layer ordering: switch to root before COPY so that
the subsequent chown is not run as the unprivileged runner user.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- namespace() now reads /var/run/secrets/kubernetes.io/serviceaccount/namespace
  as fallback when kubeconfig context has no namespace (in-cluster ARC setup)
- Dockerfile: add --platform=linux/amd64 to runner base image stage

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The release/no_volumes base uses @kubernetes/client-node v0.22+ where
API methods take an options object and return the resource directly
(no .body wrapper). Fix describePodWarningEvents() accordingly.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove --platform=linux/amd64: Jenkins build host is aarch64,
  without the flag the final image is ARM64, matching the working
  release/no_volumes build
- Revert to ghcr.io/actions/actions-runner:2.334.0 (same as
  release/no_volumes); the nju.edu.cn mirror at 2.335.1 caused
  silent 0-second exit with no logs (likely corrupted/wrong image)
- Remove USER root + chown (not needed, matches release/no_volumes)
- Update nodeSelector to arm64 to match ARM64 image architecture

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- describePodFailure(): skip container waiting reasons already in
  UNRECOVERABLE_WAITING_REASONS (they appear in the caller's first
  line via getContainerErrors, printing them again is redundant)
- describePodFailure(): suppress terminated containers with exitCode=0
  (e.g. fs-init Completed) -- successful init containers are noise
- Add tao/ to .gitignore; remove accidentally committed values file

Result: InvalidImageName/ErrImagePull now shows as one clean block:
  Pod <name> has unrecoverable container errors: container "job": ErrImagePull - <message>
  Phase: Pending
  Condition Ready=False ...

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Layer the error output into clearly separated sections:

  Pod <name> has unrecoverable container errors:
    ✗ container "job": ErrImagePull
      Error response from daemon: manifest for ... not found
  ────────────────────────────────────────────────────────────
  Pod status: Pending
    ✗ Ready=False (ContainersNotReady): containers with ...
    ✗ ContainersReady=False (ContainersNotReady): ...

  Recent warning events:
    [Failed] (x3) Error response from daemon: ...

Changes:
- getContainerErrors(): format each entry as indented ✗ lines with
  message detail on a separate indented line
- describePodFailure(): group output into sections (pod status,
  container details, warning events) separated by blank lines
- waitForPodPhases(): add ─── separator between summary and details
- describePodWarningEvents(): indent event lines consistently

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- prepare-job.ts: use err.message instead of String(err) to avoid
  double "Error: Error:" prefix; change separator to newline so the
  multi-section detail block renders on its own lines:
    pod failed to come online:
    Pod <name> has unrecoverable container errors:
      ✗ container "job": ErrImagePull
        <message>
    ────────────────────────────────────
    Pod status: Pending
      ✗ Ready=False ...
- .gitignore: fix corrupted line (tao/ was appended without newline
  to test-kind.yaml entry); restore original content, drop tao/
  (the values file should not be tracked in this repo)
- Dockerfile: restore to byte-identical match with release/no_volumes
  (only had trailing-newline diff, no content change)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add event-based fast-fail detection alongside the existing container
waiting-reason check in waitForPodPhases().

Problem: some failures never surface in container.status.state.waiting
.reason — the container stays stuck in ContainerCreating — so the hook
polled until the 3600s timeout. Examples:
  - FailedMount: hostPath directory missing, missing Secret/ConfigMap volume
  - FailedScheduling: no schedulable node (resource/nodeSelector/affinity)
  - FailedBinding: PVC cannot bind (no PV, wrong storageClass)

Changes:
- UNRECOVERABLE_EVENT_REASONS: new exported Set with the three reasons above
- getUnrecoverableEventReasons(): mirrors getUnrecoverableWaitingReasons(),
  extended at runtime via ACTIONS_RUNNER_K8S_UNRECOVERABLE_EVENT_REASONS
  env var (comma-separated, additive)
- getPodEventErrors(): queries the event API for Warning events whose
  reason is in the unrecoverable set; deduplicates by reason; gracefully
  degrades to [] when the optional events RBAC permission is missing
- waitForPodPhases(): call getPodEventErrors() alongside getContainerErrors()
  each poll; fail fast when either returns results
- Error message updated: "unrecoverable container errors" →
  "unrecoverable errors" to cover both detection paths
- Tests: add coverage for FailedMount/FailedScheduling/FailedBinding event
  detection, env-var extension, RBAC degradation, and deduplication

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…rors

The k8s client throws HttpException with a multi-line message:
  HTTP-Code: 422
  Message: Unknown API Status Code!
  Body: "{\"message\":\"Pod is invalid: ...\"}"

Parse the "message" field from the embedded JSON body so the log shows:
  failed to create job pod:
    Pod "xxx" is invalid: spec.volumes[5].name: Duplicate value: "bad-hostpath"

Falls back to the raw string if parsing fails (e.g. non-422 errors or
network errors that don't have a JSON body).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The dotAll flag (s) requires ES2018+. The JSON body is single-line so
the flag is unnecessary — remove it to restore build compatibility.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace the broken regex approach with boundary-based extraction:
1. Find 'Body: "' and '"\nHeaders:' delimiters in the raw dump
2. Unescape the JSON string (\" → " and \ → \)
3. JSON.parse and return parsed.message

Verified against the actual 422 response format produced by the k8s
client (Body contains a JSON-encoded string with \" escaping inside).
Falls back to the full raw string on any parse failure.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The previous '"\nHeaders:' boundary failed when the newline between
Body and Headers was not a real newline in the error string. Instead,
find 'Headers:' first and then use lastIndexOf('"') to locate the
closing quote of the Body value — more robust against whitespace
variations in the HttpException dump format.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ing errors

getPodConditionErrors() checks pod.status.conditions without needing
the optional 'events' list RBAC permission:
  - PodScheduled=False (Unschedulable): nodeSelector mismatch, resource
    requests exceed limits, no matching node

In waitForPodPhases():
  - eventErrors (events-API based) is tried first; it's richer (carries
    event.count and detailed message)
  - conditionErrors is only added when eventErrors is empty, i.e. when
    the events RBAC permission is absent — avoids duplicate output

This means FailedScheduling fast-fail now works in both cases:
  - RBAC allows events   → FailedScheduling event detected
  - RBAC blocks events   → PodScheduled=False condition detected

FailedBinding still relies on the FailedMount event path (the PVC
binding failure eventually surfaces as a FailedMount pod event).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…mpty

The previous logic only called getPodConditionErrors() when
getPodEventErrors() returned [] (events RBAC unavailable). This created
a race: if events take a few extra seconds to propagate, the first
several polls would return eventErrors=[], run conditionErrors (and
detect Unschedulable), but later polls (after events appear) would skip
conditionErrors even though events may show an unrelated reason.

Change: always run both checks each poll iteration. Deduplicate with a
simple filter so FailedScheduling (event) + Unschedulable (condition)
for the same pod don't both appear in the error output.

This ensures PodScheduled=False/Unschedulable is detected reliably even
when events RBAC permission is absent, covering:
  - FailedScheduling (nodeSelector mismatch)
  - FailedScheduling (resource shortage including NPU)
  - FailedMount (pod stuck Pending because NPU is busy)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…comment

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…lity

Rename utils.ts → utils/index.ts in k8s, docker, and hooklib packages so
source files match the CI coverage gate pattern '**/utils/**'. Add
collectCoverageFrom config to jest.config.js in k8s and docker packages to
enable coverage reporting. All existing imports resolve unchanged via TS
module resolution.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove collectCoverage: true from jest.config.js (avoid forced coverage
  on every local run); coverage now opt-in via --coverage flag
- Expand collectCoverageFrom to src/**/*.ts (was src/**/utils/**/*.ts)
  to prevent blind spots in coverage reporting
- Add test:coverage script to package.json for explicit CI/local use

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
k8s/tests/k8s-utils-test.ts:
- Add writeContainerStepScript: path return, env escaping, invalid workdir,
  invalid env key
- Add prepareJobScript: path return, mkdir content, empty mounts
- Add fixArgs: quoted args, single-quoted shell, plain args
- Add sleep: resolves after given ms
- Add listDirAllCommand: find command content, shell-quoting
- Add mergeObjectMeta: label/annotation merge, throws on undefined metadata
- Add useKubeScheduler: true/false/unset env var cases

docker/tests/utils-test.ts:
- Add checkEnvironment: passes with GITHUB_WORKSPACE set, throws when unset

hooklib: add jest infrastructure (jest.config.js, jest.setup.js,
tsconfig.test.json) and devDependencies (jest, ts-jest, babel-jest)
hooklib/tests/utils-test.ts:
- Add writeToResponseFile: string/object/null/undefined values, empty path,
  missing file, sequential appends
- Add getInputFromStdin: mock readline, parse JSON from emitted line

root package.json: include hooklib in test script

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add index.spec.ts alongside each utils/index.ts so the CI coverage gate
(vitest, include: **/*.{test,spec}.ts) can discover and run them:

- packages/k8s/src/k8s/utils/index.spec.ts: generateContainerName, fixArgs,
  sleep, listDirAllCommand, useKubeScheduler, mergeObjectMeta, writeRunScript,
  writeContainerStepScript, prepareJobScript, readExtensionFromFile,
  mergeContainerWithOptions, mergePodSpecWithOptions
- packages/docker/src/utils/index.spec.ts: sanitize, fixArgs,
  optionsWithDockerEnvs, checkEnvironment
- packages/hooklib/src/utils/index.spec.ts: writeToResponseFile

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…inerStepScript

'/too-short'.split('/').slice(-2) yields ['', 'too-short'] (length 2),
which does not trigger the throw. Use 'tooshort' (no slash) instead:
'tooshort'.split('/').slice(-2) = ['tooshort'] (length 1) → throws.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Revert all changes except:
- packages/{k8s,docker,hooklib}/src/**/utils/index.ts (rename from utils.ts)
- packages/{k8s,docker,hooklib}/src/**/utils/index.spec.ts (vitest spec files)

These two minimal changes are sufficient for the coverage gate:
- utils/index.ts satisfies gate pattern '**/utils/**'
- index.spec.ts satisfies vitest include '**/*.spec.ts'

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
更新 package.json 和 package-lock.json 中的 uuid 依赖版本,以获取最新的安全补丁和功能改进。
…tibility

refactor: move utils.ts to utils/index.ts for coverage gate compatibility
…-only to 11 types

- Add FailedMount to UNRECOVERABLE_WAITING_REASONS (covers waiting-state volume mount failures)
- Add UNRECOVERABLE_TERMINATED_REASONS (OOMKilled, Error, FailedPostStartHookError) for Running-phase errors
- Add getUnrecoverableTerminatedReasons() with env var extension support
- Add getContainerTerminatedErrors() for detecting terminated containers with unrecoverable reasons
- Add terminated error detection in runContainerStep catch block with describePodFailure diagnostics
- Branch based on release/no_volumes (not main) per issue #1203 request

UT coverage:
- wait-for-pod-phases-test.ts: 10 new tests for getContainerTerminatedErrors (8) and getUnrecoverableTerminatedReasons (4)
- run-container-step-terminated-test.ts: 5 new tests for runContainerStep terminated error detection (OOMKilled, Error, FailedPostStartHookError, no errors, getPodByName failure)
opensourceways-bot and others added 21 commits July 9, 2026 11:09
fix(k8s): FailedScheduling errors for fast-fail and Hint
Waiting reasons:
- Remove ErrImagePull: first-attempt pull failure can be a transient TLS
  timeout or network blip; k8s will retry and promote to ImagePullBackOff
- Remove CreateContainerError: runtime errors can be transient (node restart)
- Remove FailedMount from waiting reasons: dead code — k8s uses ContainerCreating
  as the waiting.reason when mounts fail; FailedMount surfaces only as events

Event reasons:
- Re-add FailedScheduling with PERMANENT_SCHEDULING_PATTERNS whitelist:
  fast-fail ONLY when message matches affinity/taint mismatch or PVC not found;
  resource shortages (Insufficient *) continue queuing until timeout

New pattern for PVC not found (scheduler refuses to place pod):
  /persistentvolumeclaim ".+" not found/i

Updated hints: ImagePullBackOff now mentions TLS/network in addition to
registry and credentials; FailedScheduling event hint added.

58 tests passing.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Use [^"]+ instead of .+ in PVC not found regex to avoid matching across
  multiple quoted segments and prevent unnecessary backtracking
- Cache compiled regex array in getPermanentSchedulingPatterns(), invalidating
  only when the env var value changes; avoids repeated split + RegExp
  construction on every FailedScheduling poll iteration

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
fix(k8s): 因网络问题TSL超时,导致的ErrImagePull,应该重试,不应该直接报错
…failures

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…and attempt info

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
tar.extract() appends into the destination directory; stale files that
were deleted inside the pod (e.g. git-credentials cleaned up by
actions/checkout post-job) remain on the runner side and cause a
permanent hash mismatch that no retry can resolve.

Clear targetRunnerPath before extraction so the local state mirrors the
pod exactly.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…mPod

The k8s status callback fires when the pod-side tar process exits, but
tar-fs may still be writing buffered data to the local filesystem at
that point. Resolving the Promise on writerStream 'finish' instead
ensures all files are flushed to disk before the hash verification loop
runs, preventing false mismatches where runner has fewer files than pod.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- pod-side sync: append '2>/dev/null || true' so that missing or
  restricted sync binary in minimal/distroless images does not cause
  the entire tar+chmod sh -c command to fail with a non-zero exit code
- runner-side sync: remove the spawn('sync') call entirely; it was
  made redundant by the writerStream 'finish' event fix (222a63b)
  which already guarantees all tar-fs writes are flushed to disk
  before the hash verification loop starts

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…e data durability

fix(k8s): PostCheckout复制时不匹配反复重试add sync after file copy to ensure data durability
…n failures

When prepareJob fails (pod creation/startup fails), state.jobPod is not
initialized. Subsequent runScriptStep attempts to access null.jobPod,
causing TypeError: Cannot read properties of null.

This fix adds early validation to throw a clear error when jobPod is null,
indicating that prepareJob failed. This prevents cryptic null reference
errors and improves debugging.

- Add null/undefined check at runScriptStep entry point
- Add test cases for null and undefined jobPod scenarios
- Error message clearly indicates prepareJob dependency
…ation failures

fix: add null check for jobPod in runScriptStep to handle pod creation failures
- prepare-job: format 422 pod creation errors with consistent header,
  separator, and actionable troubleshooting hints matching the
  unrecoverable-errors output style.
- k8s/index.ts: include getContainerTerminatedErrors in
  checkUnrecoverableErrors so OOMKilled / non-zero exit failures are
  reported with their hints during waitForPodPhases. When phase=Failed
  and terminated errors exist, the error now uses the
  "has unrecoverable errors" format instead of the generic
  "is unhealthy" message.
Address code review feedback:
- prepare-job.ts: refactor 422 error from single long line to array of
  strings joined by \n for readability and ESLint max-len compliance.
- Replace Unicode box-drawing character (U+2500) with standard hyphens
  across all hooks (prepare-job, index, run-script-step, run-container-step)
  for consistent rendering across terminal emulators and log aggregators.
…container hints

fix(k8s): 统一错误格式并优化提示信息unify error format and surface terminated container hints
ImagePullBackOff was in UNRECOVERABLE_WAITING_REASONS, so the hook failed
the job the moment it appeared (~2nd kubelet pull attempt). During a
transient network outage this killed jobs that would have recovered.

ImagePullBackOff/ErrImagePull now get a bounded grace window (default 300s,
configurable via ACTIONS_RUNNER_K8S_IMAGE_PULL_GRACE_SECONDS): the pull can
self-heal within it, and the hook fails only once the window is exceeded
without recovery. Waiting messages that positively identify a permanent
cause (manifest not found, pull access denied, unauthorized, repository
does not exist, ...) still fail immediately.

Setting grace to 0 restores the previous instant-fail behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
… fail

fix(k8s): grace period for image pull failures instead of instant fail
Trivy scan on PR #42 flagged js-yaml 4.3.0 with quadratic CPU consumption vulnerability in !!omap resolution (CVE-2026-59870). Upgrade to 4.3.1.
@opensourceways-bot

Copy link
Copy Markdown

Welcome To opensourceways Community

Hey @Longwt123 , thanks for your contribution to the community.

Bot Usage Manual

I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands.

Contact Guide

If you have any questions, please contact the SIG: infratructure ,
and any of the maintainers: @GeorgeCao-hw, @TangJia025, @pkking, @zhongjun2 ,
and any of the committers: @GeorgeCao-hw, @Goalina, @Hourunze1997, @JavaPythonAIForBAT, @KadenZhang3321, @LiYanghang00, @ccijunk, @drizzlezyk, @pkking, @rosecoffe, @tfhddd, @yao-xiaobai, @zhongjun2, @zkhzkhz .

@opensourceways-bot

Copy link
Copy Markdown

CLA Signature Pass

Longwt123, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@opensourceways-bot

Copy link
Copy Markdown

Linking Issue Notice

@Longwt123 , the pull request must be linked to at least one issue.
If an issue has already been linked, but the needs-issue label remains, you can remove the label by commenting /check-issue .

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updateduuid@​11.1.0 ⏵ 11.1.1100 +1100 +2100 +190 +1100
Addedjs-yaml@​4.3.110010010094100

View full report

@opensourceways-bot

Copy link
Copy Markdown
检查项 状态
敏感信息扫描
安全编码扫描
漏洞扫描
开源license合规扫描
UT测试覆盖率
开发阶段设计文档检查
流水线链接 点击跳转查看日志

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants