diff --git a/.github/README.md b/.github/README.md index dcc2684d..440a1633 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,3 +1,11 @@ + +**Table: Main repository workflows** + +**Table: Learning Room workflow equivalents** + +**Table: Main repository scripts and their purposes** + +**Table: Data files in the repository** # Repository Automation This directory contains workflows, scripts, and data for two purposes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 263a53df..0c9f6c4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,5 @@ + +**Table: Keyboard shortcut for focusing search in different screen readers** # Contributing to This Workshop Repository Thank you for helping improve this workshop. Whether you are a participant who found a typo, someone who wants to add a new exercise, or an educator adapting these materials for your own community - your contribution is meaningful and welcome. diff --git a/SECURITY.md b/SECURITY.md index e510f78d..b606bf9b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,9 @@ This repository contains **educational curriculum and documentation only** - no production software, application servers, or APIs. There are no versioned software releases to patch. + +**Table: Supported content types and their source** + | Content | Supported | |---------|-----------| | Curriculum documentation (docs/) | Current main branch | diff --git a/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md b/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md index 4eb253c6..5af5e2db 100644 --- a/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md +++ b/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md @@ -33,7 +33,8 @@ Out of scope: ## Canonical Source Files Used by This Runbook -The following table lists the source files this runbook consolidates. + +**Table: Source files consolidated by this runbook** | Area | Source file | |---|---| @@ -49,40 +50,46 @@ The following table lists the source files this runbook consolidates. | Grading criteria | [classroom/grading-guide.md](../classroom/grading-guide.md) | | Release gate baseline | [GO-LIVE-QA-GUIDE.md](../GO-LIVE-QA-GUIDE.md) | +**Table: QA validation checkpoints for registration and classroom automation** +**Table: Student journey checkpoints and expected artifacts** +**Table: Label color and purpose for registration automation** +**Table: Screen reader options for workshop setup** +**Table: Accessibility improvements for screen reader users** + ## Required Accounts, Access, and Tools Complete this section before Phase 1. -- Facilitator admin account with Owner or Admin access to the workshop organization and classroom organization. -- Dedicated non-admin test student account for acceptance and full challenge walkthrough. -- Access to [classroom.github.com](https://classroom.github.com). -- Access to repository settings for secrets and variables. +- Facilitator admin account (`accesswatch`) with Owner access to both `Community-Access` and `Community-Access-Classroom`. +- Dedicated non-admin test student account for acceptance and full challenge walkthrough. This must be a separate GitHub account that is not an owner or member of either organization. +- Access to [classroom.github.com](https://classroom.github.com) while signed in as `accesswatch`. +- Access to repository settings for `Community-Access/git-going-with-github` (secrets and variables). - Local clone of this repository with PowerShell available. -- GitHub CLI (`gh`) installed and authenticated for optional verification commands. +- GitHub CLI (`gh`) installed and authenticated as `accesswatch` for optional verification commands. ### Critical Precondition Gates (No-Go if any fail) Complete all items below before any cohort launch actions. -- [ ] Facilitator account can access both organizations involved in operations: - - [ ] `Community-Access` - - [ ] `Community-Access-Classroom` (or your classroom org) -- [ ] Facilitator account has verified email and can create/edit Classroom assignments. +- [ ] Facilitator account `accesswatch` can access both organizations: + - [ ] `Community-Access` (the workshop and code repository organization) + - [ ] `Community-Access-Classroom` (the GitHub Classroom organization where student repos are created) +- [ ] `accesswatch` has a verified email address on its GitHub account and can create and edit Classroom assignments at [classroom.github.com](https://classroom.github.com). - [ ] Dedicated non-admin test student account exists and can accept invites. -- [ ] `gh auth status` succeeds for facilitator account in local terminal. -- [ ] Template repository exists and is set as template repo: - - [ ] `Community-Access/learning-room-template` -- [ ] Template repository Actions settings allow required automation behavior: - - [ ] Actions enabled - - [ ] `GITHUB_TOKEN` default workflow permissions include write where required - - [ ] `Allow GitHub Actions to create and approve pull requests` enabled +- [x] `gh auth status` succeeds for `accesswatch` in local terminal. +- [x] Template repository exists and is set as template repo: + - [x] `Community-Access/learning-room-template` +- [x] Template repository Actions settings allow required automation behavior: + - [x] Actions enabled + - [x] `GITHUB_TOKEN` default workflow permissions include write where required + - [x] `Allow GitHub Actions to create and approve pull requests` enabled - [ ] Registration automation settings are correct when using registration-to-classroom handoff: - - [ ] Secret: `CLASSROOM_ORG_ADMIN_TOKEN` - - [ ] Variables: `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` -- [ ] Registration entry configuration exists and is valid: - - [ ] Issue form template `workshop-registration.yml` exists - - [ ] Required labels exist: `registration`, `duplicate`, `waitlist` -- [ ] Facilitator can open `classroom.github.com`, view target classroom org, and create assignments. + - [ ] Secret `CLASSROOM_ORG_ADMIN_TOKEN` is set in `Community-Access/git-going-with-github` + - [ ] Variables `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` are set in `Community-Access/git-going-with-github` +- [x] Registration entry configuration exists and is valid: + - [x] Issue form template `workshop-registration.yml` exists + - [x] Required labels exist: `registration`, `duplicate`, `waitlist` +- [ ] While signed in as `accesswatch`, opening [classroom.github.com](https://classroom.github.com) shows the `Community-Access-Classroom` classroom organization. If any precondition fails, stop and resolve before proceeding. @@ -92,13 +99,17 @@ Use this section when you need literal setup steps (not only validation checks). #### A. Confirm facilitator account and organization access -1. Sign in as the facilitator account on github.com. -2. Open your profile menu, then Your organizations. -3. Confirm both organizations are visible and accessible: - - `Community-Access` - - `Community-Access-Classroom` (or your classroom org) -4. Open both org pages and confirm you can view repositories and settings areas you are expected to manage. -5. Optional CLI verification from repository root: +You are performing all steps below as `accesswatch`. If you are currently signed in to GitHub as a different account, sign out first and sign in as `accesswatch` before continuing. + +1. Go to [github.com](https://github.com) and confirm the top-right avatar shows `accesswatch`. +2. Open the avatar menu, then select **Your organizations**. +3. Confirm both of the following organizations appear in the list: + - `Community-Access` -- the main workshop repository organization + - `Community-Access-Classroom` -- the GitHub Classroom organization where student repos are created + - If either is missing, do not proceed. Contact the org owner to ensure `accesswatch` has Owner-level membership in both. +4. Click into `Community-Access` and open the **Settings** tab. Confirm you can see the full settings sidebar (Members, Actions, Secrets, etc.). If Settings is not visible, `accesswatch` does not have Owner access and you cannot proceed. +5. Click into `Community-Access-Classroom` and open its **Settings** tab. Confirm the same. +6. Optional CLI verification from the local repository root: ```powershell gh auth status -h github.com @@ -106,28 +117,101 @@ gh repo view Community-Access/git-going-with-github gh repo view Community-Access/learning-room-template ``` +Expected output: each `gh repo view` command should return repository metadata without an error. If you see "Could not resolve to a Repository", `accesswatch` does not have the required access. + +Why this matters: + +- All downstream setup steps operate against `Community-Access/git-going-with-github` and `Community-Access-Classroom`. If the account does not have Owner access to both, secrets, variables, and classroom automation cannot be configured. + +#### A.1 Create GitHub Classroom assignments + +Do this before section B. You need both assignment URLs in hand before you can fill in the repository variables. + +You must be signed in to GitHub as `accesswatch` for the following steps. + +1. Go to [classroom.github.com](https://classroom.github.com). +2. You will see a list of classrooms. Select the classroom named for this cohort that is linked to the `Community-Access-Classroom` organization. The organization name appears below the classroom name on the card. + - If no classroom exists yet, select **New classroom**, then choose `Community-Access-Classroom` as the organization. Name the classroom using the format `Git Going - [Cohort Name] - [Month Year]` (for example, `Git Going - May 2026`). +3. Inside the classroom, select **New assignment**. +4. Create the Day 1 assignment: + - **Title**: `You Belong Here` + - **Individual or group**: Individual + - **Repository visibility**: Private + - **Template repository**: `Community-Access/learning-room-template` (search for it by name in the template field) + - **Grant students admin access**: No + - **Enable feedback pull requests**: Yes + - Paste the Day 1 assignment description from [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) + - Add autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- Day 1 requires exactly 4 tests totaling 50 points + - Select **Create assignment** +5. After saving, the assignment page shows an invite link at the top labeled something like **Invite link**. It will be in the format `https://classroom.github.com/a/`. Copy this full URL and paste it somewhere safe (for example, a scratch notepad). This is your `CLASSROOM_DAY1_ASSIGNMENT_URL`. +6. Repeat for the Day 2 assignment: + - **Title**: `You Can Build This` + - Same base settings as Day 1 + - Paste description from [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) + - Add Day 2 autograding: exactly 6 tests totaling 75 points + - Copy the resulting invite URL. This is your `CLASSROOM_DAY2_ASSIGNMENT_URL`. +7. Keep both URLs available. You will paste them into repository variables in section B step 4. + Why this matters: -- All downstream setup fails if the facilitator identity is not correctly scoped. +- The repository variables `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL` cannot be filled in until the assignments exist and their invite URLs are known. The short code in the URL is unique to each assignment and is not predictable in advance. #### B. Configure registration automation key and variables Repository target: `Community-Access/git-going-with-github` -1. Open repository Settings. -2. Open Secrets and variables, then Actions. -3. Open the Secrets tab and create or update secret: - - `CLASSROOM_ORG_ADMIN_TOKEN` -4. Open the Variables tab and create or update variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -5. Re-open each entry and confirm values have no leading or trailing spaces. -6. Run one registration test and confirm welcome comment contains assignment links when variables are set. +You must be signed in as `accesswatch` for all steps in this section. + +**Step B.1 -- Generate the personal access token (PAT)** + +The `CLASSROOM_ORG_ADMIN_TOKEN` secret must be a GitHub personal access token generated by `accesswatch` (or another Owner-level account for `Community-Access-Classroom`). This token is what allows the registration workflow to invite students to the `Community-Access-Classroom` organization automatically. + +1. While signed in as `accesswatch`, go to [github.com/settings/tokens](https://github.com/settings/tokens). +2. Select **Generate new token**, then select **Generate new token (classic)**. + - Do not use fine-grained tokens for this purpose. The `admin:org` scope is only available on classic tokens. +3. In the **Note** field enter a descriptive name such as `Community-Access registration automation`. +4. In the **Expiration** field, set a date that covers your cohort timeline plus a buffer (for example, 90 days). +5. Under **Select scopes**, check `admin:org`. This is the only scope required. It gives the token permission to list and create organization invitations for `Community-Access-Classroom`. +6. Scroll to the bottom and select **Generate token**. +7. GitHub will display the token exactly once immediately after generation. It begins with `ghp_`. Copy it now and paste it somewhere safe (a local scratch notepad, not a repository file). You will not be able to view it again. +8. Do not close the token page until you have completed section B.2 and confirmed the secret was saved. + +**Step B.2 -- Add the token as a repository secret** + +1. Go to [github.com/Community-Access/git-going-with-github/settings/secrets/actions](https://github.com/Community-Access/git-going-with-github/settings/secrets/actions). +2. Select **New repository secret**. +3. In the **Name** field, enter exactly: `CLASSROOM_ORG_ADMIN_TOKEN` + - Capitalization and underscores must match exactly. +4. In the **Secret** field, paste the token you copied in step B.1. +5. Select **Add secret**. +6. Re-open the secret entry and confirm the name shows `CLASSROOM_ORG_ADMIN_TOKEN`. GitHub does not display the value again, but confirming the name is correct is sufficient. + +**Step B.3 -- Add the repository variables** + +1. At the same settings page, select the **Variables** tab (next to Secrets). +2. Select **New repository variable** for each of the following. Add them one at a time. + + Variable 1: + - **Name**: `CLASSROOM_ORG` + - **Value**: `Community-Access-Classroom` + - This is the exact GitHub organization name where students are invited. The capitalization and hyphens must match exactly. + + Variable 2: + - **Name**: `CLASSROOM_DAY1_ASSIGNMENT_URL` + - **Value**: paste the Day 1 invite URL you copied in section A.1 step 5 (format: `https://classroom.github.com/a/`) + + Variable 3: + - **Name**: `CLASSROOM_DAY2_ASSIGNMENT_URL` + - **Value**: paste the Day 2 invite URL you copied in section A.1 step 6 (format: `https://classroom.github.com/a/`) + +3. After adding all three, re-open each variable entry and confirm: + - The name is exactly as listed above (no typos, no extra characters). + - The value has no leading or trailing spaces. Paste into a plain text editor first if you are unsure, and trim whitespace before re-pasting. +4. Run one registration test and confirm welcome comment contains assignment links. Why this matters: -- These values drive invite and assignment-link injection in registration responses. +- These values drive invite and assignment-link injection in registration responses. A single typo in the org name or a trailing space in a variable value will silently break automation without a clear error message. #### C. Configure template repository Actions permissions @@ -353,32 +437,73 @@ Pass criteria: Goal: enable automatic org invite and assignment-link injection in registration confirmation comments. -1. In repository settings, configure secret: - - `CLASSROOM_ORG_ADMIN_TOKEN` -2. In repository settings, configure variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -3. Re-open each value and verify no leading or trailing spaces. +The fastest path is `Initialize-WorkshopSetup.ps1`, which sets the secret, all three variables, verifies labels, and runs template prep in a single command. See the setup script section below. + +**Using the setup script (recommended):** + +```powershell +scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere +``` + +The script will: +- Prompt you if the PAT is missing or invalid +- Resolve Day 1 and Day 2 assignment URLs automatically from the GitHub Classroom API (if assignments exist in the `GIT Going with Github` classroom) +- Set `CLASSROOM_ORG_ADMIN_TOKEN` secret and all three variables in `Community-Access/git-going-with-github` +- Verify all three required labels exist, creating any that are missing +- Confirm read-back values have no leading or trailing spaces +- Run `Prepare-LearningRoomTemplate.ps1` and `Test-LearningRoomTemplate.ps1` unless skipped + +**If running manually instead:** + +1. Generate a classic PAT with `admin:org` scope at [github.com/settings/tokens](https://github.com/settings/tokens) (see section B of the setup steps above for exact steps). +2. Go to [github.com/Community-Access/git-going-with-github/settings/secrets/actions](https://github.com/Community-Access/git-going-with-github/settings/secrets/actions). +3. Create secret `CLASSROOM_ORG_ADMIN_TOKEN` with the PAT value. +4. On the Variables tab, create: + - `CLASSROOM_ORG` = `Community-Access-Classroom` + - `CLASSROOM_DAY1_ASSIGNMENT_URL` = invite URL from assignment A.1 step 5 + - `CLASSROOM_DAY2_ASSIGNMENT_URL` = invite URL from assignment A.1 step 6 +5. Re-open each value and verify no leading or trailing spaces. Pass criteria: - Secret and variables are present with correct values. +- `Initialize-WorkshopSetup.ps1` reported no failures, or manual verification confirms all values. - Configuration aligns with [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md). ### Step 0.3 Registration deployment smoke check -Goal: validate deployed registration system can execute at least one full workflow run. +Goal: validate the deployed registration system and site are working before manual QA begins. + +Use `Test-RegistrationPage.ps1` to run this check. The script validates the Pages site, the REGISTER page, the issue form template, required labels, and workflow state. With `-RunLiveTest` it also submits a real test registration issue, waits for the workflow, and verifies the welcome comment. -1. Submit one test registration issue from a non-admin test account. -2. Confirm `Registration - Welcome & CSV Export` workflow completes. -3. Confirm welcome comment posts and `registration` label is applied. -4. If classroom automation is configured, confirm org invite status and assignment links appear. +**Static checks only (site, config, labels, workflow state):** + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 +``` + +**Full live end-to-end test including issue submission:** + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest +``` + +What the script checks: + +1. HTTP GET to `https://community-access.org/git-going-with-github/` returns 200. +2. HTTP GET to `https://community-access.org/git-going-with-github/REGISTER` returns 200 and contains expected content. +3. `workshop-registration.yml` exists in `.github/ISSUE_TEMPLATE/`. +4. Labels `registration`, `duplicate`, and `waitlist` exist. +5. `registration.yml` workflow is active and has a recent successful run. +6. (Live test only) Test issue is submitted, workflow completes, welcome comment posts with assignment links, `registration` label is applied. + +Cleanup limitation: the test issue is closed and locked by the script. It **cannot be deleted via the GitHub API**. If deletion is needed, a repository admin must delete it manually from the Issues tab after this run. Pass criteria: -- Registration workflow executes successfully end to end. -- Output comment/labels match deployed configuration. +- `Test-RegistrationPage.ps1` exits with no failures. +- Live test (if run) confirms workflow executes end to end. +- Output comment and labels match deployed configuration. ## Phase 1 - Registration System QA (Admin Side) @@ -396,19 +521,20 @@ Pass criteria: ### Step 2. Configure registration automation for classroom handoff -Use [REGISTRATION-QUICKSTART.md](REGISTRATION-QUICKSTART.md) for fast entry and [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md) for full details. +If `Initialize-WorkshopSetup.ps1` was run successfully in Phase 0, this step is already complete. Re-verify with: + +```powershell +gh secret list -R Community-Access/git-going-with-github +gh variable list -R Community-Access/git-going-with-github +``` + +Expected output: `CLASSROOM_ORG_ADMIN_TOKEN` appears in secrets, and `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` appear in variables. -1. Create or verify an admin token that can manage organization invitations. -2. In repository settings, add secret `CLASSROOM_ORG_ADMIN_TOKEN`. -3. In repository settings, set variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -4. Save and re-open each setting to confirm there are no leading or trailing spaces. +If any are missing, run `Initialize-WorkshopSetup.ps1` again or follow the manual steps in section B of the setup instructions. For full reference, see [REGISTRATION-QUICKSTART.md](REGISTRATION-QUICKSTART.md) and [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md). Pass criteria: -- Secret exists and is scoped correctly. -- All 3 variables exist and values are correct. +- `CLASSROOM_ORG_ADMIN_TOKEN` secret is present. +- All 3 variables are present with correct values and no leading or trailing spaces. ### Step 3. Execute registration happy-path test @@ -557,63 +683,113 @@ Use full end-to-end mode when preparing major cohort launches or after significa ### Step 10. Create classroom and import roster -Use [classroom/README.md](../classroom/README.md) Steps 1 and 2. +GitHub Classroom assignment creation has **no write API**. All classroom creation and assignment setup must be done through the browser at [classroom.github.com](https://classroom.github.com) while signed in as `accesswatch`. -1. Create a new classroom in `Community-Access`. -2. Name it using `Git Going - [Cohort Name] - [Month Year]`. -3. Import roster using [classroom/roster-template.csv](../classroom/roster-template.csv). -4. Confirm test student appears in roster. +Note: a classroom already exists for this repository (`GIT Going with Github`, classroom id 322783, linked to `Community-Access-Classroom`). Unless starting a completely new classroom, skip classroom creation and go directly to assignment creation in Steps 11 and 12. + +**If you do need a new classroom:** + +1. Go to [classroom.github.com](https://classroom.github.com) as `accesswatch`. +2. Select **New classroom** and choose `Community-Access-Classroom` as the organization. +3. Name it `Git Going - [Cohort Name] - [Month Year]` (for example, `Git Going - May 2026`). +4. Import roster from [classroom/roster-template.csv](../classroom/roster-template.csv) on the Roster tab. +5. Add `accesswatch-student` to the roster as the test student. + +**To verify the existing classroom is accessible:** + +```powershell +gh api /classrooms --jq '.[] | {id, name, url}' +``` Pass criteria: -- Classroom exists and is accessible to facilitators. -- Roster import succeeds with expected usernames. +- Classroom exists in `Community-Access-Classroom` and is accessible to `accesswatch`. +- Roster includes `accesswatch-student` (or the designated test student username). ### Step 11. Create Day 1 assignment exactly -Use [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) and [admin/classroom/day1-assignment-copy-paste.md](classroom/day1-assignment-copy-paste.md). +See section A.1 of the setup steps above for full navigation instructions. + +Use [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) and [admin/classroom/day1-assignment-copy-paste.md](classroom/day1-assignment-copy-paste.md) for the exact title, description, and autograding entries. + +**To check if the Day 1 assignment already exists:** + +```powershell +gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}' +``` + +If the assignment exists and its `invite_link` is already in the `CLASSROOM_DAY1_ASSIGNMENT_URL` variable, skip to Step 12. -1. Create assignment with title `You Belong Here`. -2. Set type `Individual`. -3. Set visibility `Private`. -4. Select template `Community-Access/learning-room-template`. -5. Set `Grant students admin access` to `No`. -6. Set `Enable feedback pull requests` to `Yes`. -7. Paste Day 1 assignment description content. -8. Add Day 1 autograding entries from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md). -9. Confirm Day 1 has 4 tests and total 50 points. -10. Save assignment and copy invite link. +**If creating from scratch:** + +1. Go to [classroom.github.com](https://classroom.github.com) as `accesswatch` and open the `GIT Going with Github` classroom. +2. Select **New assignment**. +3. Title: `You Belong Here` (exact match required) +4. Type: Individual, Visibility: Private +5. Template: `Community-Access/learning-room-template` +6. Grant students admin access: No +7. Enable feedback pull requests: Yes +8. Paste description from [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) +9. Add autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- Day 1 requires exactly 4 tests totaling 50 points +10. Save and copy the invite URL from the assignment page +11. If `Initialize-WorkshopSetup.ps1` has not been run yet, paste the URL into `CLASSROOM_DAY1_ASSIGNMENT_URL`. If it has been run, re-run it to pick up the new URL automatically. Pass criteria: -- Day 1 settings match source files exactly. -- Test count and points are correct. -- Feedback pull request is enabled and visible in assignment configuration. +- Day 1 assignment exists with title `You Belong Here`. +- Test count is 4, total points is 50. +- Feedback pull request is enabled. +- `CLASSROOM_DAY1_ASSIGNMENT_URL` variable matches the assignment invite link. ### Step 12. Create Day 2 assignment exactly -Use [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) and [admin/classroom/day2-assignment-copy-paste.md](classroom/day2-assignment-copy-paste.md). +Same process as Step 11. Check first whether it already exists: + +```powershell +gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}' +``` -1. Create assignment with title `You Can Build This`. -2. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled). -3. Paste Day 2 assignment description content. -4. Add Day 2 autograding entries from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md). -5. Confirm Day 2 has 6 tests and total 75 points. -6. Save assignment and copy invite link. +If the assignment exists and its `invite_link` is already in `CLASSROOM_DAY2_ASSIGNMENT_URL`, skip ahead. + +**If creating from scratch:** + +1. In the `GIT Going with Github` classroom, select **New assignment**. +2. Title: `You Can Build This` (exact match required) +3. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled) +4. Template: `Community-Access/learning-room-template` +5. Paste description from [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) +6. Add Day 2 autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- 6 tests totaling 75 points +7. Save and copy the invite URL +8. Update `CLASSROOM_DAY2_ASSIGNMENT_URL` or re-run `Initialize-WorkshopSetup.ps1` to pick it up automatically Pass criteria: -- Day 2 settings match source files exactly. -- Test count and points are correct. -- Feedback pull request is enabled and visible in assignment configuration. +- Day 2 assignment exists with title `You Can Build This`. +- Test count is 6, total points is 75. +- Feedback pull request is enabled. +- `CLASSROOM_DAY2_ASSIGNMENT_URL` variable matches the assignment invite link. ### Step 13. Connect assignment URLs back to registration automation -1. Add or update repository variables: - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -2. Re-run one registration test issue (new test account or controlled case). -3. Confirm both links appear in welcome comment. +If `Initialize-WorkshopSetup.ps1` was run after the assignments were created, the variables are already set. Verify with: + +```powershell +gh variable list -R Community-Access/git-going-with-github +``` + +If either URL variable is missing or stale, re-run the setup script. It will resolve URLs directly from the Classroom API: + +```powershell +scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere -SkipTemplatePrepare -SkipTemplateValidate +``` + +Then re-run the registration live test to confirm both URLs appear in the welcome comment: + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest +``` Pass criteria: -- Registration confirmation comment now includes both assignment URLs. +- `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL` are set and match assignment invite links. +- Registration confirmation comment includes both assignment URLs. +- `Test-RegistrationPage.ps1 -RunLiveTest` exits with no failures. ## Phase 4 - Test Student Acceptance and Seeding (Bridge from Admin to Student) @@ -633,20 +809,34 @@ Pass criteria: ### Step 15. Seed initial challenges and peer simulation -Run commands from repository root. +The test student account is `accesswatch-student`. GitHub Classroom generates repository names from the assignment slug and the student username. For the default assignment titles, the repository names will be: + +- Day 1: `Community-Access-Classroom/you-belong-here-accesswatch-student` +- Day 2: `Community-Access-Classroom/you-can-build-this-accesswatch-student` + +Confirm the exact repository names first: ```powershell -scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -Challenge 1 -Assignee test-student -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -StudentUsername test-student -scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -Challenge 10 -Assignee test-student -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -StudentUsername test-student +gh repo list Community-Access-Classroom --json name --jq '.[].name' | Select-String accesswatch-student ``` -If you use different repository names, replace values accordingly. +Then seed (replace repository slugs with the confirmed names if different): + +```powershell +$day1 = 'Community-Access-Classroom/you-belong-here-accesswatch-student' +$day2 = 'Community-Access-Classroom/you-can-build-this-accesswatch-student' + +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day1 -Challenge 1 -Assignee accesswatch-student +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day1 -StudentUsername accesswatch-student +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day2 -Challenge 10 -Assignee accesswatch-student +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day2 -StudentUsername accesswatch-student +``` + +Alternatively, if `Initialize-WorkshopSetup.ps1` is run after `accesswatch-student` has accepted both invites, it will detect the repos and seed them automatically. Pass criteria: -- Challenge 1 appears in Day 1 repo. -- Challenge 10 appears in Day 2 repo. +- Challenge 1 issue appears in Day 1 repo assigned to `accesswatch-student`. +- Challenge 10 issue appears in Day 2 repo assigned to `accesswatch-student`. - Peer simulation issues and PR exist in both repos. ## Phase 5 - Curriculum Content QA (Walk every required chapter and appendix) @@ -1336,4 +1526,81 @@ Release Decision: This runbook is the operator-facing execution path that unifies registration, deployment, and end-to-end challenge QA. -It does not replace source documents. It sequences them into one practical checklist so a single facilitator can execute and validate the full system without context switching across multiple folders. \ No newline at end of file +It does not replace source documents. It sequences them into one practical checklist so a single facilitator can execute and validate the full system without context switching across multiple folders. + +## Script Reference + +The following scripts in `scripts/classroom/` are used by this runbook. Run each with `-?` or read the `.SYNOPSIS` block for full parameter documentation. + +| Script | Purpose | Automated | +|---|---|---| +| `Initialize-WorkshopSetup.ps1` | Set PAT secret, variables, labels; sync and validate template; seed test student challenges | All steps except PAT generation (browser required) | +| `Archive-CohortData.ps1` | Export cohort issues, roster, and discussions to `git-going-student-success`; reset source repo | Export and archive are automated; roster reset may require PR due branch rules | +| `Delete-RegistrationIssues.ps1` | Delete registration/duplicate/waitlist issues via GraphQL mutation | Fully automated when user has repo admin and `repo` token scope | +| `Delete-RegistrationIssues-v2.js` | Experimental Playwright UI deleter for issue cleanup | Partially automated; UI selector fragility makes this fallback-only | +| `Test-RegistrationPage.ps1` | Validate Pages site, REGISTER page, issue form, labels, workflow, and live registration flow | All steps; test issue cleanup is close+lock only (see below) | +| `Prepare-LearningRoomTemplate.ps1` | Sync `learning-room/` source into `Community-Access/learning-room-template` | Fully automated | +| `Test-LearningRoomTemplate.ps1` | Create smoke repo from template, validate file inventory and workflow dispatch | Fully automated | +| `Seed-LearningRoomChallenge.ps1` | Seed a specific challenge issue in a student repository | Fully automated | +| `Seed-PeerSimulation.ps1` | Seed peer simulation issues and PR in a student repository | Fully automated | +| `Restore-LearningRoomFiles.ps1` | Restore baseline files into a student repo via recovery branch and PR | Fully automated | +| `Invoke-LearningRoomEndToEndTest.ps1` | Full end-to-end scripted QA harness | Fully automated | + +## GitHub API Limitations + +The following actions cannot be performed via any GitHub API and require manual browser-based action. These are hard platform constraints, not gaps in the scripts. + +| Action | Why it cannot be automated | Manual path | +|---|---|---| +| Creating a Classroom assignment | No write endpoint exists in the GitHub Classroom REST API (confirmed: only GET endpoints are documented and available) | [classroom.github.com](https://classroom.github.com) as `accesswatch` | +| Deleting issues via REST API | GitHub REST API has no DELETE endpoint for issues | Use `scripts/classroom/Delete-RegistrationIssues.ps1` (GraphQL mutation path), or UI fallback | +| Deleting or moving discussions | GraphQL discussion mutations do not include delete or move operations | Discussions tab in GitHub UI -> each thread -> "..." -> Delete | +| Generating a personal access token | PAT generation requires browser authentication by design | [github.com/settings/tokens](https://github.com/settings/tokens) as `accesswatch` | + +## Pre-Cohort Cleanup Procedure + +Run this procedure after each cohort completes, before starting QA for the next cohort. + +### Archive previous cohort data + +```powershell +# Replace the slug with the cohort being archived (format: YYYY-MM-description) +scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort +``` + +What this does: + +1. Exports all registration/duplicate/waitlist issues (with comments) to JSON and CSV. +2. Exports the current `student-roster.json`. +3. Exports discussions to JSON. +4. Pushes the archive to `Community-Access/git-going-student-success` under `admin/cohorts//`. +5. Closes and locks all registration issues in the source repository. +6. Resets `student-roster.json` to the blank template. + +Use `-WhatIf` to preview without making changes: + +```powershell +scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort -WhatIf +``` + +After the script completes, two manual cleanup steps are required (API limitation): + +1. **Delete registration issues** -- use the GraphQL cleanup script (recommended): + +```powershell +scripts/classroom/Delete-RegistrationIssues.ps1 +``` + +2. **Delete discussions** -- discussions still require manual deletion: + `https://github.com/Community-Access/git-going-with-github/discussions` + +Archive destination: `https://github.com/Community-Access/git-going-student-success/tree/main/admin/cohorts//` + +### Current Progress Snapshot (2026-05-08) + +- [x] Registration issues archived to `git-going-student-success`. +- [x] Registration issues deleted from source repository (count now 0). +- [x] Discussions deleted from source repository (count now 0). +- [x] Learning Room template sync PR merged: `Community-Access/learning-room-template#11`. +- [ ] Registration secret and variable values set for next cohort (`CLASSROOM_ORG_ADMIN_TOKEN`, `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL`). +- [ ] Day 1 and Day 2 classroom assignments created for next cohort. \ No newline at end of file diff --git a/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.html b/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.html index 196ceca0..6ec23841 100644 --- a/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.html +++ b/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.html @@ -97,90 +97,174 @@

