Repository navigation
208 lines (192 loc) · 8.15 KB
/
Copy pathprovision-learning-rooms.yml
File metadata and controls
208 lines (192 loc) · 8.15 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
name: Provision Learning Rooms (owned, GitHub-native)
# Owned, GitHub-native replacement for GitHub Classroom provisioning.
# Implements SPEC.md section 7. The roster of record lives in a private admin
# repository; this workflow syncs enrollment intake issues into the roster,
# idempotently provisions every pending or failed learner, and commits the
# updated roster plus the provisioning log back.
#
# Nothing here is destructive: re-running heals partial failures and never
# corrupts an existing student repository (idempotent on github_handle+cohort).
#
# Triggers:
# - repository_dispatch (provision-learning-rooms): fired by the registration
# workflow when a new enrollment lands, so learners get rooms in minutes.
# - schedule: a healing sweep a few times per day that re-syncs intake and
# retries any failed entries.
# - workflow_dispatch: manual runs, with an optional dry-run.
on:
workflow_dispatch:
inputs:
dry_run:
description: List who would be provisioned without making changes
type: boolean
default: false
repository_dispatch:
types: [provision-learning-rooms]
schedule:
# Healing sweep. Event-driven provisioning (repository_dispatch above)
# covers the fast path, so the cron only needs to catch stragglers.
- cron: '17 */6 * * *'
# Least privilege: contents read to run the scripts; issues write only for the
# watchdog alert issue in this repository. All student-repo writes happen via
# the GitHub App installation token configured in the provisioning environment.
permissions:
contents: read
issues: write
concurrency:
group: provision-learning-rooms
cancel-in-progress: false
jobs:
provision:
name: Provision learning rooms
runs-on: ubuntu-latest
environment: provisioning
steps:
- name: Check out workshop repo (provisioning scripts)
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check out admin roster repo
uses: actions/checkout@v4
with:
repository: ${{ vars.ADMIN_ROSTER_REPO }}
token: ${{ secrets.PRIVATE_STUDENT_DATA_TOKEN }}
path: _roster
- name: Sync enrollment intake issues into the roster
if: ${{ !inputs.dry_run }}
run: |
node .github/scripts/provisioning/sync-intake-to-roster.js \
--roster _roster/roster.json
env:
INTAKE_REPO: ${{ vars.ADMIN_ROSTER_REPO }}
INTAKE_TOKEN: ${{ secrets.PRIVATE_STUDENT_DATA_TOKEN }}
PROVISIONING_COHORT_ID: ${{ vars.PROVISIONING_COHORT_ID }}
GITHUB_API_URL: ${{ github.api_url }}
- name: Count pending work
id: pending
run: |
count=$(node -e "
const fs = require('node:fs');
const { parseRoster, entriesToProvision } = require('./.github/scripts/provisioning/roster');
const roster = parseRoster(fs.readFileSync('_roster/roster.json', 'utf8'));
console.log(entriesToProvision(roster).length);
")
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "$count learner(s) pending or failed."
- name: Provision (dry run)
if: ${{ inputs.dry_run }}
run: |
node .github/scripts/provisioning/provision-cli.js \
--roster _roster/roster.json --dry-run
env:
LEARNING_ROOM_TEMPLATE_REPO: ${{ vars.LEARNING_ROOM_TEMPLATE_REPO }}
PROVISIONING_STUDENT_OWNER: ${{ vars.PROVISIONING_STUDENT_OWNER }}
- name: Provision learners
if: ${{ !inputs.dry_run && steps.pending.outputs.count != '0' }}
run: |
node .github/scripts/provisioning/provision-cli.js \
--roster _roster/roster.json \
--log _roster/provisioning-log.json
env:
PROVISIONING_MODE: ${{ vars.PROVISIONING_MODE }}
LEARNING_ROOM_TEMPLATE_REPO: ${{ vars.LEARNING_ROOM_TEMPLATE_REPO }}
PROVISIONING_STUDENT_OWNER: ${{ vars.PROVISIONING_STUDENT_OWNER }}
GITHUB_API_URL: ${{ github.api_url }}
PROVISIONING_APP_ID: ${{ secrets.PROVISIONING_APP_ID }}
PROVISIONING_APP_PRIVATE_KEY: ${{ secrets.PROVISIONING_APP_PRIVATE_KEY }}
# Optional: discovered from the App installations when unset.
PROVISIONING_APP_INSTALLATION_ID: ${{ secrets.PROVISIONING_APP_INSTALLATION_ID }}
# Runs even when provisioning failed: per-learner failed states and the
# provisioning log are exactly what a facilitator needs to see.
- name: Commit updated roster and provisioning log
if: ${{ always() && !inputs.dry_run }}
working-directory: _roster
run: |
if [ -n "$(git status --porcelain)" ]; then
git config user.name "glow-provisioning[bot]"
git config user.email "provisioning@users.noreply.github.com"
git add roster.json provisioning-log.json
git commit -m "chore(provisioning): update roster and provisioning log"
git push
else
echo "No roster changes to commit."
fi
# Watchdog (SPEC.md section 7.3): any failure must be visible to a human
# before a learner notices. Opens or updates a pinned alert issue.
- name: Open or update provisioning alert issue
if: ${{ failure() }}
uses: actions/github-script@v8
with:
script: |
const label = 'provisioning-alert';
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
color: 'b60205',
description: 'Automated alert: learning room provisioning is failing'
});
} catch (e) { /* label already exists */ }
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const open = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: label,
state: 'open',
per_page: 1
});
if (open.data.length) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: open.data[0].number,
body: `Provisioning failed again: ${runUrl}`
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Learning room provisioning is failing',
labels: [label],
body: [
'## Provisioning watchdog',
'',
'The Provision Learning Rooms workflow failed. Learners may be waiting on repositories.',
'',
`- Failed run: ${runUrl}`,
'- Runbook: admin/OWNED_PROVISIONING.md',
'',
'This issue closes automatically on the next successful run.'
].join('\n')
});
}
- name: Close provisioning alert issue on success
if: ${{ success() }}
uses: actions/github-script@v8
with:
script: |
const open = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'provisioning-alert',
state: 'open',
per_page: 10
});
for (const issue of open.data) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Provisioning succeeded; closing this alert.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'completed'
});
}