@cratis/pi 7.100.0 → 7.102.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 (76) hide show
  1. package/package/corpus/agents/backend-developer.md +1 -0
  2. package/package/corpus/agents/browser-operator.md +44 -0
  3. package/package/corpus/agents/code-reviewer.md +4 -0
  4. package/package/corpus/agents/coordinator.md +1 -0
  5. package/package/corpus/agents/frontend-developer.md +1 -0
  6. package/package/corpus/agents/orchestrator.md +1 -0
  7. package/package/corpus/agents/performance-reviewer.md +4 -0
  8. package/package/corpus/agents/planner.md +1 -0
  9. package/package/corpus/agents/repository-investigation-reviewer.md +4 -0
  10. package/package/corpus/agents/repository-investigator.md +1 -0
  11. package/package/corpus/agents/screenplay-modeler.md +1 -0
  12. package/package/corpus/agents/screenplay-renderer.md +1 -0
  13. package/package/corpus/agents/screenplay-reviewer.md +4 -0
  14. package/package/corpus/agents/security-reviewer.md +4 -0
  15. package/package/corpus/agents/slice-implementer.md +1 -0
  16. package/package/corpus/agents/spec-writer.md +1 -0
  17. package/package/corpus/harnesses/copilot/instructions/browser.instructions.md +46 -0
  18. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
  19. package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +3 -0
  20. package/package/corpus/harnesses/copilot/instructions/review-reports.instructions.md +11 -0
  21. package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +35 -0
  22. package/package/corpus/harnesses/pi/agents/backend-developer.md +1 -0
  23. package/package/corpus/harnesses/pi/agents/browser-operator.md +41 -0
  24. package/package/corpus/harnesses/pi/agents/code-reviewer.md +4 -0
  25. package/package/corpus/harnesses/pi/agents/coordinator.md +1 -0
  26. package/package/corpus/harnesses/pi/agents/frontend-developer.md +1 -0
  27. package/package/corpus/harnesses/pi/agents/orchestrator.md +1 -0
  28. package/package/corpus/harnesses/pi/agents/performance-reviewer.md +4 -0
  29. package/package/corpus/harnesses/pi/agents/planner.md +1 -0
  30. package/package/corpus/harnesses/pi/agents/repository-investigation-reviewer.md +4 -0
  31. package/package/corpus/harnesses/pi/agents/repository-investigator.md +1 -0
  32. package/package/corpus/harnesses/pi/agents/screenplay-modeler.md +1 -0
  33. package/package/corpus/harnesses/pi/agents/screenplay-renderer.md +1 -0
  34. package/package/corpus/harnesses/pi/agents/screenplay-reviewer.md +4 -0
  35. package/package/corpus/harnesses/pi/agents/security-reviewer.md +4 -0
  36. package/package/corpus/harnesses/pi/agents/slice-implementer.md +1 -0
  37. package/package/corpus/harnesses/pi/agents/spec-writer.md +1 -0
  38. package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +19 -6
  39. package/package/corpus/harnesses/pi/extensions/cratis-hooks/quality-gate.ts +10 -1
  40. package/package/corpus/hooks/README.md +8 -0
  41. package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +458 -0
  42. package/package/corpus/hooks/settings.template.json +4 -0
  43. package/package/corpus/rules/browser.md +45 -0
  44. package/package/corpus/rules/general.md +2 -0
  45. package/package/corpus/rules/github-actions.md +17 -0
  46. package/package/corpus/rules/local-work-artifacts.md +3 -0
  47. package/package/corpus/rules/review-reports.md +11 -0
  48. package/package/corpus/rules/secrets.md +34 -0
  49. package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
  50. package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
  51. package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
  52. package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
  53. package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
  54. package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
  55. package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
  56. package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
  57. package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
  58. package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
  59. package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
  60. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
  61. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
  62. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
  63. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
  64. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
  65. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
  66. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
  67. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
  68. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
  69. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
  70. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
  71. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
  72. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
  73. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
  74. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
  75. package/package/profile-catalog.json +2 -0
  76. package/package.json +3 -2
@@ -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
+ ]