Skip to content

Commit b6d8da4

Browse files
committed
feat: add reviewed Notion officer sync
1 parent ed90796 commit b6d8da4

22 files changed

Lines changed: 2314 additions & 57 deletions

‎.agents/skills/pre-production-checklist/SKILL.md‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,18 @@ Run this gate whenever the officer roster, a form-response snapshot, profile lin
4141

4242
1. Run `npm test` and `npm run validate:officers`.
4343
2. Confirm `src/data/officersFa26Roster.json` is the only authority for cohort membership, roles, order, stable IDs, and fallback portraits.
44-
3. Confirm `src/data/officerProfilesFa26.json` contains the real Notion response page ID for provenance and only public profile fields copied from reviewed form responses. Never add email addresses, phone numbers, birthdays, or other private form answers.
45-
4. Review every unmatched submission, invalid profile link, and officer without a current response reported by the validator. An officer without a response may retain only the roster fallback; do not invent a bio or personal link.
46-
5. On the rendered page, open at least one complete profile and confirm its full bio and Website, LinkedIn, GitHub, and ORCID destinations are labeled, keyboard accessible, and correct.
47-
6. Confirm every added or replaced headshot path resolves in a clean build, and complete Gate 1 for its framing.
48-
7. Confirm the Fall 2025 and Spring 2026 archive tabs load from the tracked files under `public/officers/archive/`; a build must not require Notion, Google Sheets, or a prior production deployment.
49-
50-
This gate passes only when the published roster matches the approved cohort, current public submissions enrich the correct people, invalid or private form values cannot leak through, and all officer tabs work from a clean checkout.
44+
3. Treat Notion as private intake. Use `officers:notion:dry-run` or `officers:notion:check` before `officers:notion:write`, then inspect the resulting Git diff. Confirm the sync did not commit, push, deploy, or write back to Notion.
45+
4. Confirm `src/data/officerProfilesFa26.json` contains the real Notion response page ID for provenance and only the allowlisted public fields: source ID, submission time, full and preferred names, headshot path, bio, personal website, LinkedIn, GitHub, and ORCID. Confirm upstream database/data-source IDs and the intake window are absent from this browser-facing snapshot.
46+
5. Search the diff for email addresses, phone numbers, birthdays, scheduling information, internal notes, and unknown form properties. None may enter Git, logs, or workflow artifacts.
47+
6. Review every unmatched submission, invalid profile link, and officer without a current response reported by the validator. An officer without a response may retain only the roster fallback; do not invent a bio or personal link.
48+
7. Confirm write preparation was atomic and idempotent: dirty managed files were not overwritten, managed files were rechecked after network work, validation completed before replacement, and repeating the sync against unchanged source data creates no diff.
49+
8. Confirm every replaced portrait uses its stable versioned path, matches the recorded response provenance, contains no private metadata, and resolves in a clean build. Confirm changing an attachment on the same response is detected. Complete Gate 1 for its rendered framing.
50+
9. Confirm `public/fetched` remains untracked, no page imports from `/fetched/`, and a clean release artifact does not contain local legacy cache files.
51+
10. Confirm the Fall 2025 and Spring 2026 archive tabs load from the intentional tracked snapshots under `public/officers/archive/`; do not confuse immutable archives with duplicate current data.
52+
11. On the rendered page, open at least one complete profile and confirm its full bio and Website, LinkedIn, GitHub, and ORCID destinations are labeled, keyboard accessible, and correct.
53+
12. Confirm local, staging, and production builds remain credential-free. If the optional drift workflow changed, verify it has read-only repository permissions, passes secrets only to the check command, fails on a missing or retyped Notion property, publishes no candidate artifact, and cannot commit or deploy.
54+
55+
This gate passes only when the published roster matches the approved cohort, current public submissions enrich the correct people through a reviewed Git snapshot, invalid or private form values cannot leak through, cache and archive boundaries remain clear, and all officer tabs work from a clean credential-free checkout.
5156

