@cratis/pi 7.101.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.
- package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
- package/package/corpus/rules/github-actions.md +17 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
- package/package/profile-catalog.json +2 -0
- 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
|
+
]
|