@cratis/pi 7.101.0 → 7.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
  2. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
  3. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
  4. package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
  5. package/package/corpus/hooks/README.md +11 -1
  6. package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
  7. package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
  8. package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
  9. package/package/corpus/rules/github-actions.md +17 -0
  10. package/package/corpus/rules/profiles.md +10 -1
  11. package/package/corpus/rules/pull-requests.md +2 -0
  12. package/package/corpus/rules/secrets.md +4 -0
  13. package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
  14. package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
  15. package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
  16. package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
  17. package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
  18. package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
  19. package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
  20. package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
  21. package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
  22. package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
  23. package/package/corpus/skills/binlog-generation/LICENSE +23 -0
  24. package/package/corpus/skills/binlog-generation/NOTICE +22 -0
  25. package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
  26. package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
  27. package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
  28. package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
  29. package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
  30. package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
  31. package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
  32. package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
  33. package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
  34. package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
  35. package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
  36. package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
  37. package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
  38. package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
  39. package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
  40. package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
  41. package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
  42. package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
  43. package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
  44. package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
  45. package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
  46. package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
  47. package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
  48. package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
  49. package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
  50. package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
  51. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
  52. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
  53. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
  54. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
  55. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
  56. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
  57. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
  58. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
  59. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
  60. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
  61. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
  62. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
  63. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
  64. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
  65. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
  66. package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
  67. package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
  68. package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
  69. package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
  70. package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
  71. package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
  72. package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
  73. package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
  74. package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
  75. package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
  76. package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
  77. package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
  78. package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
  79. package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
  80. package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
  81. package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
  82. package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
  83. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
  84. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
  85. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
  86. package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
  87. package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
  88. package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
  89. package/package/corpus/skills/dump-collect/LICENSE +23 -0
  90. package/package/corpus/skills/dump-collect/NOTICE +40 -0
  91. package/package/corpus/skills/dump-collect/SKILL.md +97 -0
  92. package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
  93. package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
  94. package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
  95. package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
  96. package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
  97. package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
  98. package/package/corpus/skills/extension-points/LICENSE +23 -0
  99. package/package/corpus/skills/extension-points/NOTICE +18 -0
  100. package/package/corpus/skills/extension-points/SKILL.md +248 -0
  101. package/package/corpus/skills/filter-syntax/LICENSE +23 -0
  102. package/package/corpus/skills/filter-syntax/NOTICE +17 -0
  103. package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
  104. package/package/corpus/skills/including-generated-files/LICENSE +23 -0
  105. package/package/corpus/skills/including-generated-files/NOTICE +17 -0
  106. package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
  107. package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
  108. package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
  109. package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
  110. package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
  111. package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
  112. package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
  113. package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
  114. package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
  115. package/package/corpus/skills/msbuild/LICENSE +23 -0
  116. package/package/corpus/skills/msbuild/NOTICE +55 -0
  117. package/package/corpus/skills/msbuild/SKILL.md +117 -0
  118. package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
  119. package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
  120. package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
  121. package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
  122. package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
  123. package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
  124. package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
  125. package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
  126. package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
  127. package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
  128. package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
  129. package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
  130. package/package/corpus/skills/orleans/LICENSE +21 -0
  131. package/package/corpus/skills/orleans/NOTICE +64 -0
  132. package/package/corpus/skills/orleans/SKILL.md +140 -0
  133. package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
  134. package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
  135. package/package/corpus/skills/orleans/references/examples.md +66 -0
  136. package/package/corpus/skills/orleans/references/grain-api.md +784 -0
  137. package/package/corpus/skills/orleans/references/grains.md +75 -0
  138. package/package/corpus/skills/orleans/references/hosting.md +71 -0
  139. package/package/corpus/skills/orleans/references/implementation.md +591 -0
  140. package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
  141. package/package/corpus/skills/orleans/references/patterns.md +811 -0
  142. package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
  143. package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
  144. package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
  145. package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
  146. package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
  147. package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
  148. package/package/corpus/skills/platform-detection/LICENSE +23 -0
  149. package/package/corpus/skills/platform-detection/NOTICE +17 -0
  150. package/package/corpus/skills/platform-detection/SKILL.md +230 -0
  151. package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
  152. package/package/corpus/skills/profiling/LICENSE +21 -0
  153. package/package/corpus/skills/profiling/NOTICE +50 -0
  154. package/package/corpus/skills/profiling/SKILL.md +144 -0
  155. package/package/corpus/skills/profiling/references/commands.md +439 -0
  156. package/package/corpus/skills/profiling/references/patterns.md +586 -0
  157. package/package/corpus/skills/profiling/references/profiling.md +190 -0
  158. package/package/corpus/skills/run-tests/LICENSE +23 -0
  159. package/package/corpus/skills/run-tests/NOTICE +19 -0
  160. package/package/corpus/skills/run-tests/SKILL.md +305 -0
  161. package/package/corpus/skills/target-authoring/LICENSE +23 -0
  162. package/package/corpus/skills/target-authoring/NOTICE +17 -0
  163. package/package/corpus/skills/target-authoring/SKILL.md +165 -0
  164. package/package/corpus/skills/vectorization/LICENSE +23 -0
  165. package/package/corpus/skills/vectorization/NOTICE +17 -0
  166. package/package/corpus/skills/vectorization/SKILL.md +189 -0
  167. package/package/profile-catalog.json +87 -1
  168. package/package.json +3 -2