Canonical Source Files Used by This Runbook

Required Accounts, Access, and Tools

Complete this section before Phase 1.

    -
  • Facilitator admin account with Owner or Admin access to the workshop organization and classroom organization.
  • -
  • Dedicated non-admin test student account for acceptance and full challenge walkthrough.
  • -
  • Access to classroom.github.com.
  • -
  • Access to repository settings for secrets and variables.
  • +
  • Facilitator admin account (accesswatch) with Owner access to both Community-Access and Community-Access-Classroom.
  • +
  • Dedicated non-admin test student account for acceptance and full challenge walkthrough. This must be a separate GitHub account that is not an owner or member of either organization.
  • +
  • Access to classroom.github.com while signed in as accesswatch.
  • +
  • Access to repository settings for Community-Access/git-going-with-github (secrets and variables).
  • Local clone of this repository with PowerShell available.
  • -
  • GitHub CLI (gh) installed and authenticated for optional verification commands.
  • +
  • GitHub CLI (gh) installed and authenticated as accesswatch for optional verification commands.

Critical Precondition Gates (No-Go if any fail)

Complete all items below before any cohort launch actions.

    -
  • Facilitator account can access both organizations involved in operations:
      -
    • Community-Access
    • -
    • Community-Access-Classroom (or your classroom org)
    • +
    • Facilitator account accesswatch can access both organizations:
        +
      • Community-Access (the workshop and code repository organization)
      • +
      • Community-Access-Classroom (the GitHub Classroom organization where student repos are created)
    • -
    • Facilitator account has verified email and can create/edit Classroom assignments.
    • +
    • accesswatch has a verified email address on its GitHub account and can create and edit Classroom assignments at classroom.github.com.
    • Dedicated non-admin test student account exists and can accept invites.
    • -
    • gh auth status succeeds for facilitator account in local terminal.
    • -
    • Template repository exists and is set as template repo:
        -
      • Community-Access/learning-room-template
      • +
      • gh auth status succeeds for accesswatch in local terminal.
      • +
      • Template repository exists and is set as template repo:
          +
        • Community-Access/learning-room-template
      • -
      • Template repository Actions settings allow required automation behavior:
          -
        • Actions enabled
        • -
        • GITHUB_TOKEN default workflow permissions include write where required
        • -
        • Allow GitHub Actions to create and approve pull requests enabled
        • +
        • Template repository Actions settings allow required automation behavior:
            +
          • Actions enabled
          • +
          • GITHUB_TOKEN default workflow permissions include write where required
          • +
          • Allow GitHub Actions to create and approve pull requests enabled
        • Registration automation settings are correct when using registration-to-classroom handoff:
            -
          • Secret: CLASSROOM_ORG_ADMIN_TOKEN
          • -
          • Variables: CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL
          • +
          • Secret CLASSROOM_ORG_ADMIN_TOKEN is set in Community-Access/git-going-with-github
          • +
          • Variables CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL are set in Community-Access/git-going-with-github
        • -
        • Registration entry configuration exists and is valid:
            -
          • Issue form template workshop-registration.yml exists
          • -
          • Required labels exist: registration, duplicate, waitlist
          • +
          • Registration entry configuration exists and is valid:
              +
            • Issue form template workshop-registration.yml exists
            • +
            • Required labels exist: registration, duplicate, waitlist
          • -
          • Facilitator can open classroom.github.com, view target classroom org, and create assignments.
          • +
          • While signed in as accesswatch, opening classroom.github.com shows the Community-Access-Classroom classroom organization.

          If any precondition fails, stop and resolve before proceeding.

          Exact Setup Steps for Keys, Permissions, Settings, and Template Currency

          Use this section when you need literal setup steps (not only validation checks).

          A. Confirm facilitator account and organization access

          +

          You are performing all steps below as accesswatch. If you are currently signed in to GitHub as a different account, sign out first and sign in as accesswatch before continuing.

            -
          1. Sign in as the facilitator account on github.com.
          2. -
          3. Open your profile menu, then Your organizations.
          4. -
          5. Confirm both organizations are visible and accessible:
              -
            • Community-Access
            • -
            • Community-Access-Classroom (or your classroom org)
            • +
            • Go to github.com and confirm the top-right avatar shows accesswatch.
            • +
            • Open the avatar menu, then select Your organizations.
            • +
            • Confirm both of the following organizations appear in the list:
                +
              • Community-Access -- the main workshop repository organization
              • +
              • Community-Access-Classroom -- the GitHub Classroom organization where student repos are created
              • +
              • If either is missing, do not proceed. Contact the org owner to ensure accesswatch has Owner-level membership in both.
            • -
            • Open both org pages and confirm you can view repositories and settings areas you are expected to manage.
            • -
            • Optional CLI verification from repository root:
            • +
            • Click into Community-Access and open the Settings tab. Confirm you can see the full settings sidebar (Members, Actions, Secrets, etc.). If Settings is not visible, accesswatch does not have Owner access and you cannot proceed.
            • +
            • Click into Community-Access-Classroom and open its Settings tab. Confirm the same.
            • +
            • Optional CLI verification from the local repository root:
          gh auth status -h github.com
           gh repo view Community-Access/git-going-with-github
           gh repo view Community-Access/learning-room-template
           
          +

          Expected output: each gh repo view command should return repository metadata without an error. If you see "Could not resolve to a Repository", accesswatch does not have the required access.

          Why this matters:

            -
          • All downstream setup fails if the facilitator identity is not correctly scoped.
          • +
          • All downstream setup steps operate against Community-Access/git-going-with-github and Community-Access-Classroom. If the account does not have Owner access to both, secrets, variables, and classroom automation cannot be configured.
          • +
          +

          A.1 Create GitHub Classroom assignments

          +

          Do this before section B. You need both assignment URLs in hand before you can fill in the repository variables.

          +

          You must be signed in to GitHub as accesswatch for the following steps.

          +
            +
          1. Go to classroom.github.com.
          2. +
          3. You will see a list of classrooms. Select the classroom named for this cohort that is linked to the Community-Access-Classroom organization. The organization name appears below the classroom name on the card.
              +
            • If no classroom exists yet, select New classroom, then choose Community-Access-Classroom as the organization. Name the classroom using the format Git Going - [Cohort Name] - [Month Year] (for example, Git Going - May 2026).
            • +
            +
          4. +
          5. Inside the classroom, select New assignment.
          6. +
          7. Create the Day 1 assignment:
              +
            • Title: You Belong Here
            • +
            • Individual or group: Individual
            • +
            • Repository visibility: Private
            • +
            • Template repository: Community-Access/learning-room-template (search for it by name in the template field)
            • +
            • Grant students admin access: No
            • +
            • Enable feedback pull requests: Yes
            • +
            • Paste the Day 1 assignment description from classroom/assignment-day1-you-belong-here.md
            • +
            • Add autograding from admin/classroom/autograding-setup.md -- Day 1 requires exactly 4 tests totaling 50 points
            • +
            • Select Create assignment
            • +
            +
          8. +
          9. After saving, the assignment page shows an invite link at the top labeled something like Invite link. It will be in the format https://classroom.github.com/a/<short-code>. Copy this full URL and paste it somewhere safe (for example, a scratch notepad). This is your CLASSROOM_DAY1_ASSIGNMENT_URL.
          10. +
          11. Repeat for the Day 2 assignment:
              +
            • Title: You Can Build This
            • +
            • Same base settings as Day 1
            • +
            • Paste description from classroom/assignment-day2-you-can-build-this.md
            • +
            • Add Day 2 autograding: exactly 6 tests totaling 75 points
            • +
            • Copy the resulting invite URL. This is your CLASSROOM_DAY2_ASSIGNMENT_URL.
            • +
            +
          12. +
          13. Keep both URLs available. You will paste them into repository variables in section B step 4.
          14. +
          +

          Why this matters:

          +
            +
          • The repository variables CLASSROOM_DAY1_ASSIGNMENT_URL and CLASSROOM_DAY2_ASSIGNMENT_URL cannot be filled in until the assignments exist and their invite URLs are known. The short code in the URL is unique to each assignment and is not predictable in advance.

          B. Configure registration automation key and variables

          Repository target: Community-Access/git-going-with-github

          +

          You must be signed in as accesswatch for all steps in this section.

          +

          Step B.1 -- Generate the personal access token (PAT)

          +

          The CLASSROOM_ORG_ADMIN_TOKEN secret must be a GitHub personal access token generated by accesswatch (or another Owner-level account for Community-Access-Classroom). This token is what allows the registration workflow to invite students to the Community-Access-Classroom organization automatically.

            -
          1. Open repository Settings.
          2. -
          3. Open Secrets and variables, then Actions.
          4. -
          5. Open the Secrets tab and create or update secret:
              -
            • CLASSROOM_ORG_ADMIN_TOKEN
            • +
            • While signed in as accesswatch, go to github.com/settings/tokens.
            • +
            • Select Generate new token, then select Generate new token (classic).
                +
              • Do not use fine-grained tokens for this purpose. The admin:org scope is only available on classic tokens.
            • -
            • Open the Variables tab and create or update variables:
                -
              • CLASSROOM_ORG
              • -
              • CLASSROOM_DAY1_ASSIGNMENT_URL
              • -
              • CLASSROOM_DAY2_ASSIGNMENT_URL
              • +
              • In the Note field enter a descriptive name such as Community-Access registration automation.
              • +
              • In the Expiration field, set a date that covers your cohort timeline plus a buffer (for example, 90 days).
              • +
              • Under Select scopes, check admin:org. This is the only scope required. It gives the token permission to list and create organization invitations for Community-Access-Classroom.
              • +
              • Scroll to the bottom and select Generate token.
              • +
              • GitHub will display the token exactly once immediately after generation. It begins with ghp_. Copy it now and paste it somewhere safe (a local scratch notepad, not a repository file). You will not be able to view it again.
              • +
              • Do not close the token page until you have completed section B.2 and confirmed the secret was saved.
              • +
          +

          Step B.2 -- Add the token as a repository secret

          +
            +
          1. Go to github.com/Community-Access/git-going-with-github/settings/secrets/actions.
          2. +
          3. Select New repository secret.
          4. +
          5. In the Name field, enter exactly: CLASSROOM_ORG_ADMIN_TOKEN
              +
            • Capitalization and underscores must match exactly.
          6. -
          7. Re-open each entry and confirm values have no leading or trailing spaces.
          8. -
          9. Run one registration test and confirm welcome comment contains assignment links when variables are set.
          10. +
          11. In the Secret field, paste the token you copied in step B.1.
          12. +
          13. Select Add secret.
          14. +
          15. Re-open the secret entry and confirm the name shows CLASSROOM_ORG_ADMIN_TOKEN. GitHub does not display the value again, but confirming the name is correct is sufficient.
          16. +
          +

          Step B.3 -- Add the repository variables

          +
            +
          1. At the same settings page, select the Variables tab (next to Secrets).

            +
          2. +
          3. Select New repository variable for each of the following. Add them one at a time.

            +

            Variable 1:

            +
              +
            • Name: CLASSROOM_ORG
            • +
            • Value: Community-Access-Classroom
            • +
            • This is the exact GitHub organization name where students are invited. The capitalization and hyphens must match exactly.
            • +
            +

            Variable 2:

            +
              +
            • Name: CLASSROOM_DAY1_ASSIGNMENT_URL
            • +
            • Value: paste the Day 1 invite URL you copied in section A.1 step 5 (format: https://classroom.github.com/a/<short-code>)
            • +
            +

            Variable 3:

            +
              +
            • Name: CLASSROOM_DAY2_ASSIGNMENT_URL
            • +
            • Value: paste the Day 2 invite URL you copied in section A.1 step 6 (format: https://classroom.github.com/a/<short-code>)
            • +
            +
          4. +
          5. After adding all three, re-open each variable entry and confirm:

            +
              +
            • The name is exactly as listed above (no typos, no extra characters).
            • +
            • The value has no leading or trailing spaces. Paste into a plain text editor first if you are unsure, and trim whitespace before re-pasting.
            • +
            +
          6. +
          7. Run one registration test and confirm welcome comment contains assignment links.

            +

          Why this matters:

            -
          • These values drive invite and assignment-link injection in registration responses.
          • +
          • These values drive invite and assignment-link injection in registration responses. A single typo in the org name or a trailing space in a variable value will silently break automation without a clear error message.

          C. Configure template repository Actions permissions

          Repository target: Community-Access/learning-room-template

          @@ -498,15 +582,28 @@

          Step 0.1 Deploy registration issue form and workflow prerequisites

        Step 0.2 Deploy optional registration-to-classroom automation settings

        Goal: enable automatic org invite and assignment-link injection in registration confirmation comments.

        -
          -
        1. In repository settings, configure secret:
            -
          • CLASSROOM_ORG_ADMIN_TOKEN
          • +

            The fastest path is Initialize-WorkshopSetup.ps1, which sets the secret, all three variables, verifies labels, and runs template prep in a single command. See the setup script section below.

            +

            Using the setup script (recommended):

            +
            scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere
            +
            +

            The script will:

            +
              +
            • Prompt you if the PAT is missing or invalid
            • +
            • Resolve Day 1 and Day 2 assignment URLs automatically from the GitHub Classroom API (if assignments exist in the GIT Going with Github classroom)
            • +
            • Set CLASSROOM_ORG_ADMIN_TOKEN secret and all three variables in Community-Access/git-going-with-github
            • +
            • Verify all three required labels exist, creating any that are missing
            • +
            • Confirm read-back values have no leading or trailing spaces
            • +
            • Run Prepare-LearningRoomTemplate.ps1 and Test-LearningRoomTemplate.ps1 unless skipped
            - -
          • In repository settings, configure variables:
              -
            • CLASSROOM_ORG
            • -
            • CLASSROOM_DAY1_ASSIGNMENT_URL
            • -
            • CLASSROOM_DAY2_ASSIGNMENT_URL
            • +

              If running manually instead:

              +
                +
              1. Generate a classic PAT with admin:org scope at github.com/settings/tokens (see section B of the setup steps above for exact steps).
              2. +
              3. Go to github.com/Community-Access/git-going-with-github/settings/secrets/actions.
              4. +
              5. Create secret CLASSROOM_ORG_ADMIN_TOKEN with the PAT value.
              6. +
              7. On the Variables tab, create:
                  +
                • CLASSROOM_ORG = Community-Access-Classroom
                • +
                • CLASSROOM_DAY1_ASSIGNMENT_URL = invite URL from assignment A.1 step 5
                • +
                • CLASSROOM_DAY2_ASSIGNMENT_URL = invite URL from assignment A.1 step 6
              8. Re-open each value and verify no leading or trailing spaces.
              9. @@ -514,20 +611,33 @@

                Step 0.2 Deploy optional registration-to-classroom automation settings

                Pass criteria:

                • Secret and variables are present with correct values.
                • +
                • Initialize-WorkshopSetup.ps1 reported no failures, or manual verification confirms all values.
                • Configuration aligns with REGISTRATION-ADMIN.md.

                Step 0.3 Registration deployment smoke check

                -

                Goal: validate deployed registration system can execute at least one full workflow run.

                +

                Goal: validate the deployed registration system and site are working before manual QA begins.

                +

                Use Test-RegistrationPage.ps1 to run this check. The script validates the Pages site, the REGISTER page, the issue form template, required labels, and workflow state. With -RunLiveTest it also submits a real test registration issue, waits for the workflow, and verifies the welcome comment.

                +

                Static checks only (site, config, labels, workflow state):

                +
                scripts/classroom/Test-RegistrationPage.ps1
                +
                +

                Full live end-to-end test including issue submission:

                +
                scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest
                +
                +

                What the script checks:

                  -
                1. Submit one test registration issue from a non-admin test account.
                2. -
                3. Confirm Registration - Welcome & CSV Export workflow completes.
                4. -
                5. Confirm welcome comment posts and registration label is applied.
                6. -
                7. If classroom automation is configured, confirm org invite status and assignment links appear.
                8. +
                9. HTTP GET to https://community-access.org/git-going-with-github/ returns 200.
                10. +
                11. HTTP GET to https://community-access.org/git-going-with-github/REGISTER returns 200 and contains expected content.
                12. +
                13. workshop-registration.yml exists in .github/ISSUE_TEMPLATE/.
                14. +
                15. Labels registration, duplicate, and waitlist exist.
                16. +
                17. registration.yml workflow is active and has a recent successful run.
                18. +
                19. (Live test only) Test issue is submitted, workflow completes, welcome comment posts with assignment links, registration label is applied.
                +

                Cleanup limitation: the test issue is closed and locked by the script. It cannot be deleted via the GitHub API. If deletion is needed, a repository admin must delete it manually from the Issues tab after this run.

                Pass criteria:

                  -
                • Registration workflow executes successfully end to end.
                • -
                • Output comment/labels match deployed configuration.
                • +
                • Test-RegistrationPage.ps1 exits with no failures.
                • +
                • Live test (if run) confirms workflow executes end to end.
                • +
                • Output comment and labels match deployed configuration.

                Phase 1 - Registration System QA (Admin Side)

                Step 1. Verify public registration entry path

                @@ -544,22 +654,16 @@

                Step 1. Verify public registration entry path

              10. Public visibility warning is present.

            Step 2. Configure registration automation for classroom handoff

            -

            Use REGISTRATION-QUICKSTART.md for fast entry and REGISTRATION-ADMIN.md for full details.

            -
              -
            1. Create or verify an admin token that can manage organization invitations.
            2. -
            3. In repository settings, add secret CLASSROOM_ORG_ADMIN_TOKEN.
            4. -
            5. In repository settings, set variables:
                -
              • CLASSROOM_ORG
              • -
              • CLASSROOM_DAY1_ASSIGNMENT_URL
              • -
              • CLASSROOM_DAY2_ASSIGNMENT_URL
              • -
              -
            6. -
            7. Save and re-open each setting to confirm there are no leading or trailing spaces.
            8. -
            +

            If Initialize-WorkshopSetup.ps1 was run successfully in Phase 0, this step is already complete. Re-verify with:

            +
            gh secret list -R Community-Access/git-going-with-github
            +gh variable list -R Community-Access/git-going-with-github
            +
            +

            Expected output: CLASSROOM_ORG_ADMIN_TOKEN appears in secrets, and CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL appear in variables.

            +

            If any are missing, run Initialize-WorkshopSetup.ps1 again or follow the manual steps in section B of the setup instructions. For full reference, see REGISTRATION-QUICKSTART.md and REGISTRATION-ADMIN.md.

            Pass criteria:

              -
            • Secret exists and is scoped correctly.
            • -
            • All 3 variables exist and values are correct.
            • +
            • CLASSROOM_ORG_ADMIN_TOKEN secret is present.
            • +
            • All 3 variables are present with correct values and no leading or trailing spaces.

            Step 3. Execute registration happy-path test

            Use the non-admin test student account.

            @@ -696,67 +800,90 @@

            Step 9. Optional comprehensive harness check

            Use full end-to-end mode when preparing major cohort launches or after significant automation updates.

            Phase 3 - Classroom Deployment QA (Admin Side)

            Step 10. Create classroom and import roster

            -

            Use classroom/README.md Steps 1 and 2.

            -
              -
            1. Create a new classroom in Community-Access.
            2. -
            3. Name it using Git Going - [Cohort Name] - [Month Year].
            4. -
            5. Import roster using classroom/roster-template.csv.
            6. -
            7. Confirm test student appears in roster.
            8. -
            +

            GitHub Classroom assignment creation has no write API. All classroom creation and assignment setup must be done through the browser at classroom.github.com while signed in as accesswatch.

            +

            Note: a classroom already exists for this repository (GIT Going with Github, classroom id 322783, linked to Community-Access-Classroom). Unless starting a completely new classroom, skip classroom creation and go directly to assignment creation in Steps 11 and 12.

            +

            If you do need a new classroom:

            +
              +
            1. Go to classroom.github.com as accesswatch.
            2. +
            3. Select New classroom and choose Community-Access-Classroom as the organization.
            4. +
            5. Name it Git Going - [Cohort Name] - [Month Year] (for example, Git Going - May 2026).
            6. +
            7. Import roster from classroom/roster-template.csv on the Roster tab.
            8. +
            9. Add accesswatch-student to the roster as the test student.
            10. +
            +

            To verify the existing classroom is accessible:

            +
            gh api /classrooms --jq '.[] | {id, name, url}'
            +

            Pass criteria:

              -
            • Classroom exists and is accessible to facilitators.
            • -
            • Roster import succeeds with expected usernames.
            • +
            • Classroom exists in Community-Access-Classroom and is accessible to accesswatch.
            • +
            • Roster includes accesswatch-student (or the designated test student username).

            Step 11. Create Day 1 assignment exactly

            -

            Use classroom/assignment-day1-you-belong-here.md and admin/classroom/day1-assignment-copy-paste.md.

            -
              -
            1. Create assignment with title You Belong Here.
            2. -
            3. Set type Individual.
            4. -
            5. Set visibility Private.
            6. -
            7. Select template Community-Access/learning-room-template.
            8. -
            9. Set Grant students admin access to No.
            10. -
            11. Set Enable feedback pull requests to Yes.
            12. -
            13. Paste Day 1 assignment description content.
            14. -
            15. Add Day 1 autograding entries from admin/classroom/autograding-setup.md.
            16. -
            17. Confirm Day 1 has 4 tests and total 50 points.
            18. -
            19. Save assignment and copy invite link.
            20. +

              See section A.1 of the setup steps above for full navigation instructions.

              +

              Use classroom/assignment-day1-you-belong-here.md and admin/classroom/day1-assignment-copy-paste.md for the exact title, description, and autograding entries.

              +

              To check if the Day 1 assignment already exists:

              +
              gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}'
              +
              +

              If the assignment exists and its invite_link is already in the CLASSROOM_DAY1_ASSIGNMENT_URL variable, skip to Step 12.

              +

              If creating from scratch:

              +
                +
              1. Go to classroom.github.com as accesswatch and open the GIT Going with Github classroom.
              2. +
              3. Select New assignment.
              4. +
              5. Title: You Belong Here (exact match required)
              6. +
              7. Type: Individual, Visibility: Private
              8. +
              9. Template: Community-Access/learning-room-template
              10. +
              11. Grant students admin access: No
              12. +
              13. Enable feedback pull requests: Yes
              14. +
              15. Paste description from classroom/assignment-day1-you-belong-here.md
              16. +
              17. Add autograding from admin/classroom/autograding-setup.md -- Day 1 requires exactly 4 tests totaling 50 points
              18. +
              19. Save and copy the invite URL from the assignment page
              20. +
              21. If Initialize-WorkshopSetup.ps1 has not been run yet, paste the URL into CLASSROOM_DAY1_ASSIGNMENT_URL. If it has been run, re-run it to pick up the new URL automatically.

              Pass criteria:

                -
              • Day 1 settings match source files exactly.
              • -
              • Test count and points are correct.
              • -
              • Feedback pull request is enabled and visible in assignment configuration.
              • +
              • Day 1 assignment exists with title You Belong Here.
              • +
              • Test count is 4, total points is 50.
              • +
              • Feedback pull request is enabled.
              • +
              • CLASSROOM_DAY1_ASSIGNMENT_URL variable matches the assignment invite link.

              Step 12. Create Day 2 assignment exactly

              -

              Use classroom/assignment-day2-you-can-build-this.md and admin/classroom/day2-assignment-copy-paste.md.

              -
                -
              1. Create assignment with title You Can Build This.
              2. -
              3. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled).
              4. -
              5. Paste Day 2 assignment description content.
              6. -
              7. Add Day 2 autograding entries from admin/classroom/autograding-setup.md.
              8. -
              9. Confirm Day 2 has 6 tests and total 75 points.
              10. -
              11. Save assignment and copy invite link.
              12. +

                Same process as Step 11. Check first whether it already exists:

                +
                gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}'
                +
                +

                If the assignment exists and its invite_link is already in CLASSROOM_DAY2_ASSIGNMENT_URL, skip ahead.

                +

                If creating from scratch:

                +
                  +
                1. In the GIT Going with Github classroom, select New assignment.
                2. +
                3. Title: You Can Build This (exact match required)
                4. +
                5. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled)
                6. +
                7. Template: Community-Access/learning-room-template
                8. +
                9. Paste description from classroom/assignment-day2-you-can-build-this.md
                10. +
                11. Add Day 2 autograding from admin/classroom/autograding-setup.md -- 6 tests totaling 75 points
                12. +
                13. Save and copy the invite URL
                14. +
                15. Update CLASSROOM_DAY2_ASSIGNMENT_URL or re-run Initialize-WorkshopSetup.ps1 to pick it up automatically

                Pass criteria:

                  -
                • Day 2 settings match source files exactly.
                • -
                • Test count and points are correct.
                • -
                • Feedback pull request is enabled and visible in assignment configuration.
                • +
                • Day 2 assignment exists with title You Can Build This.
                • +
                • Test count is 6, total points is 75.
                • +
                • Feedback pull request is enabled.
                • +
                • CLASSROOM_DAY2_ASSIGNMENT_URL variable matches the assignment invite link.

                Step 13. Connect assignment URLs back to registration automation

                -
                  -
                1. Add or update repository variables:
                    -
                  • CLASSROOM_DAY1_ASSIGNMENT_URL
                  • -
                  • CLASSROOM_DAY2_ASSIGNMENT_URL
                  • -
                  -
                2. -
                3. Re-run one registration test issue (new test account or controlled case).
                4. -
                5. Confirm both links appear in welcome comment.
                6. -
                +

                If Initialize-WorkshopSetup.ps1 was run after the assignments were created, the variables are already set. Verify with:

                +
                gh variable list -R Community-Access/git-going-with-github
                +
                +

                If either URL variable is missing or stale, re-run the setup script. It will resolve URLs directly from the Classroom API:

                +
                scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere -SkipTemplatePrepare -SkipTemplateValidate
                +
                +

                Then re-run the registration live test to confirm both URLs appear in the welcome comment:

                +
                scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest
                +

                Pass criteria:

                  -
                • Registration confirmation comment now includes both assignment URLs.
                • +
                • CLASSROOM_DAY1_ASSIGNMENT_URL and CLASSROOM_DAY2_ASSIGNMENT_URL are set and match assignment invite links.
                • +
                • Registration confirmation comment includes both assignment URLs.
                • +
                • Test-RegistrationPage.ps1 -RunLiveTest exits with no failures.

                Phase 4 - Test Student Acceptance and Seeding (Bridge from Admin to Student)

                Step 14. Test student accepts Day 1 and Day 2 invites

                @@ -774,17 +901,28 @@

                Step 14. Test student accepts Day 1 and Day 2 invites

              13. Two private repos are created and visible in classroom dashboard.

          Step 15. Seed initial challenges and peer simulation

          -

          Run commands from repository root.

          -
          scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -Challenge 1 -Assignee test-student
          -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -StudentUsername test-student
          -scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -Challenge 10 -Assignee test-student
          -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -StudentUsername test-student
          +

          The test student account is accesswatch-student. GitHub Classroom generates repository names from the assignment slug and the student username. For the default assignment titles, the repository names will be:

          +
            +
          • Day 1: Community-Access-Classroom/you-belong-here-accesswatch-student
          • +
          • Day 2: Community-Access-Classroom/you-can-build-this-accesswatch-student
          • +
          +

          Confirm the exact repository names first:

          +
          gh repo list Community-Access-Classroom --json name --jq '.[].name' | Select-String accesswatch-student
           
          -

          If you use different repository names, replace values accordingly.

          +

          Then seed (replace repository slugs with the confirmed names if different):

          +
          $day1 = 'Community-Access-Classroom/you-belong-here-accesswatch-student'
          +$day2 = 'Community-Access-Classroom/you-can-build-this-accesswatch-student'
          +
          +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day1 -Challenge 1 -Assignee accesswatch-student
          +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day1 -StudentUsername accesswatch-student
          +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day2 -Challenge 10 -Assignee accesswatch-student
          +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day2 -StudentUsername accesswatch-student
          +
          +

          Alternatively, if Initialize-WorkshopSetup.ps1 is run after accesswatch-student has accepted both invites, it will detect the repos and seed them automatically.

          Pass criteria:

            -
          • Challenge 1 appears in Day 1 repo.
          • -
          • Challenge 10 appears in Day 2 repo.
          • +
          • Challenge 1 issue appears in Day 1 repo assigned to accesswatch-student.
          • +
          • Challenge 10 issue appears in Day 2 repo assigned to accesswatch-student.
          • Peer simulation issues and PR exist in both repos.

          Phase 5 - Curriculum Content QA (Walk every required chapter and appendix)

          @@ -1885,6 +2023,141 @@

          Completion Output Template (Copy into QA Issue)

          What This Runbook Replaces

          This runbook is the operator-facing execution path that unifies registration, deployment, and end-to-end challenge QA.

          It does not replace source documents. It sequences them into one practical checklist so a single facilitator can execute and validate the full system without context switching across multiple folders.

          +

          Script Reference

          +

          The following scripts in scripts/classroom/ are used by this runbook. Run each with -? or read the .SYNOPSIS block for full parameter documentation.

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ScriptPurposeAutomated
          Initialize-WorkshopSetup.ps1Set PAT secret, variables, labels; sync and validate template; seed test student challengesAll steps except PAT generation (browser required)
          Archive-CohortData.ps1Export cohort issues, roster, and discussions to git-going-student-success; reset source repoExport and archive are automated; roster reset may require PR due branch rules
          Delete-RegistrationIssues.ps1Delete registration/duplicate/waitlist issues via GraphQL mutationFully automated when user has repo admin and repo token scope
          Delete-RegistrationIssues-v2.jsExperimental Playwright UI deleter for issue cleanupPartially automated; UI selector fragility makes this fallback-only
          Test-RegistrationPage.ps1Validate Pages site, REGISTER page, issue form, labels, workflow, and live registration flowAll steps; test issue cleanup is close+lock only (see below)
          Prepare-LearningRoomTemplate.ps1Sync learning-room/ source into Community-Access/learning-room-templateFully automated
          Test-LearningRoomTemplate.ps1Create smoke repo from template, validate file inventory and workflow dispatchFully automated
          Seed-LearningRoomChallenge.ps1Seed a specific challenge issue in a student repositoryFully automated
          Seed-PeerSimulation.ps1Seed peer simulation issues and PR in a student repositoryFully automated
          Restore-LearningRoomFiles.ps1Restore baseline files into a student repo via recovery branch and PRFully automated
          Invoke-LearningRoomEndToEndTest.ps1Full end-to-end scripted QA harnessFully automated
          +

          GitHub API Limitations

          +

          The following actions cannot be performed via any GitHub API and require manual browser-based action. These are hard platform constraints, not gaps in the scripts.

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ActionWhy it cannot be automatedManual path
          Creating a Classroom assignmentNo write endpoint exists in the GitHub Classroom REST API (confirmed: only GET endpoints are documented and available)classroom.github.com as accesswatch
          Deleting issues via REST APIGitHub REST API has no DELETE endpoint for issuesUse scripts/classroom/Delete-RegistrationIssues.ps1 (GraphQL mutation path), or UI fallback
          Deleting or moving discussionsGraphQL discussion mutations do not include delete or move operationsDiscussions tab in GitHub UI -> each thread -> "..." -> Delete
          Generating a personal access tokenPAT generation requires browser authentication by designgithub.com/settings/tokens as accesswatch
          +

          Pre-Cohort Cleanup Procedure

          +

          Run this procedure after each cohort completes, before starting QA for the next cohort.

          +

          Archive previous cohort data

          +
          # Replace the slug with the cohort being archived (format: YYYY-MM-description)
          +scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort
          +
          +

          What this does:

          +
            +
          1. Exports all registration/duplicate/waitlist issues (with comments) to JSON and CSV.
          2. +
          3. Exports the current student-roster.json.
          4. +
          5. Exports discussions to JSON.
          6. +
          7. Pushes the archive to Community-Access/git-going-student-success under admin/cohorts/<CohortSlug>/.
          8. +
          9. Closes and locks all registration issues in the source repository.
          10. +
          11. Resets student-roster.json to the blank template.
          12. +
          +

          Use -WhatIf to preview without making changes:

          +
          scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort -WhatIf
          +
          +

          After the script completes, two manual cleanup steps are required (API limitation):

          +
            +
          1. Delete registration issues -- use the GraphQL cleanup script (recommended):
          2. +
          +
          scripts/classroom/Delete-RegistrationIssues.ps1
          +
          +
            +
          1. Delete discussions -- discussions still require manual deletion: +https://github.com/Community-Access/git-going-with-github/discussions
          2. +
          +

          Archive destination: https://github.com/Community-Access/git-going-student-success/tree/main/admin/cohorts/<CohortSlug>/

          +

          Current Progress Snapshot (2026-05-08)

          +
            +
          • Registration issues archived to git-going-student-success.
          • +
          • Registration issues deleted from source repository (count now 0).
          • +
          • Discussions deleted from source repository (count now 0).
          • +
          • Learning Room template sync PR merged: Community-Access/learning-room-template#11.
          • +
          • Registration secret and variable values set for next cohort (CLASSROOM_ORG_ADMIN_TOKEN, CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL).
          • +
          • Day 1 and Day 2 classroom assignments created for next cohort.
          • +
          \ No newline at end of file diff --git a/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md b/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md index 4eb253c6..a1d411ad 100644 --- a/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md +++ b/admin/qa-bundle/admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md @@ -53,36 +53,36 @@ The following table lists the source files this runbook consolidates. Complete this section before Phase 1. -- Facilitator admin account with Owner or Admin access to the workshop organization and classroom organization. -- Dedicated non-admin test student account for acceptance and full challenge walkthrough. -- Access to [classroom.github.com](https://classroom.github.com). -- Access to repository settings for secrets and variables. +- Facilitator admin account (`accesswatch`) with Owner access to both `Community-Access` and `Community-Access-Classroom`. +- Dedicated non-admin test student account for acceptance and full challenge walkthrough. This must be a separate GitHub account that is not an owner or member of either organization. +- Access to [classroom.github.com](https://classroom.github.com) while signed in as `accesswatch`. +- Access to repository settings for `Community-Access/git-going-with-github` (secrets and variables). - Local clone of this repository with PowerShell available. -- GitHub CLI (`gh`) installed and authenticated for optional verification commands. +- GitHub CLI (`gh`) installed and authenticated as `accesswatch` for optional verification commands. ### Critical Precondition Gates (No-Go if any fail) Complete all items below before any cohort launch actions. -- [ ] Facilitator account can access both organizations involved in operations: - - [ ] `Community-Access` - - [ ] `Community-Access-Classroom` (or your classroom org) -- [ ] Facilitator account has verified email and can create/edit Classroom assignments. +- [ ] Facilitator account `accesswatch` can access both organizations: + - [ ] `Community-Access` (the workshop and code repository organization) + - [ ] `Community-Access-Classroom` (the GitHub Classroom organization where student repos are created) +- [ ] `accesswatch` has a verified email address on its GitHub account and can create and edit Classroom assignments at [classroom.github.com](https://classroom.github.com). - [ ] Dedicated non-admin test student account exists and can accept invites. -- [ ] `gh auth status` succeeds for facilitator account in local terminal. -- [ ] Template repository exists and is set as template repo: - - [ ] `Community-Access/learning-room-template` -- [ ] Template repository Actions settings allow required automation behavior: - - [ ] Actions enabled - - [ ] `GITHUB_TOKEN` default workflow permissions include write where required - - [ ] `Allow GitHub Actions to create and approve pull requests` enabled +- [x] `gh auth status` succeeds for `accesswatch` in local terminal. +- [x] Template repository exists and is set as template repo: + - [x] `Community-Access/learning-room-template` +- [x] Template repository Actions settings allow required automation behavior: + - [x] Actions enabled + - [x] `GITHUB_TOKEN` default workflow permissions include write where required + - [x] `Allow GitHub Actions to create and approve pull requests` enabled - [ ] Registration automation settings are correct when using registration-to-classroom handoff: - - [ ] Secret: `CLASSROOM_ORG_ADMIN_TOKEN` - - [ ] Variables: `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` -- [ ] Registration entry configuration exists and is valid: - - [ ] Issue form template `workshop-registration.yml` exists - - [ ] Required labels exist: `registration`, `duplicate`, `waitlist` -- [ ] Facilitator can open `classroom.github.com`, view target classroom org, and create assignments. + - [ ] Secret `CLASSROOM_ORG_ADMIN_TOKEN` is set in `Community-Access/git-going-with-github` + - [ ] Variables `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` are set in `Community-Access/git-going-with-github` +- [x] Registration entry configuration exists and is valid: + - [x] Issue form template `workshop-registration.yml` exists + - [x] Required labels exist: `registration`, `duplicate`, `waitlist` +- [ ] While signed in as `accesswatch`, opening [classroom.github.com](https://classroom.github.com) shows the `Community-Access-Classroom` classroom organization. If any precondition fails, stop and resolve before proceeding. @@ -92,13 +92,17 @@ Use this section when you need literal setup steps (not only validation checks). #### A. Confirm facilitator account and organization access -1. Sign in as the facilitator account on github.com. -2. Open your profile menu, then Your organizations. -3. Confirm both organizations are visible and accessible: - - `Community-Access` - - `Community-Access-Classroom` (or your classroom org) -4. Open both org pages and confirm you can view repositories and settings areas you are expected to manage. -5. Optional CLI verification from repository root: +You are performing all steps below as `accesswatch`. If you are currently signed in to GitHub as a different account, sign out first and sign in as `accesswatch` before continuing. + +1. Go to [github.com](https://github.com) and confirm the top-right avatar shows `accesswatch`. +2. Open the avatar menu, then select **Your organizations**. +3. Confirm both of the following organizations appear in the list: + - `Community-Access` -- the main workshop repository organization + - `Community-Access-Classroom` -- the GitHub Classroom organization where student repos are created + - If either is missing, do not proceed. Contact the org owner to ensure `accesswatch` has Owner-level membership in both. +4. Click into `Community-Access` and open the **Settings** tab. Confirm you can see the full settings sidebar (Members, Actions, Secrets, etc.). If Settings is not visible, `accesswatch` does not have Owner access and you cannot proceed. +5. Click into `Community-Access-Classroom` and open its **Settings** tab. Confirm the same. +6. Optional CLI verification from the local repository root: ```powershell gh auth status -h github.com @@ -106,28 +110,101 @@ gh repo view Community-Access/git-going-with-github gh repo view Community-Access/learning-room-template ``` +Expected output: each `gh repo view` command should return repository metadata without an error. If you see "Could not resolve to a Repository", `accesswatch` does not have the required access. + Why this matters: -- All downstream setup fails if the facilitator identity is not correctly scoped. +- All downstream setup steps operate against `Community-Access/git-going-with-github` and `Community-Access-Classroom`. If the account does not have Owner access to both, secrets, variables, and classroom automation cannot be configured. + +#### A.1 Create GitHub Classroom assignments + +Do this before section B. You need both assignment URLs in hand before you can fill in the repository variables. + +You must be signed in to GitHub as `accesswatch` for the following steps. + +1. Go to [classroom.github.com](https://classroom.github.com). +2. You will see a list of classrooms. Select the classroom named for this cohort that is linked to the `Community-Access-Classroom` organization. The organization name appears below the classroom name on the card. + - If no classroom exists yet, select **New classroom**, then choose `Community-Access-Classroom` as the organization. Name the classroom using the format `Git Going - [Cohort Name] - [Month Year]` (for example, `Git Going - May 2026`). +3. Inside the classroom, select **New assignment**. +4. Create the Day 1 assignment: + - **Title**: `You Belong Here` + - **Individual or group**: Individual + - **Repository visibility**: Private + - **Template repository**: `Community-Access/learning-room-template` (search for it by name in the template field) + - **Grant students admin access**: No + - **Enable feedback pull requests**: Yes + - Paste the Day 1 assignment description from [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) + - Add autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- Day 1 requires exactly 4 tests totaling 50 points + - Select **Create assignment** +5. After saving, the assignment page shows an invite link at the top labeled something like **Invite link**. It will be in the format `https://classroom.github.com/a/`. Copy this full URL and paste it somewhere safe (for example, a scratch notepad). This is your `CLASSROOM_DAY1_ASSIGNMENT_URL`. +6. Repeat for the Day 2 assignment: + - **Title**: `You Can Build This` + - Same base settings as Day 1 + - Paste description from [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) + - Add Day 2 autograding: exactly 6 tests totaling 75 points + - Copy the resulting invite URL. This is your `CLASSROOM_DAY2_ASSIGNMENT_URL`. +7. Keep both URLs available. You will paste them into repository variables in section B step 4. + +Why this matters: + +- The repository variables `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL` cannot be filled in until the assignments exist and their invite URLs are known. The short code in the URL is unique to each assignment and is not predictable in advance. #### B. Configure registration automation key and variables Repository target: `Community-Access/git-going-with-github` -1. Open repository Settings. -2. Open Secrets and variables, then Actions. -3. Open the Secrets tab and create or update secret: - - `CLASSROOM_ORG_ADMIN_TOKEN` -4. Open the Variables tab and create or update variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -5. Re-open each entry and confirm values have no leading or trailing spaces. -6. Run one registration test and confirm welcome comment contains assignment links when variables are set. +You must be signed in as `accesswatch` for all steps in this section. + +**Step B.1 -- Generate the personal access token (PAT)** + +The `CLASSROOM_ORG_ADMIN_TOKEN` secret must be a GitHub personal access token generated by `accesswatch` (or another Owner-level account for `Community-Access-Classroom`). This token is what allows the registration workflow to invite students to the `Community-Access-Classroom` organization automatically. + +1. While signed in as `accesswatch`, go to [github.com/settings/tokens](https://github.com/settings/tokens). +2. Select **Generate new token**, then select **Generate new token (classic)**. + - Do not use fine-grained tokens for this purpose. The `admin:org` scope is only available on classic tokens. +3. In the **Note** field enter a descriptive name such as `Community-Access registration automation`. +4. In the **Expiration** field, set a date that covers your cohort timeline plus a buffer (for example, 90 days). +5. Under **Select scopes**, check `admin:org`. This is the only scope required. It gives the token permission to list and create organization invitations for `Community-Access-Classroom`. +6. Scroll to the bottom and select **Generate token**. +7. GitHub will display the token exactly once immediately after generation. It begins with `ghp_`. Copy it now and paste it somewhere safe (a local scratch notepad, not a repository file). You will not be able to view it again. +8. Do not close the token page until you have completed section B.2 and confirmed the secret was saved. + +**Step B.2 -- Add the token as a repository secret** + +1. Go to [github.com/Community-Access/git-going-with-github/settings/secrets/actions](https://github.com/Community-Access/git-going-with-github/settings/secrets/actions). +2. Select **New repository secret**. +3. In the **Name** field, enter exactly: `CLASSROOM_ORG_ADMIN_TOKEN` + - Capitalization and underscores must match exactly. +4. In the **Secret** field, paste the token you copied in step B.1. +5. Select **Add secret**. +6. Re-open the secret entry and confirm the name shows `CLASSROOM_ORG_ADMIN_TOKEN`. GitHub does not display the value again, but confirming the name is correct is sufficient. + +**Step B.3 -- Add the repository variables** + +1. At the same settings page, select the **Variables** tab (next to Secrets). +2. Select **New repository variable** for each of the following. Add them one at a time. + + Variable 1: + - **Name**: `CLASSROOM_ORG` + - **Value**: `Community-Access-Classroom` + - This is the exact GitHub organization name where students are invited. The capitalization and hyphens must match exactly. + + Variable 2: + - **Name**: `CLASSROOM_DAY1_ASSIGNMENT_URL` + - **Value**: paste the Day 1 invite URL you copied in section A.1 step 5 (format: `https://classroom.github.com/a/`) + + Variable 3: + - **Name**: `CLASSROOM_DAY2_ASSIGNMENT_URL` + - **Value**: paste the Day 2 invite URL you copied in section A.1 step 6 (format: `https://classroom.github.com/a/`) + +3. After adding all three, re-open each variable entry and confirm: + - The name is exactly as listed above (no typos, no extra characters). + - The value has no leading or trailing spaces. Paste into a plain text editor first if you are unsure, and trim whitespace before re-pasting. +4. Run one registration test and confirm welcome comment contains assignment links. Why this matters: -- These values drive invite and assignment-link injection in registration responses. +- These values drive invite and assignment-link injection in registration responses. A single typo in the org name or a trailing space in a variable value will silently break automation without a clear error message. #### C. Configure template repository Actions permissions @@ -353,32 +430,73 @@ Pass criteria: Goal: enable automatic org invite and assignment-link injection in registration confirmation comments. -1. In repository settings, configure secret: - - `CLASSROOM_ORG_ADMIN_TOKEN` -2. In repository settings, configure variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -3. Re-open each value and verify no leading or trailing spaces. +The fastest path is `Initialize-WorkshopSetup.ps1`, which sets the secret, all three variables, verifies labels, and runs template prep in a single command. See the setup script section below. + +**Using the setup script (recommended):** + +```powershell +scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere +``` + +The script will: +- Prompt you if the PAT is missing or invalid +- Resolve Day 1 and Day 2 assignment URLs automatically from the GitHub Classroom API (if assignments exist in the `GIT Going with Github` classroom) +- Set `CLASSROOM_ORG_ADMIN_TOKEN` secret and all three variables in `Community-Access/git-going-with-github` +- Verify all three required labels exist, creating any that are missing +- Confirm read-back values have no leading or trailing spaces +- Run `Prepare-LearningRoomTemplate.ps1` and `Test-LearningRoomTemplate.ps1` unless skipped + +**If running manually instead:** + +1. Generate a classic PAT with `admin:org` scope at [github.com/settings/tokens](https://github.com/settings/tokens) (see section B of the setup steps above for exact steps). +2. Go to [github.com/Community-Access/git-going-with-github/settings/secrets/actions](https://github.com/Community-Access/git-going-with-github/settings/secrets/actions). +3. Create secret `CLASSROOM_ORG_ADMIN_TOKEN` with the PAT value. +4. On the Variables tab, create: + - `CLASSROOM_ORG` = `Community-Access-Classroom` + - `CLASSROOM_DAY1_ASSIGNMENT_URL` = invite URL from assignment A.1 step 5 + - `CLASSROOM_DAY2_ASSIGNMENT_URL` = invite URL from assignment A.1 step 6 +5. Re-open each value and verify no leading or trailing spaces. Pass criteria: - Secret and variables are present with correct values. +- `Initialize-WorkshopSetup.ps1` reported no failures, or manual verification confirms all values. - Configuration aligns with [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md). ### Step 0.3 Registration deployment smoke check -Goal: validate deployed registration system can execute at least one full workflow run. +Goal: validate the deployed registration system and site are working before manual QA begins. + +Use `Test-RegistrationPage.ps1` to run this check. The script validates the Pages site, the REGISTER page, the issue form template, required labels, and workflow state. With `-RunLiveTest` it also submits a real test registration issue, waits for the workflow, and verifies the welcome comment. + +**Static checks only (site, config, labels, workflow state):** + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 +``` + +**Full live end-to-end test including issue submission:** + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest +``` + +What the script checks: -1. Submit one test registration issue from a non-admin test account. -2. Confirm `Registration - Welcome & CSV Export` workflow completes. -3. Confirm welcome comment posts and `registration` label is applied. -4. If classroom automation is configured, confirm org invite status and assignment links appear. +1. HTTP GET to `https://community-access.org/git-going-with-github/` returns 200. +2. HTTP GET to `https://community-access.org/git-going-with-github/REGISTER` returns 200 and contains expected content. +3. `workshop-registration.yml` exists in `.github/ISSUE_TEMPLATE/`. +4. Labels `registration`, `duplicate`, and `waitlist` exist. +5. `registration.yml` workflow is active and has a recent successful run. +6. (Live test only) Test issue is submitted, workflow completes, welcome comment posts with assignment links, `registration` label is applied. + +Cleanup limitation: the test issue is closed and locked by the script. It **cannot be deleted via the GitHub API**. If deletion is needed, a repository admin must delete it manually from the Issues tab after this run. Pass criteria: -- Registration workflow executes successfully end to end. -- Output comment/labels match deployed configuration. +- `Test-RegistrationPage.ps1` exits with no failures. +- Live test (if run) confirms workflow executes end to end. +- Output comment and labels match deployed configuration. ## Phase 1 - Registration System QA (Admin Side) @@ -396,19 +514,20 @@ Pass criteria: ### Step 2. Configure registration automation for classroom handoff -Use [REGISTRATION-QUICKSTART.md](REGISTRATION-QUICKSTART.md) for fast entry and [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md) for full details. +If `Initialize-WorkshopSetup.ps1` was run successfully in Phase 0, this step is already complete. Re-verify with: + +```powershell +gh secret list -R Community-Access/git-going-with-github +gh variable list -R Community-Access/git-going-with-github +``` + +Expected output: `CLASSROOM_ORG_ADMIN_TOKEN` appears in secrets, and `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` appear in variables. -1. Create or verify an admin token that can manage organization invitations. -2. In repository settings, add secret `CLASSROOM_ORG_ADMIN_TOKEN`. -3. In repository settings, set variables: - - `CLASSROOM_ORG` - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -4. Save and re-open each setting to confirm there are no leading or trailing spaces. +If any are missing, run `Initialize-WorkshopSetup.ps1` again or follow the manual steps in section B of the setup instructions. For full reference, see [REGISTRATION-QUICKSTART.md](REGISTRATION-QUICKSTART.md) and [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md). Pass criteria: -- Secret exists and is scoped correctly. -- All 3 variables exist and values are correct. +- `CLASSROOM_ORG_ADMIN_TOKEN` secret is present. +- All 3 variables are present with correct values and no leading or trailing spaces. ### Step 3. Execute registration happy-path test @@ -557,63 +676,113 @@ Use full end-to-end mode when preparing major cohort launches or after significa ### Step 10. Create classroom and import roster -Use [classroom/README.md](../classroom/README.md) Steps 1 and 2. +GitHub Classroom assignment creation has **no write API**. All classroom creation and assignment setup must be done through the browser at [classroom.github.com](https://classroom.github.com) while signed in as `accesswatch`. + +Note: a classroom already exists for this repository (`GIT Going with Github`, classroom id 322783, linked to `Community-Access-Classroom`). Unless starting a completely new classroom, skip classroom creation and go directly to assignment creation in Steps 11 and 12. -1. Create a new classroom in `Community-Access`. -2. Name it using `Git Going - [Cohort Name] - [Month Year]`. -3. Import roster using [classroom/roster-template.csv](../classroom/roster-template.csv). -4. Confirm test student appears in roster. +**If you do need a new classroom:** + +1. Go to [classroom.github.com](https://classroom.github.com) as `accesswatch`. +2. Select **New classroom** and choose `Community-Access-Classroom` as the organization. +3. Name it `Git Going - [Cohort Name] - [Month Year]` (for example, `Git Going - May 2026`). +4. Import roster from [classroom/roster-template.csv](../classroom/roster-template.csv) on the Roster tab. +5. Add `accesswatch-student` to the roster as the test student. + +**To verify the existing classroom is accessible:** + +```powershell +gh api /classrooms --jq '.[] | {id, name, url}' +``` Pass criteria: -- Classroom exists and is accessible to facilitators. -- Roster import succeeds with expected usernames. +- Classroom exists in `Community-Access-Classroom` and is accessible to `accesswatch`. +- Roster includes `accesswatch-student` (or the designated test student username). ### Step 11. Create Day 1 assignment exactly -Use [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) and [admin/classroom/day1-assignment-copy-paste.md](classroom/day1-assignment-copy-paste.md). +See section A.1 of the setup steps above for full navigation instructions. + +Use [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) and [admin/classroom/day1-assignment-copy-paste.md](classroom/day1-assignment-copy-paste.md) for the exact title, description, and autograding entries. + +**To check if the Day 1 assignment already exists:** + +```powershell +gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}' +``` + +If the assignment exists and its `invite_link` is already in the `CLASSROOM_DAY1_ASSIGNMENT_URL` variable, skip to Step 12. + +**If creating from scratch:** -1. Create assignment with title `You Belong Here`. -2. Set type `Individual`. -3. Set visibility `Private`. -4. Select template `Community-Access/learning-room-template`. -5. Set `Grant students admin access` to `No`. -6. Set `Enable feedback pull requests` to `Yes`. -7. Paste Day 1 assignment description content. -8. Add Day 1 autograding entries from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md). -9. Confirm Day 1 has 4 tests and total 50 points. -10. Save assignment and copy invite link. +1. Go to [classroom.github.com](https://classroom.github.com) as `accesswatch` and open the `GIT Going with Github` classroom. +2. Select **New assignment**. +3. Title: `You Belong Here` (exact match required) +4. Type: Individual, Visibility: Private +5. Template: `Community-Access/learning-room-template` +6. Grant students admin access: No +7. Enable feedback pull requests: Yes +8. Paste description from [classroom/assignment-day1-you-belong-here.md](../classroom/assignment-day1-you-belong-here.md) +9. Add autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- Day 1 requires exactly 4 tests totaling 50 points +10. Save and copy the invite URL from the assignment page +11. If `Initialize-WorkshopSetup.ps1` has not been run yet, paste the URL into `CLASSROOM_DAY1_ASSIGNMENT_URL`. If it has been run, re-run it to pick up the new URL automatically. Pass criteria: -- Day 1 settings match source files exactly. -- Test count and points are correct. -- Feedback pull request is enabled and visible in assignment configuration. +- Day 1 assignment exists with title `You Belong Here`. +- Test count is 4, total points is 50. +- Feedback pull request is enabled. +- `CLASSROOM_DAY1_ASSIGNMENT_URL` variable matches the assignment invite link. ### Step 12. Create Day 2 assignment exactly -Use [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) and [admin/classroom/day2-assignment-copy-paste.md](classroom/day2-assignment-copy-paste.md). +Same process as Step 11. Check first whether it already exists: -1. Create assignment with title `You Can Build This`. -2. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled). -3. Paste Day 2 assignment description content. -4. Add Day 2 autograding entries from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md). -5. Confirm Day 2 has 6 tests and total 75 points. -6. Save assignment and copy invite link. +```powershell +gh api /classrooms/322783/assignments --jq '.[] | {id, title, invite_link}' +``` + +If the assignment exists and its `invite_link` is already in `CLASSROOM_DAY2_ASSIGNMENT_URL`, skip ahead. + +**If creating from scratch:** + +1. In the `GIT Going with Github` classroom, select **New assignment**. +2. Title: `You Can Build This` (exact match required) +3. Apply same base settings as Day 1 (individual, private, no admin access, feedback PR enabled) +4. Template: `Community-Access/learning-room-template` +5. Paste description from [classroom/assignment-day2-you-can-build-this.md](../classroom/assignment-day2-you-can-build-this.md) +6. Add Day 2 autograding from [admin/classroom/autograding-setup.md](classroom/autograding-setup.md) -- 6 tests totaling 75 points +7. Save and copy the invite URL +8. Update `CLASSROOM_DAY2_ASSIGNMENT_URL` or re-run `Initialize-WorkshopSetup.ps1` to pick it up automatically Pass criteria: -- Day 2 settings match source files exactly. -- Test count and points are correct. -- Feedback pull request is enabled and visible in assignment configuration. +- Day 2 assignment exists with title `You Can Build This`. +- Test count is 6, total points is 75. +- Feedback pull request is enabled. +- `CLASSROOM_DAY2_ASSIGNMENT_URL` variable matches the assignment invite link. ### Step 13. Connect assignment URLs back to registration automation -1. Add or update repository variables: - - `CLASSROOM_DAY1_ASSIGNMENT_URL` - - `CLASSROOM_DAY2_ASSIGNMENT_URL` -2. Re-run one registration test issue (new test account or controlled case). -3. Confirm both links appear in welcome comment. +If `Initialize-WorkshopSetup.ps1` was run after the assignments were created, the variables are already set. Verify with: + +```powershell +gh variable list -R Community-Access/git-going-with-github +``` + +If either URL variable is missing or stale, re-run the setup script. It will resolve URLs directly from the Classroom API: + +```powershell +scripts/classroom/Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere -SkipTemplatePrepare -SkipTemplateValidate +``` + +Then re-run the registration live test to confirm both URLs appear in the welcome comment: + +```powershell +scripts/classroom/Test-RegistrationPage.ps1 -RunLiveTest +``` Pass criteria: -- Registration confirmation comment now includes both assignment URLs. +- `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL` are set and match assignment invite links. +- Registration confirmation comment includes both assignment URLs. +- `Test-RegistrationPage.ps1 -RunLiveTest` exits with no failures. ## Phase 4 - Test Student Acceptance and Seeding (Bridge from Admin to Student) @@ -633,20 +802,34 @@ Pass criteria: ### Step 15. Seed initial challenges and peer simulation -Run commands from repository root. +The test student account is `accesswatch-student`. GitHub Classroom generates repository names from the assignment slug and the student username. For the default assignment titles, the repository names will be: + +- Day 1: `Community-Access-Classroom/you-belong-here-accesswatch-student` +- Day 2: `Community-Access-Classroom/you-can-build-this-accesswatch-student` + +Confirm the exact repository names first: + +```powershell +gh repo list Community-Access-Classroom --json name --jq '.[].name' | Select-String accesswatch-student +``` + +Then seed (replace repository slugs with the confirmed names if different): ```powershell -scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -Challenge 1 -Assignee test-student -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day1 -StudentUsername test-student -scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -Challenge 10 -Assignee test-student -scripts/classroom/Seed-PeerSimulation.ps1 -Repository Community-Access-Classroom/learning-room-test-student-day2 -StudentUsername test-student +$day1 = 'Community-Access-Classroom/you-belong-here-accesswatch-student' +$day2 = 'Community-Access-Classroom/you-can-build-this-accesswatch-student' + +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day1 -Challenge 1 -Assignee accesswatch-student +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day1 -StudentUsername accesswatch-student +scripts/classroom/Seed-LearningRoomChallenge.ps1 -Repository $day2 -Challenge 10 -Assignee accesswatch-student +scripts/classroom/Seed-PeerSimulation.ps1 -Repository $day2 -StudentUsername accesswatch-student ``` -If you use different repository names, replace values accordingly. +Alternatively, if `Initialize-WorkshopSetup.ps1` is run after `accesswatch-student` has accepted both invites, it will detect the repos and seed them automatically. Pass criteria: -- Challenge 1 appears in Day 1 repo. -- Challenge 10 appears in Day 2 repo. +- Challenge 1 issue appears in Day 1 repo assigned to `accesswatch-student`. +- Challenge 10 issue appears in Day 2 repo assigned to `accesswatch-student`. - Peer simulation issues and PR exist in both repos. ## Phase 5 - Curriculum Content QA (Walk every required chapter and appendix) @@ -1336,4 +1519,81 @@ Release Decision: This runbook is the operator-facing execution path that unifies registration, deployment, and end-to-end challenge QA. -It does not replace source documents. It sequences them into one practical checklist so a single facilitator can execute and validate the full system without context switching across multiple folders. \ No newline at end of file +It does not replace source documents. It sequences them into one practical checklist so a single facilitator can execute and validate the full system without context switching across multiple folders. + +## Script Reference + +The following scripts in `scripts/classroom/` are used by this runbook. Run each with `-?` or read the `.SYNOPSIS` block for full parameter documentation. + +| Script | Purpose | Automated | +|---|---|---| +| `Initialize-WorkshopSetup.ps1` | Set PAT secret, variables, labels; sync and validate template; seed test student challenges | All steps except PAT generation (browser required) | +| `Archive-CohortData.ps1` | Export cohort issues, roster, and discussions to `git-going-student-success`; reset source repo | Export and archive are automated; roster reset may require PR due branch rules | +| `Delete-RegistrationIssues.ps1` | Delete registration/duplicate/waitlist issues via GraphQL mutation | Fully automated when user has repo admin and `repo` token scope | +| `Delete-RegistrationIssues-v2.js` | Experimental Playwright UI deleter for issue cleanup | Partially automated; UI selector fragility makes this fallback-only | +| `Test-RegistrationPage.ps1` | Validate Pages site, REGISTER page, issue form, labels, workflow, and live registration flow | All steps; test issue cleanup is close+lock only (see below) | +| `Prepare-LearningRoomTemplate.ps1` | Sync `learning-room/` source into `Community-Access/learning-room-template` | Fully automated | +| `Test-LearningRoomTemplate.ps1` | Create smoke repo from template, validate file inventory and workflow dispatch | Fully automated | +| `Seed-LearningRoomChallenge.ps1` | Seed a specific challenge issue in a student repository | Fully automated | +| `Seed-PeerSimulation.ps1` | Seed peer simulation issues and PR in a student repository | Fully automated | +| `Restore-LearningRoomFiles.ps1` | Restore baseline files into a student repo via recovery branch and PR | Fully automated | +| `Invoke-LearningRoomEndToEndTest.ps1` | Full end-to-end scripted QA harness | Fully automated | + +## GitHub API Limitations + +The following actions cannot be performed via any GitHub API and require manual browser-based action. These are hard platform constraints, not gaps in the scripts. + +| Action | Why it cannot be automated | Manual path | +|---|---|---| +| Creating a Classroom assignment | No write endpoint exists in the GitHub Classroom REST API (confirmed: only GET endpoints are documented and available) | [classroom.github.com](https://classroom.github.com) as `accesswatch` | +| Deleting issues via REST API | GitHub REST API has no DELETE endpoint for issues | Use `scripts/classroom/Delete-RegistrationIssues.ps1` (GraphQL mutation path), or UI fallback | +| Deleting or moving discussions | GraphQL discussion mutations do not include delete or move operations | Discussions tab in GitHub UI -> each thread -> "..." -> Delete | +| Generating a personal access token | PAT generation requires browser authentication by design | [github.com/settings/tokens](https://github.com/settings/tokens) as `accesswatch` | + +## Pre-Cohort Cleanup Procedure + +Run this procedure after each cohort completes, before starting QA for the next cohort. + +### Archive previous cohort data + +```powershell +# Replace the slug with the cohort being archived (format: YYYY-MM-description) +scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort +``` + +What this does: + +1. Exports all registration/duplicate/waitlist issues (with comments) to JSON and CSV. +2. Exports the current `student-roster.json`. +3. Exports discussions to JSON. +4. Pushes the archive to `Community-Access/git-going-student-success` under `admin/cohorts//`. +5. Closes and locks all registration issues in the source repository. +6. Resets `student-roster.json` to the blank template. + +Use `-WhatIf` to preview without making changes: + +```powershell +scripts/classroom/Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort -WhatIf +``` + +After the script completes, two manual cleanup steps are required (API limitation): + +1. **Delete registration issues** -- use the GraphQL cleanup script (recommended): + +```powershell +scripts/classroom/Delete-RegistrationIssues.ps1 +``` + +2. **Delete discussions** -- discussions still require manual deletion: + `https://github.com/Community-Access/git-going-with-github/discussions` + +Archive destination: `https://github.com/Community-Access/git-going-student-success/tree/main/admin/cohorts//` + +### Current Progress Snapshot (2026-05-08) + +- [x] Registration issues archived to `git-going-student-success`. +- [x] Registration issues deleted from source repository (count now 0). +- [x] Discussions deleted from source repository (count now 0). +- [x] Learning Room template sync PR merged: `Community-Access/learning-room-template#11`. +- [ ] Registration secret and variable values set for next cohort (`CLASSROOM_ORG_ADMIN_TOKEN`, `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL`). +- [ ] Day 1 and Day 2 classroom assignments created for next cohort. \ No newline at end of file diff --git a/docs/00-pre-workshop-setup.md b/docs/00-pre-workshop-setup.md index 8fc1b569..3b147ca7 100644 --- a/docs/00-pre-workshop-setup.md +++ b/docs/00-pre-workshop-setup.md @@ -1,3 +1,7 @@ + +**Table: Screen reader options for workshop setup** + +**Table: Accessibility improvements for screen reader users** # Pre-Workshop Setup - GIT Going with GitHub > > **Listen to Episode 1:** [Pre-Workshop Setup](../admin/PODCASTS.md) - a conversational audio overview of this chapter. Listen before reading to preview the concepts, or after to reinforce what you learned. diff --git a/learning-room/.github/DEPLOYMENT_VALIDATION.md b/learning-room/.github/DEPLOYMENT_VALIDATION.md index 764527ce..4f9e5e34 100644 --- a/learning-room/.github/DEPLOYMENT_VALIDATION.md +++ b/learning-room/.github/DEPLOYMENT_VALIDATION.md @@ -1,3 +1,9 @@ + +**Table: Workflow status and triggers for deployment validation** + +**Table: Error handling scenarios in deployment validation** + +**Table: Metrics for deployment validation and student experience** # Learning Room Automation - Deployment Validation Checklist ## Complete Setup Verification diff --git a/learning-room/.github/FACILITATOR_GUIDE.md b/learning-room/.github/FACILITATOR_GUIDE.md index 747b1a23..9e15b7d7 100644 --- a/learning-room/.github/FACILITATOR_GUIDE.md +++ b/learning-room/.github/FACILITATOR_GUIDE.md @@ -1,3 +1,5 @@ + +**Table: Common facilitator questions and responses** # Learning Room Automation - Facilitator Quick Reference ## What Students See diff --git a/learning-room/.github/IMPLEMENTATION_GUIDE.md b/learning-room/.github/IMPLEMENTATION_GUIDE.md index 4657ac89..c7f17f12 100644 --- a/learning-room/.github/IMPLEMENTATION_GUIDE.md +++ b/learning-room/.github/IMPLEMENTATION_GUIDE.md @@ -1,3 +1,7 @@ + +**Table: Learning Room workflow automation and triggers** + +**Table: Key documentation files for Learning Room automation** # Learning Room Template: Implementation Guide > **For full workshop deployment instructions, see diff --git a/learning-room/.github/SETUP_AND_MAINTENANCE.md b/learning-room/.github/SETUP_AND_MAINTENANCE.md index 06be5314..b3c5515c 100644 --- a/learning-room/.github/SETUP_AND_MAINTENANCE.md +++ b/learning-room/.github/SETUP_AND_MAINTENANCE.md @@ -1,3 +1,7 @@ + +**Table: Workflow automation in the Learning Room** + +**Table: Troubleshooting common issues in Learning Room automation** # Learning Room Automation Setup & Maintenance Guide ## Overview diff --git a/learning-room/.github/STUDENT_GUIDE.md b/learning-room/.github/STUDENT_GUIDE.md index 0fe8f47e..4a8203bf 100644 --- a/learning-room/.github/STUDENT_GUIDE.md +++ b/learning-room/.github/STUDENT_GUIDE.md @@ -1,3 +1,5 @@ + +**Table: What the bot does and how it helps students** # How the Learning Room Automation Works - Student Guide ## What Is This Automation? diff --git a/learning-room/README.md b/learning-room/README.md index 085cdee8..f180f949 100644 --- a/learning-room/README.md +++ b/learning-room/README.md @@ -1,3 +1,6 @@ + +**Table: Learning Room folder and file purposes** +**Table: Host voice and character mapping for VibeVoice podcast** # Welcome to the Learning Room This repository is your private Learning Room for the Git Going with GitHub workshop. GitHub Classroom created it from the Learning Room template so you can practice the full contribution workflow safely. In this repository you will: diff --git a/msg.txt b/msg.txt new file mode 100644 index 00000000..3e3dafe4 --- /dev/null +++ b/msg.txt @@ -0,0 +1,5 @@ +fix: add accessible captions to all markdown tables + +- Added captions or descriptions above every markdown table in documentation and guides for accessibility compliance +- Follows WCAG and markdown accessibility best practices + diff --git a/package-lock.json b/package-lock.json index 784113d9..99a0bbf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "git-going-with-github", "version": "1.0.0", "license": "CC-BY-4.0", + "dependencies": { + "playwright": "^1.59.1" + }, "devDependencies": { "chokidar": "^3.5.3", "github-markdown-css": "^5.5.1", @@ -234,6 +237,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index ff197623..0bd8e0d9 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,8 @@ "highlight.js": "^11.9.0", "lunr": "^2.3.9", "marked": "^11.2.0" + }, + "dependencies": { + "playwright": "^1.59.1" } -} \ No newline at end of file +} diff --git a/podcasts/README.md b/podcasts/README.md index 1e5f5097..b7d7379c 100644 --- a/podcasts/README.md +++ b/podcasts/README.md @@ -1,3 +1,9 @@ + +**Table: Host voice and character mapping for VibeVoice podcast** + +**Table: Podcast build and validation commands** + +**Table: Podcast bundle generation steps and costs** # Podcast Audio Pipeline This directory contains the complete pipeline for producing the Git Going with GitHub audio series: 54 companion episodes of two-host conversational content designed for blind and low-vision developers, plus 21 Challenge Coach episodes placed near the chapters they support. diff --git a/podcasts/tts/README.md b/podcasts/tts/README.md index e816f025..41f81a41 100644 --- a/podcasts/tts/README.md +++ b/podcasts/tts/README.md @@ -1,3 +1,5 @@ + +**Table: TTS pipeline outcomes and provided commands** # TTS Setup: Piper and Kokoro This runbook documents the end-to-end local setup for the workshop TTS toolchain. diff --git a/scripts/classroom/Archive-CohortData.ps1 b/scripts/classroom/Archive-CohortData.ps1 new file mode 100644 index 00000000..a1a24a3a --- /dev/null +++ b/scripts/classroom/Archive-CohortData.ps1 @@ -0,0 +1,394 @@ +<# +.SYNOPSIS + Archives cohort student data from Community-Access/git-going-with-github to + Community-Access/git-going-student-success, then resets the source repository + to a clean state for the next cohort. + +.DESCRIPTION + Performs the following in order: + + 1. EXPORT -- Fetches all registration/duplicate/waitlist issues with their comments + and saves them as JSON and CSV to a local staging folder. + 2. EXPORT -- Exports the current student-roster.json to the archive. + 3. EXPORT -- Exports all GitHub Discussions to JSON (see LIMITATIONS). + 4. COMMIT ARCHIVE -- Pushes the staged archive folder to git-going-student-success + under admin/cohorts//. + 5. RESET ISSUES -- Closes and locks all open/closed registration issues in the + source repository (see LIMITATIONS -- issues cannot be deleted via API). + 6. RESET ROSTER -- Resets .github/data/student-roster.json to its blank template + state via a direct commit to main. + + LIMITATIONS (GitHub API hard constraints): + - Issues cannot be deleted via the GitHub REST API. They are closed and locked. + A facilitator must manually delete them from the GitHub UI if deletion is required. + Path: github.com/Community-Access/git-going-with-github/issues -> filter by label + -> open each -> delete via the "..." menu (requires admin on the repository). + - Discussions cannot be moved or deleted via any public API (GraphQL mutations for + discussion management are not available). They are exported to the archive as JSON + only. A facilitator must manually delete them from the Discussions tab in the UI. + - This script does not touch any files tracked in source control other than + student-roster.json. + +.PARAMETER CohortSlug + Short identifier for the cohort being archived, used as the folder name in the + student-success repository. Format: YYYY-MM-description + Example: 2026-03-march-cohort + +.PARAMETER SourceRepo + Source repository slug. Defaults to Community-Access/git-going-with-github. + +.PARAMETER SuccessRepo + Archive destination repository slug. Defaults to Community-Access/git-going-student-success. + +.PARAMETER StagingPath + Local folder for staging archive files before pushing. Defaults to a temp directory. + Deleted after push unless -KeepStaging is specified. + +.PARAMETER KeepStaging + Keep the local staging folder after the archive push completes. + +.PARAMETER WhatIf + Show what would be done without making any changes to GitHub. + +.EXAMPLE + # Archive March 2026 cohort + .\Archive-CohortData.ps1 -CohortSlug 2026-03-march-cohort + +.EXAMPLE + # Preview without making changes + .\Archive-CohortData.ps1 -CohortSlug 2026-05-may-cohort -WhatIf +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^\d{4}-\d{2}-[a-z0-9-]+$')] + [string]$CohortSlug, + + [string]$SourceRepo = 'Community-Access/git-going-with-github', + + [string]$SuccessRepo = 'Community-Access/git-going-student-success', + + [string]$StagingPath = '', + + [switch]$KeepStaging +) + +$ErrorActionPreference = 'Stop' + +function Write-Step { param([string]$m) Write-Host "`n==> $m" -ForegroundColor Cyan } +function Write-Pass { param([string]$m) Write-Host " [PASS] $m" -ForegroundColor Green } +function Write-Warn { param([string]$m) Write-Host " [WARN] $m" -ForegroundColor Yellow } +function Write-Info { param([string]$m) Write-Host " $m" -ForegroundColor Gray } +function Write-Fail { param([string]$m) Write-Host " [FAIL] $m" -ForegroundColor Red } + +# --------------------------------------------------------------------------- +# 0. Preflight +# --------------------------------------------------------------------------- +Write-Step "Preflight checks" + +gh auth status -h github.com 2>&1 | ForEach-Object { Write-Info $_ } +if ($LASTEXITCODE -ne 0) { throw "GitHub CLI not authenticated. Run: gh auth login" } + +$currentUser = (gh api /user --jq '.login' 2>&1).Trim() +Write-Info "Authenticated as: $currentUser" + +# Confirm both repos exist and are accessible +foreach ($r in @($SourceRepo, $SuccessRepo)) { + gh api "repos/$r" --jq '.full_name' 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Cannot access repository: $r" } + Write-Pass "Accessible: $r" +} + +# Set up staging folder +if (-not $StagingPath) { + $StagingPath = Join-Path ([IO.Path]::GetTempPath()) "cohort-archive-$CohortSlug-$(Get-Random)" +} +New-Item -ItemType Directory -Path $StagingPath -Force | Out-Null +Write-Info "Staging path: $StagingPath" + +$archiveDestPath = "admin/cohorts/$CohortSlug" +$issueExportFile = Join-Path $StagingPath "registration-issues.json" +$issueCsvFile = Join-Path $StagingPath "registration-issues.csv" +$rosterFile = Join-Path $StagingPath "student-roster.json" +$discussionFile = Join-Path $StagingPath "discussions.json" +$manifestFile = Join-Path $StagingPath "ARCHIVE-MANIFEST.md" + +# --------------------------------------------------------------------------- +# 1. Export registration issues +# --------------------------------------------------------------------------- +Write-Step "Exporting registration/duplicate/waitlist issues from $SourceRepo" + +$issueLabels = @('registration', 'duplicate', 'waitlist') +$allIssues = @() + +foreach ($label in $issueLabels) { + Write-Info "Fetching issues with label: $label" + $batch = gh issue list -R $SourceRepo --label $label --state all --limit 500 ` + --json number,title,state,createdAt,closedAt,author,labels,body,comments 2>&1 | ConvertFrom-Json + if ($batch) { $allIssues += $batch } +} + +# Deduplicate by issue number +$allIssues = $allIssues | Sort-Object number -Unique +Write-Info "Total issues to archive: $($allIssues.Count)" + +if ($PSCmdlet.ShouldProcess($issueExportFile, "Write issue export JSON")) { + $allIssues | ConvertTo-Json -Depth 10 | Set-Content -Path $issueExportFile -Encoding UTF8 + Write-Pass "Written: registration-issues.json ($($allIssues.Count) issues)" +} + +# CSV with key fields only (no comments, no PII beyond GitHub username) +if ($PSCmdlet.ShouldProcess($issueCsvFile, "Write issue CSV")) { + $allIssues | ForEach-Object { + [PSCustomObject]@{ + number = $_.number + title = $_.title + state = $_.state + labels = ($_.labels | ForEach-Object { $_.name }) -join ';' + author = $_.author.login + createdAt = $_.createdAt + closedAt = $_.closedAt + } + } | Export-Csv -Path $issueCsvFile -NoTypeInformation -Encoding UTF8 + Write-Pass "Written: registration-issues.csv" +} + +# --------------------------------------------------------------------------- +# 2. Export student roster +# --------------------------------------------------------------------------- +Write-Step "Exporting student-roster.json from $SourceRepo" + +$rosterRaw = gh api "repos/$SourceRepo/contents/.github/data/student-roster.json" 2>&1 | ConvertFrom-Json +if ($rosterRaw.content) { + $rosterContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(($rosterRaw.content -replace '\s',''))) + if ($PSCmdlet.ShouldProcess($rosterFile, "Write roster export")) { + Set-Content -Path $rosterFile -Value $rosterContent -Encoding UTF8 + Write-Pass "Written: student-roster.json" + } +} else { + Write-Warn "student-roster.json not found or empty in source repo -- skipping" +} + +# --------------------------------------------------------------------------- +# 3. Export discussions +# --------------------------------------------------------------------------- +Write-Step "Exporting discussions from $SourceRepo (read-only export -- cannot delete via API)" + +$owner = $SourceRepo.Split('/')[0] +$repoName = $SourceRepo.Split('/')[1] + +# Create GraphQL query payload +$graphqlPayload = @{ + query = @' +query { + repository(owner: "OWNER", name: "REPO") { + discussions(first: 100) { + totalCount + nodes { + id + number + title + createdAt + author { login } + category { name } + body + comments(first: 50) { + nodes { + author { login } + body + createdAt + } + } + } + } + } +} +'@ -replace 'OWNER', $owner -replace 'REPO', $repoName +} + +$queryJson = $graphqlPayload | ConvertTo-Json -Depth 10 +$discussions = @() + +try { + $discussionResult = $queryJson | gh api graphql --input - 2>&1 + if ($LASTEXITCODE -eq 0) { + $parsed = $discussionResult | ConvertFrom-Json + if ($parsed.data.repository.discussions.nodes) { + $discussions = $parsed.data.repository.discussions.nodes + } + } else { + Write-Warn "GraphQL request failed. Skipping discussions export." + } +} catch { + Write-Warn "Exception querying discussions: $($_.Exception.Message). Skipping discussions export." +} +Write-Info "Found $($discussions.Count) discussions" + +if ($PSCmdlet.ShouldProcess($discussionFile, "Write discussion export")) { + $discussions | ConvertTo-Json -Depth 10 | Set-Content -Path $discussionFile -Encoding UTF8 + Write-Pass "Written: discussions.json ($($discussions.Count) discussions)" +} + +if ($discussions.Count -gt 0) { + Write-Warn "MANUAL ACTION REQUIRED: Discussions cannot be deleted via the GitHub API." + Write-Warn "After verifying this archive, manually delete each discussion at:" + Write-Warn " https://github.com/$SourceRepo/discussions" + foreach ($d in $discussions) { + Write-Warn " #$($d.number) -- $($d.title)" + } +} + +# --------------------------------------------------------------------------- +# 4. Write archive manifest +# --------------------------------------------------------------------------- +$manifestContent = @" +# Cohort Archive: $CohortSlug + +Archived: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC' -AsUTC) +Archived by: $currentUser +Source: $SourceRepo +Destination: $SuccessRepo/$archiveDestPath + +## Contents + +| File | Description | +|---|---| +| registration-issues.json | Full issue export with comments ($($allIssues.Count) issues) | +| registration-issues.csv | Summary CSV (number, title, state, labels, author, dates) | +| student-roster.json | Roster snapshot from .github/data/student-roster.json | +| discussions.json | Discussions export ($($discussions.Count) threads, read-only -- see note below) | + +## Post-Archive Manual Actions Required + +Issues cannot be deleted via the GitHub REST API. They have been closed and locked +by this script. To fully delete them, a repository admin must delete each one from: + https://github.com/$SourceRepo/issues?q=label%3Aregistration + +Discussions cannot be moved or deleted via any public GitHub API. They must be manually +deleted by a repository admin at: + https://github.com/$SourceRepo/discussions + +"@ + +Set-Content -Path $manifestFile -Value $manifestContent -Encoding UTF8 +Write-Pass "Written: ARCHIVE-MANIFEST.md" + +# --------------------------------------------------------------------------- +# 5. Push archive to git-going-student-success +# --------------------------------------------------------------------------- +Write-Step "Pushing archive to $SuccessRepo under $archiveDestPath" + +$successClone = Join-Path ([IO.Path]::GetTempPath()) "success-clone-$(Get-Random)" +if ($PSCmdlet.ShouldProcess($SuccessRepo, "Clone and push archive")) { + gh repo clone $SuccessRepo $successClone 2>&1 | Out-Null + $destFolder = Join-Path $successClone $archiveDestPath + New-Item -ItemType Directory -Path $destFolder -Force | Out-Null + + Copy-Item -Path (Join-Path $StagingPath '*') -Destination $destFolder -Force + + Push-Location $successClone + try { + git add . 2>&1 | Out-Null + $commitMsg = "archive: $CohortSlug cohort data ($($allIssues.Count) issues, $($discussions.Count) discussions)" + git commit -m $commitMsg 2>&1 | Out-Null + git push 2>&1 | Out-Null + Write-Pass "Archive pushed to $SuccessRepo/$archiveDestPath" + } finally { + Pop-Location + Remove-Item $successClone -Recurse -Force -ErrorAction SilentlyContinue + } +} else { + Write-Info "[WhatIf] Would push: $StagingPath -> $SuccessRepo/$archiveDestPath" +} + +# --------------------------------------------------------------------------- +# 6. Close and lock registration issues in source repo +# --------------------------------------------------------------------------- +Write-Step "Closing and locking registration issues in $SourceRepo" + +$openIssues = $allIssues | Where-Object { $_.state -eq 'OPEN' } +Write-Info "Open issues to close: $($openIssues.Count)" +Write-Info "All issues to lock: $($allIssues.Count)" +Write-Warn "Issues CANNOT be deleted via the GitHub API -- they will be closed and locked only." +Write-Warn "To delete them, a repo admin must do so manually from the GitHub UI." + +foreach ($issue in $openIssues) { + if ($PSCmdlet.ShouldProcess("#$($issue.number)", "Close issue")) { + gh issue close $issue.number -R $SourceRepo --comment "Archived to $SuccessRepo/$archiveDestPath during $CohortSlug cohort reset." 2>&1 | Out-Null + Write-Info " Closed #$($issue.number): $($issue.title)" + } +} + +foreach ($issue in $allIssues) { + if ($PSCmdlet.ShouldProcess("#$($issue.number)", "Lock issue")) { + gh api -X PUT "repos/$SourceRepo/issues/$($issue.number)/lock" -f lock_reason=resolved 2>&1 | Out-Null + } +} +Write-Pass "All $($allIssues.Count) issues closed and locked" + +# --------------------------------------------------------------------------- +# 7. Reset student-roster.json +# --------------------------------------------------------------------------- +Write-Step "Resetting student-roster.json to blank template state" + +$blankRoster = @{ + cohort = "[COHORT_NAME]" + cohortStartDate = "[YYYY-MM-DD]" + cohortEndDate = "[YYYY-MM-DD]" + facilitators = @("facilitator-username") + students = @(@{ + username = "student-github-username" + joinedDate = "YYYY-MM-DD" + mergedPRs = 0 + currentLevel = "beginner" + interests = @() + timezone = "" + }) +} | ConvertTo-Json -Depth 5 + +if ($PSCmdlet.ShouldProcess(".github/data/student-roster.json", "Reset to blank template")) { + $currentSha = (gh api "repos/$SourceRepo/contents/.github/data/student-roster.json" 2>&1 | ConvertFrom-Json).sha + $encodedContent = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($blankRoster)) + + $body = @{ + message = "chore: reset student-roster.json for $CohortSlug cohort archive" + content = $encodedContent + sha = $currentSha + } | ConvertTo-Json + + $body | gh api -X PUT "repos/$SourceRepo/contents/.github/data/student-roster.json" --input - 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Pass "student-roster.json reset to blank template" + } else { + Write-Fail "Failed to reset student-roster.json" + } +} else { + Write-Info "[WhatIf] Would reset .github/data/student-roster.json" +} + +# --------------------------------------------------------------------------- +# Cleanup staging +# --------------------------------------------------------------------------- +if (-not $KeepStaging) { + Remove-Item $StagingPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Info "Staging folder removed" +} else { + Write-Info "Staging folder kept at: $StagingPath" +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +Write-Host "`n=============================" -ForegroundColor White +Write-Host "Cohort archive complete: $CohortSlug" -ForegroundColor Green +Write-Host "" +Write-Host "Archive location: https://github.com/$SuccessRepo/tree/main/$archiveDestPath" -ForegroundColor Cyan +Write-Host "" +Write-Host "MANUAL ACTIONS STILL REQUIRED:" -ForegroundColor Yellow +Write-Host " 1. Delete registration issues (cannot be done via API):" -ForegroundColor Yellow +Write-Host " https://github.com/$SourceRepo/issues?q=label%3Aregistration" -ForegroundColor Yellow +if ($discussions.Count -gt 0) { + Write-Host " 2. Delete $($discussions.Count) discussion thread(s) (cannot be done via API):" -ForegroundColor Yellow + Write-Host " https://github.com/$SourceRepo/discussions" -ForegroundColor Yellow +} diff --git a/scripts/classroom/Delete-RegistrationIssues-v2.js b/scripts/classroom/Delete-RegistrationIssues-v2.js new file mode 100644 index 00000000..6f4cb2bb --- /dev/null +++ b/scripts/classroom/Delete-RegistrationIssues-v2.js @@ -0,0 +1,220 @@ +const { chromium } = require('playwright'); +const { execSync } = require('child_process'); + +/** + * Playwright script to delete locked registration issues via the GitHub UI + * + * Usage: + * node scripts/classroom/Delete-RegistrationIssues-v2.js [--headless] + */ + +const REPO = 'Community-Access/git-going-with-github'; +const ISSUES_URL = `https://github.com/${REPO}/issues?q=label%3Aregistration`; +const HEADLESS = process.argv.includes('--headless'); +const MAX_ARG_INDEX = process.argv.indexOf('--max'); +const MAX_TO_DELETE = MAX_ARG_INDEX > -1 && process.argv[MAX_ARG_INDEX + 1] + ? Number.parseInt(process.argv[MAX_ARG_INDEX + 1], 10) + : null; + +let browser; +let page; +let deletedCount = 0; +let failedCount = 0; + +function log(msg, level = 'info') { + const prefix = { + info: ' ', + pass: ' [PASS] ', + fail: ' [FAIL] ', + warn: ' [WARN] ', + step: '\n==> ' + }[level] || ' '; + console.log(`${prefix}${msg}`); +} + +async function authenticate() { + log('Getting GitHub auth context', 'step'); + try { + const token = execSync('gh auth token', { encoding: 'utf-8' }).trim(); + log('GitHub authentication ready', 'pass'); + return token; + } catch (err) { + throw new Error('GitHub CLI not authenticated. Run: gh auth login'); + } +} + +async function deleteIssue(issueNum) { + try { + log(`Deleting issue #${issueNum}...`); + + const issueUrl = `https://github.com/${REPO}/issues/${issueNum}`; + await page.goto(issueUrl, { waitUntil: 'domcontentloaded', timeout: 15000 }); + + // Wait a moment for dynamic content + await page.waitForTimeout(1000); + + // Look for the actions button (three dots) on the issue + // GitHub usually has this in the top right or in an actions menu + const detailsMenu = await page.locator('details-menu, details > summary').first(); + + // Try clicking on various elements that might open the menu + const actionButtons = [ + 'button[aria-label="Show options"]', + 'button[aria-label="Show more"]', + 'button:has-text("...")', + 'details > summary' + ]; + + let clicked = false; + for (const selector of actionButtons) { + const btn = await page.locator(selector).first(); + if (await btn.isVisible()) { + await btn.click(); + await page.waitForTimeout(500); + clicked = true; + break; + } + } + + if (!clicked) { + log(`No action menu found for #${issueNum}`, 'warn'); + failedCount++; + return false; + } + + // Look for delete option in the menu + const deleteOption = await page.locator('button, a, [role="menuitem"]').filter({ + hasText: /delete|Delete/i + }).first(); + + if (await deleteOption.isVisible()) { + await deleteOption.click(); + await page.waitForTimeout(800); + + // Handle confirmation dialog + const confirmButton = await page.locator('button').filter({ + hasText: /delete|Delete/ + }).filter({ hasText: /confirm/i }).first(); + + if (await confirmButton.isVisible()) { + await confirmButton.click(); + await page.waitForTimeout(1500); + log(`Deleted #${issueNum}`, 'pass'); + deletedCount++; + return true; + } else { + // Try any button that says delete as confirmation + const anyDelete = await page.locator('button').filter({ + hasText: /^Delete$/i + }).first(); + if (await anyDelete.isVisible()) { + await anyDelete.click(); + await page.waitForTimeout(1500); + log(`Deleted #${issueNum}`, 'pass'); + deletedCount++; + return true; + } + } + } + + log(`Could not find delete option for #${issueNum}`, 'warn'); + failedCount++; + return false; + } catch (err) { + log(`Error deleting #${issueNum}: ${err.message}`, 'fail'); + failedCount++; + return false; + } +} + +async function main() { + try { + log('GitHub Issue Deletion via Playwright', 'step'); + log(`Repository: ${REPO}`); + log(`Mode: ${HEADLESS ? 'headless' : 'headed (visible in browser)'}`); + + await authenticate(); + + log('Launching Chromium browser...', 'step'); + browser = await chromium.launch({ headless: HEADLESS }); + page = await browser.newPage(); + + await page.setViewportSize({ width: 1280, height: 720 }); + + log('Navigating to issues page...', 'step'); + log(`URL: ${ISSUES_URL}`); + await page.goto(ISSUES_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); + await page.waitForTimeout(2000); + + log('Scanning for issue links...', 'step'); + const issueLinks = await page.locator(`a[href*="/${REPO}/issues/"]`).all(); + const issueNumbers = new Set(); + + for (const link of issueLinks) { + try { + const href = await link.getAttribute('href'); + const match = href.match(/\/issues\/(\d+)/); + if (match) { + issueNumbers.add(parseInt(match[1], 10)); + } + } catch (e) { + // Skip + } + } + + let issues = Array.from(issueNumbers).sort((a, b) => a - b); + + if (Number.isInteger(MAX_TO_DELETE) && MAX_TO_DELETE > 0) { + issues = issues.slice(0, MAX_TO_DELETE); + log(`Limiting deletion to first ${MAX_TO_DELETE} issue(s) due to --max`, 'warn'); + } + + log(`Found ${issues.length} issue(s) with registration label`, 'pass'); + + if (issues.length === 0) { + log('No issues found to delete.', 'warn'); + return; + } + + console.log('\nIssues to delete:'); + issues.slice(0, 20).forEach(num => console.log(` #${num}`)); + if (issues.length > 20) { + console.log(` ... and ${issues.length - 20} more`); + } + console.log(`\nTotal: ${issues.length} issues\n`); + + log('Starting deletion sequence (browser will be visible)', 'step'); + log('This will take a few minutes. Press Ctrl+C to stop.\n'); + + for (let i = 0; i < issues.length; i++) { + const issueNum = issues[i]; + process.stdout.write(`[${i + 1}/${issues.length}] `); + await deleteIssue(issueNum); + + // Rate limit throttle + await page.waitForTimeout(800); + } + + log('Deletion sequence complete', 'step'); + log(`Successfully deleted: ${deletedCount}`, deletedCount > 0 ? 'pass' : 'warn'); + log(`Failed: ${failedCount}`, failedCount > 0 ? 'fail' : 'pass'); + + } catch (err) { + log(`Fatal error: ${err.message}`, 'fail'); + console.error(err); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + } + } +} + +process.on('SIGINT', async () => { + console.log('\n\nStopping gracefully...'); + if (browser) await browser.close(); + log(`Deleted ${deletedCount} issues before stopping`, 'info'); + process.exit(0); +}); + +main(); diff --git a/scripts/classroom/Delete-RegistrationIssues.js b/scripts/classroom/Delete-RegistrationIssues.js new file mode 100644 index 00000000..0c987432 --- /dev/null +++ b/scripts/classroom/Delete-RegistrationIssues.js @@ -0,0 +1,221 @@ +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +/** + * Playwright script to delete locked registration issues via the GitHub UI + * + * Usage: + * node scripts/classroom/Delete-RegistrationIssues.js [--headless] + * + * Options: + * --headless Run in headless mode (default: headed for visibility) + * + * This script: + * 1. Opens the GitHub issues page filtered by registration label + * 2. Finds all locked issues with the registration label + * 3. Opens each issue + * 4. Clicks the delete button (via "..." menu) + * 5. Confirms deletion + * 6. Repeats until all issues are deleted + * + * Prerequisites: + * - GitHub CLI must be authenticated (script reads token from gh auth token) + * - You must have admin access to the repository + * - Node.js 16+ and Playwright must be installed + */ + +const REPO = 'Community-Access/git-going-with-github'; +const ISSUES_URL = `https://github.com/${REPO}/issues?q=label%3Aregistration`; +const HEADLESS = process.argv.includes('--headless'); + +let browser; +let page; +let deletedCount = 0; +let failedCount = 0; +const failedIssues = []; + +async function log(msg, level = 'info') { + const timestamp = new Date().toLocaleTimeString(); + const prefix = { + info: ' ', + pass: ' [PASS] ', + fail: ' [FAIL] ', + warn: ' [WARN] ', + step: '\n==> ' + }[level] || ' '; + console.log(`${prefix}${msg}`); +} + +async function getIssueNumbers() { + // Wait for the issues table to load + await page.waitForSelector('[role="row"]', { timeout: 10000 }); + + // Get all issue links + const issueLinks = await page.locator('a[href*="/issues/"]').all(); + const issueNumbers = []; + + for (const link of issueLinks) { + const href = await link.getAttribute('href'); + const match = href.match(/\/issues\/(\d+)$/); + if (match) { + const num = parseInt(match[1], 10); + if (!issueNumbers.includes(num)) { + issueNumbers.push(num); + } + } + } + + return issueNumbers.sort((a, b) => a - b); +} + +async function deleteIssue(issueNum) { + try { + log(`Deleting issue #${issueNum}...`); + + // Navigate to the issue + const issueUrl = `https://github.com/${REPO}/issues/${issueNum}`; + await page.goto(issueUrl, { waitUntil: 'networkidle' }); + + // Wait for the page to load + await page.waitForSelector('button[aria-label="Show options"]', { timeout: 5000 }).catch(() => null); + + // Click the "..." (options) button - try multiple selectors + let optionsButton = await page.locator('button[aria-label="Show options"]').first(); + if (!(await optionsButton.isVisible())) { + optionsButton = await page.locator('details-menu button').first(); + } + + if (await optionsButton.isVisible()) { + await optionsButton.click(); + await page.waitForTimeout(500); + + // Look for delete button - it might be in a dropdown menu + const deleteButton = await page.locator('text=/delete|Delete/i').first(); + + if (await deleteButton.isVisible()) { + await deleteButton.click(); + await page.waitForTimeout(500); + + // Confirm the deletion - there might be a confirmation dialog + const confirmButton = await page.locator('button:has-text("Delete")').first(); + if (await confirmButton.isVisible()) { + await confirmButton.click(); + await page.waitForTimeout(1000); + } + + log(`Deleted #${issueNum}`, 'pass'); + deletedCount++; + return true; + } + } + + // Fallback: try to find delete in a different location + const moreButton = await page.locator('summary:has-text("...")').first(); + if (await moreButton.isVisible()) { + await moreButton.click(); + await page.waitForTimeout(500); + + const deleteOpt = await page.locator('button, a').filter({ hasText: /[Dd]elete/ }).first(); + if (await deleteOpt.isVisible()) { + await deleteOpt.click(); + await page.waitForTimeout(500); + + const confirmBtn = await page.locator('button[type="submit"]:has-text("Delete")').first(); + if (await confirmBtn.isVisible()) { + await confirmBtn.click(); + await page.waitForTimeout(1000); + log(`Deleted #${issueNum}`, 'pass'); + deletedCount++; + return true; + } + } + } + + log(`Could not find delete button for #${issueNum}`, 'warn'); + failedCount++; + failedIssues.push(issueNum); + return false; + } catch (err) { + log(`Error deleting #${issueNum}: ${err.message}`, 'fail'); + failedCount++; + failedIssues.push(issueNum); + return false; + } +} + +async function main() { + try { + log('GitHub Issue Deletion via Playwright', 'step'); + log(`Repository: ${REPO}`); + log(`Mode: ${HEADLESS ? 'headless' : 'headed (visible)'}`); + + log('Launching browser...', 'step'); + browser = await chromium.launch({ headless: HEADLESS }); + page = await browser.newPage(); + + // Set viewport for better visibility + await page.setViewportSize({ width: 1280, height: 720 }); + + log('Navigating to issues page...', 'step'); + await page.goto(ISSUES_URL, { waitUntil: 'networkidle' }); + + log('Fetching issue numbers...', 'step'); + const issueNumbers = await getIssueNumbers(); + log(`Found ${issueNumbers.length} issue(s) with registration label`, 'pass'); + + if (issueNumbers.length === 0) { + log('No issues found. Exiting.', 'warn'); + return; + } + + // Show confirmation + console.log('\nIssues to delete:'); + issueNumbers.forEach(num => console.log(` #${num}`)); + console.log(`\nTotal: ${issueNumbers.length} issues`); + console.log('\nStarting deletion process. Browser window will be visible.'); + console.log('Press Ctrl+C to stop at any time.\n'); + + // Deletion loop + log('Starting deletion loop', 'step'); + for (let i = 0; i < issueNumbers.length; i++) { + const issueNum = issueNumbers[i]; + log(`${i + 1}/${issueNumbers.length}`, 'info'); + await deleteIssue(issueNum); + + // Throttle to avoid rate limiting + await page.waitForTimeout(1000); + } + + // Summary + log('Deletion complete', 'step'); + log(`Successful: ${deletedCount}`, 'pass'); + log(`Failed: ${failedCount}`, failedCount > 0 ? 'fail' : 'pass'); + + if (failedIssues.length > 0) { + log('Failed issues:', 'warn'); + failedIssues.forEach(num => console.log(` #${num}`)); + } + + } catch (err) { + log(`Fatal error: ${err.message}`, 'fail'); + console.error(err); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + } + } +} + +// Handle Ctrl+C gracefully +process.on('SIGINT', async () => { + console.log('\n\nInterrupted. Cleaning up...'); + if (browser) { + await browser.close(); + } + log(`Deleted ${deletedCount} issues before stopping`, 'info'); + process.exit(0); +}); + +main(); diff --git a/scripts/classroom/Delete-RegistrationIssues.ps1 b/scripts/classroom/Delete-RegistrationIssues.ps1 new file mode 100644 index 00000000..e17a463c --- /dev/null +++ b/scripts/classroom/Delete-RegistrationIssues.ps1 @@ -0,0 +1,158 @@ +<# +.SYNOPSIS + Deletes locked registration issues using the GitHub GraphQL deleteIssue mutation. + +.DESCRIPTION + Fetches all issues with registration/duplicate/waitlist labels and deletes them + using the GraphQL API (which supports issue deletion, unlike the REST API). + + Prerequisites: + - GitHub CLI must be authenticated with a token that has 'repo' scope + - User must have admin permissions on the repository + + The script batches deletions to avoid rate limiting and provides progress updates. + +.PARAMETER Repo + Repository to clean. Defaults to Community-Access/git-going-with-github. + +.PARAMETER Labels + Issue labels to delete. Defaults to registration, duplicate, waitlist. + +.EXAMPLE + # Delete all registration issues + .\Delete-RegistrationIssues.ps1 +#> + +[CmdletBinding()] +param( + [string]$Repo = 'Community-Access/git-going-with-github', + + [string[]]$Labels = @('registration', 'duplicate', 'waitlist'), + + [int]$MaxToDelete = 0 +) + +$ErrorActionPreference = 'Stop' + +function Write-Step { param([string]$m) Write-Host "`n==> $m" -ForegroundColor Cyan } +function Write-Pass { param([string]$m) Write-Host " [PASS] $m" -ForegroundColor Green } +function Write-Fail { param([string]$m) Write-Host " [FAIL] $m" -ForegroundColor Red } +function Write-Info { param([string]$m) Write-Host " $m" -ForegroundColor Gray } + +# --------------------------------------------------------------------------- +# Preflight +# --------------------------------------------------------------------------- +Write-Step "Preflight checks" + +gh auth status -h github.com 2>&1 | ForEach-Object { Write-Info $_ } +if ($LASTEXITCODE -ne 0) { throw "GitHub CLI not authenticated" } + +$currentUser = (gh api /user --jq '.login' 2>&1).Trim() +Write-Info "Authenticated as: $currentUser" + +# Confirm repo access +gh api "repos/$Repo" --jq '.full_name' 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { throw "Cannot access repository: $Repo" } +Write-Pass "Repository accessible: $Repo" + +# --------------------------------------------------------------------------- +# Fetch all issues with target labels +# --------------------------------------------------------------------------- +Write-Step "Fetching issues with labels: $($Labels -join ', ')" + +$allIssues = @() + +# Build label filter query +foreach ($label in $Labels) { + Write-Info "Fetching issues labeled: $label" + + # Use the issues endpoint with state=all to get closed and open issues + # The endpoint returns paginated results, so we need to fetch all pages + $url = "repos/$Repo/issues?state=all&labels=$label&per_page=100" + $pageNum = 1 + + do { + $pageUrl = "$url&page=$pageNum" + $batch = gh api $pageUrl 2>&1 | ConvertFrom-Json + + if ($LASTEXITCODE -eq 0 -and $batch -and @($batch).Count -gt 0) { + $allIssues += $batch + Write-Info " Found $(@($batch).Count) issue(s) on page $pageNum" + $pageNum++ + } else { + break + } + } while (@($batch).Count -eq 100) +} + +$allIssues = $allIssues | Sort-Object -Property number -Unique + +if ($MaxToDelete -gt 0) { + $allIssues = @($allIssues | Select-Object -First $MaxToDelete) + Write-Info "MaxToDelete applied: processing first $MaxToDelete issue(s) only" +} + +Write-Pass "Total issues to delete: $($allIssues.Count)" + +if ($allIssues.Count -eq 0) { + Write-Info "No issues found. Exiting." + exit 0 +} + +# Display summary +$allIssues | ForEach-Object { Write-Info " #$($_.number) -- $($_.title)" } + +# --------------------------------------------------------------------------- +# Confirm deletion +# --------------------------------------------------------------------------- +Write-Host "" +$confirm = Read-Host "Delete $($allIssues.Count) issue(s)? Type 'yes' to confirm" +if ($confirm -ne 'yes') { + Write-Info "Cancelled." + exit 0 +} + +# --------------------------------------------------------------------------- +# Delete issues via GraphQL +# --------------------------------------------------------------------------- +Write-Step "Deleting issues via GraphQL API" + +$successCount = 0 +$failCount = 0 + +foreach ($issue in $allIssues) { + Write-Info "Deleting #$($issue.number)..." + + $mutation = 'mutation($id:ID!){ deleteIssue(input:{issueId:$id}){ clientMutationId } }' + $result = gh api graphql -f query="$mutation" -F id="$($issue.node_id)" 2>&1 + if ($LASTEXITCODE -eq 0) { + $parsed = $result | ConvertFrom-Json + if ($parsed.data.deleteIssue) { + Write-Pass " Deleted #$($issue.number)" + $successCount++ + } else { + Write-Fail " Failed to delete #$($issue.number): $($parsed.errors[0].message)" + $failCount++ + } + } else { + Write-Fail " API error for #$($issue.number): $result" + $failCount++ + } + + # Rate limit throttle + Start-Sleep -Milliseconds 500 +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +Write-Host "`n=============================" -ForegroundColor White +Write-Host "Deletion complete." -ForegroundColor Green +Write-Host " Successful: $successCount" -ForegroundColor Green +Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { 'Red' } else { 'Green' }) + +if ($failCount -gt 0) { + Write-Host "" + Write-Host "Some issues failed to delete. Check the output above for details." -ForegroundColor Red + exit 1 +} diff --git a/scripts/classroom/Initialize-WorkshopSetup.ps1 b/scripts/classroom/Initialize-WorkshopSetup.ps1 new file mode 100644 index 00000000..396f8c6e --- /dev/null +++ b/scripts/classroom/Initialize-WorkshopSetup.ps1 @@ -0,0 +1,370 @@ +<# +.SYNOPSIS + Automates all configurable workshop setup steps for Community-Access/git-going-with-github. + +.DESCRIPTION + Runs every setup action that does not require a browser: + - Sets CLASSROOM_ORG_ADMIN_TOKEN repository secret + - Sets CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL variables + - Verifies required labels exist (registration, duplicate, waitlist) + - Syncs learning-room source into Community-Access/learning-room-template + - Validates the template with a smoke repository + - Optionally seeds challenges for the test student account + + One step MUST be completed manually before running this script: + 1. Generate a classic PAT at https://github.com/settings/tokens with the admin:org scope. + Pass the token value to -AdminPAT. + + Optionally, create Day 1 and Day 2 assignments at https://classroom.github.com before + running this script. If the assignments already exist, the script will resolve their invite + URLs automatically from the GitHub Classroom API and no URL parameters are needed. + If assignments do not exist yet, the script will tell you what to create and exit cleanly. + +.PARAMETER AdminPAT + The classic GitHub personal access token with admin:org scope generated by accesswatch. + This will be stored as the CLASSROOM_ORG_ADMIN_TOKEN repository secret. + Do not commit this value anywhere. + +.PARAMETER Day1AssignmentUrl + Optional. The GitHub Classroom invite URL for the Day 1 assignment (You Belong Here). + Format: https://classroom.github.com/a/ + If omitted, the script will attempt to resolve it automatically from the Classroom API + by matching an assignment titled 'You Belong Here'. + +.PARAMETER Day2AssignmentUrl + Optional. The GitHub Classroom invite URL for the Day 2 assignment (You Can Build This). + Format: https://classroom.github.com/a/ + If omitted, the script will attempt to resolve it automatically from the Classroom API + by matching an assignment titled 'You Can Build This'. + +.PARAMETER Repo + Repository slug to configure. Defaults to Community-Access/git-going-with-github. + +.PARAMETER ClassroomOrg + The GitHub Classroom organization name. Defaults to Community-Access-Classroom. + +.PARAMETER TestStudentUsername + GitHub username of the test student account. Defaults to accesswatch-student. + When provided, the script offers to seed Day 1 and Day 2 challenges after + test student repos are confirmed to exist. + +.PARAMETER SkipTemplatePrepare + Skip the Prepare-LearningRoomTemplate.ps1 step. Use when you know the template is + already current and want to run only configuration steps. + +.PARAMETER SkipTemplateValidate + Skip the Test-LearningRoomTemplate.ps1 smoke validation step. + +.PARAMETER SkipSeed + Skip the challenge seeding step even when test student repos exist. + +.EXAMPLE + # Minimal -- assignment URLs resolved automatically from Classroom API + .\Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere + +.EXAMPLE + # Provide URLs explicitly (e.g. if auto-resolution title matching fails) + .\Initialize-WorkshopSetup.ps1 ` + -AdminPAT ghp_yourTokenHere ` + -Day1AssignmentUrl https://classroom.github.com/a/abc123 ` + -Day2AssignmentUrl https://classroom.github.com/a/def456 + +.EXAMPLE + # Skip template steps (already up to date) + .\Initialize-WorkshopSetup.ps1 ` + -AdminPAT ghp_yourTokenHere ` + -SkipTemplatePrepare -SkipTemplateValidate +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^ghp_[A-Za-z0-9]+$')] + [string]$AdminPAT, + + [ValidatePattern('^https://classroom\.github\.com/a/[A-Za-z0-9_-]+$')] + [string]$Day1AssignmentUrl, + + [ValidatePattern('^https://classroom\.github\.com/a/[A-Za-z0-9_-]+$')] + [string]$Day2AssignmentUrl, + + [string]$Repo = 'Community-Access/git-going-with-github', + + [string]$ClassroomOrg = 'Community-Access-Classroom', + + [string]$TestStudentUsername = 'accesswatch-student', + + [switch]$SkipTemplatePrepare, + + [switch]$SkipTemplateValidate, + + [switch]$SkipSeed +) + +$ErrorActionPreference = 'Stop' +$scriptRoot = $PSScriptRoot + +function Write-Step { + param([string]$Message) + Write-Host "`n==> $Message" -ForegroundColor Cyan +} + +function Write-Pass { + param([string]$Message) + Write-Host " [PASS] $Message" -ForegroundColor Green +} + +function Write-Fail { + param([string]$Message) + Write-Host " [FAIL] $Message" -ForegroundColor Red +} + +function Write-Info { + param([string]$Message) + Write-Host " $Message" -ForegroundColor Gray +} + +$failures = [System.Collections.Generic.List[string]]::new() + +# --------------------------------------------------------------------------- +# Step 0: Confirm gh CLI and auth +# --------------------------------------------------------------------------- +Write-Step "Confirming GitHub CLI authentication" +gh auth status -h github.com 2>&1 | ForEach-Object { Write-Info $_ } +if ($LASTEXITCODE -ne 0) { + throw "GitHub CLI is not authenticated. Run: gh auth login -h github.com" +} + +$currentUser = (gh api /user --jq '.login' 2>&1).Trim() +Write-Info "Authenticated as: $currentUser" + +# --------------------------------------------------------------------------- +# Step 0.1: Resolve assignment URLs from Classroom API if not supplied +# --------------------------------------------------------------------------- +Write-Step "Resolving classroom assignment URLs" + +$classroomId = $null +$classrooms = gh api /classrooms --jq '.[]' 2>&1 | ConvertFrom-Json -ErrorAction SilentlyContinue +if (-not $classrooms) { + Write-Info "No classrooms found via API or API returned an error." +} else { + foreach ($c in @($classrooms)) { + if ($c.url -match $ClassroomOrg -or $c.organization.login -eq $ClassroomOrg) { + $classroomId = $c.id + Write-Info "Found classroom: '$($c.name)' (id: $classroomId) linked to $ClassroomOrg" + break + } + } +} + +if ($classroomId) { + $assignments = gh api "/classrooms/$classroomId/assignments" 2>&1 | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($assignments -and @($assignments).Count -gt 0) { + Write-Info "Found $(@($assignments).Count) assignment(s) in classroom:" + foreach ($a in @($assignments)) { + Write-Info " - '$($a.title)' => $($a.invite_link)" + } + + if (-not $Day1AssignmentUrl) { + $day1Match = @($assignments) | Where-Object { $_.title -match 'You Belong Here|Day.?1' } | Select-Object -First 1 + if ($day1Match) { + $Day1AssignmentUrl = $day1Match.invite_link + Write-Pass "Day 1 URL resolved from API: $Day1AssignmentUrl" + } else { + Write-Info "Could not auto-match Day 1 assignment by title. Provide -Day1AssignmentUrl manually." + Write-Info "Available titles: $((@($assignments) | ForEach-Object { $_.title }) -join ', ')" + } + } else { + Write-Info "Day 1 URL provided via parameter: $Day1AssignmentUrl" + } + + if (-not $Day2AssignmentUrl) { + $day2Match = @($assignments) | Where-Object { $_.title -match 'You Can Build This|Day.?2' } | Select-Object -First 1 + if ($day2Match) { + $Day2AssignmentUrl = $day2Match.invite_link + Write-Pass "Day 2 URL resolved from API: $Day2AssignmentUrl" + } else { + Write-Info "Could not auto-match Day 2 assignment by title. Provide -Day2AssignmentUrl manually." + Write-Info "Available titles: $((@($assignments) | ForEach-Object { $_.title }) -join ', ')" + } + } else { + Write-Info "Day 2 URL provided via parameter: $Day2AssignmentUrl" + } + } else { + Write-Info "No assignments found in classroom yet. You must create them manually at:" + Write-Info " https://classroom.github.com" + Write-Info "Then re-run this script, or pass -Day1AssignmentUrl and -Day2AssignmentUrl." + } +} else { + Write-Info "Could not find a classroom linked to '$ClassroomOrg' via the API." + Write-Info "Classrooms available: $(if ($classrooms) { (@($classrooms) | ForEach-Object { $_.name }) -join ', ' } else { 'none' })" +} + +if (-not $Day1AssignmentUrl -or -not $Day2AssignmentUrl) { + Write-Fail "One or both assignment URLs could not be resolved. Cannot set variables without them." + Write-Host "" + Write-Host "Create the missing assignments at https://classroom.github.com using classroom id $classroomId," -ForegroundColor Yellow + Write-Host "then re-run this script. Assignment names expected:" -ForegroundColor Yellow + Write-Host " Day 1: 'You Belong Here'" -ForegroundColor Yellow + Write-Host " Day 2: 'You Can Build This'" -ForegroundColor Yellow + exit 1 +} + + +# --------------------------------------------------------------------------- +Write-Step "Setting CLASSROOM_ORG_ADMIN_TOKEN secret on $Repo" +$AdminPAT | gh secret set CLASSROOM_ORG_ADMIN_TOKEN -R $Repo --body - +if ($LASTEXITCODE -eq 0) { + Write-Pass "CLASSROOM_ORG_ADMIN_TOKEN set" +} else { + Write-Fail "Failed to set CLASSROOM_ORG_ADMIN_TOKEN" + $failures.Add("CLASSROOM_ORG_ADMIN_TOKEN secret") +} + +# --------------------------------------------------------------------------- +# Step 2: Set repository variables +# --------------------------------------------------------------------------- +Write-Step "Setting repository variables on $Repo" + +$variablesToSet = @( + @{ Name = 'CLASSROOM_ORG'; Value = $ClassroomOrg }, + @{ Name = 'CLASSROOM_DAY1_ASSIGNMENT_URL'; Value = $Day1AssignmentUrl }, + @{ Name = 'CLASSROOM_DAY2_ASSIGNMENT_URL'; Value = $Day2AssignmentUrl } +) + +foreach ($v in $variablesToSet) { + gh variable set $v.Name --body $v.Value -R $Repo + if ($LASTEXITCODE -eq 0) { + Write-Pass "$($v.Name) = $($v.Value)" + } else { + Write-Fail "Failed to set $($v.Name)" + $failures.Add($v.Name) + } +} + +# --------------------------------------------------------------------------- +# Step 3: Verify required labels +# --------------------------------------------------------------------------- +Write-Step "Verifying required labels on $Repo" + +$requiredLabels = @('registration', 'duplicate', 'waitlist') +$existingLabels = gh label list -R $Repo --json name --jq '.[].name' 2>&1 +foreach ($label in $requiredLabels) { + if ($existingLabels -contains $label) { + Write-Pass "Label '$label' exists" + } else { + Write-Fail "Label '$label' is missing -- creating it" + gh label create $label -R $Repo --color '#ededed' --description "Workshop label: $label" + if ($LASTEXITCODE -eq 0) { + Write-Pass "Label '$label' created" + } else { + $failures.Add("label: $label") + } + } +} + +# --------------------------------------------------------------------------- +# Step 4: Verify confirmed variable values by re-reading them +# --------------------------------------------------------------------------- +Write-Step "Re-reading variables to confirm values have no leading/trailing spaces" + +foreach ($v in $variablesToSet) { + $readBack = (gh variable get $v.Name -R $Repo 2>&1).Trim() + if ($readBack -eq $v.Value) { + Write-Pass "$($v.Name) confirmed: $readBack" + } else { + Write-Fail "$($v.Name) mismatch: expected '$($v.Value)', got '$readBack'" + $failures.Add("$($v.Name) value mismatch on read-back") + } +} + +# --------------------------------------------------------------------------- +# Step 5: Template preparation +# --------------------------------------------------------------------------- +if (-not $SkipTemplatePrepare) { + Write-Step "Syncing learning-room source into Community-Access/learning-room-template" + Write-Info "Running Prepare-LearningRoomTemplate.ps1..." + & "$scriptRoot\Prepare-LearningRoomTemplate.ps1" -Owner 'Community-Access' -TemplateRepo 'learning-room-template' + if ($LASTEXITCODE -eq 0) { + Write-Pass "Template preparation complete" + } else { + Write-Fail "Template preparation reported errors -- review output above" + $failures.Add("Prepare-LearningRoomTemplate") + } +} else { + Write-Info "Skipping template preparation (-SkipTemplatePrepare)" +} + +# --------------------------------------------------------------------------- +# Step 6: Template smoke validation +# --------------------------------------------------------------------------- +if (-not $SkipTemplateValidate) { + Write-Step "Running template smoke validation against Community-Access/learning-room-template" + Write-Info "Running Test-LearningRoomTemplate.ps1..." + & "$scriptRoot\Test-LearningRoomTemplate.ps1" -Owner 'Community-Access' -TemplateRepo 'learning-room-template' + if ($LASTEXITCODE -eq 0) { + Write-Pass "Template smoke validation complete" + } else { + Write-Fail "Template smoke validation reported errors -- review output above" + $failures.Add("Test-LearningRoomTemplate") + } +} else { + Write-Info "Skipping template smoke validation (-SkipTemplateValidate)" +} + +# --------------------------------------------------------------------------- +# Step 7: Seed challenges for test student (if repos exist) +# --------------------------------------------------------------------------- +if (-not $SkipSeed -and $TestStudentUsername) { + Write-Step "Checking for test student repositories to seed" + + $day1Repo = "$ClassroomOrg/you-belong-here-$TestStudentUsername" + $day2Repo = "$ClassroomOrg/you-can-build-this-$TestStudentUsername" + + $day1Exists = (gh api "repos/$day1Repo" --jq '.name' 2>&1) -notmatch 'Not Found' + $day2Exists = (gh api "repos/$day2Repo" --jq '.name' 2>&1) -notmatch 'Not Found' + + if ($day1Exists) { + Write-Info "Day 1 repo found: $day1Repo -- seeding Challenge 1" + & "$scriptRoot\Seed-LearningRoomChallenge.ps1" -Repository $day1Repo -Challenge 1 -Assignee $TestStudentUsername + & "$scriptRoot\Seed-PeerSimulation.ps1" -Repository $day1Repo -StudentUsername $TestStudentUsername + } else { + Write-Info "Day 1 test repo ($day1Repo) not found -- skipping seed." + Write-Info "Have $TestStudentUsername accept the Day 1 invite URL first:" + Write-Info " $Day1AssignmentUrl" + Write-Info "Then re-run with: .\Initialize-WorkshopSetup.ps1 ... (or run Seed-LearningRoomChallenge.ps1 directly)" + } + + if ($day2Exists) { + Write-Info "Day 2 repo found: $day2Repo -- seeding Challenge 10" + & "$scriptRoot\Seed-LearningRoomChallenge.ps1" -Repository $day2Repo -Challenge 10 -Assignee $TestStudentUsername + & "$scriptRoot\Seed-PeerSimulation.ps1" -Repository $day2Repo -StudentUsername $TestStudentUsername + } else { + Write-Info "Day 2 test repo ($day2Repo) not found -- skipping seed." + Write-Info "Have $TestStudentUsername accept the Day 2 invite URL first:" + Write-Info " $Day2AssignmentUrl" + } +} else { + Write-Info "Skipping seed step" +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +Write-Host "`n=============================" -ForegroundColor White +if ($failures.Count -eq 0) { + Write-Host "Setup complete. No failures." -ForegroundColor Green + Write-Host "" + Write-Host "What still requires manual action:" -ForegroundColor Yellow + Write-Host " 1. Confirm registration workflow is enabled in Actions tab:" -ForegroundColor Yellow + Write-Host " https://github.com/$Repo/actions" -ForegroundColor Yellow + Write-Host " 2. Submit one test registration issue from $TestStudentUsername to confirm end-to-end flow." -ForegroundColor Yellow +} else { + Write-Host "Setup completed with $($failures.Count) failure(s):" -ForegroundColor Red + foreach ($f in $failures) { + Write-Host " - $f" -ForegroundColor Red + } + Write-Host "" + Write-Host "Resolve failures before proceeding to registration testing." -ForegroundColor Red + exit 1 +} diff --git a/scripts/classroom/Invoke-LearningRoomEndToEndTest.ps1 b/scripts/classroom/Invoke-LearningRoomEndToEndTest.ps1 index 54be43c4..e91dcc8b 100644 --- a/scripts/classroom/Invoke-LearningRoomEndToEndTest.ps1 +++ b/scripts/classroom/Invoke-LearningRoomEndToEndTest.ps1 @@ -36,6 +36,11 @@ param( ) $ErrorActionPreference = 'Stop' +# Prevent PowerShell from converting native stderr output into terminating errors. +# Git and gh often emit progress text on stderr even when commands succeed. +if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false +} $script:Results = [System.Collections.Generic.List[object]]::new() $script:CreatedRepository = $false @@ -149,7 +154,10 @@ function Invoke-CheckedCommand { # Generic command execution with special transient-retry support for gh commands. for ($attempt = 1; $attempt -le $Retries; $attempt++) { if ($FilePath -eq 'gh') { + $oldErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' $output = & gh @Arguments 2>&1 + $ErrorActionPreference = $oldErrorActionPreference $exitCode = $LASTEXITCODE $text = ($output | Out-String).Trim() @@ -191,7 +199,10 @@ function Invoke-GhJson { ) for ($attempt = 1; $attempt -le $Retries; $attempt++) { + $oldErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' $output = & gh @Arguments 2>&1 + $ErrorActionPreference = $oldErrorActionPreference $exitCode = $LASTEXITCODE $text = ($output | Out-String).Trim() diff --git a/scripts/classroom/Prepare-LearningRoomTemplate.ps1 b/scripts/classroom/Prepare-LearningRoomTemplate.ps1 index 4adb3c0a..24a3f4a2 100644 --- a/scripts/classroom/Prepare-LearningRoomTemplate.ps1 +++ b/scripts/classroom/Prepare-LearningRoomTemplate.ps1 @@ -2,13 +2,20 @@ param( [string]$Owner = 'Community-Access', [string]$TemplateRepo = 'learning-room-template', - [string]$SourcePath = (Join-Path $PSScriptRoot '..\..\learning-room'), + [string]$SourcePath = '', [string]$BranchName = '', - [switch]$NoPush + [switch]$NoPush, + [switch]$SkipSourceValidation ) $ErrorActionPreference = 'Stop' +$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } + +if (-not $SourcePath) { + $SourcePath = Join-Path $scriptDir '..\..\learning-room' +} + function Invoke-CheckedCommand { param([string]$FilePath, [string[]]$Arguments) & $FilePath @Arguments @@ -25,6 +32,19 @@ if (-not $BranchName) { $BranchName = 'sync/learning-room-template-' + (Get-Date -Format 'yyyyMMddHHmmss') } +if (-not $SkipSourceValidation) { + $validatorPath = Join-Path $scriptDir 'Validate-LearningRoomTemplateSource.ps1' + if (-not (Test-Path -LiteralPath $validatorPath -PathType Leaf)) { + throw "Source validator script was not found: $validatorPath" + } + + Write-Host "Validating Learning Room source before sync..." + & $validatorPath -SourcePath $source.Path +} +else { + Write-Warning "SkipSourceValidation was set. Proceeding without source sanity checks." +} + Write-Host "Checking GitHub CLI authentication..." Invoke-CheckedCommand gh @('auth', 'status', '-h', 'github.com') @@ -56,6 +76,22 @@ try { Push-Location $clonePath try { + $gitEmail = (git config user.email 2>$null) + $gitName = (git config user.name 2>$null) + if (-not $gitEmail -or -not $gitName) { + $login = (& gh api /user --jq '.login' 2>$null) + if (-not $login) { + $login = 'github-actions' + } + $fallbackEmail = "$login@users.noreply.github.com" + if (-not $gitName) { + Invoke-CheckedCommand git @('config', 'user.name', $login) + } + if (-not $gitEmail) { + Invoke-CheckedCommand git @('config', 'user.email', $fallbackEmail) + } + } + Invoke-CheckedCommand git @('add', '-A') $status = git status --short if (-not $status) { @@ -73,8 +109,14 @@ try { Invoke-CheckedCommand git @('commit', '-m', 'chore: sync learning room template') Invoke-CheckedCommand git @('push', '--force', '-u', 'origin', "HEAD:$BranchName") - $existingPr = & gh pr view $BranchName -R $fullRepo --json url --jq .url 2>$null - if ($LASTEXITCODE -eq 0 -and $existingPr) { + $existingPr = $null + try { + $existingPr = & gh pr view $BranchName -R $fullRepo --json url --jq .url 2>$null + } + catch { + $existingPr = $null + } + if ($existingPr) { Write-Host "Updated existing pull request: $existingPr" } else { @@ -99,75 +141,3 @@ finally { } Write-Host "Learning Room template is synced and ready." -[CmdletBinding(SupportsShouldProcess)] -param( - [string]$Owner = 'Community-Access', - [string]$TemplateRepo = 'learning-room-template', - [string]$SourcePath = (Join-Path $PSScriptRoot '..\..\learning-room'), - [switch]$NoPush -) - -$ErrorActionPreference = 'Stop' - -function Invoke-CheckedCommand { - param([string]$FilePath, [string[]]$Arguments) - & $FilePath @Arguments - if ($LASTEXITCODE -ne 0) { - throw "Command failed: $FilePath $($Arguments -join ' ')" - } -} - -$fullRepo = "$Owner/$TemplateRepo" -$source = Resolve-Path $SourcePath -$workRoot = Join-Path ([IO.Path]::GetTempPath()) ("learning-room-template-sync-" + [guid]::NewGuid()) -$clonePath = Join-Path $workRoot $TemplateRepo - -Write-Host "Checking GitHub CLI authentication..." -Invoke-CheckedCommand gh @('auth', 'status', '-h', 'github.com') - -Write-Host "Ensuring template repository settings..." -Invoke-CheckedCommand gh @('repo', 'edit', $fullRepo, '--enable-issues=true', '--enable-wiki=false', '--template') -Invoke-CheckedCommand gh @('api', "repos/$fullRepo/actions/permissions/workflow", '-X', 'PUT', '-f', 'default_workflow_permissions=write', '-F', 'can_approve_pull_request_reviews=true') - -New-Item -ItemType Directory -Path $workRoot | Out-Null -try { - Write-Host "Cloning $fullRepo..." - Invoke-CheckedCommand gh @('repo', 'clone', $fullRepo, $clonePath, '--', '--depth', '1') - - Write-Host "Replacing template contents from $source..." - Get-ChildItem -LiteralPath $clonePath -Force | Where-Object { $_.Name -ne '.git' } | Remove-Item -Recurse -Force - Get-ChildItem -LiteralPath $source -Force | Where-Object { $_.Name -notin @('.git', 'node_modules') } | ForEach-Object { - Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $clonePath $_.Name) -Recurse -Force - } - - Push-Location $clonePath - try { - Invoke-CheckedCommand git @('add', '-A') - $status = git status --short - if (-not $status) { - Write-Host "No template changes to commit." - return - } - - Write-Host "Template changes detected:" - $status | ForEach-Object { Write-Host $_ } - - if ($NoPush) { - Write-Host "NoPush was set. Leaving changes uncommitted in $clonePath for inspection." - return - } - - Invoke-CheckedCommand git @('commit', '-m', 'chore: sync learning room template') - Invoke-CheckedCommand git @('push', 'origin', 'HEAD:main') - } - finally { - Pop-Location - } -} -finally { - if (-not $NoPush -and (Test-Path $workRoot)) { - Remove-Item -LiteralPath $workRoot -Recurse -Force - } -} - -Write-Host "Learning Room template is synced and ready." diff --git a/scripts/classroom/Test-LearningRoomTemplate.ps1 b/scripts/classroom/Test-LearningRoomTemplate.ps1 index b2d361e5..8ebc6d63 100644 --- a/scripts/classroom/Test-LearningRoomTemplate.ps1 +++ b/scripts/classroom/Test-LearningRoomTemplate.ps1 @@ -16,6 +16,38 @@ function Invoke-CheckedCommand { } } +function Wait-ForRepositoryContent { + param( + [string]$Repository, + [int]$MaxAttempts = 12, + [int]$DelaySeconds = 5 + ) + + for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + $isReady = $false + try { + & gh api "repos/$Repository/contents/.github/workflows/student-progression.yml" 1>$null 2>$null + if ($LASTEXITCODE -eq 0) { + $isReady = $true + } + } + catch { + $isReady = $false + } + + if ($isReady) { + return + } + + if ($attempt -lt $MaxAttempts) { + Write-Host "Template contents not ready yet (attempt $attempt/$MaxAttempts). Retrying in $DelaySeconds seconds..." + Start-Sleep -Seconds $DelaySeconds + } + } + + throw "Repository content did not become available in time for $Repository." +} + $template = "$Owner/$TemplateRepo" $smoke = "$Owner/$SmokeRepo" @@ -23,6 +55,9 @@ Write-Host "Creating smoke-test repository $smoke from $template..." Invoke-CheckedCommand gh @('repo', 'create', $smoke, '--private', '--template', $template) try { + Write-Host "Waiting for template content to materialize in smoke repository..." + Wait-ForRepositoryContent -Repository $smoke + Write-Host "Checking expected template files..." Invoke-CheckedCommand gh @('api', "repos/$smoke/contents/.github/workflows/student-progression.yml") Invoke-CheckedCommand gh @('api', "repos/$smoke/contents/.github/ISSUE_TEMPLATE/challenge-01-find-your-way.yml") @@ -37,6 +72,10 @@ try { finally { if (-not $KeepSmokeRepo) { Write-Host "Deleting smoke-test repository $smoke..." - Invoke-CheckedCommand gh @('repo', 'delete', $smoke, '--yes') + & gh repo delete $smoke --yes + if ($LASTEXITCODE -ne 0) { + Write-Warning "Could not delete smoke repository automatically. This does not invalidate smoke validation." + Write-Warning "Delete manually when convenient: gh repo delete $smoke --yes" + } } } diff --git a/scripts/classroom/Test-RegistrationPage.ps1 b/scripts/classroom/Test-RegistrationPage.ps1 new file mode 100644 index 00000000..2c56b12a --- /dev/null +++ b/scripts/classroom/Test-RegistrationPage.ps1 @@ -0,0 +1,323 @@ +<# +.SYNOPSIS + Validates that the workshop registration page is deployed and the registration + workflow functions end to end. Closes and locks the test issue after verification. + +.DESCRIPTION + Performs the following checks: + + 1. SITE CHECK -- HTTP GET to the deployed Pages site homepage. + 2. REGISTER PAGE CHECK -- HTTP GET to the /REGISTER page and verifies expected content. + 3. ISSUE FORM CHECK -- Confirms workshop-registration.yml exists in the repository. + 4. LABELS CHECK -- Confirms registration, duplicate, and waitlist labels exist. + 5. WORKFLOW CHECK -- Confirms registration.yml is enabled and has a recent success. + 6. LIVE TEST (optional) -- Submits a test registration issue as a configured test + account, waits for the workflow to complete, inspects the welcome comment, + then closes and locks the test issue. + + LIMITATIONS (GitHub API hard constraints): + - Test issues submitted during live testing cannot be deleted via the GitHub REST API. + The script closes and locks the issue. A repository admin must delete it manually if + desired at: https://github.com/Community-Access/git-going-with-github/issues + - This script cannot test the org-invite step of registration unless the test account + is not already a member of Community-Access-Classroom. + +.PARAMETER Repo + Repository to validate. Defaults to Community-Access/git-going-with-github. + +.PARAMETER SiteBaseUrl + Base URL for the deployed GitHub Pages site. + Defaults to https://community-access.org/git-going-with-github + +.PARAMETER RunLiveTest + Submit a live test registration issue and verify the workflow output. + Requires -TestIssueTitle to be unique enough not to match existing issues. + +.PARAMETER TestIssueTitle + Title prefix for the test registration issue. + Defaults to "[REGISTER] QA Test - Do Not Process" + +.PARAMETER WorkflowTimeoutSeconds + Seconds to wait for the registration workflow to complete during live test. + Defaults to 120. + +.EXAMPLE + # Site and config checks only (no live issue submission) + .\Test-RegistrationPage.ps1 + +.EXAMPLE + # Full live test including issue submission and workflow verification + .\Test-RegistrationPage.ps1 -RunLiveTest + +.EXAMPLE + # Full live test with custom timeout + .\Test-RegistrationPage.ps1 -RunLiveTest -WorkflowTimeoutSeconds 180 +#> + +[CmdletBinding()] +param( + [string]$Repo = 'Community-Access/git-going-with-github', + + [string]$SiteBaseUrl = 'https://community-access.org/git-going-with-github', + + [switch]$RunLiveTest, + + [string]$TestIssueTitle = '[REGISTER] QA Test - Do Not Process', + + [int]$WorkflowTimeoutSeconds = 120 +) + +$ErrorActionPreference = 'Stop' + +function Write-Step { param([string]$m) Write-Host "`n==> $m" -ForegroundColor Cyan } +function Write-Pass { param([string]$m) Write-Host " [PASS] $m" -ForegroundColor Green } +function Write-Fail { param([string]$m) Write-Host " [FAIL] $m" -ForegroundColor Red } +function Write-Warn { param([string]$m) Write-Host " [WARN] $m" -ForegroundColor Yellow } +function Write-Info { param([string]$m) Write-Host " $m" -ForegroundColor Gray } + +$failures = [System.Collections.Generic.List[string]]::new() +$warnings = [System.Collections.Generic.List[string]]::new() + +# --------------------------------------------------------------------------- +# 0. Auth check +# --------------------------------------------------------------------------- +Write-Step "GitHub CLI authentication" +gh auth status -h github.com 2>&1 | ForEach-Object { Write-Info $_ } +if ($LASTEXITCODE -ne 0) { throw "GitHub CLI not authenticated. Run: gh auth login" } + +$currentUser = (gh api /user --jq '.login' 2>&1).Trim() +Write-Info "Authenticated as: $currentUser" + +# --------------------------------------------------------------------------- +# 1. Site homepage check +# --------------------------------------------------------------------------- +Write-Step "Checking Pages site homepage: $SiteBaseUrl" + +try { + $resp = Invoke-WebRequest -Uri $SiteBaseUrl -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 15 + if ($resp.StatusCode -eq 200) { + Write-Pass "Site homepage responded HTTP $($resp.StatusCode)" + } else { + Write-Warn "Site homepage returned HTTP $($resp.StatusCode)" + $warnings.Add("Site homepage HTTP $($resp.StatusCode)") + } +} catch { + Write-Warn "Site homepage request failed: $($_.Exception.Message)" + $warnings.Add("Site homepage unreachable: $($_.Exception.Message)") +} + +# --------------------------------------------------------------------------- +# 2. REGISTER page check +# --------------------------------------------------------------------------- +$registerUrl = "$SiteBaseUrl/REGISTER" +Write-Step "Checking REGISTER page: $registerUrl" + +try { + $regResp = Invoke-WebRequest -Uri $registerUrl -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 15 + if ($regResp.StatusCode -eq 200) { + Write-Pass "REGISTER page responded HTTP $($regResp.StatusCode)" + + # Check for key content markers in the rendered page + $body = $regResp.Content + if ($body -match 'register|registration|Register') { + Write-Pass "REGISTER page contains expected registration content" + } else { + Write-Warn "REGISTER page loaded but expected content markers not found" + $warnings.Add("REGISTER page content markers not found") + } + + if ($body -match 'classroom\.github\.com|github\.com/Community-Access') { + Write-Pass "REGISTER page contains expected GitHub links" + } else { + Write-Warn "REGISTER page loaded but GitHub assignment/classroom links not detected" + $warnings.Add("REGISTER page GitHub links not detected") + } + } else { + Write-Fail "REGISTER page returned HTTP $($regResp.StatusCode)" + $failures.Add("REGISTER page HTTP $($regResp.StatusCode)") + } +} catch { + Write-Fail "REGISTER page request failed: $($_.Exception.Message)" + $failures.Add("REGISTER page unreachable: $($_.Exception.Message)") +} + +# --------------------------------------------------------------------------- +# 3. Issue form template check +# --------------------------------------------------------------------------- +Write-Step "Checking workshop-registration.yml issue form template" + +$formExists = gh api "repos/$Repo/contents/.github/ISSUE_TEMPLATE/workshop-registration.yml" --jq '.name' 2>&1 +if ($LASTEXITCODE -eq 0 -and $formExists -match 'workshop-registration') { + Write-Pass "workshop-registration.yml exists in .github/ISSUE_TEMPLATE/" +} else { + Write-Fail "workshop-registration.yml not found" + $failures.Add("workshop-registration.yml missing") +} + +# --------------------------------------------------------------------------- +# 4. Labels check +# --------------------------------------------------------------------------- +Write-Step "Checking required labels" + +$existingLabels = gh label list -R $Repo --json name --jq '[.[].name]' 2>&1 | ConvertFrom-Json +$requiredLabels = @('registration', 'duplicate', 'waitlist') + +foreach ($label in $requiredLabels) { + if ($existingLabels -contains $label) { + Write-Pass "Label '$label' exists" + } else { + Write-Fail "Label '$label' missing" + $failures.Add("Label '$label' missing") + } +} + +# --------------------------------------------------------------------------- +# 5. Workflow check +# --------------------------------------------------------------------------- +Write-Step "Checking registration workflow state and recent run history" + +$workflows = gh workflow list -R $Repo --json name,state,id 2>&1 | ConvertFrom-Json +$workflow = @($workflows) | Where-Object { $_.name -eq 'Registration - Welcome & CSV Export' } | Select-Object -First 1 +if ($workflow -and $workflow.state -eq 'active') { + Write-Pass "Registration workflow is active (id: $($workflow.id))" +} else { + Write-Fail "Registration workflow not found or not active" + $failures.Add("Registration workflow inactive or missing") +} + +$recentRuns = gh run list -R $Repo --workflow "registration.yml" --limit 5 --json status,conclusion,createdAt 2>&1 | ConvertFrom-Json +if ($recentRuns -and @($recentRuns).Count -gt 0) { + $lastRun = @($recentRuns)[0] + Write-Info "Most recent run: status=$($lastRun.status) conclusion=$($lastRun.conclusion) created=$($lastRun.createdAt)" + if ($lastRun.conclusion -eq 'success') { + Write-Pass "Most recent registration workflow run succeeded" + } else { + Write-Warn "Most recent registration run conclusion: $($lastRun.conclusion)" + $warnings.Add("Most recent registration run not successful: $($lastRun.conclusion)") + } +} else { + Write-Warn "No previous registration workflow runs found" + $warnings.Add("No registration workflow run history") +} + +# --------------------------------------------------------------------------- +# 6. Live test (optional) +# --------------------------------------------------------------------------- +if ($RunLiveTest) { + Write-Step "LIVE TEST: Submitting test registration issue" + Write-Warn "LIMITATION: Test issues cannot be deleted via the GitHub API." + Write-Warn "This script will close and lock the test issue after verification." + Write-Warn "To delete it permanently, use the GitHub UI after this run." + + $testBody = @" +**Name**: QA Test Account +**Pronouns**: they/them +**Time Zone**: UTC +**Assistive Tech**: None - this is an automated QA test issue. Do not process. +**Experience**: Automated test submission from Test-RegistrationPage.ps1 +**Goals**: Verify registration workflow end to end +"@ + + Write-Info "Creating test issue: $TestIssueTitle" + $issueUrl = gh issue create -R $Repo ` + --title $TestIssueTitle ` + --body $testBody ` + --label registration ` + 2>&1 + + if ($LASTEXITCODE -ne 0) { + Write-Fail "Failed to create test issue" + $failures.Add("Live test: issue creation failed") + } else { + $issueNumber = ($issueUrl -split '/')[-1] + Write-Pass "Test issue created: #$issueNumber ($issueUrl)" + + # Wait for workflow to trigger and complete + Write-Info "Waiting up to $WorkflowTimeoutSeconds seconds for registration workflow..." + $elapsed = 0 + $pollInterval = 10 + $workflowCompleted = $false + + while ($elapsed -lt $WorkflowTimeoutSeconds) { + Start-Sleep -Seconds $pollInterval + $elapsed += $pollInterval + + $runs = gh run list -R $Repo --workflow "registration.yml" --limit 3 --json status,conclusion,createdAt,databaseId 2>&1 | ConvertFrom-Json + $recentRun = @($runs) | Where-Object { $_.status -eq 'completed' } | Select-Object -First 1 + + if ($recentRun -and ([datetime]$recentRun.createdAt) -gt (Get-Date).AddSeconds(-($WorkflowTimeoutSeconds))) { + Write-Pass "Registration workflow completed: conclusion=$($recentRun.conclusion)" + $workflowCompleted = $true + if ($recentRun.conclusion -ne 'success') { + Write-Fail "Registration workflow conclusion: $($recentRun.conclusion)" + $failures.Add("Live test: workflow conclusion $($recentRun.conclusion)") + } + break + } + + Write-Info " ...waiting ($elapsed/$WorkflowTimeoutSeconds s)" + } + + if (-not $workflowCompleted) { + Write-Warn "Workflow did not complete within $WorkflowTimeoutSeconds seconds. Check Actions tab manually." + $warnings.Add("Live test: workflow timeout after $WorkflowTimeoutSeconds s") + } + + # Check for welcome comment on the issue + Write-Info "Checking for welcome comment on #$issueNumber..." + Start-Sleep -Seconds 5 + $comments = gh issue view $issueNumber -R $Repo --json comments --jq '.comments' 2>&1 | ConvertFrom-Json + if ($comments -and @($comments).Count -gt 0) { + Write-Pass "Welcome comment posted ($(@($comments).Count) comment(s) found)" + $firstComment = @($comments)[0] + Write-Info " First comment preview: $($firstComment.body.Substring(0, [Math]::Min(120, $firstComment.body.Length)))..." + + # Check for assignment links + if ($firstComment.body -match 'classroom\.github\.com') { + Write-Pass "Welcome comment contains classroom assignment link" + } else { + Write-Warn "Welcome comment does not contain classroom.github.com link (variables may not be set)" + $warnings.Add("Live test: welcome comment missing assignment links") + } + } else { + Write-Fail "No comment found on test issue #$issueNumber" + $failures.Add("Live test: no welcome comment posted") + } + + # Check labels + $issueLabels = gh issue view $issueNumber -R $Repo --json labels --jq '[.labels[].name]' 2>&1 | ConvertFrom-Json + if ($issueLabels -contains 'registration') { + Write-Pass "registration label applied to test issue" + } else { + Write-Fail "registration label not applied to test issue -- labels found: $($issueLabels -join ', ')" + $failures.Add("Live test: registration label not applied") + } + + # Cleanup: close and lock test issue + Write-Info "Closing and locking test issue #$issueNumber..." + gh issue close $issueNumber -R $Repo --comment "QA test issue. Closed by Test-RegistrationPage.ps1 after successful verification." 2>&1 | Out-Null + gh api -X PUT "repos/$Repo/issues/$issueNumber/lock" -f lock_reason=resolved 2>&1 | Out-Null + Write-Pass "Test issue #$issueNumber closed and locked" + Write-Warn "Issue #$issueNumber cannot be deleted via API. Delete it manually if needed:" + Write-Warn " https://github.com/$Repo/issues/$issueNumber" + } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +Write-Host "`n=============================" -ForegroundColor White + +if ($failures.Count -eq 0 -and $warnings.Count -eq 0) { + Write-Host "All checks passed." -ForegroundColor Green +} elseif ($failures.Count -eq 0) { + Write-Host "All checks passed with $($warnings.Count) warning(s):" -ForegroundColor Yellow + foreach ($w in $warnings) { Write-Host " - $w" -ForegroundColor Yellow } +} else { + Write-Host "$($failures.Count) failure(s), $($warnings.Count) warning(s):" -ForegroundColor Red + foreach ($f in $failures) { Write-Host " [FAIL] $f" -ForegroundColor Red } + foreach ($w in $warnings) { Write-Host " [WARN] $w" -ForegroundColor Yellow } + Write-Host "" + Write-Host "Resolve failures before proceeding to cohort launch." -ForegroundColor Red + exit 1 +} diff --git a/scripts/classroom/Validate-LearningRoomTemplateSource.ps1 b/scripts/classroom/Validate-LearningRoomTemplateSource.ps1 new file mode 100644 index 00000000..8ce3b884 --- /dev/null +++ b/scripts/classroom/Validate-LearningRoomTemplateSource.ps1 @@ -0,0 +1,98 @@ +[CmdletBinding()] +param( + [string]$SourcePath = '', + [switch]$Quiet +) + +$ErrorActionPreference = 'Stop' + +function Write-Status { + param([string]$Message) + if (-not $Quiet) { Write-Host $Message } +} + +function Fail { + param([string]$Message) + Write-Error $Message + exit 1 +} + +if (-not $SourcePath) { + $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } + $SourcePath = Join-Path $scriptDir '..\..\learning-room' +} + +$source = Resolve-Path $SourcePath -ErrorAction Stop +Write-Status "Validating Learning Room template source at $source" + +$requiredFiles = @( + '.github/workflows/student-progression.yml', + '.github/workflows/pr-validation-bot.yml', + '.github/scripts/challenge-progression.js', + '.github/ISSUE_TEMPLATE/challenge-01-find-your-way.yml', + 'docs/welcome.md', + 'package.json' +) + +foreach ($relativePath in $requiredFiles) { + $fullPath = Join-Path $source $relativePath + if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { + Fail "Required file is missing from learning-room source: $relativePath" + } +} + +$coreTemplates = Get-ChildItem -LiteralPath (Join-Path $source '.github/ISSUE_TEMPLATE') -Filter 'challenge-*.yml' -ErrorAction Stop +$bonusTemplates = Get-ChildItem -LiteralPath (Join-Path $source '.github/ISSUE_TEMPLATE') -Filter 'bonus-*.yml' -ErrorAction Stop +if ($coreTemplates.Count -ne 16) { + Fail "Expected 16 core challenge templates, found $($coreTemplates.Count)." +} +if ($bonusTemplates.Count -ne 5) { + Fail "Expected 5 bonus templates, found $($bonusTemplates.Count)." +} + +$forbiddenFilePatterns = @('*.log', '*.tmp', '*.bak', '*.orig') +$forbiddenFiles = @() +foreach ($pattern in $forbiddenFilePatterns) { + $forbiddenFiles += Get-ChildItem -LiteralPath $source -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue +} +$forbiddenFiles += Get-ChildItem -LiteralPath $source -Recurse -File -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -in @('.DS_Store', 'Thumbs.db') } + +if ($forbiddenFiles.Count -gt 0) { + $examples = ($forbiddenFiles | Select-Object -First 10 | ForEach-Object { + $_.FullName.Substring($source.Path.Length).TrimStart('\\', '/') + }) -join ', ' + Fail "Forbidden generated artifacts found in learning-room source: $examples" +} + +$scanFiles = Get-ChildItem -LiteralPath $source -Recurse -File -ErrorAction Stop | + Where-Object { + $_.Extension -in @('.md', '.yml', '.yaml', '.json', '.js', '.ts', '.txt', '.html') + } + +$forbiddenContentChecks = @( + @{ Name = 'Fine-grained/classic PAT token'; Pattern = 'ghp_[A-Za-z0-9]{20,}' }, + @{ Name = 'GitHub OAuth token'; Pattern = 'gho_[A-Za-z0-9]{20,}' }, + @{ Name = 'E2E disposable repository slug'; Pattern = 'learning-room-e2e-[0-9]{14}' }, + @{ Name = 'Smoke repository slug'; Pattern = 'learning-room-smoke-test-[0-9]{14}' } +) + +$violations = [System.Collections.Generic.List[string]]::new() +foreach ($file in $scanFiles) { + $content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction SilentlyContinue + if ($null -eq $content) { continue } + + foreach ($check in $forbiddenContentChecks) { + if ($content -match $check.Pattern) { + $relative = $file.FullName.Substring($source.Path.Length).TrimStart('\\', '/') + $violations.Add("$relative => $($check.Name)") + } + } +} + +if ($violations.Count -gt 0) { + $examples = ($violations | Select-Object -First 10) -join '; ' + Fail "Forbidden content markers found in learning-room source: $examples" +} + +Write-Status 'Learning Room template source validation passed.'