@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.
Files changed (30) hide show
  1. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
  2. package/package/corpus/rules/github-actions.md +17 -0
  3. package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
  4. package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
  5. package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
  6. package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
  7. package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
  8. package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
  9. package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
  10. package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
  11. package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
  12. package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
  13. package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
  14. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
  15. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
  16. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
  17. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
  18. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
  19. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
  20. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
  21. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
  22. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
  23. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
  24. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
  25. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
  26. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
  27. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
  28. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
  29. package/package/profile-catalog.json +2 -0
  30. package/package.json +3 -2
@@ -45,6 +45,23 @@ paths:
45
45
  `cancel-in-progress: true`, keyed `${{ github.workflow }}-${{ github.ref }}`.
46
46
  **Never** cancel-in-progress on publish, release, or deploy workflows — a cancelled
47
47
  half-publish is worse than a queued one.
48
+ - **Publish and release workflows never cancel** (`cancel-in-progress: false`) and set
49
+ `queue: max`. A group holds one running and one pending run; a newer arrival replaces
50
+ the pending one. Merges into the default branch share a group (a push, a repository-wide
51
+ key, or `github.ref` on a merged `pull_request` `closed` event, which is the base
52
+ branch), so without `queue: max` a newer merge replaces the older pending release.
53
+ `queue: max` keeps up to 100 pending runs, processed in order, and applies only while
54
+ `cancel-in-progress` is false or unset. Keep a single pending slot only for a documented
55
+ design where each run releases everything since the last release, so a replaced run
56
+ loses nothing; say so in a comment beside the `concurrency` block.
57
+ - **A required check must not be skippable by `paths:` alone.** A workflow that never
58
+ starts never reports, and the pull request hangs. Gate jobs on a change-detection job
59
+ and require one always-run aggregator job (`if: always()`, checks every `needs.*.result`).
60
+ - **Pin third-party actions by full commit SHA** (version in a comment) and set minimal
61
+ `permissions:` on each job. Run `zizmor` locally on edited workflows: its `unpinned-uses`
62
+ and `excessive-permissions` audits check exactly this. `actionlint` checks syntax,
63
+ expressions and embedded shell, not pinning. Do not assume CI runs either tool.
64
+ - To make a workflow faster, use the `cratis-ci-pipeline-performance` skill.
48
65
 
49
66
  ## Matrices and operating systems
50
67
 
@@ -44,6 +44,23 @@ paths:
44
44
  `cancel-in-progress: true`, keyed `${{ github.workflow }}-${{ github.ref }}`.
45
45
  **Never** cancel-in-progress on publish, release, or deploy workflows — a cancelled
46
46
  half-publish is worse than a queued one.
47
+ - **Publish and release workflows never cancel** (`cancel-in-progress: false`) and set
48
+ `queue: max`. A group holds one running and one pending run; a newer arrival replaces
49
+ the pending one. Merges into the default branch share a group (a push, a repository-wide
50
+ key, or `github.ref` on a merged `pull_request` `closed` event, which is the base
51
+ branch), so without `queue: max` a newer merge replaces the older pending release.
52
+ `queue: max` keeps up to 100 pending runs, processed in order, and applies only while
53
+ `cancel-in-progress` is false or unset. Keep a single pending slot only for a documented
54
+ design where each run releases everything since the last release, so a replaced run
55
+ loses nothing; say so in a comment beside the `concurrency` block.
56
+ - **A required check must not be skippable by `paths:` alone.** A workflow that never
57
+ starts never reports, and the pull request hangs. Gate jobs on a change-detection job
58
+ and require one always-run aggregator job (`if: always()`, checks every `needs.*.result`).
59
+ - **Pin third-party actions by full commit SHA** (version in a comment) and set minimal
60
+ `permissions:` on each job. Run `zizmor` locally on edited workflows: its `unpinned-uses`
61
+ and `excessive-permissions` audits check exactly this. `actionlint` checks syntax,
62
+ expressions and embedded shell, not pinning. Do not assume CI runs either tool.
63
+ - To make a workflow faster, use the `cratis-ci-pipeline-performance` skill.
47
64
 
48
65
  ## Matrices and operating systems
