Repository navigation
feat(timeouts): report how long jobs really took in their latest runs (--suggest-timeouts) - #275
Conversation
… (--suggest-timeouts) Closes #121. The "historical timeout recommendations, report-only" item. For each workflow it reads the latest successful runs and the jobs of each run from GitHub (implies --online) and reports samples, min, median and max per job, the dates of the runs, and a timeout-minutes to consider: the longest duration times --timeout-buffer (default 2), rounded up, for a job with at least 3 samples. Report only: it writes nothing and no rule or fix uses a number from it. It says what it cannot know: a matrix is one job whose legs share the timeout, a call of a reusable workflow has no numbers, jobs it cannot match to the file are listed, a failed lookup makes it incomplete, and a longest run on record is not a bound on the next one. Also: the GitHub repository of a linked work tree is read with git config when .git is a file. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3e73cb9. Configure here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- ## [2.2.0](v2.1.0...v2.2.0) (2026-10-14) ### Features * **action-input-dependencies:** report inputs of well-known actions that need each other ([#207](#207)) ([ef17270](ef17270)) * **action-release-cooldown:** report an action at a version younger than N days (online, opt-in) ([#235](#235)) ([7dbd838](7dbd838)) * **action:** check the inputs of any remote action against its action.yml with --online ([#231](#231)) ([ac30cbe](ac30cbe)) * **actions-lock:** lint GitHub's Actions lock file against the workflows ([#237](#237)) ([c9d8944](c9d8944)) * **argument-injection:** report free text in the program or the options of sed, awk, find and tar ([#221](#221)) ([ecf46dc](ecf46dc)) * **aube:** understand aube in the rules that know npm, pnpm, yarn and bun ([#254](#254)) ([f6d9319](f6d9319)) * **cache-poisoning:** add pnpm/setup, graalvm and composer, and report caches of credential files ([#216](#216)) ([e3c29f0](e3c29f0)) * **cache-poisoning:** report more zizmor findings and add a pedantic tier ([#160](#160)) ([c725459](c725459)) * **cli:** add --ruff, --show-doc-urls, --finding-context, platform forgejo and jujutsu roots ([#215](#215)) ([8e31c5c](8e31c5c)) * **cli:** add shell completions and --explain RULE ([#204](#204)) ([b3b09d3](b3b09d3)) * **cli:** add the options select, no-ignores and list-rules ([#200](#200)) ([dca5092](dca5092)) * **cli:** show only the findings on what changed since a revision (--changed-since) ([#274](#274)) ([ea0ac90](ea0ac90)) * **command-injection:** report input that a script runs as code after it moved to env ([#202](#202)) ([ebeeb6f](ebeeb6f)) * **comment-trigger-authorization:** report comment-triggered jobs that do not check who commented (pedantic) ([#217](#217)) ([60fa75e](60fa75e)) * **concurrency-deadlock:** report a group that a workflow shares with the workflow it calls ([#210](#210)) ([c41e2ad](c41e2ad)) * **concurrency-limits:** report schedule-only and manual-only workflows (pedantic) ([#266](#266)) ([c1ec820](c1ec820)) * **config:** JSON Schemas for the config file and for --format json ([#195](#195)) ([5f83de6](5f83de6)) * **dangerous-triggers:** say what the token and the secrets can do, and make a workflow without privileges pedantic ([#189](#189)) ([97e79e7](97e79e7)) * **deprecated-node-runtime:** report a Node.js runtime that GitHub removed in the metadata of an action (pedantic) ([#225](#225)) ([84dd922](84dd922)) * **deprecated-node-runtime:** report popular actions at majors that declare node20 (pedantic) ([#230](#230)) ([8a9fc95](8a9fc95)) * **deprecated-node-runtime:** report the environment variables that chose the Node.js runtime (pedantic) ([#234](#234)) ([6531d87](6531d87)) * **environment-file-redirect:** report writes to GITHUB_OUTPUT that lose the data ([#184](#184)) ([79cc5ef](79cc5ef)) * **event-name-never-received:** report github.event_name compared with an event the workflow never receives ([#214](#214)) ([b3bfcc4](b3bfcc4)) * **external-tools:** say when shellcheck or pyflakes did not run ([#188](#188)) ([f7280e5](f7280e5)) * **facts:** export the events, permissions, concurrency and calls of a repository as versioned JSON (--export-facts) ([#272](#272)) ([881dde9](881dde9)) * **fail-fast-action-inputs:** report actions that pass when nothing was produced ([#196](#196)) ([f856144](f856144)) * **fix:** say why more findings have no fix, add --show-fix-status and the fix prerequisites to --explain ([#269](#269)) ([7aa3187](7aa3187)) * **gh-without-token:** report the gh command without GH_TOKEN or GITHUB_TOKEN ([#177](#177)) ([1d997da](1d997da)) * **github-app:** check two more token actions and cover local composite actions in dependabot-missing-actions-update ([#229](#229)) ([e5b620e](e5b620e)) * **github-env:** follow text that a script reads with gh and git ([#218](#218)) ([9d0a4bb](9d0a4bb)) * **github-script-syntax:** check the JavaScript of actions/github-script with node --check ([#242](#242)) ([f106562](f106562)) * **hardcoded-registry-credentials:** report credentials in plain text in the registries of dependabot.yml ([#265](#265)) ([765521c](765521c)) * **macos-gnu-flags:** report GNU-only options in run scripts of macOS jobs (pedantic) ([#241](#241)) ([871c07d](871c07d)) * **malformed-expression-delimiters:** report ${ github.ref }, $ {{ x }} and {{ github.sha }} ([#180](#180)) ([e452449](e452449)) * **missing-version-comment:** report a hash pin without a version comment and add it with --online --fix ([#232](#232)) ([97767b4](97767b4)) * **multiline-plain-run:** report a run folded into one line ([#205](#205)) ([f4e76c4](f4e76c4)) * **online:** look up the actions of a GitHub Enterprise Server on github.com ([#250](#250)) ([d8a1280](d8a1280)) * **online:** report coverage and cache freshness outside SARIF ([#166](#166)) ([5fd779e](5fd779e)) * **online:** report the jobs whose environment has no required reviewers ([#247](#247)) ([6c13781](6c13781)) * **output-clobbering:** report input with line breaks written to GITHUB_OUTPUT as a record ([#212](#212)) ([c992eef](c992eef)) * **permissions:** report a job whose permissions are too narrow for its actions ([#192](#192)) ([07d78b7](07d78b7)) * **ref-version-mismatch:** fix the version comment (unsafe) and read ratchet comments ([#227](#227)) ([60842f7](60842f7)) * **release:** publish wheels to PyPI with go-to-wheel ([#277](#277)) ([0440a3a](0440a3a)) * **repojacking:** report an action whose owner name anyone can register ([#248](#248)) ([3b62077](3b62077)) * **runner-label:** accept the labels of hosted-runner providers ([#199](#199)) ([27e453c](27e453c)) * **runner-label:** check runner labels built from a matrix value and text ([#233](#233)) ([b67bbd4](b67bbd4)) * **runner-label:** warn ahead of the retirement of a runner label and name it afterwards ([#182](#182)) ([0c3756b](0c3756b)) * **sarif:** say why a finding with a fix has none in the log ([#164](#164)) ([7a2a509](7a2a509)) * **self-hosted-runner:** count the configured labels and runner groups, and say when a pull request reaches the runner ([#194](#194)) ([80c43f8](80c43f8)) * **shell-syntax:** report syntax errors in run scripts without shellcheck ([#273](#273)) ([31ee66e](31ee66e)) * **shellcheck:** read a shellcheckrc, extra arguments and optional checks from the config ([#191](#191)) ([4d02a46](4d02a46)) * **superfluous-actions:** resolve runs-on expressions the repository tells ([#271](#271)) ([22a3f8e](22a3f8e)) * **template-injection:** report more free text of outsiders in the default profile ([#173](#173)) ([08afe4a](08afe4a)) * **timeouts:** report how long jobs really took in their latest runs (--suggest-timeouts) ([#275](#275)) ([033f615](033f615)) * **ubuntu-slim:** report what the single-CPU runner cannot run ([#190](#190)) ([51a5e7f](51a5e7f)) * **undefined-property:** check the properties of github.event against the webhook payloads ([#249](#249)) ([81a1038](81a1038)) * **unlocked-install:** model the lifecycle scripts and the release age of aube (pedantic) ([#255](#255)) ([f5dfab7](f5dfab7)) * **unlocked-install:** report installs without a release-age cooldown (pedantic) ([#201](#201)) ([13779f6](13779f6)) * **unlocked-install:** report npm, yarn and bun installs that run lifecycle scripts (pedantic) ([#187](#187)) ([ee74c67](ee74c67)) * **unmatched-paths-filter:** report paths filters that match no file of the repository (pedantic) ([#238](#238)) ([2baf2cb](2baf2cb)) * **unpinnable-action:** report a hash-pinned action whose own dependencies are not pinned ([#240](#240)) ([3cc0ddb](3cc0ddb)) * **unpinned-images:** check docker run images in scripts, add per-image policies and image allow/deny lists ([#211](#211)) ([376b065](376b065)) * **unpinned-images:** pin container images to a digest with --online --fix=unsafe ([#239](#239)) ([2f63474](2f63474)) * **unpinned-uses:** accept a ref which the Actions lock file pins to a commit ([#228](#228)) ([c702b7c](c702b7c)) * **unpinned-uses:** accept the tag of an immutable release under --online ([#251](#251)) ([c112760](c112760)) * **unpinned-uses:** name the most specific version tag in the comment of --online --fix ([#224](#224)) ([84dd5fc](84dd5fc)) * **unredacted-secrets:** report values cut from a secret and printed by a script (pedantic) ([#222](#222)) ([c45a29f](c45a29f)) * **untrusted-artifact:** know more download actions and report a download of every artifact with the option pedantic ([#219](#219)) ([bc216f9](bc216f9)) * **untrusted-checkout:** count the actions that run the workspace, with the input that makes them do it ([#213](#213)) ([29fea2c](29fea2c)) * **untrusted-checkout:** cover issue_comment workflows that build a pull request ([#206](#206)) ([6b73b42](6b73b42)) * **untrusted-checkout:** follow a pull request revision through variables and script files ([#159](#159)) ([78813f4](78813f4)) * **untrusted-checkout:** follow the head of a pull request into local reusable workflows and composite actions ([#208](#208)) ([29843f2](29843f2)) * **untrusted-checkout:** honor the fork pull request protection of actions/checkout ([#203](#203)) ([1acb258](1acb258)) * **untrusted-checkout:** report gates that hold at one moment in front of code that can change, and labels that last ([#220](#220)) ([09fc91e](09fc91e)) * **use-trusted-publishing:** report jobs that build, lack an environment or drop attestations (pedantic) ([#209](#209)) ([1c26e5f](1c26e5f)) * **workflow-call:** check the calls of reusable workflows of other repositories with --online ([#236](#236)) ([89f2c75](89f2c75)) * **workflow-secret-scope:** job-level secrets, secrets passed to reusable workflows, and unused workflow_call secrets ([#223](#223)) ([e7a53ef](e7a53ef)) ### Bug Fixes * **action:** check popular actions pinned to a full version or to a commit with a version comment ([#174](#174)) ([d324b7c](d324b7c)) * **api:** accept nil LinterOptions, print the rule ID in Error and PrettyPrint, correct API comments ([#283](#283)) ([e2d3304](e2d3304)) * **changed-since:** count shared findings once, keep ..foo inside the repository, refuse --export-facts ([#279](#279)) ([341ccb5](341ccb5)) * **config:** a missing file in "extends" of a discovered config file is an error ([#285](#285)) ([1a848ff](1a848ff)) * **download-script:** report a missing release, accept amd64, fix the help text ([#282](#282)) ([3165939](3165939)) * **if-always-true:** do not report the newlines at the end of a block scalar ([#226](#226)) ([871eb35](871eb35)) * **ignore:** read own-line ignore directives from real YAML comments only ([#284](#284)) ([3f9e648](3f9e648)) * **missing-timeout:** add accept-step-timeouts for jobs whose steps all set timeout-minutes ([#252](#252)) ([10cfa99](10cfa99)) * **multiline-plain-run:** rename commandWords so the package builds again ([#256](#256)) ([24d8900](24d8900)) * **parser:** accept a whole mapping or list written as one expression where the schema allows it ([#178](#178)) ([5689451](5689451)) * **permissions:** report the pedantic persona's permission findings ([#176](#176)) ([016f588](016f588)) * **runner-label:** follow the images behind the -latest labels ([#171](#171)) ([8e6159e](8e6159e)) * **syntax:** accept keys and values the workflow schema allows but the docs do not list ([#170](#170)) ([66df2e0](66df2e0)) * **syntax:** report what the runner rejects in schedules, merge_group, self-repository uses and action.yml ([#186](#186)) ([f498457](f498457)) * **untrusted-checkout:** follow pull/N/head fetches, skip merged pull requests, count sed -f, awk -f, tar -P and unzip -: ([#175](#175)) ([5fd975f](5fd975f)) ### Performance Improvements * analyze each run script once and parse the callers of an action once ([#270](#270)) ([ac37b69](ac37b69)) * bound concurrent workflow checks to cut peak memory on huge repositories ([#158](#158)) ([bbe49da](bbe49da)) * **online:** read the advisories of all actions with one request ([#268](#268)) ([a04bcd2](a04bcd2)) * share the parsed expressions of a string with the rules that still parsed them again ([#165](#165)) ([9bcdbb0](9bcdbb0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Mechanical version and documentation updates only; no runtime or rule logic changes in the diff. > > **Overview** > **Release Please** bumps the published version from **2.1.0** to **2.2.0** and keeps every `x-release-please-version` marker aligned: `.release-please-manifest.json`, the GitHub Action’s default `version` input in `action.yml`, the `jactionlint-docker` pre-commit image tag, docs site version (`docs/.vitepress/version.ts`), and install examples in `docs/ci.md` and `docs/hooks.md`. > > `CHANGELOG.md` gains the **2.2.0** section (2026-10-14) summarizing what ships in this release—many new lint rules and CLI options (`--changed-since`, `--export-facts`, `--explain`, online action/workflow validation, Actions lock file linting), security-focused checks (untrusted checkout, cache poisoning, repojacking, etc.), bug fixes, and performance work. This PR does not change linter implementation; it is the version and changelog cut for code already on `main`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e0e7f9d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

