forked from ChelseaKR/gtfs-scorecard
-
Notifications
You must be signed in to change notification settings - Fork 0
260 lines (240 loc) · 11.8 KB
/
Copy pathtargeted-score.yml
File metadata and controls
260 lines (240 loc) · 11.8 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
248
249
250
251
252
253
254
255
256
257
258
259
260
name: Targeted agency activation
# Operator-only path for activating a small, reviewed registry set without
# waiting for the next full daily matrix. S3 remains authoritative: hydrate a
# bounded complete view, score sequentially, then publish only the selected
# agency directories and aggregates derived from the complete current corpus.
on:
workflow_dispatch:
inputs:
agency_ids:
description: "Registry agency ids (comma, newline, or space separated; maximum 25)"
required: true
type: string
permissions:
contents: read
jobs:
activate:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # AWS OIDC for the authoritative artifact store and validator cache
# All artifact publishers share this repository-wide lock. Scoring shards
# remain parallel; only jobs that can mutate the public S3 state serialize.
concurrency:
group: artifacts-publish
cancel-in-progress: false
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: temurin
java-version: "17"
- uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0
with:
python-version: "3.12"
- name: Require the authoritative artifact bucket
env:
ARTIFACTS_BUCKET: ${{ vars.ARTIFACTS_BUCKET }}
run: |
if [ -z "$ARTIFACTS_BUCKET" ]; then
echo "::error title=ARTIFACTS_BUCKET is required::Targeted activation cannot safely publish without the authoritative S3 store."
exit 1
fi
- name: Validate the bounded registry selection
working-directory: pipeline
env:
TARGET_INPUT: ${{ inputs.agency_ids }}
run: uv run scorecard activation-targets --ids "$TARGET_INPUT" --out "$RUNNER_TEMP/targets.txt"
- name: Authenticate to AWS
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
- name: Hydrate the authoritative corpus and selected history
env:
ARTIFACTS_BUCKET: ${{ vars.ARTIFACTS_BUCKET }}
run: |
set -euo pipefail
# Checkout contains a deliberately small committed snapshot whose
# contents are not authoritative. Start clean before exact S3 reads.
rm -rf data/artifacts
mkdir -p data/artifacts
rm -f data/liveness.json
# index.json is the commit manifest. One Python process captures its
# exact bytes and ETag from the same GET, then concurrently exact-GETs
# every registered latest.json, its indexed current dated object, and
# optional fixlog.json. A dated-object 404 falls back locally to the
# byte-identical latest payload without recreating the expired S3 key.
# Source mtimes are preserved so later bounded syncs skip unchanged
# selected and aggregate objects.
# Only the <=25 selected directories and small rollups/changes/run
# prefixes are listed in full.
cd pipeline
uv run --with boto3 python -m scorecard_pipeline.cli activation-hydrate \
--bucket "$ARTIFACTS_BUCKET" \
--targets-file "$RUNNER_TEMP/targets.txt" \
--index-before-out "$RUNNER_TEMP/index.before.json" \
--etag-out "$RUNNER_TEMP/index.etag"
cd ..
# Ignore legacy public-path fix logs. Hydrate the private durable copy
# that the publisher replaces only after reconciliation.
find data/artifacts -mindepth 2 -maxdepth 2 -type f -name fixlog.json -delete
aws s3 sync "s3://${ARTIFACTS_BUCKET}/cache/fixlog" data/artifacts \
--only-show-errors
mkdir -p data/cache/structure
while IFS= read -r id; do
aws s3 cp "s3://${ARTIFACTS_BUCKET}/cache/structure/${id}.json" \
"data/cache/structure/${id}.json" --only-show-errors || true
done < "$RUNNER_TEMP/targets.txt"
- name: Score selected agencies sequentially
working-directory: pipeline
env:
VALIDATOR_CACHE_BUCKET: ${{ vars.ARTIFACTS_BUCKET }}
ARTIFACTS_BUCKET: ${{ vars.ARTIFACTS_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION || 'us-west-2' }}
run: |
set -euo pipefail
while IFS= read -r id; do
echo "::group::score $id"
uv run --with boto3 python -m scorecard_pipeline.cli run --agency "$id"
echo "::endgroup::"
done < "$RUNNER_TEMP/targets.txt"
- name: Rebuild corpus aggregates
working-directory: pipeline
run: |
set -euo pipefail
# `scorecard run` maintains index.json incrementally from the selected
# directories. Restore the authoritative compact history first so
# reindex merges retained S3 dates instead of truncating other trend
# points that have already reached their lifecycle expiry.
cp "$RUNNER_TEMP/index.before.json" ../data/artifacts/index.json
uv run scorecard reindex
uv run scorecard rollups
uv run scorecard render-site
- name: Guard and publish only the bounded change set
env:
ARTIFACTS_BUCKET: ${{ vars.ARTIFACTS_BUCKET }}
run: |
set -euo pipefail
tag_dated_artifact() {
local key="$1"
local attempt
for attempt in 1 2 3 4; do
if aws s3api put-object-tagging \
--bucket "$ARTIFACTS_BUCKET" \
--key "$key" \
--tagging 'TagSet=[{Key=artifact-class,Value=dated}]' \
--output text >/dev/null; then
return 0
fi
if [ "$attempt" -eq 4 ]; then
echo "::error title=lifecycle tagging failed::Could not tag ${key} after ${attempt} attempts"
return 1
fi
sleep $((attempt * 2))
done
}
artifact_uri="s3://${ARTIFACTS_BUCKET}/data/artifacts"
expected_etag=$(<"$RUNNER_TEMP/index.etag")
current_etag=$(aws s3api head-object \
--bucket "$ARTIFACTS_BUCKET" \
--key data/artifacts/index.json \
--query ETag --output text)
if [ "$current_etag" != "$expected_etag" ]; then
echo "::error title=artifact index changed::Another publisher changed index.json after hydration; no targeted artifacts were published. Re-run this activation."
exit 1
fi
# Retire exact mutable pointers before publishing the rebuilt current
# catalog. The generated manifest contains ids, not arbitrary keys;
# the publisher expands only latest/badge/conformance/mark/geometry
# names and cannot delete a dated score snapshot.
retirement_stage="$RUNNER_TEMP/retirement-cleanup"
rm -rf "$retirement_stage"
mkdir -p "$retirement_stage"
uv run --project pipeline --with boto3 scorecard publish-artifacts \
--root "$retirement_stage" \
--bucket "$ARTIFACTS_BUCKET" --prefix data/artifacts \
--retirement-manifest data/artifacts/.retired-current-artifacts.json
fixlog_stage="$RUNNER_TEMP/private-fixlogs"
rm -rf "$fixlog_stage"
mkdir -p "$fixlog_stage"
jq -r '.agencies | keys[]' data/artifacts/index.json | while IFS= read -r id; do
if [ -f "data/artifacts/${id}/fixlog.json" ]; then
mkdir -p "$fixlog_stage/$id"
cp "data/artifacts/${id}/fixlog.json" "$fixlog_stage/$id/fixlog.json"
fi
done
# Upload and tag the immutable dated snapshot before latest.json can
# expose it. A tagging failure therefore leaves only an unreachable
# dated object and safely blocks the aggregate/index commit.
while IFS= read -r id; do
snapshot_date=$(jq -er '.snapshot_date' "data/artifacts/${id}/latest.json")
dated="data/artifacts/${id}/${snapshot_date}.json"
if [ ! -f "$dated" ]; then
echo "::error title=dated artifact missing::${id}/${snapshot_date}.json was not produced"
exit 1
fi
aws s3 cp "$dated" "${artifact_uri}/${id}/${snapshot_date}.json" \
--cache-control "max-age=300" --only-show-errors
tag_dated_artifact "data/artifacts/${id}/${snapshot_date}.json"
# Publish each remaining selected-directory file, never the whole
# tree. Additive sync only: no --delete or unrelated agency path.
aws s3 sync "data/artifacts/${id}" "${artifact_uri}/${id}" \
--exclude "${snapshot_date}.json" --exclude "validator-cache.json" \
--exclude "structure.json" --exclude "fixlog.json" \
--exclude "corrected.zip" \
--cache-control "max-age=300" --only-show-errors
for internal in validator-cache.json structure.json fixlog.json corrected.zip; do
aws s3 rm "${artifact_uri}/${id}/${internal}" --only-show-errors
done
done < "$RUNNER_TEMP/targets.txt"
# Aggregates were regenerated from every authoritative latest.json.
# run/* is deliberately absent: targeted activation is not a daily
# pipeline run and must not replace its public health status.
aws s3 cp data/artifacts/directory.json "${artifact_uri}/directory.json" \
--cache-control "max-age=300" --only-show-errors
aws s3 cp data/artifacts/scoring.json "${artifact_uri}/scoring.json" \
--cache-control "max-age=300" --only-show-errors
aws s3 sync data/artifacts/rollups "${artifact_uri}/rollups" \
--cache-control "max-age=300" --only-show-errors
aws s3 sync data/artifacts/changes "${artifact_uri}/changes" \
--delete --cache-control "max-age=300" --only-show-errors
# index.json is the commit pointer and goes last, only when its bytes
# changed. If-Match makes the final write itself conditional on the
# exact bytes hydrated, closing the race after the early ETag guard.
if ! cmp -s "$RUNNER_TEMP/index.before.json" data/artifacts/index.json; then
aws s3api put-object \
--bucket "$ARTIFACTS_BUCKET" \
--key data/artifacts/index.json \
--body data/artifacts/index.json \
--if-match "$expected_etag" \
--cache-control "max-age=300" \
--content-type application/json >/dev/null
fi
# The public commit completed. Comparison memory may now advance to
# the selected feed states without getting ahead of publication.
while IFS= read -r id; do
if [ -f "data/cache/structure/${id}.json" ]; then
aws s3 cp "data/cache/structure/${id}.json" \
"s3://${ARTIFACTS_BUCKET}/cache/structure/${id}.json" \
--only-show-errors
fi
done < "$RUNNER_TEMP/targets.txt"
aws s3 sync "$fixlog_stage" "s3://${ARTIFACTS_BUCKET}/cache/fixlog" \
--delete --only-show-errors
# S3 writes do not update main, so explicitly render and deploy the newly
# activated records. Performance remains advisory for a data-only refresh;
# the reusable Pages workflow still blocks on accessibility.
deploy:
needs: activate
permissions:
contents: read
pages: write
id-token: write
uses: ./.github/workflows/pages.yml
with:
perf_gate: advisory
secrets:
PAGES_AWS_ROLE_ARN: ${{ secrets.PAGES_AWS_ROLE_ARN }}