Skip to content

Claude/job failed exit detection lmzgq6#76

Merged
wirwolf merged 16 commits into
masterfrom
claude/job-failed-exit-detection-lmzgq6
Jul 14, 2026
Merged

Claude/job failed exit detection lmzgq6#76
wirwolf merged 16 commits into
masterfrom
claude/job-failed-exit-detection-lmzgq6

Conversation

@wirwolf

@wirwolf wirwolf commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

claude added 10 commits July 10, 2026 11:43
Job.status.conditions[].type === 'Failed' alone can appear with
status: 'False' or 'Unknown' per the JobCondition spec, causing
false-positive aborts. Require status: 'True' as well.
Previously only a generic "Job is failed. Exit!" message was logged,
giving no indication of the underlying cause (e.g. BackoffLimitExceeded
vs DeadlineExceeded). Surface job name, condition reason and message.
The Job selector only matches on app.kubernetes.io/instance, so a
Failed Job left over from a previous release (not yet GC'd) could
match and abort a brand-new upgrade before its own Job even starts.
Record the watcher start time and skip any matched Job whose
creationTimestamp predates it.
…ejection

createChildProcess(wait=true) rejects on unexpected kubectl exit codes
(network errors, RBAC denials, etc.). Previously this rejection escaped
the setInterval callback and was only caught by the global
unhandledRejection handler, which aborts via SIGTERM without setting
process.exitCode, making the real cause (kubectl failure vs. actual
Job failure) unclear. Catch and log it explicitly at the source instead.
createChildProcess(wait=true) accepted exit codes 0 and 1 as success,
inherited from an older "kubectl get job <name>" invocation where 1
meant NotFound. After the switch to --selector-based queries, an empty
match returns 0 with {"items":[]}, so 1 now only masks real errors
(e.g. RBAC Forbidden) as success, silently returning invalid output.
… used

Explicitly defer sub-task 6: re-deriving terminal failure from
status.failed + backoffLimit would duplicate and could race with the
Job controller's own logic, whereas status.conditions is set as soon
as the controller reaches a terminal state. Recorded as a comment so
the decision isn't re-litigated later.
When multiple Jobs (or conditions) matched as Failed in the same
tick, the forEach loops called process.emit('SIGTERM') once per
match, producing duplicate log lines. Guard with a local flag so
detection and shutdown happen exactly once per tick.
Existing fixtures/CI only cover the successful Job scenario
(job-complited.yaml). Add job-failed.yaml, which deploys a Job that
deterministically fails on its first attempt (backoffLimit: 0,
APP_EXIT_CODE: 1), plus deploy_job_failed.sh which asserts
helm-assistant exits non-zero and logs the "Job is failed" detection
line, and wire it into the CI workflow as a new step.
…testing

Add src/Kubernetes/JobTypes.ts with typed interfaces for the batch/v1
Job API (V1Job, V1JobStatus, V1JobCondition, etc.), no `any` anywhere.

Extract evaluateJobItems() (decides whether any matched Job has
terminally failed) and interpretChildProcessExit() (decides how a
finished child process should settle its promise) as pure,
side-effect-free, exported functions. Both take/return plain typed
values, so they can be unit tested directly with hand-written
fixtures - no real kubectl process or mocked child_process required.

watchJobStatus() and createChildProcess() now just call these
functions and act on the result; behavior is unchanged.
Cover evaluateJobItems() with hand-written V1Job fixtures: Failed
status True/False/Unknown, BackoffLimitExceeded and DeadlineExceeded
reasons, stale job filtering, multi-job dedup, missing conditions,
Complete condition, and empty item list.

Cover interpretChildProcessExit() with exit code 0 (success), 1 and
other non-zero codes (now correctly errors, not silently accepted),
SIGINT, and the null/no-signal edge case.

No real kubectl/cluster or child_process mocking needed - both units
are pure functions. Wire `yarn test` into CI as its own step.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50c97dcda6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Modules/UpgradeModule.ts Outdated
@wirwolf wirwolf self-assigned this Jul 14, 2026
@wirwolf
wirwolf merged commit 1295a7e into master Jul 14, 2026
2 checks passed
wirwolf added a commit that referenced this pull request Jul 14, 2026
* fix(job-watch): require condition.status === 'True' for Failed detection

Job.status.conditions[].type === 'Failed' alone can appear with
status: 'False' or 'Unknown' per the JobCondition spec, causing
false-positive aborts. Require status: 'True' as well.

* feat(job-watch): log job name, reason and message on Failed detection

Previously only a generic "Job is failed. Exit!" message was logged,
giving no indication of the underlying cause (e.g. BackoffLimitExceeded
vs DeadlineExceeded). Surface job name, condition reason and message.

* fix(job-watch): ignore stale Jobs created before this upgrade started

The Job selector only matches on app.kubernetes.io/instance, so a
Failed Job left over from a previous release (not yet GC'd) could
match and abort a brand-new upgrade before its own Job even starts.
Record the watcher start time and skip any matched Job whose
creationTimestamp predates it.

* fix(job-watch): catch polling errors instead of leaking to unhandledRejection

createChildProcess(wait=true) rejects on unexpected kubectl exit codes
(network errors, RBAC denials, etc.). Previously this rejection escaped
the setInterval callback and was only caught by the global
unhandledRejection handler, which aborts via SIGTERM without setting
process.exitCode, making the real cause (kubectl failure vs. actual
Job failure) unclear. Catch and log it explicitly at the source instead.

* fix(job-watch): stop treating kubectl exit code 1 as success

createChildProcess(wait=true) accepted exit codes 0 and 1 as success,
inherited from an older "kubectl get job <name>" invocation where 1
meant NotFound. After the switch to --selector-based queries, an empty
match returns 0 with {"items":[]}, so 1 now only masks real errors
(e.g. RBAC Forbidden) as success, silently returning invalid output.

* docs(job-watch): document why status.failed/succeeded counters aren't used

Explicitly defer sub-task 6: re-deriving terminal failure from
status.failed + backoffLimit would duplicate and could race with the
Job controller's own logic, whereas status.conditions is set as soon
as the controller reaches a terminal state. Recorded as a comment so
the decision isn't re-litigated later.

* fix(job-watch): emit SIGTERM only once per polling tick

When multiple Jobs (or conditions) matched as Failed in the same
tick, the forEach loops called process.emit('SIGTERM') once per
match, producing duplicate log lines. Guard with a local flag so
detection and shutdown happen exactly once per tick.

* test(ci): add CI coverage for the Failed-Job detection path

Existing fixtures/CI only cover the successful Job scenario
(job-complited.yaml). Add job-failed.yaml, which deploys a Job that
deterministically fails on its first attempt (backoffLimit: 0,
APP_EXIT_CODE: 1), plus deploy_job_failed.sh which asserts
helm-assistant exits non-zero and logs the "Job is failed" detection
line, and wire it into the CI workflow as a new step.

* refactor(job-watch): extract typed, pure decision functions for unit testing

Add src/Kubernetes/JobTypes.ts with typed interfaces for the batch/v1
Job API (V1Job, V1JobStatus, V1JobCondition, etc.), no `any` anywhere.

Extract evaluateJobItems() (decides whether any matched Job has
terminally failed) and interpretChildProcessExit() (decides how a
finished child process should settle its promise) as pure,
side-effect-free, exported functions. Both take/return plain typed
values, so they can be unit tested directly with hand-written
fixtures - no real kubectl process or mocked child_process required.

watchJobStatus() and createChildProcess() now just call these
functions and act on the result; behavior is unchanged.

* test: add unit tests for Job-failed detection and exit-code handling

Cover evaluateJobItems() with hand-written V1Job fixtures: Failed
status True/False/Unknown, BackoffLimitExceeded and DeadlineExceeded
reasons, stale job filtering, multi-job dedup, missing conditions,
Complete condition, and empty item list.

Cover interpretChildProcessExit() with exit code 0 (success), 1 and
other non-zero codes (now correctly errors, not silently accepted),
SIGINT, and the null/no-signal edge case.

No real kubectl/cluster or child_process mocking needed - both units
are pure functions. Wire `yarn test` into CI as its own step.

* [feat] refactor repo structure, upgrade dependencies, and refine job handling logic

* [chore] upgrade upload-artifact action to v4 in CI workflow

* [chore] upgrade dependencies and replace "pkg" with scoped "@yao-pkg/pkg"

* [chore] update download-artifact action to v4 in CI workflow

* [fix] correct exit code in job-failed test case

* [fix] refine job-failed test case logic for accurate detection

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants