Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
da1044a
feat: surface unrecoverable container errors during pod wait
Longwt123 Jun 22, 2026
58f99dd
feat: add comprehensive pod failure diagnostics and configurable erro…
Longwt123 Jun 25, 2026
79aeb19
style: fix prettier 2.6.2 formatting in index.ts
Longwt123 Jun 25, 2026
893606e
fix: upgrade actions-runner base image to 2.335.1
Longwt123 Jun 26, 2026
820f097
fix: add namespace() in-cluster fallback and amd64 platform flag
Longwt123 Jun 26, 2026
a56d0b6
fix: update listNamespacedEvent to new k8s client API style
Longwt123 Jun 26, 2026
1257123
fix: revert runner base image to match release/no_volumes (2.334.0)
Longwt123 Jun 27, 2026
8071664
fix: deduplicate and clean up pod failure error output
Longwt123 Jun 27, 2026
1bb105f
feat: improve pod failure error output formatting
Longwt123 Jun 27, 2026
bbb2d33
fix: clean up error layering and stray gitignore entry
Longwt123 Jun 27, 2026
974ae50
feat: detect unrecoverable pod event errors for fast-fail
Longwt123 Jun 27, 2026
6c58886
fix: extract k8s API message from raw HttpException for pod create er…
Longwt123 Jun 27, 2026
9045356
fix: remove regex 's' flag incompatible with pre-ES2018 tsconfig target
Longwt123 Jun 27, 2026
75aaa7e
fix: reliably extract k8s API error message from HttpException body
Longwt123 Jun 27, 2026
574910c
fix: use lastIndexOf to find Body boundary before Headers line
Longwt123 Jun 27, 2026
3d1ad69
feat: add condition-based fast-fail as RBAC-free fallback for schedul…
Longwt123 Jun 29, 2026
3740490
fix: always run condition-based fast-fail, not only when events are e…
Longwt123 Jun 29, 2026
20e2ef2
chore: bump js-yaml to 4.2.0 and uuid to 11.1.1, clean up jest setup …
Longwt123 Jun 30, 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
18 changes: 18 additions & 0 deletions packages/k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,31 @@ rules:
resources: ["secrets"]
verbs: ["get", "list", "create", "delete"]
```
- (Optional, recommended) Granting `events` read access lets the hooks attach
recent `Warning` events (e.g. `FailedScheduling`, `FailedMount`) to the error
message when a pod fails to come online. This permission is **not required** —
if it is missing the hooks still work, they just omit the event section from
diagnostics.
```
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list"]
```
- The `ACTIONS_RUNNER_POD_NAME` env should be set to the name of the pod
- The `ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER` env should be set to true to prevent the runner from running any jobs outside of a container
- The runner pod should map a persistent volume claim into the `_work` directory
- The `ACTIONS_RUNNER_CLAIM_NAME` env should be set to the persistent volume claim that contains the runner's working directory, otherwise it defaults to `${ACTIONS_RUNNER_POD_NAME}-work`
- Some actions runner env's are expected to be set. These are set automatically by the runner.
- `RUNNER_WORKSPACE` is expected to be set to the workspace of the runner
- `GITHUB_WORKSPACE` is expected to be set to the workspace of the job
- Optional tuning env's
- `ACTIONS_RUNNER_K8S_UNRECOVERABLE_WAITING_REASONS` — comma-separated list of
extra container `waiting` reasons that should be treated as deterministic
terminal failures (fail fast instead of waiting for the timeout). These are
*added* to the built-ins (`ImagePullBackOff`, `ErrImagePull`,
`InvalidImageName`, `CreateContainerConfigError`, `CreateContainerError`);
the built-ins can never be removed. Use with care — only list reasons that
are truly unrecoverable for your workloads (e.g. `CrashLoopBackOff`).


## Limitations
Expand Down
1 change: 0 additions & 1 deletion packages/k8s/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
// eslint-disable-next-line filenames/match-regex, no-undef
jest.setTimeout(500000)
4 changes: 2 additions & 2 deletions packages/k8s/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"@actions/io": "^1.1.3",
"@kubernetes/client-node": "^1.3.0",
"hooklib": "file:../hooklib",
"js-yaml": "^4.1.0",
"js-yaml": "^4.2.0",
"shlex": "^3.0.0",
"tar-fs": "^3.1.0",
"uuid": "^11.1.0"
"uuid": "^11.1.1"
},
"devDependencies": {
"@babel/core": "^7.28.3",
Expand Down
45 changes: 42 additions & 3 deletions packages/k8s/src/hooks/prepare-job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,43 @@ export async function prepareJob(
} catch (err) {
await prunePods()
core.debug(`createPod failed: ${JSON.stringify(err)}`)
const message = (err as any)?.response?.body?.message || err
throw new Error(`failed to create job pod: ${message}`)
// The k8s client throws HttpException whose message is a multi-line string
// containing the raw HTTP dump. Extract the human-readable "message" field
// from the embedded JSON body so the log shows something like:
// failed to create job pod:
// spec.volumes[5].name: Duplicate value: "bad-hostpath"
// instead of the full HTTP dump.
const raw = err instanceof Error ? err.message : String(err)
let detail = raw
// The k8s HttpException message is a multi-line dump:
// HTTP-Code: 422
// Body: "{\"kind\":\"Status\",\"message\":\"...\", ...}"
// Headers: {...}
// Extract the Body JSON string, unescape it, and pull out "message".
try {
const bodyStart = raw.indexOf('Body: "')
// The boundary may be '"\nHeaders:' (real newline) or the literal
// string ends before Headers — use the last '"' before 'Headers:' as fallback
const headersIdx = raw.indexOf('Headers:')
const bodyEnd = headersIdx !== -1
? raw.lastIndexOf('"', headersIdx) - 0 // last " before Headers:
: raw.indexOf('"\nHeaders:')
if (bodyStart !== -1 && bodyEnd !== -1 && bodyEnd > bodyStart) {
const escaped = raw.substring(bodyStart + 7, bodyEnd)
// Body uses JSON string escaping: \" → " and \\ → \
const unescaped = escaped
.replace(/\\\\/g, '\x00') // protect \\ first
.replace(/\\"/g, '"') // unescape \"
.replace(/\x00/g, '\\') // restore \\
const parsed = JSON.parse(unescaped)
if (typeof parsed?.message === 'string') {
detail = parsed.message
}
}
} catch {
// Parsing failed — fall through and show the raw string
}
throw new Error(`failed to create job pod:\n ${detail}`)
}

if (!createdPod?.metadata?.name) {
Expand All @@ -112,7 +147,11 @@ export async function prepareJob(
)
} catch (err) {
await prunePods()
throw new Error(`pod failed to come online with error: ${err}`)
// Unwrap nested "Error: " prefix so the message renders as:
// pod failed to come online:
// <detail from waitForPodPhases, already formatted with sections>
const detail = err instanceof Error ? err.message : String(err)
throw new Error(`pod failed to come online:\n${detail}`)
}

await execCpToPod(createdPod.metadata.name, runnerWorkspace, '/__w')
Expand Down
Loading