Skip to content

Commit 9b661c1

Browse files
authored
Merge pull request #212 from Community-Access/chore/harden-learning-room-progression
chore: harden progression automation and recovery tooling
2 parents 8b167bd + 30af585 commit 9b661c1

6 files changed

Lines changed: 377 additions & 34 deletions

File tree

‎admin/qa-bundle/learning-room/.github/workflows/skills-progression.yml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ on:
1616
description: "Action (check_progress, assign_next, reset)"
1717
required: false
1818
default: "check_progress"
19+
create:
20+
types: [repository]
1921

2022
concurrency:
2123
group: skills-progression-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
@@ -162,3 +164,47 @@ jobs:
162164
} catch (error) {
163165
console.error('Error awarding achievement:', error);
164166
}
167+
168+
create-first-issue:
169+
name: Create First Challenge Issue
170+
runs-on: ubuntu-latest
171+
if: github.event_name == 'create'
172+
173+
steps:
174+
- name: Create First Issue
175+
uses: actions/github-script@v7
176+
with:
177+
script: |
178+
const issueTitle = "Welcome to Your First Challenge!";
179+
const issueBody = `
180+
## Your First Challenge
181+
Welcome to the repository! Your first challenge is to complete the following steps:
182+
- [ ] Clone this repository to your local machine.
183+
- [ ] Make a small change to the README file.
184+
- [ ] Commit and push your changes.
185+
- [ ] Open a pull request to merge your changes.
186+
187+
Once you complete these steps, close this issue to unlock the next challenge!
188+
`;
189+
190+
const { data: existingIssues } = await github.rest.issues.listForRepo({
191+
owner: context.repo.owner,
192+
repo: context.repo.repo,
193+
state: 'all',
194+
labels: 'first-issue',
195+
per_page: 100
196+
});
197+
198+
const alreadyExists = existingIssues.some((issue) => issue.title === issueTitle);
199+
if (alreadyExists) {
200+
console.log('First challenge issue already exists. Skipping creation.');
201+
return;
202+
}
203+
204+
await github.rest.issues.create({
205+
owner: context.repo.owner,
206+
repo: context.repo.repo,
207+
title: issueTitle,
208+
body: issueBody,
209+
labels: ['challenge', 'first-issue']
210+
});

‎learning-room/.github/workflows/skills-progression.yml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ on:
1616
description: "Action (check_progress, assign_next, reset)"
1717
required: false
1818
default: "check_progress"
19+
create:
20+
types: [repository]
1921

2022
concurrency:
2123
group: skills-progression-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
@@ -162,3 +164,47 @@ jobs:
162164
} catch (error) {
163165
console.error('Error awarding achievement:', error);
164166
}
167+
168+
create-first-issue:
169+
name: Create First Challenge Issue
170+
runs-on: ubuntu-latest
171+
if: github.event_name == 'create'
172+
173+
steps:
174+
- name: Create First Issue
175+
uses: actions/github-script@v7
176+
with:
177+
script: |
178+
const issueTitle = "Welcome to Your First Challenge!";
179+
const issueBody = `
180+
## Your First Challenge
181+
Welcome to the repository! Your first challenge is to complete the following steps:
182+
- [ ] Clone this repository to your local machine.
183+
- [ ] Make a small change to the README file.
184+
- [ ] Commit and push your changes.
185+
- [ ] Open a pull request to merge your changes.
186+
187+
Once you complete these steps, close this issue to unlock the next challenge!
188+
`;
189+
190+
const { data: existingIssues } = await github.rest.issues.listForRepo({
191+
owner: context.repo.owner,
192+
repo: context.repo.repo,
193+
state: 'all',
194+
labels: 'first-issue',
195+
per_page: 100
196+
});
197+
198+
const alreadyExists = existingIssues.some((issue) => issue.title === issueTitle);
199+
if (alreadyExists) {
200+
console.log('First challenge issue already exists. Skipping creation.');
201+
return;
202+
}
203+
204+
await github.rest.issues.create({
205+
owner: context.repo.owner,
206+
repo: context.repo.repo,
207+
title: issueTitle,
208+
body: issueBody,
209+
labels: ['challenge', 'first-issue']
210+
});

