-
Notifications
You must be signed in to change notification settings - Fork 5
389 lines (333 loc) · 15.6 KB
/
Copy pathexcel_inbox.yaml
File metadata and controls
389 lines (333 loc) · 15.6 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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# .github/workflows/excel_inbox.yaml
#
# Excel Inbox — apply vocabulary changes and validate
# ====================================================
# Triggered when a PR to main touches the inbox/ folder.
#
# Contributor flow:
# 1. Download docs/assets/coremeta4cat_vocabulary.xlsx.
# 2. Edit it (add/modify/delete rows), re-save as coremeta4cat_vocabulary.xlsx.
# 3. Open a PR that places the file at inbox/coremeta4cat_vocabulary.xlsx.
# 4. This workflow:
# a. Applies the changes to the schema YAML source files.
# b. Runs `just test` to validate the modified schema.
# c. Regenerates docs/assets/coremeta4cat_vocabulary.xlsx from the schema.
# d. Runs a round-trip check (inbox Excel vs updated schema).
# e. Posts a detailed PR comment with all results.
# f. On success: commits schema + docs Excel back to the branch and
# removes the inbox file (with [skip ci] to avoid infinite loops).
# g. On error: posts the full error list as a PR comment and fails.
# 5. A maintainer reviews the diff, verifies the bot commit, and merges.
#
# Security note: uses pull_request_target with two-checkout pattern.
# Contributor-controlled files (the xlsx) are only opened as data, never
# executed. All scripts run from main (checked out in _main_branch/).
# See: https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
#
# The Excel is derived output — the schema YAML is the ground truth.
---
name: Excel inbox — apply and validate
on: # yamllint disable-line rule:truthy
pull_request_target:
branches:
- main
types: [opened, reopened, synchronize]
paths:
- "inbox/**"
workflow_dispatch:
env:
FORCE_COLOR: "1"
INBOX_FILE: "inbox/coremeta4cat_vocabulary.xlsx"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
apply-inbox:
name: Apply inbox Excel to schema
if: ${{ !github.event.pull_request.merged }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write # to commit schema changes back to the PR branch
pull-requests: write # to post the summary comment
steps:
# ── Checkout ─────────────────────────────────────────────────────────
- name: Check out PR branch (fork-safe)
uses: actions/checkout@v6.0.3
with:
# Check out the contributor's branch so we can push back to it.
# pull_request_target + explicit head repo is the standard pattern
# for safe write-back to fork PRs.
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
persist-credentials: true
- name: Check out main branch into _main_branch/
# Always run OUR scripts from main — never from the PR.
# This is the key security boundary for pull_request_target.
# Full checkout needed: just auto-loads config.public.mk and test suite
# needs examples/, tests/data/, config.yaml, etc.
uses: actions/checkout@v6.0.3
with:
ref: main
path: _main_branch
fetch-depth: 1
persist-credentials: false
# ── Tool setup ────────────────────────────────────────────────────────
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install project from main branch
run: uv sync --dev
working-directory: _main_branch
- name: Install just
run: uv tool install rust-just
# ── Pre-flight ────────────────────────────────────────────────────────
- name: Check inbox file exists
id: inbox_check
run: |
if [ -f "${{ env.INBOX_FILE }}" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
echo "Inbox file found: ${{ env.INBOX_FILE }}"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "No inbox file at ${{ env.INBOX_FILE }} — nothing to process."
fi
# ── Step 1: Apply inbox Excel changes to schema YAMLs ─────────────────
- name: Apply inbox to schema (inbox_to_schema.py)
if: steps.inbox_check.outputs.present == 'true'
id: apply
run: |
# Run inbox_to_schema.py from _main_branch/ against the inbox Excel
# (which lives in the PR branch working directory, one level up).
#
# set +e: GitHub Actions shells run with -eo pipefail, which means
# a failed command substitution (OUTPUT=$(cmd)) would abort the shell
# before we can write the output to GITHUB_ENV for the PR comment.
set +e
OUTPUT=$(uv run python scripts/inbox_to_schema.py \
"../${{ env.INBOX_FILE }}" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"
# Capture output for PR comment
{
echo "apply_output<<APPLY_EOF"
echo "$OUTPUT"
echo "APPLY_EOF"
} >> "$GITHUB_ENV"
# Classify exit code
case "$EXIT_CODE" in
0) echo "status=ok" >> "$GITHUB_OUTPUT" ;;
2) echo "status=warnings" >> "$GITHUB_OUTPUT" ;;
*) echo "status=errors" >> "$GITHUB_OUTPUT" ;;
esac
# Exit code 3 = errors, nothing was written to the schema — fail here.
# (Comment will be posted in the Post PR comment step below.)
if [ "$EXIT_CODE" -eq 3 ] || [ "$EXIT_CODE" -eq 1 ]; then
exit 1
fi
working-directory: _main_branch
# Continue so we can post the PR comment even when this step fails
continue-on-error: true
- name: Record apply failure
if: steps.apply.outcome == 'failure'
run: echo "apply_failed=true" >> "$GITHUB_ENV"
# ── Step 2: Validate the modified schema ───────────────────────────────
- name: Validate schema (just test)
if: >
steps.inbox_check.outputs.present == 'true' &&
steps.apply.outputs.status != 'errors' &&
steps.apply.outcome != 'failure'
id: test
run: |
# Capture output + exit code; disable set -e so a failing test suite
# does not abort the shell before we can write to GITHUB_ENV.
set +e
TEST_OUTPUT=$(just test 2>&1)
TEST_EXIT=$?
set -e
echo "$TEST_OUTPUT"
{
echo "test_output<<TEST_EOF"
echo "$TEST_OUTPUT"
echo "TEST_EOF"
} >> "$GITHUB_ENV"
if [ "$TEST_EXIT" -eq 0 ]; then
echo "status=ok" >> "$GITHUB_OUTPUT"
else
echo "status=fail" >> "$GITHUB_OUTPUT"
fi
# Step always exits 0 — failure recorded in status output above
working-directory: _main_branch
# ── Step 3: Regenerate docs Excel from updated schema ─────────────────
- name: Regenerate docs/assets Excel (just schema-to-excel)
if: >
steps.inbox_check.outputs.present == 'true' &&
steps.apply.outputs.status != 'errors' &&
steps.apply.outcome != 'failure' &&
steps.test.outputs.status == 'ok'
id: regen_excel
run: just schema-to-excel
working-directory: _main_branch
continue-on-error: true
# ── Step 4: Round-trip check ───────────────────────────────────────────
- name: Round-trip check (inbox Excel vs updated schema)
if: >
steps.inbox_check.outputs.present == 'true' &&
steps.apply.outputs.status != 'errors' &&
steps.apply.outcome != 'failure' &&
steps.test.outputs.status == 'ok' &&
steps.regen_excel.outcome == 'success'
id: roundtrip
run: |
set +e
RT_OUTPUT=$(uv run python scripts/excel_to_schema.py \
"../${{ env.INBOX_FILE }}" 2>&1)
set -e
echo "$RT_OUTPUT"
{
echo "roundtrip_output<<RT_EOF"
echo "$RT_OUTPUT"
echo "RT_EOF"
} >> "$GITHUB_ENV"
if echo "$RT_OUTPUT" | grep -q "Schema and workbook are fully aligned"; then
echo "status=ok" >> "$GITHUB_OUTPUT"
else
echo "status=diff" >> "$GITHUB_OUTPUT"
fi
working-directory: _main_branch
continue-on-error: true
# ── Step 5: Post combined PR comment ──────────────────────────────────
- name: Post PR comment
if: steps.inbox_check.outputs.present == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const applyStatus = "${{ steps.apply.outputs.status }}";
const testStatus = "${{ steps.test.outputs.status }}";
const rtStatus = "${{ steps.roundtrip.outputs.status }}";
const applyOutput = (process.env.apply_output || "").trim();
const testOutput = (process.env.test_output || "").trim();
const rtOutput = (process.env.roundtrip_output || "").trim();
const applyFailed = applyStatus === "errors";
const testFailed = testStatus === "fail";
const rtDiff = rtStatus === "diff";
const allOk = !applyFailed && !testFailed && !rtDiff &&
applyStatus !== "" && testStatus !== "";
const sections = [];
// ── inbox_to_schema.py report (always shown) ─────────────────
if (applyOutput) {
sections.push(applyOutput, "");
} else {
sections.push(
"## 📋 Inbox vocabulary — processing report", "",
"*(No output from inbox_to_schema.py)*", ""
);
}
// ── schema validation ────────────────────────────────────────
if (testStatus) {
const icon = testStatus === "ok" ? "✅" : "❌";
const msg = testStatus === "ok"
? "All LinkML validation checks passed."
: "**LinkML validation failed.** Fix the errors and push again.";
sections.push(
`### ${icon} Schema validation (\`just test\`)`, "",
msg, "",
"<details><summary>Test output</summary>", "",
"```", testOutput.slice(0, 8000), "```",
"</details>", ""
);
}
// ── round-trip check ─────────────────────────────────────────
if (rtStatus) {
const icon = rtStatus === "ok" ? "✅" : "⚠️";
const msg = rtStatus === "ok"
? "The inbox workbook and the updated schema are fully aligned."
: "Some fields in the inbox workbook differ from the regenerated schema. "
+ "This may indicate fields that could not be automatically converted.";
sections.push(
`### ${icon} Round-trip check`, "",
msg, "",
"<details><summary>Diff output</summary>", "",
"```", rtOutput.slice(0, 4000), "```",
"</details>", ""
);
}
// ── summary ───────────────────────────────────────────────────
sections.push("---", "");
if (allOk) {
sections.push(
"✅ **All checks passed.** The schema changes have been applied to this "
+ "branch and the docs Excel has been regenerated. "
+ "A maintainer will review the diff and merge.", ""
);
} else {
sections.push(
"❌ **Some checks failed.** Fix the issues listed above, "
+ "update the workbook, and push again.", ""
);
}
const body = sections.join("\n");
// Update existing bot comment or create a new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.type === "Bot" &&
(c.body.includes("Inbox vocabulary") || c.body.includes("Excel inbox"))
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: existing.id, body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, body,
});
}
# ── Step 6: Fail explicitly after comment is posted ───────────────────
- name: Fail on errors (after comment posted)
if: >
steps.inbox_check.outputs.present == 'true' && (
steps.apply.outcome == 'failure' ||
steps.test.outputs.status == 'fail'
)
run: |
echo "::error::Inbox processing failed. See the PR comment for details."
exit 1
# ── Step 7: Commit schema changes back to the PR branch ───────────────
- name: Commit schema changes and remove inbox file
if: >
steps.inbox_check.outputs.present == 'true' &&
steps.apply.outcome != 'failure' &&
steps.apply.outputs.status != 'errors' &&
steps.test.outputs.status == 'ok'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Copy modified schema YAMLs from _main_branch/ back to PR branch
mkdir -p src/coremeta4cat/schema/
cp -f _main_branch/src/coremeta4cat/schema/*.yaml \
src/coremeta4cat/schema/
# Copy regenerated docs Excel
mkdir -p docs/assets/
cp -f _main_branch/docs/assets/coremeta4cat_vocabulary.xlsx \
docs/assets/coremeta4cat_vocabulary.xlsx
# Stage schema YAMLs, docs Excel, and remove inbox file
git add src/coremeta4cat/schema/*.yaml
git add docs/assets/coremeta4cat_vocabulary.xlsx
git rm --force "${{ env.INBOX_FILE }}"
# [skip ci] prevents this bot commit from re-triggering CI loops
git commit -m \
"ci: apply inbox vocabulary changes and regenerate Excel [skip ci]"
git push