@@ -0,0 +1,82 @@
1
+ # Test setup cost and where to test a guarantee
2
+
3
+ Use this when a test lane is slow but the runner's per-test times look small, when
4
+ a suite re-tests a dependency, or when compile errors surface late.
5
+
6
+ ## Contents
7
+
8
+ - Time hidden outside per-test timing
9
+ - Profile the test host
10
+ - Fix setup upstream
11
+ - Test each guarantee where it can break
12
+ - Surface compile errors early
13
+ - Count compiles per push
14
+
15
+ ## Time hidden outside per-test timing
16
+
17
+ Compare the test runner's reported total test time with the wall time of the test
18
+ step. A large gap means time is spent outside what the runner attributes to tests:
19
+ per-class or per-fixture setup (scenario, host or container construction), discovery,
20
+ or process start. Specification frameworks that build a context in a base-class
21
+ setup often charge it to no test at all.
22
+
23
+ 1. Read both numbers from the same run: the runner's summary and the step duration
24
+ from `ci-timings.sh`'s slowest-steps table.
25
+ 2. If setup dominates, sharding only divides the waste across more jobs. Fix the
26
+ setup first.
27
+
28
+ ## Profile the test host
29
+
30
+ Measure before guessing, with the language's profiler on the test process:
31
+
32
+ ```bash
33
+ dotnet tool install --global dotnet-trace
34
+ dotnet-trace collect --format speedscope -- dotnet test Project.Specs --no-build --configuration Release
35
+ ```
36
+
37
+ Read the inclusive time of the setup methods. Pass means the profile names the
38
+ dominant constructor or discovery call; a flat profile means the cost is elsewhere
39
+ (I/O, container start, process start).
40
+
41
+ ## Fix setup upstream
42
+
43
+ Per-test construction of identical, immutable data is the usual cause. Fix it where
44
+ the data originates rather than skipping specifications:
45
+
46
+ - Build shared metadata (type discovery, schemas, registrations) once per process
47
+ and reuse it across scenarios.
48
+ - Share expensive generators and caches instead of creating one per instance.
49
+ - Cache reflection results that are recomputed on every call.
50
+ - Use a prebuilt service collection as a template and copy it per test.
51
+ - Do not share mutable state between tests to save time; share only what is immutable.
52
+
53
+ A fix belongs in the library that owns the setup, not in each consuming repository.
54
+
55
+ ## Test each guarantee where it can break
56
+
57
+ - A suite that tests a dependency's semantics belongs in that dependency's own
58
+ repository. In a consumer, run it only when the dependency pin changes, or nightly.
59
+ - Check a wire or API shape with a committed snapshot and a diff, not a live
60
+ integration run.
61
+ - Mixed-version or upgrade matrices run nightly or before a release, not on every
62
+ pull request.
63
+ - Where a behavior is only meaningful in a deployed environment, prefer a
64
+ post-deploy synthetic check plus unit specifications and contract snapshots over
65
+ a pull-request integration environment.
66
+ - Never remove the only check of a required guarantee; relocate it and say where it
67
+ now runs.
68
+
69
+ ## Surface compile errors early
70
+
71
+ A pull request that fails to compile should fail in the first minutes, not after a
72
+ long test leg. Make a fast build or lint job a `needs` of the long legs, and keep
73
+ it short enough that it is not itself the critical path.
74
+
75
+ ## Count compiles per push
76
+
77
+ Count how many times one push compiles the same code across all jobs and
78
+ workflows (`dotnet build` or `dotnet test` without `--no-build` in the step list).
79
+ Each extra compile is avoidable: build once, reuse the output (see
80
+ `build-once-and-shard.md`). Analyzers can roughly double compile time; run them once
81
+ in a parallel lint job and disable them in the shared build, while keeping the
82
+ strictest configuration that consumers enforce somewhere on the required path.
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) Cratis. All rights reserved.
3
+ # Licensed under the MIT license. See LICENSE file in the project root for full license information.
4
+ #
5
+ # Summarize the timing, failure and queue behavior of one GitHub Actions workflow
6
+ # from its recent runs. Read-only; uses gh, jq and POSIX utilities.
7
+ #
8
+ # Exit codes: 0 ok, 2 usage error, 3 no runs or no data, 4 missing gh/jq or gh auth failure.
9
+
10
+ set -u
11
+
12
+ usage() {
13
+ cat <<'USAGE'
14
+ Usage: ci-timings.sh --repo OWNER/REPO --workflow FILE_OR_NAME [options]
15
+ ci-timings.sh --self-test
16
+ ci-timings.sh --help
17
+
18
+ Options:
19
+ --repo OWNER/REPO Repository to analyze (required).
20
+ --workflow FILE_OR_NAME Workflow file name (ci.yml) or display name (required).
21
+ --limit N Runs to list, most recent first (default 50).
22
+ --jobs N Most recent successful first-attempt runs to fetch job
23
+ and step timings for (default 10).
24
+ --branch B Only runs on branch B.
25
+ --event E Only runs triggered by event E (pull_request, push, ...).
26
+ --self-test Run the analysis on bundled fixtures; needs no network or gh.
27
+
28
+ Prints a markdown report on stdout: conclusion counts, cancel/fail/rerun rates,
29
+ wall-clock median and p90, per-run queue, execution span and runner-minutes,
30
+ the slowest jobs and steps, and a classification hint.
31
+
32
+ Percentiles take the lower nearest rank: sorted[floor((n-1)*p)].
33
+ Exit codes: 0 ok, 2 usage error, 3 no runs or no data, 4 missing gh/jq or gh auth failure.
34
+ USAGE
35
+ }
36
+
37
+ die() { # die CODE MESSAGE
38
+ code=$1
39
+ shift
40
+ printf 'ci-timings: %s\n' "$*" >&2
41
+ exit "$code"
42
+ }
43
+
44
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
45
+
46
+ # Selects the runs whose job timings are fetched: successful first attempts, newest first.
47
+ read -r -d '' SAMPLE_JQ <<'JQ' || true
48
+ map(select(.status == "completed" and .conclusion == "success" and .attempt == 1))
49
+ | sort_by(.createdAt) | reverse | .[0:$n] | map(.databaseId)
50
+ JQ
51
+
52
+ # Inputs: --slurpfile runs (gh run list JSON), --slurpfile jobs ([{id, jobs: [API job objects]}]),
53
+ # --arg repo, --arg workflow, --arg filters, --argjson njobs.
54
+ read -r -d '' REPORT_JQ <<'JQ' || true
55
+ def ts: fromdateiso8601;
56
+ def pct($p): sort as $s | if ($s | length) == 0 then null else $s[((($s | length) - 1) * $p) | floor] end;
57
+ def f1: if . == null then "n/a" else ((. * 10 | round) as $r | "\($r / 10 | floor).\($r % 10)") end;
58
+ def mins: if . == null then null else . / 60 end;
59
+ def rate($a; $b): if $b == 0 then 0 else $a / $b * 100 end;
60
+ def dur($o): (($o.completed_at | ts) - ($o.started_at | ts));
61
+ def usable: select(.started_at != null and .completed_at != null and .conclusion != "skipped");
62
+
63
+ $runs[0] as $all
64
+ | ($all | map(select(.status == "completed" and .conclusion != "skipped"))) as $done
65
+ | ($done | map(select(.conclusion == "success"))) as $succ
66
+ | ($done | map(select(.conclusion == "failure"))) as $fail
67
+ | ($done | map(select(.conclusion == "cancelled"))) as $canc
68
+ | ($done | map(select(.attempt > 1))) as $rerun
69
+ | ($done | map(select((.conclusion == "success" or .conclusion == "failure") and .attempt > 1)) | length) as $rerunExcluded
70
+ | ($done | map(select((.conclusion == "success" or .conclusion == "failure") and .attempt == 1)
71
+ | ((.updatedAt | ts) - (.createdAt | ts)))) as $walls
72
+ | (rate($fail | length; $done | length)) as $failP
73
+ | (rate($canc | length; $done | length)) as $cancP
74
+ | (rate($rerun | length; $done | length)) as $rerunP
75
+ | ($all | map({id: .databaseId, created: (.createdAt | ts), wall: ((.updatedAt | ts) - (.createdAt | ts))})) as $runIndex
76
+ | ($jobs[0]
77
+ | map(. as $e
78
+ | ($runIndex | map(select(.id == $e.id)) | first) as $r
79
+ | select($r != null)
80
+ | ($e.jobs | map(usable)) as $j
81
+ | select(($j | length) > 0)
82
+ | {id: $e.id, wall: $r.wall, jobs: $j,
83
+ count: ($j | length),
84
+ first: (($j | map(.started_at | ts) | min) - $r.created),
85
+ maxq: ($j | map(((.started_at | ts) - ((.created_at // .started_at) | ts))) | max),
86
+ sumq: ($j | map(((.started_at | ts) - ((.created_at // .started_at) | ts))) | add),
87
+ span: (($j | map(.completed_at | ts) | max) - ($j | map(.started_at | ts) | min)),
88
+ runner: ($j | map(dur(.)) | add)})) as $sample
89
+ | ($sample | map(.jobs | length) | add // 0) as $njobsTotal
90
+ | ($sample | map(.jobs[] | (.steps // [])[] | select(.started_at != null and .completed_at != null and .conclusion != "skipped")) | length) as $nsteps
91
+ | ($sample | map(.maxq / .wall * 100 | select(. != null))) as $shares
92
+ | (if ($shares | length) == 0 then null else ($shares | pct(0.5)) end) as $qshare
93
+ | ([ (if ($failP >= 15 or $rerunP >= 15) then "failure-bound" else empty end),
94
+ (if $cancP >= 20 then "cancel-bound" else empty end),
95
+ (if ($qshare != null and $qshare >= 25) then "queue-bound" else empty end) ]) as $flags
96
+ | (if ($flags | length) == 0 then "execution-bound" else $flags[0] end) as $primary
97
+ | (if ($flags | length) > 1 then " (also: " + ($flags[1:] | join(", ")) + ")" else "" end) as $also
98
+ | "@@COUNTS \($done | length) \($walls | length) \($sample | length) \($nsteps)",
99
+ "# CI timings: \($repo) / \($workflow)",
100
+ "",
101
+ "Sample: \($all | length) runs listed\($filters), \($done | length) completed, \($all | map(select(.status != "completed")) | length) not finished, \($all | map(select(.conclusion == "skipped")) | length) skipped.",
102
+ "Analyzed: \($done | length) completed runs, \($walls | length) first-attempt runs for wall clock, \($sample | length) sampled runs with \($njobsTotal) jobs and \($nsteps) steps.",
103
+ (if ($done | length) < 10 then "Small sample: fewer than 10 completed runs; treat every figure as indicative." else empty end),
104
+ "",
105
+ "| Conclusion | Runs |",
106
+ "|---|---|",
107
+ ($all | map({k: (if .status == "completed" then .conclusion else .status end)})
108
+ | group_by(.k) | map({k: .[0].k, n: length}) | sort_by([-.n, .k])
109
+ | .[] | "| \(.k) | \(.n) |"),
110
+ "",
111
+ "Rates over \($done | length) completed runs: cancelled \($cancP | f1)%, failed \($failP | f1)%, rerun (attempt > 1) \($rerunP | f1)%.",
112
+ "Wall clock (success and failure, first attempts only, \($walls | length) runs; \($rerunExcluded) rerun runs excluded): median \($walls | pct(0.5) | mins | f1) min, p90 \($walls | pct(0.9) | mins | f1) min.",
113
+ "",
114
+ (
115
+ "## Per run (\($sample | length) most recent successful first-attempt runs)",
116
+ "",
117
+ "Wall is run creation to last update. First start wait is run creation to the first job start. Max job queue is the longest job wait for a runner. Execution span is first job start to last job end. Runner-min is the sum of job durations.",
118
+ "",
119
+ "| Run | Wall min | First start wait min | Max job queue min | Sum job queue min | Execution span min | Runner-min | Jobs |",
120
+ "|---|---|---|---|---|---|---|---|",
121
+ ($sample | .[] | "| \(.id) | \(.wall | mins | f1) | \(.first | mins | f1) | \(.maxq | mins | f1) | \(.sumq | mins | f1) | \(.span | mins | f1) | \(.runner | mins | f1) | \(.count) |"),
122
+ "| median | \($sample | map(.wall) | pct(0.5) | mins | f1) | \($sample | map(.first) | pct(0.5) | mins | f1) | \($sample | map(.maxq) | pct(0.5) | mins | f1) | \($sample | map(.sumq) | pct(0.5) | mins | f1) | \($sample | map(.span) | pct(0.5) | mins | f1) | \($sample | map(.runner) | pct(0.5) | mins | f1) | \($sample | map(.count) | pct(0.5)) |",
123
+ "",
124
+ "## Slowest jobs (median minutes across the sampled runs)",
125
+ "",
126
+ "| Job | Median min | Runs |",
127
+ "|---|---|---|",
128
+ ($sample | map(.jobs[] | {k: .name, d: dur(.)}) | group_by(.k)
129
+ | map({k: .[0].k, m: (map(.d) | pct(0.5)), n: length}) | sort_by([-.m, .k]) | .[0:8]
130
+ | .[] | "| \(.k) | \(.m | mins | f1) | \(.n) |"),
131
+ "",
132
+ "## Slowest steps (median minutes across the sampled runs)",
133
+ "",
134
+ "| Job / step | Median min | Runs |",
135
+ "|---|---|---|",
136
+ ($sample | map(.jobs[] | .name as $jn | (.steps // [])[]
137
+ | select(.started_at != null and .completed_at != null and .conclusion != "skipped")
138
+ | {k: "\($jn) / \(.name)", d: dur(.)})
139
+ | group_by(.k) | map({k: .[0].k, m: (map(.d) | pct(0.5)), n: length}) | sort_by([-.m, .k]) | .[0:8]
140
+ | .[] | "| \(.k) | \(.m | mins | f1) | \(.n) |")
141
+ ),
142
+ "",
143
+ "## Classification",
144
+ "",
145
+ "Classification: \($primary)\($also) (queue share \(if $qshare == null then "n/a" else ($qshare | f1) + "%" end), failed \($failP | f1)%, cancelled \($cancP | f1)%, rerun \($rerunP | f1)%).",
146
+ "Hint only: thresholds are failure-bound at failed or rerun >= 15%, cancel-bound at cancelled >= 20%, queue-bound at median max-job-queue share of wall >= 25%, otherwise execution-bound. Confirm on the critical path of the required checks."
147
+ JQ
148
+
149
+ # report RUNS_JSON JOBS_JSON REPO WORKFLOW FILTERS NJOBS
150
+ # Every analyzed population must be non-empty; otherwise exit 3 with the reason on stderr.
151
+ report() {
152
+ local count out counts done_n walls_n sample_n steps_n why
153
+ count=$(jq 'length' "$1") || return 4
154
+ if [ "$count" = "0" ]; then
155
+ printf 'ci-timings: no runs found for %s / %s%s\n' "$3" "$4" "$5" >&2
156
+ return 3
157
+ fi
158
+ out=$(jq -n -r --slurpfile runs "$1" --slurpfile jobs "$2" \
159
+ --arg repo "$3" --arg workflow "$4" --arg filters "$5" --argjson njobs "$6" \
160
+ "$REPORT_JQ") || return 4
161
+ counts=$(printf '%s\n' "$out" | head -n 1)
162
+ read -r _ done_n walls_n sample_n steps_n <<EOF_COUNTS
163
+ $counts
164
+ EOF_COUNTS
165
+ why=""
166
+ if [ "$done_n" = "0" ]; then
167
+ why="$count runs found but none completed (all unfinished or skipped)"
168
+ elif [ "$walls_n" = "0" ]; then
169
+ why="no first-attempt run ended in success or failure (all cancelled, rerun or skipped)"
170
+ elif [ "$sample_n" = "0" ]; then
171
+ why="no successful first-attempt run has usable job timestamps; job, queue and runner-minute figures cannot be computed"
172
+ elif [ "$steps_n" = "0" ]; then
173
+ why="the sampled jobs carry no step timings"
174
+ fi
175
+ if [ -n "$why" ]; then
176
+ printf 'ci-timings: no data for %s / %s%s: %s. Widen --limit, drop --branch/--event, or pick another workflow.\n' "$3" "$4" "$5" "$why" >&2
177
+ return 3
178
+ fi
179
+ printf '%s\n' "$out" | tail -n +2
180
+ }
181
+
182
+ sample_ids() { # sample_ids RUNS_JSON N
183
+ jq -r --argjson n "$2" "$SAMPLE_JQ | .[]" "$1"
184
+ }
185
+
186
+ expect_exit3() { # expect_exit3 LABEL RUNS JOBS (uses and updates failures of self_test)
187
+ local out rc
188
+ out=$(report "$2" "$3" "example/repo" "ci.yml" "" 3 2>/dev/null)
189
+ rc=$?
190
+ if [ "$rc" -ne 3 ] || [ -n "$out" ]; then
191
+ printf 'FAIL %s: expected exit 3 and empty stdout, got exit %s\n' "$1" "$rc" >&2
192
+ failures=$((failures + 1))
193
+ fi
194
+ }
195
+
196
+ self_test() {
197
+ local fx="$SCRIPT_DIR/fixtures" tmp out rc ids line
198
+ failures=0
199
+ command -v jq >/dev/null 2>&1 || die 4 "jq is required"
200
+ tmp=$(mktemp -d) || die 4 "mktemp failed"
201
+ trap 'rm -rf "$tmp"' EXIT
202
+
203
+ ids=$(sample_ids "$fx/runs.json" 3 | tr '\n' ' ')
204
+ if [ "$ids" != "1010 1007 1003 " ]; then
205
+ printf 'FAIL sample selection: expected "1010 1007 1003 " got "%s"\n' "$ids" >&2
206
+ failures=$((failures + 1))
207
+ fi
208
+
209
+ : >"$tmp/jobs.ndjson"
210
+ for id in $ids; do
211
+ jq -n --argjson id "$id" --slurpfile j "$fx/jobs/$id.json" '{id: $id, jobs: $j[0]}' >>"$tmp/jobs.ndjson"
212
+ done
213
+ jq -s '.' "$tmp/jobs.ndjson" >"$tmp/jobs.json"
214
+
215
+ out=$(report "$fx/runs.json" "$tmp/jobs.json" "example/repo" "ci.yml" "" 3)
216
+ rc=$?
217
+ if [ "$rc" -ne 0 ]; then
218
+ printf 'FAIL report exited %s\n' "$rc" >&2
219
+ failures=$((failures + 1))
220
+ fi
221
+
222
+ while IFS= read -r line; do
223
+ [ -z "$line" ] && continue
224
+ if ! printf '%s\n' "$out" | grep -F -x -q -- "$line"; then
225
+ printf 'FAIL missing expected line: %s\n' "$line" >&2
226
+ failures=$((failures + 1))
227
+ fi
228
+ done <<'EXPECTED'
229
+ Sample: 10 runs listed, 8 completed, 1 not finished, 1 skipped.
230
+ Analyzed: 8 completed runs, 6 first-attempt runs for wall clock, 3 sampled runs with 9 jobs and 21 steps.
231
+ | success | 6 |
232
+ | cancelled | 1 |
233
+ | failure | 1 |
234
+ | in_progress | 1 |
235
+ | skipped | 1 |
236
+ Rates over 8 completed runs: cancelled 12.5%, failed 12.5%, rerun (attempt > 1) 12.5%.
237
+ Wall clock (success and failure, first attempts only, 6 runs; 1 rerun runs excluded): median 15.0 min, p90 25.0 min.
238
+ | 1010 | 25.0 | 1.2 | 3.0 | 5.0 | 11.0 | 11.0 | 3 |
239
+ | 1007 | 15.0 | 1.2 | 3.0 | 5.0 | 11.0 | 11.0 | 3 |
240
+ | 1003 | 30.0 | 1.2 | 3.0 | 5.0 | 16.0 | 16.0 | 3 |
241
+ | median | 25.0 | 1.2 | 3.0 | 5.0 | 11.0 | 11.0 | 3 |
242
+ | build | 5.0 | 3 |
243
+ | test | 5.0 | 3 |
244
+ | lint | 1.0 | 3 |
245
+ | test / dotnet test | 4.7 | 3 |
246
+ | build / dotnet build | 4.5 | 3 |
247
+ | lint / eslint | 0.8 | 3 |
248
+ Classification: execution-bound (queue share 12.0%, failed 12.5%, cancelled 12.5%, rerun 12.5%).
249
+ EXPECTED
250
+
251
+ # A rerun that ended cancelled is not excluded from wall clock; only success or failure reruns are.
252
+ jq 'map(if .databaseId == 1005 then .attempt = 2 else . end)' "$fx/runs.json" >"$tmp/rerun-cancelled.json"
253
+ out=$(report "$tmp/rerun-cancelled.json" "$tmp/jobs.json" "example/repo" "ci.yml" "" 3)
254
+ if ! printf '%s\n' "$out" | grep -F -x -q -- 'Wall clock (success and failure, first attempts only, 6 runs; 1 rerun runs excluded): median 15.0 min, p90 25.0 min.'; then
255
+ printf 'FAIL rerun exclusion count: %s\n' "$(printf '%s\n' "$out" | grep -F 'Wall clock')" >&2
256
+ failures=$((failures + 1))
257
+ fi
258
+
259
+ # A skipped job must not be counted as a job.
260
+ if printf '%s\n' "$out" | grep -F -q '| deploy |'; then
261
+ printf 'FAIL skipped job appeared in the report\n' >&2
262
+ failures=$((failures + 1))
263
+ fi
264
+
265
+ # Non-vacuity: every empty population must exit 3 and print nothing on stdout.
266
+ printf '[]\n' >"$tmp/empty.json"
267
+ jq 'map(.conclusion = "cancelled")' "$fx/runs.json" >"$tmp/cancelled.json"
268
+ jq 'map(.jobs |= map(.started_at = null))' "$tmp/jobs.json" >"$tmp/jobs-untimed.json"
269
+ jq 'map(.jobs |= map(.steps = []))' "$tmp/jobs.json" >"$tmp/jobs-nosteps.json"
270
+ expect_exit3 "empty run list" "$tmp/empty.json" "$tmp/jobs.json"
271
+ expect_exit3 "all runs cancelled" "$tmp/cancelled.json" "$tmp/jobs.json"
272
+ expect_exit3 "no job data" "$fx/runs.json" "$tmp/empty.json"
273
+ expect_exit3 "jobs without timestamps" "$fx/runs.json" "$tmp/jobs-untimed.json"
274
+ expect_exit3 "jobs without steps" "$fx/runs.json" "$tmp/jobs-nosteps.json"
275
+
276
+ if [ "$failures" -ne 0 ]; then
277
+ printf 'ci-timings self-test: %s failure(s)\n' "$failures" >&2
278
+ exit 1
279
+ fi
280
+ printf 'ci-timings self-test: ok\n'
281
+ exit 0
282
+ }
283
+
284
+ is_uint() { case "$1" in '' | *[!0-9]*) return 1 ;; *) [ "$1" -gt 0 ] ;; esac }
285
+
286
+ REPO=""
287
+ WORKFLOW=""
288
+ LIMIT=50
289
+ JOBS=10
290
+ BRANCH=""
291
+ EVENT=""
292
+ SELF_TEST=0
293
+
294
+ while [ $# -gt 0 ]; do
295
+ case "$1" in
296
+ --help | -h) usage; exit 0 ;;
297
+ --self-test) SELF_TEST=1; shift ;;
298
+ --repo | --workflow | --limit | --jobs | --branch | --event)
299
+ [ $# -ge 2 ] || { usage >&2; die 2 "option $1 needs a value"; }
300
+ case "$1" in
301
+ --repo) REPO=$2 ;;
302
+ --workflow) WORKFLOW=$2 ;;
303
+ --limit) LIMIT=$2 ;;
304
+ --jobs) JOBS=$2 ;;
305
+ --branch) BRANCH=$2 ;;
306
+ --event) EVENT=$2 ;;
307
+ esac
308
+ shift 2
309
+ ;;
310
+ *) usage >&2; die 2 "unknown argument: $1" ;;
311
+ esac
312
+ done
313
+
314
+ if [ "$SELF_TEST" -eq 1 ]; then
315
+ self_test
316
+ fi
317
+
318
+ [ -n "$REPO" ] || { usage >&2; die 2 "--repo is required"; }
319
+ [ -n "$WORKFLOW" ] || { usage >&2; die 2 "--workflow is required"; }
320
+ case "$REPO" in */*) ;; *) die 2 "--repo must be OWNER/REPO" ;; esac
321
+ is_uint "$LIMIT" || die 2 "--limit must be a positive integer"
322
+ is_uint "$JOBS" || die 2 "--jobs must be a positive integer"
323
+
324
+ command -v jq >/dev/null 2>&1 || die 4 "jq is required"
325
+ command -v gh >/dev/null 2>&1 || die 4 "gh is required"
326
+ gh auth status >/dev/null 2>&1 || die 4 "gh is not authenticated; run 'gh auth login'"
327
+
328
+ TMP=$(mktemp -d) || die 4 "mktemp failed"
329
+ trap 'rm -rf "$TMP"' EXIT
330
+
331
+ set -- run list --repo "$REPO" --workflow "$WORKFLOW" --limit "$LIMIT" \
332
+ --json databaseId,event,headBranch,status,conclusion,attempt,createdAt,updatedAt
333
+ [ -z "$BRANCH" ] || set -- "$@" --branch "$BRANCH"
334
+ [ -z "$EVENT" ] || set -- "$@" --event "$EVENT"
335
+ FILTERS=""
336
+ [ -z "$BRANCH" ] || FILTERS="$FILTERS, branch $BRANCH"
337
+ [ -z "$EVENT" ] || FILTERS="$FILTERS, event $EVENT"
338
+
339
+ if ! gh "$@" >"$TMP/runs.json"; then
340
+ die 3 "gh run list failed for $REPO / $WORKFLOW (see the gh message above)"
341
+ fi
342
+
343
+ : >"$TMP/jobs.ndjson"
344
+ fetched=0
345
+ for id in $(sample_ids "$TMP/runs.json" "$JOBS"); do
346
+ if gh api --paginate "repos/$REPO/actions/runs/$id/jobs?per_page=100&filter=latest" --jq '.jobs[]' >"$TMP/job-$id.ndjson"; then
347
+ jq -s --argjson id "$id" '{id: $id, jobs: .}' "$TMP/job-$id.ndjson" >>"$TMP/jobs.ndjson"
348
+ fetched=$((fetched + 1))
349
+ else
350
+ printf 'ci-timings: skipping run %s: could not fetch its jobs\n' "$id" >&2
351
+ fi
352
+ done
353
+ jq -s '.' "$TMP/jobs.ndjson" >"$TMP/jobs.json"
354
+
355
+ report "$TMP/runs.json" "$TMP/jobs.json" "$REPO" "$WORKFLOW" "$FILTERS" "$JOBS"
356
+ exit $?
@@ -0,0 +1,79 @@
1
+ [
2
+ {
3
+ "name": "build",
4
+ "conclusion": "success",
5
+ "created_at": "2030-01-01T03:00:10Z",
6
+ "started_at": "2030-01-01T03:01:10Z",
7
+ "completed_at": "2030-01-01T03:11:10Z",
8
+ "steps": [
9
+ {
10
+ "name": "Checkout",
11
+ "conclusion": "success",
12
+ "started_at": "2030-01-01T03:01:10Z",
13
+ "completed_at": "2030-01-01T03:01:20Z"
14
+ },
15
+ {
16
+ "name": "dotnet build",
17
+ "conclusion": "success",
18
+ "started_at": "2030-01-01T03:01:20Z",
19
+ "completed_at": "2030-01-01T03:10:50Z"
20
+ },
21
+ {
22
+ "name": "Complete job",
23
+ "conclusion": "success",
24
+ "started_at": "2030-01-01T03:10:50Z",
25
+ "completed_at": "2030-01-01T03:11:10Z"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "name": "test",
31
+ "conclusion": "success",
32
+ "created_at": "2030-01-01T03:11:10Z",
33
+ "started_at": "2030-01-01T03:12:10Z",
34
+ "completed_at": "2030-01-01T03:17:10Z",
35
+ "steps": [
36
+ {
37
+ "name": "dotnet test",
38
+ "conclusion": "success",
39
+ "started_at": "2030-01-01T03:12:10Z",
40
+ "completed_at": "2030-01-01T03:16:50Z"
41
+ },
42
+ {
43
+ "name": "Complete job",
44
+ "conclusion": "success",
45
+ "started_at": "2030-01-01T03:16:50Z",
46
+ "completed_at": "2030-01-01T03:17:10Z"
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "name": "lint",
52
+ "conclusion": "success",
53
+ "created_at": "2030-01-01T03:00:10Z",
54
+ "started_at": "2030-01-01T03:03:10Z",
55
+ "completed_at": "2030-01-01T03:04:10Z",
56
+ "steps": [
57
+ {
58
+ "name": "eslint",
59
+ "conclusion": "success",
60
+ "started_at": "2030-01-01T03:03:10Z",
61
+ "completed_at": "2030-01-01T03:04:00Z"
62
+ },
63
+ {
64
+ "name": "Complete job",
65
+ "conclusion": "success",
66
+ "started_at": "2030-01-01T03:04:00Z",
67
+ "completed_at": "2030-01-01T03:04:10Z"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "name": "deploy",
73
+ "conclusion": "skipped",
74
+ "created_at": "2030-01-01T03:00:10Z",
75
+ "started_at": null,
76
+ "completed_at": null,
77
+ "steps": []
78
+ }
79
+ ]
@@ -0,0 +1,79 @@
1
+ [
2
+ {
3
+ "name": "build",
4
+ "conclusion": "success",
5
+ "created_at": "2030-01-01T07:00:10Z",
6
+ "started_at": "2030-01-01T07:01:10Z",
7
+ "completed_at": "2030-01-01T07:06:10Z",
8
+ "steps": [
9
+ {
10
+ "name": "Checkout",
11
+ "conclusion": "success",
12
+ "started_at": "2030-01-01T07:01:10Z",
13
+ "completed_at": "2030-01-01T07:01:20Z"
14
+ },
15
+ {
16
+ "name": "dotnet build",
17
+ "conclusion": "success",
18
+ "started_at": "2030-01-01T07:01:20Z",
19
+ "completed_at": "2030-01-01T07:05:50Z"
20
+ },
21
+ {
22
+ "name": "Complete job",
23
+ "conclusion": "success",
24
+ "started_at": "2030-01-01T07:05:50Z",
25
+ "completed_at": "2030-01-01T07:06:10Z"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "name": "test",
31
+ "conclusion": "success",
32
+ "created_at": "2030-01-01T07:06:10Z",
33
+ "started_at": "2030-01-01T07:07:10Z",
34
+ "completed_at": "2030-01-01T07:12:10Z",
35
+ "steps": [
36
+ {
37
+ "name": "dotnet test",
38
+ "conclusion": "success",
39
+ "started_at": "2030-01-01T07:07:10Z",
40
+ "completed_at": "2030-01-01T07:11:50Z"
41
+ },
42
+ {
43
+ "name": "Complete job",
44
+ "conclusion": "success",
45
+ "started_at": "2030-01-01T07:11:50Z",
46
+ "completed_at": "2030-01-01T07:12:10Z"
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "name": "lint",
52
+ "conclusion": "success",
53
+ "created_at": "2030-01-01T07:00:10Z",
54
+ "started_at": "2030-01-01T07:03:10Z",
55
+ "completed_at": "2030-01-01T07:04:10Z",
56
+ "steps": [
57
+ {
58
+ "name": "eslint",
59
+ "conclusion": "success",
60
+ "started_at": "2030-01-01T07:03:10Z",
61
+ "completed_at": "2030-01-01T07:04:00Z"
62
+ },
63
+ {
64
+ "name": "Complete job",
65
+ "conclusion": "success",
66
+ "started_at": "2030-01-01T07:04:00Z",
67
+ "completed_at": "2030-01-01T07:04:10Z"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "name": "deploy",
73
+ "conclusion": "skipped",
74
+ "created_at": "2030-01-01T07:00:10Z",
75
+ "started_at": null,
76
+ "completed_at": null,
77
+ "steps": []
78
+ }
79
+ ]
@@ -0,0 +1,79 @@
1
+ [
2
+ {
3
+ "name": "build",
4
+ "conclusion": "success",
5
+ "created_at": "2030-01-01T10:00:10Z",
6
+ "started_at": "2030-01-01T10:01:10Z",
7
+ "completed_at": "2030-01-01T10:06:10Z",
8
+ "steps": [
9
+ {
10
+ "name": "Checkout",
11
+ "conclusion": "success",
12
+ "started_at": "2030-01-01T10:01:10Z",
13
+ "completed_at": "2030-01-01T10:01:20Z"
14
+ },
15
+ {
16
+ "name": "dotnet build",
17
+ "conclusion": "success",
18
+ "started_at": "2030-01-01T10:01:20Z",
19
+ "completed_at": "2030-01-01T10:05:50Z"
20
+ },
21
+ {
22
+ "name": "Complete job",
23
+ "conclusion": "success",
24
+ "started_at": "2030-01-01T10:05:50Z",
25
+ "completed_at": "2030-01-01T10:06:10Z"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "name": "test",
31
+ "conclusion": "success",
32
+ "created_at": "2030-01-01T10:06:10Z",
33
+ "started_at": "2030-01-01T10:07:10Z",
34
+ "completed_at": "2030-01-01T10:12:10Z",
35
+ "steps": [
36
+ {
37
+ "name": "dotnet test",
38
+ "conclusion": "success",
39
+ "started_at": "2030-01-01T10:07:10Z",
40
+ "completed_at": "2030-01-01T10:11:50Z"
41
+ },
42
+ {
43
+ "name": "Complete job",
44
+ "conclusion": "success",
45
+ "started_at": "2030-01-01T10:11:50Z",
46
+ "completed_at": "2030-01-01T10:12:10Z"
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "name": "lint",
52
+ "conclusion": "success",
53
+ "created_at": "2030-01-01T10:00:10Z",
54
+ "started_at": "2030-01-01T10:03:10Z",
55
+ "completed_at": "2030-01-01T10:04:10Z",
56
+ "steps": [
57
+ {
58
+ "name": "eslint",
59
+ "conclusion": "success",
60
+ "started_at": "2030-01-01T10:03:10Z",
61
+ "completed_at": "2030-01-01T10:04:00Z"
62
+ },
63
+ {
64
+ "name": "Complete job",
65
+ "conclusion": "success",
66
+ "started_at": "2030-01-01T10:04:00Z",
67
+ "completed_at": "2030-01-01T10:04:10Z"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "name": "deploy",
73
+ "conclusion": "skipped",
74
+ "created_at": "2030-01-01T10:00:10Z",
75
+ "started_at": null,
76
+ "completed_at": null,
77
+ "steps": []
78
+ }
79
+ ]