5257
## Release discipline
5358

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Check officer profile drift
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "17 16 * * 1"
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: officer-profile-drift
13+
cancel-in-progress: true
14+
15+
jobs:
16+
check:
17+
environment:
18+
name: github-pages
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 10
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v5
24+
25+
- name: Set up Node
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: lts/*
29+
cache: npm
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Check reviewed officer snapshot against Notion
35+
shell: bash
36+
run: |
37+
set +e
38+
npm run officers:notion:check
39+
status=$?
40+
set -e
41+
if [[ "$status" -eq 2 ]]; then
42+
echo "::warning::Reviewed officer profile drift is waiting for maintainer review."
43+
exit 0
44+
fi
45+
exit "$status"
46+
env:
47+
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
48+
NOTION_OFFICERS_DB_ID: ${{ secrets.NOTION_OFFICERS_DB_ID }}

‎README.md‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,24 @@ Run `npm run build` for the same credential-free production build used by GitHub
1717
Fall 2026 deliberately separates approved roster decisions from submitted public profile content:
1818

1919
- `src/data/officersFa26Roster.json` owns cohort membership, roles, order, stable IDs, and fallback portraits.
20-
- `src/data/officerProfilesFa26.json` is a reviewed, public-only snapshot of the latest officer form responses.
20+
- `src/data/officerProfilesFa26.json` is a reviewed, public-only snapshot of in-window officer form responses.
21+
- `scripts/lib/officer-profile-cohorts.js` owns operational file paths and the explicit intake window; upstream database identifiers stay in protected environment secrets and out of the client bundle.
2122
- `src/data/compileOfficerDirectory.js` matches responses to the roster, keeps the newest nonblank public fields, normalizes links, and ignores unmatched people.
2223
- `public/officers/fa26/` stores stable headshot assets.
2324
- `public/officers/archive/` stores immutable Fall 2025 and Spring 2026 snapshots so historical tabs work in a clean checkout.
2425

25-
Never copy private form fields such as email, phone number, or birthday into the repository. Run:
26+
Notion is private intake, not a publishing system. Maintainers use the controlled
27+
`officers:notion:dry-run`, `officers:notion:check`, and
28+
`officers:notion:write` commands described in `scripts/README.md` to prepare a
29+
sanitized working-tree change. The `write` command does not commit, push,
30+
deploy, or write anything back to Notion. A human must review the Git diff and
31+
the staging site before promotion.
32+
33+
Each profile record in the public snapshot is limited to provenance ID,
34+
submission time, full and preferred names, headshot path, bio, personal
35+
website, LinkedIn, GitHub, and ORCID. Never copy email, phone number, birthday,
36+
scheduling data, another private form field, or an upstream database identifier
37+
into the snapshot. Run:
2638

2739
```sh
2840
npm test
@@ -36,4 +48,8 @@ The validator reports unmatched responses, invalid public links, missing assets,
3648
- Pushes to `dev` deploy the staging GitHub Pages site from the `Hilo-Hilo` staging repository.
3749
- Pushes to `main` deploy production from `CompbioAtBerkeley/compbioatberkeley.github.io`.
3850

39-
Neither build reads Notion or Google Sheets credentials. Form intake is reviewed and versioned before publication, so the same commit produces the same site in local, staging, and production environments.
51+
Neither build reads Notion or Google Sheets credentials. Form intake is
52+
reviewed and versioned before publication, so the same commit produces the same
53+
site in local, staging, and production environments. The separate weekly/manual
54+
drift workflow may read Notion with a read-only integration, but it cannot
55+
modify repository contents or publish candidate data.

‎package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"dev": "vite",
99
"test": "node --test scripts/lib/*.test.js",
1010
"validate:officers": "node scripts/validate-officer-data.js",
11+
"officers:notion:dry-run": "node scripts/sync-officer-profiles-notion.js --term fa26 --dry-run",
12+
"officers:notion:check": "node scripts/sync-officer-profiles-notion.js --term fa26 --check",
13+
"officers:notion:write": "node scripts/sync-officer-profiles-notion.js --term fa26 --write",
1114
"build": "npm run validate:officers && vite build",
1215
"build:dev": "npm run validate:officers && vite build --mode development",
1316
"build:preview": "npm run validate:officers && vite build",
35.1 KB
Loading

‎scripts/README.md‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,68 @@
11
# Officer data workflow
22

3-
Officer data is reviewed before publication. A normal build never queries Notion or Google Sheets.
3+
Officer data is reviewed before publication. Notion is private intake; the
4+
reviewed Git snapshot is the public publication boundary. A normal build never
5+
queries Notion or Google Sheets.
46

57
## Updating Fall 2026
68

79
1. Confirm the person and approved role in `src/data/officersFa26Roster.json`. The roster, not the form, controls membership and role.
8-
2. Copy only the latest public form fields into `src/data/officerProfilesFa26.json`: the real Notion response page ID as `sourceId`, submission time, full and preferred names, headshot path, bio, personal website, LinkedIn, GitHub, and ORCID.
9-
3. Save the reviewed headshot under `public/officers/fa26/` and reference that stable path from the profile snapshot.
10-
4. Do not copy email, phone, birthday, scheduling, or any other private response field.
11-
5. Run `npm test` and `npm run validate:officers`.
12-
6. Run the project pre-production checklist, including rendered portrait inspection at desktop and mobile widths.
10+
2. Preview the sanitized Notion-to-Git proposal with `npm run officers:notion:dry-run`.
11+
3. Use `npm run officers:notion:check` when a machine-readable drift result is needed. It performs the same read-only comparison and writes no candidate files.
12+
4. When ready to prepare a reviewable change, run `npm run officers:notion:write`. It may update only the public snapshot and matched portrait files in the working tree.
13+
5. Review the complete Git diff. The command never commits, pushes, deploys, or writes back to Notion.
14+
6. Run `npm test` and `npm run validate:officers`.
15+
7. Run the project pre-production checklist, including rendered portrait inspection at desktop and mobile widths.
1316

1417
Duplicate form responses are supported. `compileOfficerDirectory.js` takes the newest nonblank value per public field, so an optional blank in a newer response does not erase a valid earlier response. Names must match a canonical roster name or a deliberately reviewed alias.
1518

19+
### Command guarantees
20+
21+
- `dry-run` fetches, sanitizes, matches, and compares without changing tracked files.
22+
- `check` is also read-only: it exits cleanly when Git matches the sanitized intake, reports drift separately, and treats authentication, schema, or matching failures as errors.
23+
- `write` refuses to overwrite dirty managed files, fingerprints the snapshot and portrait tree before network work, and rechecks that state immediately before committing. It validates a complete candidate in temporary storage before atomically replacing the snapshot or a portrait.
24+
- Repeating a command against unchanged Notion content is idempotent. Portraits are streamed through a 15 MB bound, decoded, metadata-stripped, and normalized before comparison; expiring attachment query strings do not create a diff, while replacing an attachment on an existing response does.
25+
- The roster remains authoritative for membership, roles, order, stable IDs, and fallback portraits. Form answers may enrich only matched officers.
26+
27+
The explicit cohort window and managed paths live in
28+
`scripts/lib/officer-profile-cohorts.js`. The private upstream database ID is
29+
read only from `NOTION_OFFICERS_DB_ID`; it is not serialized into the public
30+
snapshot or browser bundle. Before querying responses, the sync retrieves the
31+
database schema and requires the expected names and Notion property types, so a
32+
renamed or retyped form field is an operational error rather than publishable
33+
blank content.
34+
35+
### Public privacy allowlist
36+
37+
Only these fields may enter `src/data/officerProfilesFa26.json`:
38+
39+
- `sourceId` using the real Notion response page ID for provenance
40+
- `submittedAt`
41+
- `fullName`
42+
- `preferredName`
43+
- `image`
44+
- `bio`
45+
- `personal website`
46+
- `linkedin`
47+
- `github`
48+
- `orcid`
49+
50+
Email, phone number, birthday, scheduling information, internal notes, and
51+
unknown form properties are private and must never be serialized, logged,
52+
uploaded as workflow artifacts, or copied into Git. Upstream database and data
53+
source identifiers must likewise remain outside the browser-facing snapshot.
54+
Automated checks report only the count of unmatched submissions; inspect their
55+
identities inside the private intake rather than exposing them in public Actions
56+
logs.
57+
58+
### Portrait and provenance review
59+
60+
- Store the reviewed current-term portrait at a stable, versioned path under `public/officers/fa26/`.
61+
- Confirm every profile `sourceId` and `submittedAt` traces to the intended form response.
62+
- Never save temporary Notion URLs. Every submitted portrait is decoded and re-encoded as WebP to strip private metadata; a failed or oversized download aborts before the approved snapshot is changed. Replacing a portrait on the same response is detected by normalized content comparison.
63+
- Inspect every changed portrait on desktop and mobile; verify centering through the project pre-production skill.
64+
- Do not publish an ignored importer cache or treat it as an approved snapshot.
65+
1666
## Historical data
1767

1868
The immutable Fall 2025 and Spring 2026 JSON and portrait assets live in:
@@ -30,3 +80,19 @@ These files are tracked so local, staging, and production builds contain the sam
3080
`fetch-officers-fa25.js` and `fetch-officers-notion.js` are retained only as migration helpers for creating an ignored local staging copy under `public/fetched/`. They are not package scripts, are not invoked by GitHub Actions, and their output is not publishable until it has been reviewed and moved into the versioned sources above.
3181

3282
The historical Notion importer uses `NOTION_API_KEY` and `NOTION_OFFICERS_DB_ID` when run manually. Those credentials must remain outside the repository. The website build and deploy workflows do not require them.
83+
84+
Before release, confirm `git ls-files public/fetched` is empty and that no
85+
current or historical page imports from `/fetched/`. Local files there are
86+
scratch/cache output only; because the directory is under `public/`, remove it
87+
from any local release artifact and validate from a clean checkout.
88+
89+
## Drift monitoring
90+
91+
`.github/workflows/officer-profile-drift.yml` is separate from deployment. It
92+
runs manually and weekly with `contents: read`, passes the read-only Notion
93+
credentials only to `npm run officers:notion:check`, and does not upload
94+
candidate profiles or portraits. The existing secret names live in the
95+
organization repository's protected `github-pages` environment; normal build
96+
and deploy steps do not reference them. Drift is a prompt for a maintainer to
97+
run the controlled review flow; it is never permission to commit or publish
98+
automatically.

0 commit comments

Comments
 (0)