49
66
 
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the canonical Cratis/AI repository for the full license text.
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: cratis-ci-pipeline-performance
3
+ description: Make a Cratis repository's GitHub Actions CI/CD faster and cheaper by measuring workflow timings first, picking the lever that fits the measured bottleneck (build once and shard, caching, path filters with an aggregator, concurrency, runner routing, publish from artifacts) and verifying before and after. Use when a workflow is slow, queues, burns runner minutes, reruns or cancels a lot, or when editing .github/workflows to speed up CI. Not for authoring a release (use cratis-governed-release-methodology), recurring regressions on one surface (use cratis-engineering-regression-cluster), runtime performance of application code (use cratis-performance-review), or fixing a red build.
4
+ license: LICENSE
5
+ ---
6
+
7
+ # Measure the workflow, then pick the lever
8
+
9
+ A workflow is slow for one of four different reasons, and each has a different
10
+ fix. Adding runners cures a queue and does nothing for a serial build; a faster
11
+ build does nothing for a workflow that fails a quarter of the time. Measure,
12
+ classify, change one lever, measure again. Never optimize from the YAML alone.
13
+
14
+ Capacity, timeout, concurrency and runner invariants live in
15
+ [github-actions.md](../../rules/github-actions.md); obey them and do not restate them in a change.
16
+ Workflow-only pull requests carry the `no-release` label per
17
+ [pull-requests.md](../../rules/pull-requests.md).
18
+
19
+ ## 1. Measure first
20
+
21
+ Execute the bundled script (needs `gh` authenticated for the repository, and `jq`):
22
+
23
+ ```bash
24
+ bash scripts/ci-timings.sh --repo OWNER/REPO --workflow ci.yml --limit 50 --jobs 10
25
+ ```
26
+
27
+ Add `--branch main` or `--event pull_request` to compare populations; PR and
28
+ default-branch runs behave differently. `bash scripts/ci-timings.sh --self-test`
29
+ proves the script itself works without network access. Exit codes: `0` report
30
+ printed, `2` usage error, `3` no runs or no data, `4` missing `gh`/`jq` or `gh`
31
+ not authenticated. Exit `3` and `4` are an unknown, not a clean workflow: say the
32
+ measurement was not made.
33
+
34
+ The report separates **wall clock** (what a developer waits for) from
35
+ **runner-minutes** (what the organization pays for or occupies). A change can
36
+ cut one and raise the other; report both. It prints a one-line classification:
37
+
38
+ | Class | Hint threshold | Meaning | First lever |
39
+ |---|---|---|---|
40
+ | failure-bound | failed or rerun >= 15% of completed runs | Time and minutes go into runs that are redone | Flaky and hung jobs; fail fast |
41
+ | cancel-bound | cancelled >= 20% | Superseded work still ran heavy legs | Cheap gate first, path filters, smaller PR matrix |
42
+ | queue-bound | median longest job wait >= 25% of wall | Jobs wait for runners, not for work | Fewer, bigger jobs; runner routing |
43
+ | execution-bound | none of the above | The critical path is serial work | Build once and shard; caching |
44
+
45
+ The thresholds are a hint, not a verdict. Several classes can hold at once; the
46
+ script lists extras after the primary class. Then find the **critical path over
47
+ the required checks**: the longest chain of `needs` from the start to the last
48
+ required job. Only work on that chain shortens wall clock; shaving a parallel
49
+ sibling does not.
50
+
51
+ Without `gh` access, read the workflow YAML, state that the result is
52
+ **static-only analysis**, and rank findings as hypotheses to measure later.
53
+
54
+ ## 2. Guardrails before proposing a fix
55
+
56
+ - **Never hide required validation.** Release, schema, migration, analyzer and
57
+ shared-library checks stay on the path that gates a merge or a release. Moving
58
+ them to a schedule needs an explicit owner decision, not an optimization.
59
+ - **Say what less parallelism costs.** Merging jobs lowers queue and setup cost
60
+ but lengthens the critical path when the merged work was parallel. State the
61
+ expected wall change and runner-minute change together.
62
+ - **Separate repository changes from organization settings.** Workflow YAML is a
63
+ repository change. Runner capacity, scale sets and plan limits are not; report
64
+ them as a request to the owner and do not work around them in YAML.
65
+ - **One lever per change.** Two levers at once make the measured difference
66
+ unattributable.
67
+ - **Rank by what it saves.** Estimate minutes per day as (saving per run) x
68
+ (runs per day) from the report, and take the top one to three items.
69
+
70
+ ## 3. Lever catalogue
71
+
72
+ Each lever lists when it fits, the Cratis default and the main pitfall. Read the
73
+ named reference before applying one.
74
+
75
+ **Build once, shard the tests.** Fits execution-bound workflows where build and
76
+ tests run serially or several jobs each rebuild. Default: one job builds in
77
+ `Release`, uploads the output with `actions/upload-artifact`, and a matrix of shard jobs downloads it and runs
78
+ `dotnet test --no-build`. Pitfall: a shard whose filter matches nothing still
79
+ passes, so assert that every shard ran tests. Read
80
+ `references/build-once-and-shard.md` when applying or reviewing this.
81
+
82
+ **Fewer, bigger jobs.** Fits queue-bound workflows: every job pays a scheduler
83
+ start, checkout and toolchain setup. Default: merge small sibling jobs that share
84
+ a setup; keep shards only where the work is long enough to repay a start. Pitfall:
85
+ merging the long pole into a serial chain. Read `references/build-once-and-shard.md`.
86
+
87
+ **Dependency caching.** Fits any job with a measurable restore or install step.
88
+ Default: the shared `Cratis/Workflows` composite actions `dotnet-cache` and
89
+ `yarn-cache` (restore on pull requests, save on the default branch), NuGet lock files and
90
+ `yarn install --immutable`. Pitfall: a cache key that changes every run, or a
91
+ different path list per workflow, which creates separate caches. Read
92
+ `references/caching.md`.
93
+
94
+ **Docker layer cache.** Fits image builds that repeat unchanged layers. Default:
95
+ `docker/build-push-action` with `cache-from`/`cache-to` of type `gha` and a custom
96
+ `scope` per image and platform. Pitfall: the default scope is shared, so builds
97
+ overwrite each other's cache. Read `references/caching.md`.
98
+
99
+ **Path filters with an always-run aggregator.** Fits cancel-bound workflows and
100
+ documentation-only changes. Default: filter heavy jobs by changed paths, and make
101
+ a single aggregator job with `if: always()` check every `needs.*.result`; require
102
+ only the aggregator. Pitfall: a required check skipped by `paths:` never reports
103
+ and the pull request hangs. Read `references/required-checks-and-path-filters.md`.
104
+
105
+ **Concurrency.** Pull-request verification uses `cancel-in-progress: true` keyed
106
+ by workflow and ref. Publish never cancels; where releases share one group, `queue:
107
+ max` keeps up to 100 pending runs in order instead of replacing the single pending
108
+ one. Pitfall: leaving the single pending slot where a replaced merge's run alone
109
+ would have released something (merges into the default branch share a group, also
110
+ when keyed on `github.ref` for merged `pull_request` `closed` events). A workflow
111
+ whose release tooling aggregates since the last release may keep one pending slot,
112
+ documented. Read `references/publish-pipelines.md`.
113
+
114
+ **Runner routing.** Fits queue-bound workflows. Default: private-repository jobs
115
+ on the organization scale set `cratis-arc` or a repository variable that falls
116
+ back to a hosted label, hosted runners for public repositories. Pitfall: never
117
+ route `pull_request` jobs of a public repository to self-hosted runners; that is
118
+ fork code on our infrastructure. The rule owns the details.
119
+
120
+ **Publish from tested artifacts.** Fits release workflows that rebuild what the
121
+ pull-request run already built and tested. Default: build and pack once, then
122
+ publish exactly those packages. Pitfall: a second build can differ from the
123
+ tested one. Read `references/publish-pipelines.md`.
124
+
125
+ **Fail fast on cheap gates.** Fits failure-bound and cancel-bound workflows.
126
+ Default: formatting, lint and unit-level checks run first, and expensive legs
127
+ depend on them with `needs`. Pitfall: serializing checks that were parallel; keep
128
+ the cheap gate short enough to repay its place on the critical path.
129
+
130
+ **Expensive matrices off the PR path.** Fits cancel-bound workflows with a wide
131
+ OS or framework matrix. Default: full matrix on `schedule` and
132
+ `workflow_dispatch`, a Linux-only representative smoke on pull requests (the
133
+ rule states this). Pitfall: the full matrix needs an owner who reads its failures.
134
+
135
+ **Flaky and hung jobs.** Fits failure-bound workflows. Default: a
136
+ `timeout-minutes` on every job, `--blame-hang`, retry only a diagnosed flake, and
137
+ quarantine a test with an issue. Pitfall: a blanket rerun hides the defect and
138
+ doubles minutes. Read `references/flaky-and-hung-jobs.md`.
139
+
140
+ **Merge queue.** Fits repositories where passing pull requests break each other
141
+ after merge. Default: add the `merge_group` trigger, and make the aggregator
142
+ handle it. Pitfall: without `merge_group` the required check never reports in the
143
+ queue. Read `references/required-checks-and-path-filters.md`.
144
+
145
+ **Compile once, analyze once.** Fits any workflow where one push compiles the same
146
+ code several times. Default: build once and reuse the output; run analyzers in one
147
+ parallel lint job instead of in every build, and let that job fail early on compile
148
+ and analyzer errors before long test legs. Pitfall: dropping analyzers from the only
149
+ build that consumers enforce. Read `references/test-setup-cost.md`.
150
+
151
+ **Cores per job before job count.** Fits uniformly slow jobs. Default: check `nproc`
152
+ and the runner's resources before sharding; undersized runners make every job slow.
153
+ Pitfall: adding jobs to an undersized pool. Read `references/runner-sizing-and-images.md`.
154
+
155
+ **Hidden test setup.** Fits test lanes whose step time far exceeds the runner's
156
+ reported test time. Default: profile the test host and fix per-class setup upstream
157
+ rather than sharding it. Pitfall: sharding that divides the waste. Read
158
+ `references/test-setup-cost.md`.
159
+
160
+ **Test a guarantee where it can break.** Fits suites that re-test a dependency.
161
+ Default: run them upstream, or on a dependency pin change or nightly; use snapshots
162
+ for shape and post-deploy synthetic checks where possible. Pitfall: relocating the
163
+ only check of a required guarantee. Read `references/test-setup-cost.md`.
164
+
165
+ **Images and runner contents.** Fits jobs that pull unused images or install tools per
166
+ run. Default: pull only what the job uses, in parallel, and bake tools into the runner
167
+ image; content-addressed caches before warm volumes, publish stays cold. Read
168
+ `references/runner-sizing-and-images.md`.
169
+
170
+ Speed work must not weaken supply-chain hygiene. Read
171
+ `references/security-and-pinning.md` when a change adds or updates an action,
172
+ touches `permissions:`, uses `pull_request_target`, or adds a cache.
173
+
174
+ ## 4. Verify
175
+
176
+ 1. Re-run the same `ci-timings.sh` command on the same workflow after enough new
177
+ runs exist (at least the sample size of the baseline; say so when fewer).
178
+ 2. Report expected against measured savings, wall and runner-minutes separately,
179
+ and whether the classification moved. A saving that is not measured is labeled
180
+ as expected.
181
+ 3. Run the linters locally on every edited workflow when installed: `actionlint`
182
+ (syntax, expressions, embedded shell) and `zizmor` (security audits such as
183
+ `unpinned-uses` and `excessive-permissions`). Pass means exit `0` with no
184
+ findings; a tool that is not installed is reported as not run, not as clean.
185
+ Do not assume CI runs either.
186
+ 4. Compare required check names before and after; a renamed job silently removes
187
+ a required check. See `references/required-checks-and-path-filters.md`.
188
+ 5. Confirm the workflow change ships as `no-release`.
189
+
190
+ ## References
191
+
192
+ - Read `references/build-once-and-shard.md` when changing how a build is shared across jobs or how tests are split.
193
+ - Read `references/caching.md` when adding or changing a dependency, Docker or build cache.
194
+ - Read `references/required-checks-and-path-filters.md` when adding path filters, an aggregator, or `merge_group`.
195
+ - Read `references/publish-pipelines.md` when a release or deploy workflow queues, drops runs or rebuilds.
196
+ - Read `references/flaky-and-hung-jobs.md` when runs fail, hang or are rerun.
197
+ - Read `references/test-setup-cost.md` when test steps outlast reported test time, tests duplicate a dependency's suite, or compiles repeat per push.
198
+ - Read `references/runner-sizing-and-images.md` when jobs are uniformly slow, pull images, install tools or reuse workspaces.
199
+ - Read `references/security-and-pinning.md` when a change touches actions, permissions, triggers or caches.
200
+ - Read `references/sources.md` when attributing or extending the method.
201
+ - Execute `scripts/ci-timings.sh` for every measurement; read it only to change it. Its self-test reads the synthetic inputs `scripts/fixtures/runs.json` and `scripts/fixtures/jobs/1003.json`, `scripts/fixtures/jobs/1007.json`, `scripts/fixtures/jobs/1010.json`; edit them only together with the expected lines in the script.
@@ -0,0 +1,158 @@
1
+ # Build once, shard the tests
2
+
3
+ ## Contents
4
+
5
+ - When it fits
6
+ - Shape
7
+ - Decisions
8
+ - Pitfalls
9
+ - Fewer, bigger jobs
10
+
11
+ Use this when one workflow builds the same code more than once, or runs build and
12
+ tests serially in one long job. The pattern: a single build job compiles in
13
+ `Release` and uploads the output; a shard matrix downloads it and runs the tests
14
+ with `--no-build`. The wall clock becomes build + the slowest shard instead of
15
+ build + all tests.
16
+
17
+ ## When it fits
18
+
19
+ - The report shows `execution-bound` and one job holds most of the span.
20
+ - Several jobs each restore and compile the same solution.
21
+ - Tests are independent by project or namespace.
22
+
23
+ It does not fit when the build is short compared with job start cost, or when
24
+ the report is `queue-bound`: more shards are more independently scheduled jobs.
25
+ Merge shards first, then re-measure.
26
+
27
+ ## Shape
28
+
29
+ ```yaml
30
+ jobs:
31
+ build:
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 45
34
+ permissions:
35
+ contents: read
36
+ steps:
37
+ - uses: actions/checkout@v7 # pin by full commit SHA
38
+ - uses: actions/setup-dotnet@v6 # pin by full commit SHA
39
+ - uses: Cratis/Workflows/.github/actions/dotnet-cache@main
40
+ - run: dotnet build Repo.slnx --configuration Release
41
+ - name: Archive build output
42
+ run: |
43
+ set -euo pipefail
44
+ find . -type d -name bin -not -path '*/node_modules/*' -not -path './.git/*' -print0 \
45
+ | tar --null -T - --zstd -cf build-output.tar.zst
46
+ - uses: actions/upload-artifact@v7 # pin by full commit SHA
47
+ with:
48
+ name: build-output
49
+ path: build-output.tar.zst
50
+ compression-level: 0
51
+ retention-days: 1
52
+ if-no-files-found: error
53
+
54
+ specs:
55
+ name: ${{ matrix.shard }}
56
+ needs: [build]
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 30
59
+ permissions:
60
+ contents: read
61
+ strategy:
62
+ fail-fast: false
63
+ matrix:
64
+ include:
65
+ - shard: specs-1
66
+ projects: Source/Core.Specs Source/Storage.Specs
67
+ - shard: specs-2
68
+ projects: Source/Api.Specs
69
+ - shard: specs-3
70
+ projects: Source/Client.Specs Source/Tools.Specs
71
+ steps:
72
+ - uses: actions/checkout@v7 # pin by full commit SHA
73
+ - uses: actions/setup-dotnet@v6 # pin by full commit SHA
74
+ - uses: actions/download-artifact@v8 # pin by full commit SHA
75
+ with:
76
+ name: build-output
77
+ - run: tar --zstd -xf build-output.tar.zst
78
+ - name: Test the shard's projects
79
+ env:
80
+ PROJECTS: ${{ matrix.projects }}
81
+ run: |
82
+ failed=0
83
+ for project in $PROJECTS; do
84
+ dotnet test "$project" --no-build --configuration Release \
85
+ --logger "trx;LogFileName=$(basename "$project").trx" \
86
+ --results-directory "$RUNNER_TEMP/test-results" || failed=1
87
+ done
88
+ exit "$failed"
89
+ - name: Verify every project ran tests
90
+ if: ${{ !cancelled() }}
91
+ env:
92
+ PROJECTS: ${{ matrix.projects }}
93
+ run: |
94
+ failed=0
95
+ for project in $PROJECTS; do
96
+ file="$RUNNER_TEMP/test-results/$(basename "$project").trx"
97
+ executed=$(grep -o -m1 'executed="[0-9]*"' "$file" 2>/dev/null | grep -o '[0-9]*' || echo 0)
98
+ echo "$project executed $executed tests"
99
+ [ "$executed" -gt 0 ] || failed=1
100
+ done
101
+ exit "$failed"
102
+ - uses: actions/upload-artifact@v7 # pin by full commit SHA
103
+ if: ${{ !cancelled() }}
104
+ with:
105
+ name: test-results-${{ matrix.shard }}
106
+ path: ${{ runner.temp }}/test-results/*.trx
107
+ if-no-files-found: warn
108
+ ```
109
+
110
+ Each matrix entry carries its own `projects` list, which the test step reads, so
111
+ shards run different tests. For a test framework filter instead of a project list,
112
+ carry the filter in the entry (`filter: FullyQualifiedName~Core`) and pass it as
113
+ `--filter`. Keep the assignment in the workflow or one checked-in script so that
114
+ adding a project cannot leave it unassigned; review the list when a project is added.
115
+
116
+ To balance, read each shard job's median duration from the slowest-jobs table of
117
+ `ci-timings.sh`, move a project from the longest shard to the shortest, and
118
+ re-measure; stop when the spread is within the job start cost.
119
+
120
+ ## Decisions
121
+
122
+ - **Artifact, not cache, between jobs of one run.** A cache keyed on the commit
123
+ SHA is immutable: one failed save poisons the key for every later attempt and a
124
+ rerun cannot recover. An artifact belongs to the run and is the intended
125
+ primitive for passing build output between its jobs.
126
+ - **Archive with `tar`.** Artifact upload does not preserve file permissions or
127
+ empty directories; a tar keeps both. Output that was already compressed should
128
+ not be compressed again: `compression-level: 0`.
129
+ - **Include every consumed output.** Frontend-dependent specs need the built
130
+ `dist` folders as well as `bin`; list them in the archive.
131
+ - **Short retention.** `retention-days: 1` for build output nobody downloads by
132
+ hand. Test results can live longer.
133
+ - **Same configuration end to end.** Test with the configuration that built
134
+ (`--configuration Release --no-build`); a different one silently rebuilds.
135
+ - **`fail-fast: false`** on shard matrices, so one failing shard does not hide the
136
+ results of the others, and a rerun of failed jobs reruns only the failed shard.
137
+ - **Upload results with `if: ${{ !cancelled() }}`** so failures keep their TRX.
138
+
139
+ ## Pitfalls
140
+
141
+ - A filter or project list that matches nothing passes `dotnet test`. End every
142
+ shard with a check that each assigned project or namespace ran at least one test,
143
+ and print the counts. Zero tests is a failed shard.
144
+ - Shards balance by test time, not by project count (see the balancing steps
145
+ after the example).
146
+ - A shard that downloads a large artifact pays that transfer on every job; measure
147
+ the download step in the slowest-steps table before adding shards.
148
+ - Static assets outside `bin`/`dist` (generated sources, local tool caches) must
149
+ be in the archive or regenerated by the shard.
150
+
151
+ ## Fewer, bigger jobs
152
+
153
+ When the report is `queue-bound`, every extra job costs a scheduler start, a
154
+ checkout and a toolchain setup. Merge sibling jobs that share setup (several
155
+ lint or format checks, several small build legs) into one job with several steps.
156
+ Keep a job separate only when it is on the critical path and long enough that
157
+ parallelism repays its start. State the expected effect on both wall clock and
158
+ runner-minutes, since merging parallel work lengthens the chain.
@@ -0,0 +1,86 @@
1
+ # Caching
2
+
3
+ Use this when a job spends measurable time restoring packages, installing a
4
+ frontend toolchain or rebuilding Docker layers. Check the slowest-steps table
5
+ first: a step under a minute is not worth a cache.
6
+
7
+ ## Contents
8
+
9
+ - Shared Cratis cache actions
10
+ - NuGet and Yarn determinism
11
+ - Docker layer cache
12
+ - Scope, eviction and safety
13
+
14
+ ## Shared Cratis cache actions
15
+
16
+ The `Cratis/Workflows` repository ships two composite actions. Use them instead
17
+ of hand-written `actions/cache` steps so every workflow of a repository uses one
18
+ key and one path list.
19
+
20
+ ```yaml
21
+ - uses: Cratis/Workflows/.github/actions/dotnet-cache@main
22
+ - uses: Cratis/Workflows/.github/actions/yarn-cache@main
23
+ with:
24
+ working-directory: Source/JavaScript
25
+ ```
26
+
27
+ - `mode` is `auto` by default: pull requests only restore; a push to the default
28
+ branch, a schedule or a manual run on the default branch saves. Use `restore`
29
+ to never save and `save` to always save.
30
+ - `dotnet-cache` inputs: `path`, `key-files`, `key-prefix`, `mode`. Keep `path`
31
+ identical everywhere; a different path list is a different cache. Bump the
32
+ version suffix of `key-prefix` to start a fresh cache.
33
+ - Outputs: `cache-hit`, `matched-key`, `key`, `mode`.
34
+ - A cache is only saved from the default branch, so a repository with no push
35
+ workflow on that branch never warms one. Add a scheduled warm-up workflow that
36
+ runs the same restore and build.
37
+
38
+ ## NuGet and Yarn determinism
39
+
40
+ A cache is only as stable as its key. Make the key inputs deterministic.
41
+
42
+ - NuGet: enable lock files (`<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>`)
43
+ and restore with `dotnet restore --locked-mode` in CI, so the lock file hash
44
+ keys the cache and a drifting dependency fails loudly.
45
+ - Yarn: commit `yarn.lock` and install with `yarn install --immutable`; the
46
+ install fails instead of rewriting the lock file.
47
+ - Cache the package download directories, not `bin`/`obj` output. Build output
48
+ caches across commits are rarely correct; share build output within a run as an
49
+ artifact instead (see `build-once-and-shard.md`).
50
+
51
+ ## Docker layer cache
52
+
53
+ ```yaml
54
+ - uses: docker/setup-buildx-action@v4 # pin by full commit SHA
55
+ - uses: docker/build-push-action@v7 # pin by full commit SHA
56
+ with:
57
+ context: .
58
+ push: false
59
+ cache-from: type=gha,scope=api-linux-amd64
60
+ cache-to: type=gha,scope=api-linux-amd64,mode=max
61
+ ```
62
+
63
+ - Set a custom `scope` per image and platform. The default scope is shared, so
64
+ two builds overwrite each other's cache and both see misses.
65
+ - `mode=max` caches intermediate layers of multi-stage builds; the default caches
66
+ only the final image layers.
67
+ - Order the Dockerfile so rarely changing layers (base image, dependency
68
+ restore) come before source copies.
69
+ - When an image is built once and pulled by several jobs, pushing it to a
70
+ registry and pulling it costs a transfer per job; measure that pull in the
71
+ slowest-steps table against rebuilding from cache.
72
+
73
+ ## Scope, eviction and safety
74
+
75
+ - A pull request run can read caches of its own ref and of the base branch; a
76
+ cache saved on the default branch is readable by every branch. A cache saved by
77
+ one pull request is not visible to others.
78
+ - Repository cache storage is bounded (10 GB by default). When full, or after a
79
+ week without access, entries are evicted, least recently used first. A large
80
+ rarely used cache can evict a hot one.
81
+ - Caches must not contain secrets, tokens or credentials: restored content is
82
+ executed and readable by later runs of the same scope.
83
+ - Never restore a cache written by an untrusted pull request into a privileged
84
+ workflow. See `security-and-pinning.md`.
85
+ - Verify a cache works by reading the `cache-hit` output (or the restore step
86
+ log) on a second run, then re-measure with `ci-timings.sh`.
@@ -0,0 +1,70 @@
1
+ # Flaky and hung jobs
2
+
3
+ Use this when the report is `failure-bound` (failed or rerun at or above the
4
+ hint threshold) or a job occasionally runs to its timeout. Reruns and failures
5
+ cost runner-minutes and developer trust; diagnose before retrying.
6
+
7
+ ## Diagnose
8
+
9
+ 1. Run `ci-timings.sh` with `--branch main` and again with `--event pull_request`.
10
+ Failures on the default branch with no code change are flakes or environment
11
+ problems; failures only on pull requests are mostly real.
12
+ 2. Group the failing runs by job and step name. One step that fails across many
13
+ runs is a defect in that step, not noise.
14
+ 3. Separate the cause: a hung test, a timeout from an external service, an
15
+ out-of-memory kill, or an infrastructure cancellation.
16
+
17
+ ## Bound every job
18
+
19
+ - `timeout-minutes` on every job, per the rule. A hung job otherwise holds a
20
+ runner slot for up to six hours.
21
+ - Set the job timeout above the test timeout, so dump collection and cleanup have
22
+ time to finish.
23
+
24
+ ## Find hangs and crashes in .NET
25
+
26
+ ```bash
27
+ dotnet test Repo.slnx --configuration Release --no-build \
28
+ --blame-hang --blame-hang-timeout 5m --blame-crash \
29
+ --results-directory TestResults
30
+ ```
31
+
32
+ - This is the VSTest form. Microsoft.Testing.Platform projects use their own
33
+ crash and hang dump extensions and options; do not mix the two sets of switches.
34
+ - Upload `TestResults` as an artifact with `if: ${{ !cancelled() }}`, so the dump
35
+ survives the failure.
36
+
37
+ ## Retry only a diagnosed flake
38
+
39
+ - Retry the smallest unit: a single test or a single failed job, never the whole
40
+ suite. With Microsoft.Testing.Platform, the retry extension provides
41
+ `--retry-failed-tests` with a failure-percentage ceiling; plain VSTest has no
42
+ equivalent flag.
43
+ - Keep the first-attempt failure visible. A retry that turns red green without a
44
+ record hides the flake and doubles minutes.
45
+ - Retrying a deterministic failure only consumes capacity.
46
+
47
+ ## Quarantine
48
+
49
+ A quarantined test is a decision with an owner, not a deletion:
50
+
51
+ - Mark it with the framework's skip or category mechanism and a reason that names
52
+ an issue.
53
+ - Run quarantined tests in a separate, visible, non-required lane so they keep
54
+ producing signal.
55
+ - Give the issue an owner and an expiry; an entry nobody revisits is a check
56
+ quietly deleted.
57
+
58
+ ## Fail fast
59
+
60
+ Put the cheap, high-signal gates (format, lint, unit-level tests) before the
61
+ expensive legs and make the expensive legs `needs` them. A failure then costs
62
+ minutes instead of the full pipeline. Keep the gate short: a slow gate on the
63
+ critical path moves the cost instead of removing it.
64
+
65
+ ## Cancelled work
66
+
67
+ A high cancelled share is expected on pull requests with `cancel-in-progress`.
68
+ It is waste only when the cancelled runs had already spent their expensive legs.
69
+ Reduce it by ordering cheap legs first and by keeping the full matrix off the
70
+ pull-request path, not by removing the cancellation.
@@ -0,0 +1,67 @@
1
+ # Publish and release pipelines
2
+
3
+ Use this when a release, package-publish or deploy workflow queues, loses runs,
4
+ or rebuilds what a pull-request run already built and tested. Release method and
5
+ assurance tiers belong to `cratis-governed-release-methodology`.
6
+
7
+ ## Concurrency for publishing
8
+
9
+ ```yaml
10
+ concurrency:
11
+ group: publish-${{ github.repository }}
12
+ cancel-in-progress: false
13
+ queue: max
14
+ ```
15
+
16
+ - Pull-request verification cancels superseded runs. Publish never does: a
17
+ cancelled half-publish is worse than a queued one.
18
+ - A concurrency group holds one running and, by default, one pending run; a newer
19
+ arrival replaces the pending one. `queue: max` keeps up to 100 pending runs in
20
+ the group and processes them in order. It is valid only while
21
+ `cancel-in-progress` is `false` or unset; never combine it with `true`.
22
+ - Replacement only happens between runs of the same group, so read the group key.
23
+ Merges into the default branch share one group: a push, a repository-wide key, or
24
+ `github.ref` on a merged `pull_request` `closed` event, where `github.ref` is the
25
+ base branch (for example `refs/heads/main`). A newer merge's pending run then
26
+ replaces the older pending one unless `queue: max` is set. Only a `closed` event
27
+ of an unmerged pull request keeps `refs/pull/N/merge`, a group of its own.
28
+ - A replaced pending run is lost work only when that run alone would have released
29
+ something. Release tooling that aggregates everything since the last release
30
+ loses nothing when a pending run is replaced; such a workflow may keep one
31
+ pending slot, with a comment beside the `concurrency` block saying so.
32
+ - Groups are repository-scoped; this is not an organization-wide release lock.
33
+ - Check the symptom in the report: many cancelled publish runs under one group
34
+ point at a replaced pending slot.
35
+
36
+ ## Publish from the tested artifacts
37
+
38
+ A publish job that rebuilds can ship something different from what was tested,
39
+ and pays the build twice.
40
+
41
+ - Build once per publish run (per architecture where a native build differs),
42
+ pack with `--no-build`, and publish exactly those packages and images.
43
+ - Share build output between jobs of the run as an artifact (see
44
+ `build-once-and-shard.md`). Do not pull a pull-request run's artifact into a
45
+ release: artifacts expire and belong to another commit.
46
+ - Build the way consumers' strictest CI builds (`Release`, analyzers on); a
47
+ `--no-build` pack of a `Debug` build ships the wrong thing.
48
+ - When several services publish images from one build, build the image once,
49
+ push it, and have the per-service jobs reuse it; compare the per-job pull time
50
+ against rebuilding from the Docker cache.
51
+ - Keep a version resolved once in an early job and passed down as an output, so
52
+ every job in the run packs the same version.
53
+
54
+ ## Timeouts and permissions
55
+
56
+ - Publish and release jobs use a 60-minute `timeout-minutes` tier, per the rule.
57
+ - Grant `contents: write` or `packages: write` on the job that publishes, not on
58
+ the workflow, and keep build jobs on `contents: read`.
59
+ - Prefer OIDC trusted publishing to a long-lived registry API key where the
60
+ registry supports it. See `security-and-pinning.md`.
61
+
62
+ ## Speed without weakening the release
63
+
64
+ - Do not skip validation on the release path to save minutes; move other work off
65
+ it instead (docs jobs, matrices already covered by schedule).
66
+ - Canary before promotion and recovery disposition are release-method questions;
67
+ hand them to `cratis-governed-release-methodology`.