Closes #121 (PR 4 of 4, so this is the one that closes it once the earlier ones are merged: #269, #272, #274). It is independent of the others, except that all four edit
docs/usage.md,man/jactionlint.1.ronnandcommand_options.go, so expect trivial merge conflicts there.This is the "Later" item "historical timeout recommendations, report-only" (ghatm is the precedent). The issue asks for: sample count, date range, job/matrix matching limitations and a suggested buffer; review before applying a value; historical maxima are not guarantees. All of them are in the output.
What it does
jactionlint --suggest-timeouts(implies--online;--timeout-history-runs N, default 20, 1 to 100;--timeout-buffer FACTOR, default 2, at least 1) reads, for each workflow, the latest successful runs and the jobs of each run, and prints per job: declaredtimeout-minutes, samples, min, median, max (minutes, from the start on a runner to the end, so the wait for a runner is not in it) and a suggestion.--format jsonprints the same as a versioned document; other formats are refused.missing-timeoutstill never chooses a value (the CONTRIBUTING rule), anddefault-minutesstays the person's decision. The output ends by saying so, and tells the person the largest suggestion, which is the least that onedefault-minutescan be without cancelling the slowest job on record.ceil(max x buffer)for a job with at least 3 samples. Below that, no number, and the note says why.complete: falsewith the count, and--online=strictfails on it like the other online checks; "runs may predate the current file".name:(an expression in it matches any text), or the ID, with the matrix values in parentheses that GitHub adds (Unit tests (macos-latest)forname: Unit testswith a matrix; I found that rule by running it against the real API, the first version missed it).Plumbing
A new optional
GitHubRunsClientinterface (WorkflowRuns,RunJobs) on the HTTP client (so the cache, ETags, rate limit handling, redaction and the token rules apply as for every other lookup), the fixture client (workflow_runsin the fixtures JSON), and the GitHub Enterprise fallback wrapper (which asks the Server only, like environments: the repository of the workflow is on the Server). The recording client does not record runs. Names are validated before they are put in a path (validWorkflowFile). The repository isGITHUB_REPOSITORYor theoriginremote on github.com;githubRepositoryOfnow also asksgit configwhen.gitis a file (linked work trees and submodules, where it returned nothing; the environment rule uses the same function).Testing
go vet,go test -race ./...,hk check --allpass locally; the WebAssembly build still compiles. Tests: the name matching (matrix, named, expression names, reusable calls), a fake API with the exact paths and query parameters (status=success,per_page), the numbers (min/median/max, the 8-sample matrix, a failed and a skipped execution that are not samples, 2 samples that get no suggestion, a job that never ran, a call of a workflow, an unmatched name, the buffer), the text, the JSON, an unknown workflow (404), an incomplete run (403 for one run, and--online=strict), the option errors, the fixture client, and that the workflow file is not touched.Real API: I ran it against the history of jdx/jactionlint itself (the 13 workflows of the repository, 6 runs each and then 20 for two of them, about 100 requests with a token) and read the output: the numbers are plausible, a workflow that never succeeded recently says "no successful run on record", the reusable-workflow call has no numbers, and the matrix of
ci.yamlis one job with 3 legs and 60 samples.Not done / not measured
caller / job); the note says so.docs/usage.md.What the series did for #121 (so it can close)
Read against the issue body and the maintainer's comment ("items 1 to 4 done; open: item 5 and the Later list"):
--show-fix-statusand the prerequisites in--explainworkflow-secret-scope--online-report(any format) and SARIF carry status, skipped lookups by kind, excluded repositories, and the freshness of cached answers (oldestFetched,cacheTtlSeconds, counts of network, cache, stale and immutable answers) with the timestamp source documentedactions.lockcompatibility--explain#204,--print-config-schema#195)Not done: per-finding freshness (which finding used a cached answer). The cache layer does not know the findings and the rules share memoized answers, so it would be a new tracking path through every online rule for a number the run-level report already bounds; the run-level
answersandoldestFetchedare what consumers get.🤖 Generated with Claude Code
Note
Medium Risk
New GitHub Actions API traffic and token permissions (Actions read), plus non-trivial job-name matching logic; behavior is report-only and does not modify workflows or lint results.
Overview
Adds
--suggest-timeouts, a read-only report mode that turns on online checks, pulls each workflow’s latest successful runs from GitHub, and prints per-job duration stats (min/median/max) plus optionaltimeout-minutessuggestions. Suggestions useceil(max × --timeout-buffer)with at least three samples;--timeout-history-runs(1–100, default 20) controls how many runs are fetched. Output is text or json only; nothing is written to workflow files and lint rules are unchanged.New
GitHubRunsClient(WorkflowRuns,RunJobs) extends the existing HTTP client, fixtures, and GHE fallback with the same caching and rate-limit behavior as other online lookups.Linter.SuggestTimeouts()builds theTimeoutReport, matches run job names to workflow jobs (matrix legs, expression names, reusable-workflow calls), and documents gaps (unmatched names, truncated large matrices, incomplete API failures).githubRepositoryOfnow resolvesoriginviagit configwhen.gitis a file (worktrees/submodules).CLI guards reject combining this mode with
--fix,--export-facts,--no-online,--changed-since, stdin, and incompatible formats. Docs, man page, and tests cover matching, API queries, JSON/text output, and error cases.Reviewed by Cursor Bugbot for commit fd8189c. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
--suggest-timeoutsto report job durations from recent successful GitHub Actions runs and suggesttimeout-minutesvalues for jobs with enough samples.