Repository navigation
247 lines (216 loc) · 10.9 KB
/
Copy pathci.yml
File metadata and controls
247 lines (216 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# Two mechanisms, one per event kind:
# - PR runs share a group per branch (`github.head_ref` is non-empty only on
# `pull_request`), and `cancel-in-progress` cancels the superseded one.
# - Everything else (`push`, `workflow_dispatch`) is keyed on `github.sha`,
# unique per commit, so each `main` run sits alone in its own group.
# The second is not redundant with the first. A group holds at most one
# *pending* run regardless of `cancel-in-progress`, so under one branch-keyed
# group a third push evicts the second's queued run before it ever starts —
# a commit lands on the protected branch with no CI signal at all (#44).
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lint:
name: lint
runs-on: ubuntu-latest
# WIF: mint an OIDC token for google-github-actions/auth to impersonate the
# read-only co-pypi-reader SA and pull the cannobserv wheelhouse.
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install Python
run: uv python install 3.12
# co-core / co-core-aio resolve from a wheelhouse mirrored from the
# private GCS index. Authenticate keyless via Workload Identity Federation
# — the read-scoped provider impersonating the objectViewer-only
# co-pypi-reader SA — then sync before `uv sync`.
# Fail fast and legibly when the org variable is not visible to this repo.
# google-github-actions/auth validates its own inputs before contacting
# Google, so an empty provider surfaces as "must specify exactly one of
# workload_identity_provider or credentials_json" — which reads like a
# workflow-authoring bug and sends you looking at GCP IAM, where the
# problem is not. Assert the precondition here instead.
- name: Assert wheelhouse auth is configured
env:
WIF_PROVIDER: ${{ vars.GCP_WIF_PROVIDER }}
run: |
if [ -z "$WIF_PROVIDER" ]; then
echo "::error::vars.GCP_WIF_PROVIDER is empty for this repo. GCP is not the problem — the org variable is not visible here. Fix under Org Settings > Secrets and variables > Actions > Variables > GCP_WIF_PROVIDER > Repository access."
exit 1
fi
echo "WIF provider: $WIF_PROVIDER"
- name: Authenticate to Google Cloud (WIF, read-only)
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: co-pypi-reader@co-gcs.iam.gserviceaccount.com
- name: Sync cannobserv wheelhouse
run: uv run --no-project --with 'google-cloud-storage>=2,<4' python scripts/sync_wheelhouse.py
- name: uv sync
run: uv sync --frozen --group dev
- name: ruff check
run: uv run ruff check .
- name: ruff format --check
run: uv run ruff format --check .
test:
name: test
runs-on: ubuntu-latest
# WIF: mint an OIDC token for google-github-actions/auth (see the lint job).
permissions:
contents: read
id-token: write
steps:
# `submodules` is load-bearing for tests/test_skills_hook.py, which
# dereferences the .claude/hooks/ symlink into skills-vendor/ — an
# unpopulated submodule makes that link dangle and the test cannot pass.
# Both submodules are public HTTPS in .gitmodules, so no token is needed.
# The coupling this buys: an upstream force-push that GCs a pinned SHA
# fails the job *at checkout*, which looks nothing like a test failure.
# The lint job deliberately omits this — ruff extend-excludes
# skills-vendor/ (pyproject.toml), so it never reads what it does not
# fetch. Costs ~2.2s of a ~1m10s job. See docs/SKILLS.md.
- uses: actions/checkout@v5
with:
submodules: true
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install Python
run: uv python install 3.12
# Fail fast and legibly when the org variable is not visible to this repo.
# google-github-actions/auth validates its own inputs before contacting
# Google, so an empty provider surfaces as "must specify exactly one of
# workload_identity_provider or credentials_json" — which reads like a
# workflow-authoring bug and sends you looking at GCP IAM, where the
# problem is not. Assert the precondition here instead.
- name: Assert wheelhouse auth is configured
env:
WIF_PROVIDER: ${{ vars.GCP_WIF_PROVIDER }}
run: |
if [ -z "$WIF_PROVIDER" ]; then
echo "::error::vars.GCP_WIF_PROVIDER is empty for this repo. GCP is not the problem — the org variable is not visible here. Fix under Org Settings > Secrets and variables > Actions > Variables > GCP_WIF_PROVIDER > Repository access."
exit 1
fi
echo "WIF provider: $WIF_PROVIDER"
- name: Authenticate to Google Cloud (WIF, read-only)
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: co-pypi-reader@co-gcs.iam.gserviceaccount.com
- name: Sync cannobserv wheelhouse
run: uv run --no-project --with 'google-cloud-storage>=2,<4' python scripts/sync_wheelhouse.py
- name: uv sync
run: uv sync --frozen --group dev
# Validates the wheelhouse end to end: the extras the MVP loop depends on
# ([extract] for fingerprinting, [bus] for the Redis Streams driver) must
# import, not merely resolve.
- name: co-core import smoke (extras wired)
run: |
uv run python -c "
from co_core.pure.util.hashing import sha256
from co_core.pure.extract import simhash
from co_core.pure.adapters.bus import streams
from co_core.pure.models.changes import BlobAvailableEvent, ContentFetchCommand
from co_core_aio.bus import AsyncBusConsumer, AsyncBusPublisher
from co_core_aio.fetch import AsyncFetchDriver
assert streams.CONTENT_FETCH == 'content.fetch'
assert streams.CONTENT_BLOBS == 'content.blobs'
print('co-core extras OK')
"
# No Postgres service and no alembic steps: Replicator is DB-free. Its
# durable state is the Redis consumer group's PEL plus content-addressed
# blobs on disk. Unit tests use fakeredis; anything needing a real broker
# is marked @pytest.mark.integration and excluded by default.
- name: pytest (default, excludes integration)
run: uv run pytest
# The only job in this workflow that holds a delete-capable identity, and it is
# a separate job for exactly that reason (#53). The `test` job authenticates as
# `co-pypi-reader` — read on the package index and nothing else — and giving it
# a second identity that can write and delete objects would widen every unit
# test's blast radius to buy coverage that three tests need.
#
# Keyless: the test SA is bound to this repo's WIF provider, so no key exists to
# leak. `google-github-actions/auth` writes a credentials file and exports
# GOOGLE_APPLICATION_CREDENTIALS; the fixtures in tests/conftest.py strip that
# variable from every test and re-point it, per test, from
# REPLICATOR_TEST_GCS_CREDENTIALS — so the path is threaded explicitly rather
# than inherited, and an unmarked test cannot reach the identity at all.
gcs:
name: gcs (T4 against the test bucket)
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install Python
run: uv python install 3.12
- name: Assert wheelhouse auth is configured
env:
WIF_PROVIDER: ${{ vars.GCP_WIF_PROVIDER }}
run: |
if [ -z "$WIF_PROVIDER" ]; then
echo "::error::vars.GCP_WIF_PROVIDER is empty for this repo. GCP is not the problem — the org variable is not visible here. Fix under Org Settings > Secrets and variables > Actions > Variables > GCP_WIF_PROVIDER > Repository access."
exit 1
fi
# First identity: read-only, and only to mirror the wheelhouse.
- name: Authenticate to Google Cloud (WIF, read-only)
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: co-pypi-reader@co-gcs.iam.gserviceaccount.com
- name: Sync cannobserv wheelhouse
run: uv run --no-project --with 'google-cloud-storage>=2,<4' python scripts/sync_wheelhouse.py
- name: uv sync
run: uv sync --frozen --group dev
# Second identity, after the wheelhouse is already mirrored: objectAdmin on
# co-gcs-test-replication and no grant whatsoever on the production bucket
# (docs/DEPLOYMENT.md, "The GCS test bucket").
- name: Authenticate to Google Cloud (WIF, test bucket writer)
id: gcs_auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: co-gcs-test-replicator@co-gcs.iam.gserviceaccount.com
# Skipping is not passing here, and a skip is silent — the fixtures skip a
# `gcs` test when either variable is unset, which on a developer's machine
# is correct and in CI would mean the provisioning or the WIF binding
# regressed while the job stayed green. The bucket is a literal below, so
# only the credentials path can be empty; assert it rather than discover it.
- name: Assert the test identity resolved
run: |
if [ -z "${{ steps.gcs_auth.outputs.credentials_file_path }}" ]; then
echo "::error::WIF auth produced no credentials file for co-gcs-test-replicator. The gcs tests would skip, not fail. Check the SA's roles/iam.workloadIdentityUser binding on this repo's principalSet (docs/DEPLOYMENT.md)."
exit 1
fi
# --no-cov: these rows do not exercise all of src/, and the gate measures
# it. `-m gcs` is deselected from the default suite, so this is the only
# place they run. `-rs` prints the reason for any skip that survives the
# assertion above, so a future one is legible in the log rather than a
# missing line.
- name: pytest (T4 rows against the real bucket)
env:
REPLICATOR_TEST_GCS_BUCKET: co-gcs-test-replication
REPLICATOR_TEST_GCS_CREDENTIALS: ${{ steps.gcs_auth.outputs.credentials_file_path }}
run: uv run pytest --no-cov -m gcs -rs