tool-compass 2.2.2__tar.gz → 2.4.0__tar.gz

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 (86) hide show
  1. tool_compass-2.4.0/.dockerignore +84 -0
  2. {tool_compass-2.2.2 → tool_compass-2.4.0}/.env.example +4 -2
  3. tool_compass-2.4.0/.github/dependabot.yml +89 -0
  4. {tool_compass-2.2.2 → tool_compass-2.4.0}/.github/workflows/ci.yml +100 -13
  5. tool_compass-2.4.0/.github/workflows/publish.yml +285 -0
  6. tool_compass-2.4.0/.github/workflows/release-binaries.yml +175 -0
  7. tool_compass-2.4.0/.github/workflows/release.yml +165 -0
  8. {tool_compass-2.2.2 → tool_compass-2.4.0}/.gitignore +14 -0
  9. tool_compass-2.4.0/.pre-commit-config.yaml +55 -0
  10. tool_compass-2.4.0/CHANGELOG.md +486 -0
  11. {tool_compass-2.2.2 → tool_compass-2.4.0}/CONTRIBUTING.md +45 -26
  12. {tool_compass-2.2.2 → tool_compass-2.4.0}/Dockerfile +16 -6
  13. {tool_compass-2.2.2 → tool_compass-2.4.0}/Makefile +15 -8
  14. {tool_compass-2.2.2 → tool_compass-2.4.0}/PKG-INFO +52 -33
  15. {tool_compass-2.2.2 → tool_compass-2.4.0}/README.md +49 -31
  16. tool_compass-2.4.0/SCORECARD.md +36 -0
  17. {tool_compass-2.2.2 → tool_compass-2.4.0}/SECURITY.md +22 -7
  18. {tool_compass-2.2.2 → tool_compass-2.4.0}/SHIP_GATE.md +2 -2
  19. {tool_compass-2.2.2 → tool_compass-2.4.0}/analytics.py +124 -36
  20. {tool_compass-2.2.2 → tool_compass-2.4.0}/backend_client_mcp.py +81 -10
  21. tool_compass-2.4.0/backend_client_simple.py +1815 -0
  22. tool_compass-2.4.0/bootstrap.py +128 -0
  23. {tool_compass-2.2.2 → tool_compass-2.4.0}/chain_indexer.py +139 -11
  24. tool_compass-2.4.0/cli.py +1858 -0
  25. {tool_compass-2.2.2 → tool_compass-2.4.0}/compass_config.example.json +16 -1
  26. {tool_compass-2.2.2 → tool_compass-2.4.0}/config.py +465 -17
  27. tool_compass-2.4.0/embedder.py +934 -0
  28. {tool_compass-2.2.2 → tool_compass-2.4.0}/gateway.py +1183 -275
  29. {tool_compass-2.2.2 → tool_compass-2.4.0}/indexer.py +365 -156
  30. {tool_compass-2.2.2 → tool_compass-2.4.0}/llms.txt +20 -4
  31. {tool_compass-2.2.2 → tool_compass-2.4.0}/pyproject.toml +40 -6
  32. tool_compass-2.4.0/scripts/regenerate-scorecard.sh +116 -0
  33. tool_compass-2.4.0/scripts/verify-metrics.sh +125 -0
  34. {tool_compass-2.2.2 → tool_compass-2.4.0}/sync_manager.py +310 -37
  35. {tool_compass-2.2.2 → tool_compass-2.4.0}/tool_manifest.py +32 -4
  36. tool_compass-2.4.0/ui.py +1955 -0
  37. tool_compass-2.2.2/.dockerignore +0 -56
  38. tool_compass-2.2.2/.github/dependabot.yml +0 -24
  39. tool_compass-2.2.2/.github/workflows/publish.yml +0 -178
  40. tool_compass-2.2.2/AUDIT_REPORT.md +0 -245
  41. tool_compass-2.2.2/CHANGELOG.md +0 -269
  42. tool_compass-2.2.2/README.es.md +0 -324
  43. tool_compass-2.2.2/README.fr.md +0 -324
  44. tool_compass-2.2.2/README.hi.md +0 -323
  45. tool_compass-2.2.2/README.it.md +0 -324
  46. tool_compass-2.2.2/README.ja.md +0 -322
  47. tool_compass-2.2.2/README.pt-BR.md +0 -324
  48. tool_compass-2.2.2/README.zh.md +0 -324
  49. tool_compass-2.2.2/SCORECARD.md +0 -36
  50. tool_compass-2.2.2/assets/logo.png +0 -0
  51. tool_compass-2.2.2/backend_client_simple.py +0 -861
  52. tool_compass-2.2.2/bootstrap.py +0 -80
  53. tool_compass-2.2.2/cli.py +0 -283
  54. tool_compass-2.2.2/docs/assets/social-preview.png +0 -0
  55. tool_compass-2.2.2/docs/assets/social-preview.svg +0 -41
  56. tool_compass-2.2.2/docs/assets/tool-compass-logo-dark-bg.jpg +0 -0
  57. tool_compass-2.2.2/embedder.py +0 -432
  58. tool_compass-2.2.2/logo.png +0 -0
  59. tool_compass-2.2.2/site/astro.config.mjs +0 -30
  60. tool_compass-2.2.2/site/package-lock.json +0 -7031
  61. tool_compass-2.2.2/site/package.json +0 -18
  62. tool_compass-2.2.2/site/src/content/docs/handbook/architecture.md +0 -140
  63. tool_compass-2.2.2/site/src/content/docs/handbook/beginners.md +0 -170
  64. tool_compass-2.2.2/site/src/content/docs/handbook/configuration.md +0 -137
  65. tool_compass-2.2.2/site/src/content/docs/handbook/getting-started.md +0 -157
  66. tool_compass-2.2.2/site/src/content/docs/handbook/index.md +0 -30
  67. tool_compass-2.2.2/site/src/content/docs/handbook/operations.md +0 -173
  68. tool_compass-2.2.2/site/src/content/docs/handbook/tools.md +0 -110
  69. tool_compass-2.2.2/site/src/content.config.ts +0 -7
  70. tool_compass-2.2.2/site/src/pages/index.astro +0 -33
  71. tool_compass-2.2.2/site/src/site-config.ts +0 -158
  72. tool_compass-2.2.2/site/src/styles/global.css +0 -3
  73. tool_compass-2.2.2/site/src/styles/starlight-custom.css +0 -17
  74. tool_compass-2.2.2/site/tsconfig.json +0 -5
  75. tool_compass-2.2.2/ui.py +0 -1337
  76. {tool_compass-2.2.2 → tool_compass-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  77. {tool_compass-2.2.2 → tool_compass-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  78. {tool_compass-2.2.2 → tool_compass-2.4.0}/CODE_OF_CONDUCT.md +0 -0
  79. {tool_compass-2.2.2 → tool_compass-2.4.0}/LICENSE +0 -0
  80. {tool_compass-2.2.2 → tool_compass-2.4.0}/_version.py +0 -0
  81. {tool_compass-2.2.2 → tool_compass-2.4.0}/docker-compose.yml +0 -0
  82. {tool_compass-2.2.2 → tool_compass-2.4.0}/docs/index.md +0 -0
  83. {tool_compass-2.2.2 → tool_compass-2.4.0}/fly.toml +0 -0
  84. {tool_compass-2.2.2 → tool_compass-2.4.0}/requirements-dev.txt +0 -0
  85. {tool_compass-2.2.2 → tool_compass-2.4.0}/requirements.txt +0 -0
  86. {tool_compass-2.2.2 → tool_compass-2.4.0}/scripts/check-org-urls.sh +0 -0
@@ -0,0 +1,84 @@
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Docker
6
+ Dockerfile
7
+ docker-compose.yml
8
+ .dockerignore
9
+
10
+ # Python
11
+ __pycache__
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
16
+ venv/
17
+ .venv/
18
+ ENV/
19
+ env/
20
+ .eggs/
21
+ *.egg-info/
22
+ *.egg
23
+
24
+ # IDE
25
+ .vscode/
26
+ .idea/
27
+ *.swp
28
+ *.swo
29
+
30
+ # Testing
31
+ .pytest_cache/
32
+ .coverage
33
+ htmlcov/
34
+ .tox/
35
+ .hypothesis/
36
+ .benchmarks/
37
+
38
+ # Build artifacts
39
+ dist/
40
+ build/
41
+ *.manifest
42
+ *.spec
43
+
44
+ # OS files
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ # Local development files
49
+ *.local.json
50
+ .env.local
51
+ .env*
52
+
53
+ # Gradio cache
54
+ flagged/
55
+
56
+ # Keep db directory structure but not contents
57
+ db/*.db
58
+ db/*.hnsw
59
+ !db/.gitkeep
60
+
61
+ # Sources that don't belong in the production image (CT-B-004).
62
+ # These are intentionally excluded so a future regression to
63
+ # `COPY . .` doesn't silently ship them.
64
+ tests/
65
+ docs/
66
+ site/
67
+ archive/
68
+ .github/
69
+ .claude/
70
+ # Translation READMEs (re-run on TranslateGemma 12B; not runtime artifacts)
71
+ README.ja.md
72
+ README.zh.md
73
+ README.es.md
74
+ README.fr.md
75
+ README.hi.md
76
+ README.it.md
77
+ README.pt-BR.md
78
+ # Audit / governance docs — not needed at runtime
79
+ SCORECARD.md
80
+ SHIP_GATE.md
81
+ CODE_OF_CONDUCT.md
82
+ CONTRIBUTING.md
83
+ SECURITY.md
84
+ CHANGELOG.md
@@ -55,8 +55,10 @@ GRADIO_SERVER_PORT=7860
55
55
  # Analytics (optional)
56
56
  # =============================================================================
57
57
 
58
- # Disable analytics tracking
58
+ # Disable analytics tracking (truthy: 1/true/yes/on). Overrides the
59
+ # analytics_enabled config-file key when set.
59
60
  # TOOL_COMPASS_ANALYTICS_DISABLED=true
60
61
 
61
- # Hot cache size (number of frequently used tools to pre-load)
62
+ # Hot cache size (number of frequently used tools to pre-load). Overrides the
63
+ # hot_cache_size config-file key; clamped to a safe minimum like any config value.
62
64
  # TOOL_COMPASS_HOT_CACHE_SIZE=10
@@ -0,0 +1,89 @@
1
+ version: 2
2
+ updates:
3
+ # =============================================================================
4
+ # pip — Python runtime + dev dependencies
5
+ # =============================================================================
6
+ - package-ecosystem: "pip"
7
+ directory: "/"
8
+ schedule:
9
+ interval: "monthly"
10
+ open-pull-requests-limit: 3
11
+ groups:
12
+ # Minor + patch only so breaking majors open as SEPARATE PRs.
13
+ # (Avoids the ollama-intern PR #4 incident where 5 breaking majors
14
+ # shipped bundled under a wildcard group.)
15
+ all-pip:
16
+ patterns: ["*"]
17
+ update-types: ["minor", "patch"]
18
+
19
+ # Daily security-only overlay (CT-B-006) — CVE updates from the GitHub
20
+ # Advisory Database don't wait the monthly cadence. open-pull-requests-limit
21
+ # raised to 10 since these are infrequent and load-bearing.
22
+ # NOTE: Dependabot security advisories are auto-emitted; this second entry
23
+ # exists to give them their own schedule/labels/PR cap independent of the
24
+ # monthly version-update entry above. The `allow: dependency-type: all`
25
+ # block scopes this entry to all dependencies; security advisories are
26
+ # always opened regardless of the version-update grouping rules.
27
+ - package-ecosystem: "pip"
28
+ directory: "/"
29
+ schedule:
30
+ interval: "daily"
31
+ open-pull-requests-limit: 10
32
+ allow:
33
+ - dependency-type: "all"
34
+ labels:
35
+ - "security"
36
+ - "dependencies"
37
+
38
+ # =============================================================================
39
+ # github-actions — workflow file action references
40
+ # =============================================================================
41
+ - package-ecosystem: "github-actions"
42
+ directory: "/"
43
+ schedule:
44
+ interval: "monthly"
45
+ open-pull-requests-limit: 3
46
+ groups:
47
+ all-github-actions:
48
+ patterns: ["*"]
49
+ update-types: ["minor", "patch"]
50
+
51
+ # Daily security-only overlay for actions (CT-B-006).
52
+ - package-ecosystem: "github-actions"
53
+ directory: "/"
54
+ schedule:
55
+ interval: "daily"
56
+ open-pull-requests-limit: 10
57
+ allow:
58
+ - dependency-type: "all"
59
+ labels:
60
+ - "security"
61
+ - "dependencies"
62
+
63
+ # =============================================================================
64
+ # docker — Dockerfile base-image FROM directives (CT-B-005)
65
+ # =============================================================================
66
+ # Pairs with the digest-pinned `FROM python:3.11-slim@sha256:...` in
67
+ # Dockerfile (CT-B-003). Without this ecosystem entry the digest pin
68
+ # would become a stale anchor with no machine-readable refresh path.
69
+ - package-ecosystem: "docker"
70
+ directory: "/"
71
+ schedule:
72
+ interval: "monthly"
73
+ open-pull-requests-limit: 2
74
+ groups:
75
+ all-docker:
76
+ patterns: ["*"]
77
+ update-types: ["minor", "patch"]
78
+
79
+ # Daily security-only overlay for docker base images (CT-B-006).
80
+ - package-ecosystem: "docker"
81
+ directory: "/"
82
+ schedule:
83
+ interval: "daily"
84
+ open-pull-requests-limit: 10
85
+ allow:
86
+ - dependency-type: "all"
87
+ labels:
88
+ - "security"
89
+ - "dependencies"
@@ -35,9 +35,16 @@ on:
35
35
  - cron: '0 9 * * 1,3,5'
36
36
  workflow_dispatch:
37
37
 
38
- concurrency:
39
- group: ${{ github.workflow }}-${{ github.ref }}
40
- cancel-in-progress: true
38
+ # NOTE: concurrency is intentionally NOT set at the workflow level.
39
+ # A workflow-level cancel-in-progress would cancel the whole run when a
40
+ # new push lands on the same ref — including the pages-deploy job that
41
+ # carries its own concurrency: pages / cancel-in-progress: false. The
42
+ # job-level setting cannot override workflow-level cancellation because
43
+ # the entire run is cancelled before per-job concurrency takes effect.
44
+ # Each test/lint/integration/docker job below carries its own
45
+ # concurrency block scoped to PR refs only (push events on main are
46
+ # never cancelled). The pages-build / pages-deploy jobs keep their
47
+ # dedicated concurrency: pages group with cancel-in-progress: false.
41
48
 
42
49
  # Default least-privilege; individual jobs that need more elevate explicitly.
43
50
  permissions:
@@ -47,6 +54,13 @@ jobs:
47
54
  lint:
48
55
  name: Org URL sanity check
49
56
  runs-on: ubuntu-latest
57
+ # CT-B-011: lint is a sub-30-second shell check; 5 min is generous.
58
+ timeout-minutes: 5
59
+ # PR-scoped concurrency: a newer push to the same PR cancels the
60
+ # older run, but pushes to main never cancel themselves.
61
+ concurrency:
62
+ group: lint-${{ github.event.pull_request.number || github.run_id }}
63
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
50
64
  permissions:
51
65
  contents: read
52
66
  steps:
@@ -56,12 +70,27 @@ jobs:
56
70
  - name: Check for stale org/user URLs
57
71
  run: bash scripts/check-org-urls.sh
58
72
 
59
- # CDS-FT-001: SCORECARD drift guard. Soft-fail for now — shipcheck
60
- # markdown output is still settling. Flip continue-on-error to false
61
- # once the format stabilizes.
62
- # TODO(swarm): enforce once shipcheck JSON format stabilizes
63
- - name: Verify SCORECARD is in sync
73
+ - name: Set up Python (pre-commit)
74
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
75
+ with:
76
+ python-version: '3.11'
77
+
78
+ # CT-B-018: run the pre-commit hooks (ruff format + ruff check +
79
+ # standard hygiene + gitleaks). Same hooks the contributor runs
80
+ # locally; this is the CI-side enforcement. Soft-fail for the first
81
+ # cycle to surface formatting drift without blocking PRs while
82
+ # contributors install pre-commit locally.
83
+ # TODO(swarm): flip continue-on-error to false after one full release
84
+ # cycle so the pre-commit gate becomes blocking.
85
+ - name: Run pre-commit hooks
64
86
  continue-on-error: true
87
+ uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
88
+
89
+ # CDS-FT-001: SCORECARD drift guard — now BLOCKING. The dogfood-swarm v3
90
+ # pass fixed CIDOCS-01 (regenerate-scorecard.sh forces NO_COLOR + strips
91
+ # ANSI), so `--check` diff-cleans deterministically in non-TTY CI. The
92
+ # committed SCORECARD was verified in sync at flip time.
93
+ - name: Verify SCORECARD is in sync
65
94
  run: |
66
95
  if [ -f Makefile ] && grep -q "^verify-scorecard:" Makefile; then
67
96
  make verify-scorecard
@@ -69,9 +98,32 @@ jobs:
69
98
  echo "Makefile verify-scorecard target missing — skipping"
70
99
  fi
71
100
 
101
+ # CT-B-008 cross-domain: verify that /metrics emits the expected
102
+ # Four Golden Signals surface. Soft-fail because the saturation
103
+ # gauges land in BE-B-002 (backend domain); flip to fatal once that
104
+ # work merges.
105
+ - name: Verify metrics surface (Four Golden Signals)
106
+ continue-on-error: true
107
+ run: |
108
+ if [ -f Makefile ] && grep -q "^verify-metrics:" Makefile; then
109
+ python -m pip install --upgrade pip
110
+ pip install -r requirements.txt || true
111
+ make verify-metrics || true
112
+ else
113
+ echo "Makefile verify-metrics target missing — skipping"
114
+ fi
115
+
72
116
  test:
73
117
  name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
74
118
  runs-on: ${{ matrix.os }}
119
+ # CT-B-011: per-matrix-cell budget. pytest has its own 30s per-test cap
120
+ # (pyproject.toml [tool.pytest.ini_options]); 15 min envelope covers
121
+ # install + coverage + pip-audit with headroom for a slow runner.
122
+ timeout-minutes: 15
123
+ # PR-scoped concurrency (see workflow-top NOTE).
124
+ concurrency:
125
+ group: test-${{ matrix.python-version }}-${{ github.event.pull_request.number || github.run_id }}
126
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
75
127
  permissions:
76
128
  contents: read
77
129
 
@@ -116,6 +168,9 @@ jobs:
116
168
  name: junit-reports-${{ matrix.python-version }}
117
169
  path: junit-${{ matrix.python-version }}.xml
118
170
  if-no-files-found: warn
171
+ # CT-B-012: JUnit reports are short-lived diagnostic data — only
172
+ # useful for the most recent few runs. Default 90d is wasted.
173
+ retention-days: 14
119
174
 
120
175
  - name: Run tests with coverage
121
176
  if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
@@ -142,13 +197,17 @@ jobs:
142
197
  name: pip-audit-report
143
198
  path: pip-audit-report.json
144
199
  if-no-files-found: ignore
200
+ # CT-B-012: CVE diagnostic snapshot — 14d aligns with the JUnit
201
+ # retention and is plenty of window for incident response.
202
+ retention-days: 14
145
203
 
204
+ # Now BLOCKING. The dogfood-swarm v3 pass reviewed the CVE baseline:
205
+ # `pip-audit -r requirements.txt` reports no known vulnerabilities. The
206
+ # report-only artifact step above still runs for the full JSON snapshot.
146
207
  - name: Audit dependencies for vulnerabilities
147
208
  if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
148
209
  run: |
149
210
  pip-audit -r requirements.txt
150
- # TODO(swarm): enforce after CVE baseline reviewed — issue #TBD
151
- continue-on-error: true # Warn-only until clean baseline established
152
211
 
153
212
  # TST-FT-002: nightly Hypothesis fuzzing. Runs on schedule (Mon/Wed/Fri 09:00
154
213
  # UTC) or manually via workflow_dispatch. Never runs on push/PR — nightly-only.
@@ -157,6 +216,9 @@ jobs:
157
216
  name: Nightly Hypothesis fuzz
158
217
  if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
159
218
  runs-on: ubuntu-latest
219
+ # CT-B-011: Hypothesis nightly profile is deliberately slow — 45 min cap
220
+ # is wider than the test job because fuzz examples are unbounded.
221
+ timeout-minutes: 45
160
222
  permissions:
161
223
  contents: read
162
224
  issues: write
@@ -184,9 +246,10 @@ jobs:
184
246
 
185
247
  - name: Open tracking issue on failure
186
248
  if: failure() && steps.fuzz.conclusion == 'failure'
187
- # TODO(swarm): pin actions/github-script to SHA once nightly-fuzz run
188
- # history confirms the job shape is stable.
189
- uses: actions/github-script@v7
249
+ # SHA-pinned per SLSA L3 supply-chain hygiene (CT-B-001). nightly-fuzz
250
+ # carries issues:write, so a compromised v7 mutable tag would inherit
251
+ # issue-creation capability. Bump via dependabot's github-actions sweep.
252
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
190
253
  with:
191
254
  script: |
192
255
  const title = `[nightly-fuzz] Hypothesis failures on ${context.sha}`;
@@ -212,6 +275,13 @@ jobs:
212
275
  name: Integration Tests
213
276
  runs-on: ubuntu-latest
214
277
  needs: test # Only run after unit tests pass
278
+ # CT-B-011: Ollama install + nomic-embed-text pull + integration suite
279
+ # dominates; 25 min envelope covers the cold-start pull path.
280
+ timeout-minutes: 25
281
+ # PR-scoped concurrency (see workflow-top NOTE).
282
+ concurrency:
283
+ group: integration-${{ github.event.pull_request.number || github.run_id }}
284
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
215
285
  permissions:
216
286
  contents: read
217
287
 
@@ -271,6 +341,12 @@ jobs:
271
341
  name: Docker Build
272
342
  runs-on: ubuntu-latest
273
343
  needs: test
344
+ # CT-B-011: docker buildx + gha cache; 20 min covers a cold-cache build.
345
+ timeout-minutes: 20
346
+ # PR-scoped concurrency (see workflow-top NOTE).
347
+ concurrency:
348
+ group: docker-${{ github.event.pull_request.number || github.run_id }}
349
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
274
350
  permissions:
275
351
  contents: read
276
352
 
@@ -299,6 +375,8 @@ jobs:
299
375
  name: Build site
300
376
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
301
377
  runs-on: ubuntu-latest
378
+ # CT-B-011: Starlight npm ci + Astro build; 10 min is generous.
379
+ timeout-minutes: 10
302
380
  permissions:
303
381
  contents: read
304
382
  steps:
@@ -308,6 +386,12 @@ jobs:
308
386
  with:
309
387
  node-version: 22
310
388
 
389
+ # CT-B-016: read repo-level Pages configuration so the build is not
390
+ # silently dependent on Settings → Pages UI state. configure-pages
391
+ # is idempotent and exposes the base path to downstream steps.
392
+ - name: Configure Pages
393
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
394
+
311
395
  - name: Install site dependencies
312
396
  working-directory: site
313
397
  run: npm ci
@@ -325,6 +409,9 @@ jobs:
325
409
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
326
410
  needs: pages-build
327
411
  runs-on: ubuntu-latest
412
+ # CT-B-011: deploy is a metadata operation against the Pages backend;
413
+ # 5 min is the fail-fast budget.
414
+ timeout-minutes: 5
328
415
  # Dedicated concurrency group so a long deploy is never cancelled by a
329
416
  # newer CI run (CDS-A-012).
330
417
  concurrency:
@@ -0,0 +1,285 @@
1
+ # Tool Compass Release Pipeline
2
+ # Publishes to PyPI (trusted publishing) and GHCR on release
3
+
4
+ name: Publish
5
+
6
+ # Triggers — the load-bearing one for the auto-chain is `workflow_run`:
7
+ # * `release: published` does NOT fire when the Release was created by
8
+ # release.yml using secrets.GITHUB_TOKEN (GitHub's recursion guard). Without
9
+ # the workflow_run trigger below, a tag-push release would publish to npm
10
+ # (via release.yml) but silently SKIP PyPI + GHCR here. The `workflow_run`
11
+ # trigger gated on Release.conclusion == 'success' closes the chain.
12
+ # * `release: published` retained for manual hand-cut Releases (gh release
13
+ # create from a maintainer's terminal — attributed to a user, fires normally).
14
+ # * `workflow_dispatch` retained for re-run-from-UI on transient failure.
15
+ on:
16
+ release:
17
+ types: [published]
18
+ workflow_run:
19
+ workflows: [Release]
20
+ types: [completed]
21
+ workflow_dispatch:
22
+
23
+ # Publish workflows must serialize, not cancel. Cancelling a mid-flight twine
24
+ # upload leaves PyPI in a state where the next retry fails with E409 ('File
25
+ # already exists') and needs manual --skip-existing recovery. Group per release
26
+ # tag — resolved across all trigger types (under workflow_run github.ref is the
27
+ # default branch, not the tag) — so distinct tags run independently while
28
+ # same-tag re-fires queue safely.
29
+ concurrency:
30
+ group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.event.release.tag_name || github.ref }}
31
+ cancel-in-progress: false
32
+
33
+ env:
34
+ REGISTRY: ghcr.io
35
+ IMAGE_NAME: ${{ github.repository }}
36
+
37
+ jobs:
38
+ build:
39
+ name: Build package
40
+ runs-on: ubuntu-latest
41
+ # When triggered by workflow_run, only proceed if the upstream Release
42
+ # workflow succeeded. release: published / workflow_dispatch invocations set
43
+ # workflow_run to null, which evaluates this expression to truthy (we only
44
+ # want to short-circuit FAILED workflow_run events).
45
+ if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
46
+ # CT-B-011: fail-fast budgets per SRE 'fail fast' (Google SRE Book ch.21).
47
+ # Default GitHub timeout is 360 min; a wedged dependency install or twine
48
+ # check is well outside the legitimate envelope at 10 min.
49
+ timeout-minutes: 10
50
+
51
+ steps:
52
+ - name: Checkout repository
53
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
54
+ with:
55
+ # Under workflow_run, github.ref is the default branch (main), NOT the
56
+ # tag — build the tagged commit, not main's HEAD. Resolve the tag from
57
+ # the single canonical expression used throughout this workflow.
58
+ ref: ${{ github.event.workflow_run.head_branch || github.event.release.tag_name || github.ref_name }}
59
+
60
+ - name: Set up Python
61
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
62
+ with:
63
+ python-version: '3.11'
64
+
65
+ - name: Install build tools
66
+ run: |
67
+ python -m pip install --upgrade pip
68
+ pip install build twine
69
+
70
+ - name: Build package
71
+ run: python -m build
72
+
73
+ - name: Check package with twine
74
+ run: twine check dist/*
75
+
76
+ - name: Upload build artifacts
77
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
78
+ with:
79
+ name: dist
80
+ path: dist/
81
+ # CT-B-012: dist/ is an ephemeral build-to-publish handoff; the
82
+ # canonical artifacts live at PyPI + GHCR after publish completes.
83
+ # Keep the workflow copy around 7 days for incident-response replays.
84
+ retention-days: 7
85
+
86
+ publish-pypi:
87
+ name: Publish to PyPI
88
+ runs-on: ubuntu-latest
89
+ needs: build
90
+ environment: pypi
91
+ # CT-B-011: PyPI upload + propagation; 10 min covers retries.
92
+ timeout-minutes: 10
93
+ permissions:
94
+ id-token: write
95
+ # CT-B-009: attestations:write enables SLSA provenance attestations on
96
+ # the published wheel + sdist via pypa/gh-action-pypi-publish v1.13+.
97
+ # Combined with `attestations: true` below, PyPI surfaces a verifiable
98
+ # build attestation tied to this commit + workflow run.
99
+ attestations: write
100
+ contents: read
101
+
102
+ steps:
103
+ - name: Download build artifacts
104
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
105
+ with:
106
+ name: dist
107
+ path: dist/
108
+
109
+ - name: Publish to PyPI
110
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
111
+ with:
112
+ # CT-B-009: emit SLSA build provenance attestations for the wheel
113
+ # and sdist. PyPI verifies the attestation against the workflow's
114
+ # OIDC identity; downstream consumers can audit the chain from a
115
+ # specific commit -> workflow run -> published artifact.
116
+ attestations: true
117
+
118
+ docker:
119
+ name: Publish to GHCR
120
+ runs-on: ubuntu-latest
121
+ # PyPI ships first (CDS-B-005). If PyPI fails, Docker must not ship a
122
+ # half-release; if PyPI succeeds, Docker becomes the recoverable tail.
123
+ needs: publish-pypi
124
+ # CT-B-011: multi-arch buildx (linux/amd64 + linux/arm64) + push to GHCR
125
+ # is the longest leg; 30 min covers the warm cache path with headroom.
126
+ timeout-minutes: 30
127
+ permissions:
128
+ contents: read
129
+ packages: write
130
+ # CT-B-009: id-token + attestations enable buildx SLSA provenance
131
+ # mode=max on the image manifest below.
132
+ id-token: write
133
+ attestations: write
134
+
135
+ steps:
136
+ - name: Checkout repository
137
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
138
+ with:
139
+ # Under workflow_run, github.ref is main, NOT the tag — build the
140
+ # tagged commit, not main's HEAD. Same resolved-tag expression used
141
+ # throughout this workflow.
142
+ ref: ${{ github.event.workflow_run.head_branch || github.event.release.tag_name || github.ref_name }}
143
+
144
+ - name: Log in to GHCR
145
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
146
+ with:
147
+ registry: ${{ env.REGISTRY }}
148
+ username: ${{ github.actor }}
149
+ password: ${{ secrets.GITHUB_TOKEN }}
150
+
151
+ # Resolve the release version explicitly. Under workflow_run github.ref
152
+ # points at the default branch (main), NOT the tag — so
153
+ # docker/metadata-action's type=semver rules (which only fire on a
154
+ # refs/tags/... ref) emit NOTHING and the image never gets a :X.Y.Z / :X.Y
155
+ # tag (only :latest + :sha-...). The tag that started the upstream Release
156
+ # run is carried on github.event.workflow_run.head_branch (= the tag ref).
157
+ # Feed the resolved version into metadata-action via type=raw below.
158
+ - name: Resolve release version
159
+ id: ver
160
+ shell: bash
161
+ run: |
162
+ set -euo pipefail
163
+ REF="${{ github.event.workflow_run.head_branch || github.event.release.tag_name || github.ref_name }}"
164
+ V="${REF#v}"
165
+ echo "version=${V}" >> "$GITHUB_OUTPUT"
166
+ echo "major_minor=$(echo "$V" | cut -d. -f1-2)" >> "$GITHUB_OUTPUT"
167
+ echo "resolved=${V} (event=${{ github.event_name }} ref=${REF})"
168
+
169
+ - name: Extract metadata
170
+ id: meta
171
+ uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
172
+ with:
173
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
174
+ # type=raw from the explicitly-resolved version (see Resolve step) —
175
+ # NOT type=semver, which silently emits nothing under workflow_run
176
+ # because github.ref is main, not the tag. Each enable=... gates its
177
+ # raw tag so an empty/missing version never pushes a bogus tag.
178
+ tags: |
179
+ type=raw,value=${{ steps.ver.outputs.version }},enable=${{ steps.ver.outputs.version != '' }}
180
+ type=raw,value=${{ steps.ver.outputs.major_minor }},enable=${{ steps.ver.outputs.major_minor != '' }}
181
+ type=sha
182
+ type=raw,value=latest,enable={{is_default_branch}}
183
+
184
+ # QEMU is required to cross-build linux/arm64 from an amd64 runner.
185
+ - name: Set up QEMU
186
+ # SHA-pinned per SLSA L3 supply-chain hygiene (CT-B-002). publish job
187
+ # carries packages:write — a compromised mutable v3 tag would inherit
188
+ # GHCR write capability during a release window. Bump via dependabot.
189
+ uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
190
+
191
+ - name: Set up Docker Buildx
192
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
193
+
194
+ - name: Build and push
195
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
196
+ with:
197
+ context: .
198
+ push: true
199
+ platforms: linux/amd64,linux/arm64
200
+ tags: ${{ steps.meta.outputs.tags }}
201
+ labels: ${{ steps.meta.outputs.labels }}
202
+ cache-from: type=gha
203
+ cache-to: type=gha,mode=max
204
+ # CT-B-009: SLSA provenance mode=max emits buildx provenance
205
+ # attestation (including all input source digests) attached to the
206
+ # image manifest. sbom: true emits an SPDX SBOM attestation in the
207
+ # same manifest. Both consumable via `cosign verify-attestation`
208
+ # or `docker buildx imagetools inspect`.
209
+ provenance: mode=max
210
+ sbom: true
211
+
212
+ release-smoke:
213
+ name: Release smoke test
214
+ runs-on: ubuntu-latest
215
+ needs: [publish-pypi, docker]
216
+ # CT-B-011: PyPI propagation retry loop + docker pull + version handshake.
217
+ # 15 min is the propagation-retry window plus headroom.
218
+ timeout-minutes: 15
219
+ permissions:
220
+ contents: read
221
+ packages: read
222
+ steps:
223
+ - name: Derive version (strip leading v)
224
+ id: ver
225
+ run: |
226
+ set -euo pipefail
227
+ # Resolve the tag across all trigger types. Under workflow_run
228
+ # github.event.release.tag_name is null and github.ref is main —
229
+ # workflow_run.head_branch carries the tag ref (e.g. v2.4.0).
230
+ TAG="${{ github.event.workflow_run.head_branch || github.event.release.tag_name || github.ref_name }}"
231
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
232
+ echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
233
+
234
+ - name: Set up Python
235
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
236
+ with:
237
+ python-version: '3.11'
238
+
239
+ - name: PyPI smoke — install + --version matches tag
240
+ run: |
241
+ set -euo pipefail
242
+ VERSION="${{ steps.ver.outputs.version }}"
243
+ # PyPI propagation is fast but not instant; retry briefly. If no
244
+ # iteration succeeds the loop falls through and the subsequent
245
+ # `tool-compass --version` invocation fails with a clear "command
246
+ # not found", which is the desired loud failure mode (CT-B-014).
247
+ install_ok=0
248
+ for _ in $(seq 1 12); do
249
+ if pip install "tool-compass==${VERSION}"; then
250
+ install_ok=1
251
+ break
252
+ fi
253
+ sleep 10
254
+ done
255
+ if [ "$install_ok" -ne 1 ]; then
256
+ echo "::error::PyPI smoke failed: tool-compass==${VERSION} did not become installable in the propagation window"
257
+ exit 1
258
+ fi
259
+ OUT="$(tool-compass --version 2>&1 || true)"
260
+ echo "tool-compass --version → $OUT"
261
+ echo "$OUT" | grep -Fq "${VERSION}" || {
262
+ echo "::error::PyPI smoke failed: --version output did not contain ${VERSION}"
263
+ exit 1
264
+ }
265
+
266
+ - name: Log in to GHCR (pull)
267
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
268
+ with:
269
+ registry: ${{ env.REGISTRY }}
270
+ username: ${{ github.actor }}
271
+ password: ${{ secrets.GITHUB_TOKEN }}
272
+
273
+ - name: Docker smoke — pull + --version matches tag
274
+ run: |
275
+ set -euo pipefail
276
+ TAG="${{ steps.ver.outputs.tag }}"
277
+ VERSION="${{ steps.ver.outputs.version }}"
278
+ IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}"
279
+ docker pull "$IMAGE"
280
+ OUT="$(docker run --rm --entrypoint tool-compass "$IMAGE" --version 2>&1 || true)"
281
+ echo "docker --version → $OUT"
282
+ echo "$OUT" | grep -Fq "${VERSION}" || {
283
+ echo "::error::Docker smoke failed: --version output did not contain ${VERSION}"
284
+ exit 1
285
+ }