‎scripts/classroom/Restore-LearningRoomFiles.ps1‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ function Get-ProfilePaths {
4545
'docs/setup-guide.md'
4646
)
4747
'core-day2' = @(
48-
'docs/samples/copilot-improvement-before.md',
49-
'docs/samples/peer-review-practice.md'
48+
'docs/samples/copilot-improvement-before.md'
5049
)
5150
'automation-core' = @(
5251
'.github/workflows/pr-validation-bot.yml',
@@ -138,7 +137,7 @@ try {
138137
Invoke-CheckedCommand git @('checkout', "template/$TemplateRef", '--', $file)
139138
}
140139

141-
Invoke-CheckedCommand git @('add', '--', $selectedPaths)
140+
Invoke-CheckedCommand git (@('add', '--') + $selectedPaths)
142141

143142
$status = git status --short
144143
if (-not $status) {

‎scripts/classroom/Seed-LearningRoomChallenge.ps1‎

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,34 +30,3 @@ Invoke-CheckedCommand gh $arguments
3030

3131
Write-Host "Workflow started. Check the repository Actions tab, then verify the issue list:"
3232
Write-Host " gh issue list -R $Repository --search \"Challenge $Challenge\""
33-
[CmdletBinding()]
34-
param(
35-
[Parameter(Mandatory = $true)]
36-
[string]$Repository,
37-
38-
[ValidateRange(1, 16)]
39-
[int]$Challenge = 1,
40-
41-
[string]$Assignee = ''
42-
)
43-
44-
$ErrorActionPreference = 'Stop'
45-
46-
function Invoke-CheckedCommand {
47-
param([string]$FilePath, [string[]]$Arguments)
48-
& $FilePath @Arguments
49-
if ($LASTEXITCODE -ne 0) {
50-
throw "Command failed: $FilePath $($Arguments -join ' ')"
51-
}
52-
}
53-
54-
$fields = @('-f', "start_challenge=$Challenge")
55-
if ($Assignee) {
56-
$fields += @('-f', "assignee=$Assignee")
57-
}
58-
59-
Write-Host "Triggering student progression workflow in $Repository for Challenge $Challenge..."
60-
Invoke-CheckedCommand gh @('workflow', 'run', 'student-progression.yml', '-R', $Repository) + $fields
61-
62-
Write-Host "Workflow started. Check the repository Actions tab, then verify the issue list:"
63-
Write-Host " gh issue list -R $Repository --search \"Challenge $Challenge\""
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
[CmdletBinding()]
2+
param(
3+
[string]$StudentRepository = '',
4+
5+
[string]$ClassroomOrg = 'Community-Access-Classroom',
6+
7+
[string]$TemplateRef = 'main',
8+
9+
[ValidateSet('core-day1', 'core-day2', 'automation-core', 'all-core', 'custom')]
10+
[string]$Profile = 'all-core',
11+
12+
[string[]]$RepoPrefixes = @('you-belong-here-', 'you-can-build-this-'),
13+
14+
[switch]$OpenPullRequest,
15+
16+
[switch]$AutoMerge,
17+
18+
[switch]$AdminMerge,
19+
20+
[switch]$DryRun,
21+
22+
[switch]$Force
23+
)
24+
25+
$ErrorActionPreference = 'Stop'
26+
27+
function Invoke-CheckedCommand {
28+
param([string]$FilePath, [string[]]$Arguments)
29+
30+
& $FilePath @Arguments
31+
if ($LASTEXITCODE -ne 0) {
32+
throw "Command failed: $FilePath $($Arguments -join ' ')"
33+
}
34+
}
35+
36+
function Invoke-GhJson {
37+
param([string[]]$Arguments)
38+
39+
$json = & gh @Arguments
40+
if ($LASTEXITCODE -ne 0) {
41+
throw "Command failed: gh $($Arguments -join ' ')"
42+
}
43+
44+
if (-not $json) {
45+
return $null
46+
}
47+
48+
return $json | ConvertFrom-Json
49+
}
50+
51+
function Test-RepositoryHasActivity {
52+
param([string]$Repository)
53+
54+
$activity = [ordered]@{
55+
HasNonMainBranch = $false
56+
HasOpenPR = $false
57+
}
58+
59+
$branches = Invoke-GhJson -Arguments @('api', "repos/$Repository/branches?per_page=100")
60+
if ($branches) {
61+
$nonMain = @($branches | Where-Object { $_.name -ne 'main' })
62+
if ($nonMain.Count -gt 0) {
63+
$activity.HasNonMainBranch = $true
64+
}
65+
}
66+
67+
$openPrs = Invoke-GhJson -Arguments @('pr', 'list', '-R', $Repository, '--state', 'open', '--json', 'number')
68+
if ($openPrs) {
69+
if (@($openPrs).Count -gt 0) {
70+
$activity.HasOpenPR = $true
71+
}
72+
}
73+
74+
return [PSCustomObject]$activity
75+
}
76+
77+
function Get-TargetRepositories {
78+
param(
79+
[string]$SingleRepository,
80+
[string]$Organization,
81+
[string[]]$Prefixes
82+
)
83+
84+
if ($SingleRepository) {
85+
return @($SingleRepository)
86+
}
87+
88+
$repos = Invoke-GhJson -Arguments @('repo', 'list', $Organization, '--limit', '500', '--json', 'name,nameWithOwner,isPrivate')
89+
if (-not $repos) {
90+
return @()
91+
}
92+
93+
$targets = @()
94+
foreach ($repo in $repos) {
95+
if (-not $repo.isPrivate) {
96+
continue
97+
}
98+
99+
foreach ($prefix in $Prefixes) {
100+
if ($repo.name.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
101+
$targets += $repo.nameWithOwner
102+
break
103+
}
104+
}
105+
}
106+
107+
return $targets
108+
}
109+
110+
Write-Host 'Checking GitHub CLI authentication...'
111+
Invoke-CheckedCommand gh @('auth', 'status', '-h', 'github.com')
112+
113+
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
114+
$restoreScript = Join-Path $scriptDir 'Restore-LearningRoomFiles.ps1'
115+
if (-not (Test-Path -LiteralPath $restoreScript -PathType Leaf)) {
116+
throw "Required script not found: $restoreScript"
117+
}
118+
119+
$targets = Get-TargetRepositories -SingleRepository $StudentRepository -Organization $ClassroomOrg -Prefixes $RepoPrefixes
120+
if (-not $targets -or $targets.Count -eq 0) {
121+
Write-Host 'No target repositories found. Nothing to do.'
122+
return
123+
}
124+
125+
Write-Host "Found $($targets.Count) target repository/repositories."
126+
127+
$summary = @()
128+
129+
foreach ($repo in $targets) {
130+
Write-Host ''
131+
Write-Host "Processing $repo"
132+
133+
$row = [ordered]@{
134+
Repository = $repo
135+
Status = 'unknown'
136+
Details = ''
137+
}
138+
139+
try {
140+
if (-not $Force) {
141+
$activity = Test-RepositoryHasActivity -Repository $repo
142+
if ($activity.HasNonMainBranch -or $activity.HasOpenPR) {
143+
$reasons = @()
144+
if ($activity.HasNonMainBranch) { $reasons += 'non-main branches exist' }
145+
if ($activity.HasOpenPR) { $reasons += 'open PRs exist' }
146+
147+
$row.Status = 'skipped'
148+
$row.Details = ($reasons -join '; ')
149+
Write-Host "Skipping due to activity: $($row.Details)"
150+
$summary += [PSCustomObject]$row
151+
continue
152+
}
153+
}
154+
155+
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
156+
$branchName = "recovery/template-sync-$stamp"
157+
158+
$restoreParams = @{
159+
StudentRepository = $repo
160+
Profile = $Profile
161+
TemplateRef = $TemplateRef
162+
BranchName = $branchName
163+
CommitMessage = 'chore: sync learning-room template baseline'
164+
}
165+
166+
if ($OpenPullRequest -or $AutoMerge) {
167+
$restoreParams.OpenPullRequest = $true
168+
}
169+
170+
if ($DryRun) {
171+
$row.Status = 'dry-run'
172+
$row.Details = "Would run restore on branch $branchName"
173+
Write-Host $row.Details
174+
$summary += [PSCustomObject]$row
175+
continue
176+
}
177+
178+
& $restoreScript @restoreParams
179+
if ($LASTEXITCODE -ne 0) {
180+
throw "Restore script failed for $repo"
181+
}
182+
183+
if ($AutoMerge) {
184+
$prNumber = & gh pr list -R $repo --head $branchName --state open --json number --jq '.[0].number'
185+
if ($LASTEXITCODE -ne 0) {
186+
throw "Could not query PR number for $repo"
187+
}
188+
189+
if (-not $prNumber) {
190+
$row.Status = 'updated'
191+
$row.Details = 'No open recovery PR found after restore (possibly no diff)'
192+
Write-Host $row.Details
193+
$summary += [PSCustomObject]$row
194+
continue
195+
}
196+
197+
$mergeArgs = @('pr', 'merge', $prNumber, '-R', $repo, '--squash', '--delete-branch')
198+
if ($AdminMerge) {
199+
$mergeArgs += '--admin'
200+
}
201+
202+
Invoke-CheckedCommand gh $mergeArgs
203+
$row.Status = 'merged'
204+
$row.Details = "Merged PR #$prNumber"
205+
Write-Host $row.Details
206+
}
207+
else {
208+
$row.Status = 'updated'
209+
$row.Details = 'Restore completed'
210+
Write-Host $row.Details
211+
}
212+
}
213+
catch {
214+
$row.Status = 'failed'
215+
$row.Details = $_.Exception.Message
216+
Write-Warning $row.Details
217+
}
218+
219+
$summary += [PSCustomObject]$row
220+
}
221+
222+
Write-Host ''
223+
Write-Host 'Summary'
224+
$summary | Format-Table -AutoSize

0 commit comments

Comments
 (0)