Manual Test Runbook — B1: snowops-onboarder GitHub App
Owner: Sagar | Time: ~60 min | Requires: test GitHub org + sandbox Azure tenant + Azure AD app reg from B2
Purpose
Verify that the B1 onboarder, given an A4-shaped webhook payload, provisions a brand-new client repo to SnowOps standards: repo created from the C4 template, branch protection applied, GitHub Environments with reviewers, Azure OIDC variables seeded, and federated identity credentials attached to the Azure AD App Registration.
Prerequisites
- Test GitHub org (e.g.
snowops-sandbox) where you have admin access. - A
snowops-onboarderGitHub App created with permissions: administration:write,contents:write,environments:write,secrets:write,actions:read.- The App is installed on the test org.
- Sandbox Azure tenant (X1 lineage) with an App Registration created via B2 — note its object ID and client ID.
- Sandbox subscription IDs handy for at least one environment.
- Local Node 20.10+ and
azCLI. - A
templates/client-repo/tree present at the repo root (it is — shipped with C4).
Steps
1. Unit test suite
- Build succeeds (
dist/populated). - All 6 test suites, 38 tests pass:
template.test.ts(placeholder substitution)azure-graph.test.ts(federated cred upsert)octokit-client.test.ts(GitHub API request shapes)onboarder.test.ts(orchestrator happy path, idempotency, validation)webhook.test.ts(payload parse + dispatch)load-template.test.ts(real disk tree loads cleanly)
2. Acquire a GitHub App installation token
# Generate JWT with your App ID + private key, then exchange for installation token.
# Easiest: use `gh` with App authentication, or a one-off node script using @octokit/app.
node -e '
import("@octokit/app").then(async ({ App }) => {
const app = new App({ appId: process.env.GH_APP_ID, privateKey: process.env.GH_APP_PRIVATE_KEY });
const installation = await app.octokit.request("GET /orgs/{org}/installation", { org: process.env.GH_ORG });
const inst = await app.getInstallationOctokit(installation.data.id);
const tok = await inst.auth({ type: "installation" });
console.log(tok.token);
});'
- Token printed (starts with
ghs_). Note it.
3. Acquire a Microsoft Graph token
az login --tenant <client-tenant-id>
az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv
- Token printed.
4. Run the onboarder against the sandbox
Create /tmp/spec.json:
{
"client": {
"slug": "b1-smoke",
"displayName": "B1 Smoke Client",
"githubOrg": "snowops-sandbox",
"platformTeam": "snowops-sandbox/platform",
"securityTeam": "snowops-sandbox/security",
"snowopsDeliveryTeam": "snowops/delivery"
},
"azure": {
"tenantId": "<sandbox tenant id>",
"applicationObjectId": "<from B2>",
"applicationClientId": "<from B2>"
},
"environments": [
{
"name": "sandbox",
"subscriptionId": "<sandbox sub id>",
"subscriptionDisplayName": "B1 Smoke Sandbox"
}
],
"hubspot_deal_id": "0"
}
Then run from apps/github-onboarder:
GH_TOKEN=<installation token from step 2> \
GRAPH_TOKEN=<graph token from step 3> \
node --input-type=module -e '
import { OctokitGitHubClient } from "./dist/octokit-client.js";
import { GraphFederatedCredClient } from "./dist/azure-graph.js";
import { handleOnboardWebhook, loadTemplateFromDisk } from "./dist/webhook.js";
import { Octokit } from "@octokit/rest";
import fs from "node:fs";
const payload = JSON.parse(fs.readFileSync("/tmp/spec.json", "utf8"));
const oct = new Octokit({ auth: process.env.GH_TOKEN });
const github = new OctokitGitHubClient(oct.request);
const graph = new GraphFederatedCredClient(process.env.GRAPH_TOKEN);
const result = await handleOnboardWebhook(payload, {
github, graph,
loadTemplate: (dir) => loadTemplateFromDisk("../../" + dir),
});
console.log(JSON.stringify(result, null, 2));
'
- Script completes with
"status": "created". -
repoFullNameissnowops-sandbox/b1-smoke-infra. -
filesPushedlists all 8 template files.
5. Verify the repo via GitHub UI
- Browse https://github.com/snowops-sandbox/b1-smoke-infra.
- Default branch is
main. - All 8 template files are present.
-
.github/CODEOWNERShas{{ORG}}replaced withsnowops-sandbox. -
README.mdhas{{CLIENT_DISPLAY_NAME}}replaced withB1 Smoke Client.
6. Verify branch protection on main
GitHub → Settings → Branches → main:
- Require PR before merging ✔️
- Required approving reviews: 1
- Require review from CODEOWNERS ✔️
- Dismiss stale approvals ✔️
- Required status checks listed (
pre-commit-fast,checkov,tfsec,trivy-fs,gitleaks,conftest,pr-template-check). - Require branches to be up to date ✔️
- Require linear history ✔️
- Require conversation resolution ✔️
- Include administrators ✔️
- Allow force pushes ❌
- Allow deletions ❌
7. Verify Environments
GitHub → Settings → Environments → sandbox:
- Environment exists.
- Variables visible:
AZURE_TENANT_ID= your sandbox tenant GUIDAZURE_CLIENT_ID= the B2 app client IDAZURE_SUBSCRIPTION_ID= your sandbox subscription GUIDAZURE_SUBSCRIPTION_NAME=B1 Smoke Sandbox- No secrets (you didn't pass any
extraSecrets).
8. Verify federated identity credential in Azure AD
- One credential named
gh-actions-b1-smoke-infra-sandboxexists. - Subject is
repo:snowops-sandbox/b1-smoke-infra:environment:sandbox. - Issuer is
https://token.actions.githubusercontent.com. - Audience is
api://AzureADTokenExchange.
9. Idempotency check
Re-run the script from step 4 verbatim.
- Exit code 0.
-
"status": "already_provisioned"(notcreated). -
filesPushedis empty. - Federated credential count in Azure AD is still one (upserted, not duplicated).
- No new branch protection diff; existing rules preserved.
10. End-to-end OIDC smoke test (optional, ~10 min extra)
In the new repo, push a trivial workflow that exchanges OIDC and runs az account show:
# .github/workflows/oidc-smoke.yml
on: { workflow_dispatch: {} }
permissions: { id-token: write, contents: read }
jobs:
smoke:
runs-on: ubuntu-latest
environment: sandbox
steps:
- uses: azure/login@v2
with:
tenant-id: ${{ vars.AZURE_TENANT_ID }}
client-id: ${{ vars.AZURE_CLIENT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: az account show
- Manually dispatch the workflow.
- Azure login step succeeds (means the federated credential worked).
-
az account showreturns the sandbox subscription.
11. Teardown
# Delete federated credentials
for id in $(az ad app federated-credential list --id <applicationObjectId> --query "[?starts_with(name, 'gh-actions-b1-smoke')].id" -o tsv); do
az ad app federated-credential delete --id <applicationObjectId> --federated-credential-id "$id"
done
# Delete the test repo
gh repo delete snowops-sandbox/b1-smoke-infra --yes
- Federated creds removed.
- Repo deleted.
Pass criteria
- All 38 unit tests pass.
- Step 4 returns
status: "created"and pushes all 8 template files. - Branch protection matches §6 of this runbook exactly.
- Environment variables visible with correct GUIDs.
- Federated credential exists with subject
repo:<org>/<repo>:environment:sandbox. - Re-running is a no-op:
status: "already_provisioned", no duplicates. - (Optional) OIDC smoke workflow logs in to Azure successfully.
Failure modes & escalation
| Symptom | Action |
|---|---|
404 creating repo |
App not installed on org or missing administration:write. |
403 setting environment |
Missing environments:write permission on the App. |
Graph 403 on PATCH/POST |
Token lacks Application.ReadWrite.OwnedBy consented in client tenant. |
| Federated cred created but Azure login fails | Subject string mismatch — check the env name in workflow matches what was provisioned. |
| Branch protection rejected | Required check names not registered yet — push at least one commit so GH has seen the check contexts. |
libsodium import fails at runtime |
Make sure dist/ was built; npm run build updates the lazy-imported wrapper. |
Sign-off
- Tester: ___ | Date: _ | Result: PASS / FAIL / N/A
- Notes: