Skip to content

Chore(ci): add job-level timeout-minutes to the remaining five workflows - #403

Open
AmaadMartin wants to merge 1 commit into
mainfrom
feat/workflow-job-timeout-minutes
Open

Chore(ci): add job-level timeout-minutes to the remaining five workflows#403
AmaadMartin wants to merge 1 commit into
mainfrom
feat/workflow-job-timeout-minutes

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    Closes: #issue_number
    Related: #issue_number
  2. Or, if no issue exists, describe the change:
    Problem: Every job in the five workflows below inherits GitHub's default job timeout of 360 minutes ("The maximum number of minutes to let a job run before GitHub automatically cancels it. Default: 360."). None of them sets timeout-minutes today. The one that matters is cross-language-integration.yml: its single macos-latest job reaches the network three times without a cache (npm install, plus go mod tidy in tests/cross_language/a2a/go_ts/go_client and tests/cross_language/a2a/ts_go/go_backend). If the npm registry or the Go module proxy wedges, the job sits there producing no output until the 6-hour cap fires — 10 hours of billed time, since macOS runners bill at 10x — and the PR check just spins. The other four are single-digit-second bash/github-script jobs that have no legitimate reason to run for hours either.

Solution: Add one job-level timeout-minutes key to each of the five workflows — five files, five inserted lines, zero deletions. Each value is sized from that workflow's own observed job execution time, not guessed and not copied between workflows.

File Job timeout-minutes
.github/workflows/cross-language-integration.yml run-tests 15
.github/workflows/release-please.yml release-please 10
.github/workflows/license-check.yml check-license 5
.github/workflows/auto-assignment.yml auto-assign 5
.github/workflows/csat.yml welcome 5

Where the numbers come from

Measured against google/adk-js via the public API on 2026-07-31: runs enumerated with gh run list --workflow <file> --status success, then per-job started_at/completed_at read from gh api repos/google/adk-js/actions/runs/<id>/jobs. These are job execution times. Run-level wall clock is not usable for this: it is inflated by runner queue wait, which is not charged against timeout-minutes (license-check run 29957830146 has a 758s run-level wall clock around a job that executed for 5s).

Workflow (job) n p50 p90 p95 max chosen margin over max
cross-language-integration (run-tests, macos-latest, success) 100 99s 115s 127s 143s 15 min ~6.3x
cross-language-integration (run-tests, failing runs) 63 55s 116s 119s 167s 15 min ~5.4x
release-please (release-please) 100 26s 49s 51s 64s 10 min ~9.4x
license-check (check-license) 100 6s 7s 8s 24s 5 min ~12.5x
auto-assignment (auto-assign) 100 5s 7s 8s 24s 5 min ~12.5x
csat (welcome, entire history) 34 6s 7s 7s 8s 5 min ~37.5x

Per-workflow rationale:

  • cross-language-integration → 15. Deliberately the loosest multiplier of the five, because it is the only job whose duration is genuinely variable: macos-latest is the slowest and most contended hosted pool, and the job makes three uncached network fetches. A cold registry, a slow proxy, or growth in the cross-language suite must not start producing spurious red checks. It still cuts the worst case from 360 minutes to 15 — a 24x reduction in wasted runner time, which is the whole point.
  • release-please → 10. Larger than the trivial three because on a release-cutting push googleapis/release-please-action does much more than on the common no-op push: it walks history and creates/updates release PRs, tags and releases for all four components in release-please-config.json (., core, dev, integrations). That path is API-bound and under-represented in the p50.
  • license-check / auto-assignment / csat → 5 each. One bash script over the tree and two actions/github-script calls. A uniform 5 beats three bespoke values, which would encode false precision on a 6-second job.

The 24s maxima on license-check/auto-assign and the 143s max on cross-language are recent-window outliers (the p95s are 8s and 127s); the chosen ceilings clear even those by 6x–12x.

Placement follows google/adk-python, which already uses this key at job level as a sibling of runs-on: (.github/workflows/continuous-integration.yml sets timeout-minutes: 10; .github/workflows/issue-maintenance.yml sets 120 and 60).

Deliberately not in this PR

  • No strategy / fail-fast. All five of these workflows are single-job with no matrix (grep -rn strategy .github/workflows/ matches only validation.yaml), and fail-fast is a matrix-only key — adding it here would be inert config.
  • .github/workflows/validation.yaml is untouched, and is confirmed absent from the diff. It is the subject of separate changes.
  • No caching, no concurrency, no action-version bumps or SHA pinning (csat.yml stays on checkout@v3/github-script@v6), no step-level timeouts. This is not a performance change.

Collision check

Ran before writing anything, per contribution hygiene:

gh pr list --repo AmaadMartin/adk-js --state open --limit 300   # 300 open PRs
git fetch fork '+refs/heads/*:refs/remotes/fork/*'              # 454 branches
for b in <every fork branch>; do git grep -l 'timeout-minutes' "$b" -- .github/workflows/; done

Exactly three branches add timeout-minutes to a workflow, and all three touch only .github/workflows/validation.yaml (#218, #235, #276) — disjoint from these five files, so no stacking is needed and neither side can conflict. Four other open PRs edit cross-language-integration.yml (#133, #306, #338, #393); none of them adds a timeout key of any kind (verified with gh pr diff <n> | grep -E '^[+-].*timeout'). No PR duplicates this change.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.

No unit tests are added, deliberately: this change adds zero lines of executable code — five declarative YAML keys — so there is no surface for vitest to cover, and the repo has no workflow-linting test to extend. Inventing a vitest suite that parses .github/workflows/*.yml purely to produce a coverage number would be the only test of its kind in the repo and would break on every legitimate workflow edit. The parse-and-assert protocol below is the substitute. Note also that nothing in the repo's own tooling validates these files: npm run lint is eslint "**/*.ts" and npm run format:check is prettier "**/*.ts" --check (neither matches .yml/.yaml), and scripts/check_license.sh only scans *.js/*.ts, which is why no license header was added to the edited YAML.

1. Every workflow still parses (all six files, including the two untouched ones):

$ python3 -c "import glob,yaml; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/*.y*ml')]; print('all workflows parse')"
all workflows parse

2. The key landed on the right job, with the right value and the right type:

$ python3 verify.py
OK .github/workflows/cross-language-integration.yml: run-tests timeout-minutes=15 runs-on=macos-latest
OK .github/workflows/release-please.yml: release-please timeout-minutes=10 runs-on=ubuntu-latest
OK .github/workflows/license-check.yml: check-license timeout-minutes=5 runs-on=ubuntu-latest
OK .github/workflows/auto-assignment.yml: auto-assign timeout-minutes=5 runs-on=ubuntu-latest
OK .github/workflows/csat.yml: welcome timeout-minutes=5 runs-on=ubuntu-latest
validation.yaml jobs (must be unchanged by this PR): ['run-tests']

The script asserts, per file, that the workflow declares exactly the expected single job id (renaming one would silently orphan any branch-protection rule matching the check by name), that timeout-minutes equals the expected value, that it parsed as an int and not a str, that runs-on survived, and that no strategy key was introduced.

Proof these assertions can actually fail. Both mutations were applied to the real files, run, and reverted:

  • Quoted the value — timeout-minutes: "5" in csat.yml:
    AssertionError: .github/workflows/csat.yml: timeout-minutes is '5', expected 5
  • Moved the key to the wrong parent — deleted it from the check-license job and indented it under the first step in license-check.yml, which is the realistic version of this mistake. A naive grep -c timeout-minutes on that file still returns 1; the parsed-tree assertion catches it:
    AssertionError: .github/workflows/license-check.yml: timeout-minutes is None, expected 5

3. The diff is minimal:

$ git diff main --shortstat
 5 files changed, 5 insertions(+)
$ git diff main -U0 | grep '^+' | grep -v '^+++'
+    timeout-minutes: 5
+    timeout-minutes: 15
+    timeout-minutes: 5
+    timeout-minutes: 5
+    timeout-minutes: 10
$ git diff main --name-only | grep -c 'validation.yaml'
0

Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.

The ceiling was observed firing, on a throwaway branch on a fork (never against google/adk-js). To reproduce: set timeout-minutes: 1 on check-license, insert a - run: sleep 300 step before the license step, push, and open a PR so the pull_request trigger fires. Observed on that probe run (gh api repos/<fork>/actions/runs/<id>/jobs):

job  check-license   started 16:24:37Z  completed 16:25:52Z  conclusion: cancelled
step "Hang for longer than the ceiling"  16:24:40Z -> 16:25:49Z  conclusion: cancelled   # sleep 300, killed after 69s
step "Run license check"                                         conclusion: skipped

The job was killed ~1 minute after start instead of running its 300-second sleep, and the check surfaced on the PR as a red, non-passing check in ~1m15s. One correction worth recording: the job's API conclusion is cancelled, not failure — it renders red and blocks a merge exactly like a failure, but anyone matching on conclusion == "failure" should know it is cancelled. The probe branch and its PR were deleted/closed; git diff main --shortstat is back to 5 files changed, 5 insertions(+).

Live checks on this PR exercise three of the five new ceilings for free, and all three passed well inside their new budgets — a healthy run is unaffected:

Check (new ceiling) Result Duration Budget used
Cross-Language Tests / run-tests (15 min) pass 1m21s 9%
License Header Check / check-license (5 min) pass 6s 2%
auto-assignment / auto-assign (5 min) pass 7s 2%

csat.yml fires only on issues: closed and release-please.yml only on push to main, so neither can be exercised from a PR; their correctness rests on check 2 above, which validates the parsed job object rather than the file text.

All checks on this PR are green, but one of them needed two reruns and that is worth disclosing rather than hiding: validation / run-tests (windows-latest) — a job from the untouched .github/workflows/validation.yaml — failed twice (8m11s, 8m46s) before passing (9m13s), each failure being the same single test, tests/integration/app_loader/app_loader_test.ts > should discover apps vs agents across directories and standalone files, with Error: Test timed out in 40000ms against 2674 passing tests. That test measures cold AgentLoader.listApps() discovery against a 40s per-test cap and is the known slow-Windows-runner flake already being addressed by separate PRs; this change adds no executable code for any test to exercise. Corroboration that it is unrelated: the identical five workflow edits on the throwaway probe branch passed validation on all three OSes ten minutes before the first failure (run-tests (windows-latest) success, 16:24:39Z → 16:33:14Z), and run-tests (ubuntu-latest) (5m28s) and run-tests (macos-latest) (7m3s) were green on every attempt here.

Checklist

[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.

Every job in these five workflows inherits GitHub's 360-minute default job
timeout. The cross-language job on macos-latest makes three uncached network
fetches (npm install and two go mod tidy runs); a hung fetch burns up to six
hours of runner time with no signal on the PR. The other four are seconds-long
github-script/bash jobs that have no legitimate reason to run for hours either.

Each value is sized from that workflow's own observed job execution time
(completed_at - started_at from the jobs API), not from run-level wall clock,
which is inflated by runner queue wait that is not charged against
timeout-minutes:

  cross-language-integration  run-tests       max 143s  -> 15 (~6.3x)
  release-please              release-please  max  64s  -> 10 (~9.4x)
  license-check               check-license   max  24s  ->  5 (~12.5x)
  auto-assignment             auto-assign     max  24s  ->  5 (~12.5x)
  csat                        welcome         max   8s  ->  5 (~37.5x)

No strategy/fail-fast is added: none of these workflows has a matrix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant