From 2e1524c3fa9264485b398ffa3bf63094718ac5d4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 00:22:21 +0000 Subject: [PATCH 1/7] feat: workshop operator guide and provisioning scripts Add complete operator toolkit for provisioning Devin Enterprise workshops: - README.md: Full operator guide with architecture, workflow, and API findings - scripts/verify-auth.sh: Verify API credentials and display enterprise state - scripts/provision-workshop.sh: End-to-end provisioning (org + permissions + sessions) - scripts/teardown-workshop.sh: Post-workshop cleanup - scripts/lib/: Shared functions (common.sh, manage-org.sh, manage-repos.sh, invoke-setup.sh) - scripts/examples/mirror-repos.sh: GitHub repo mirroring helper - configs/: Template and DC April 2026 example config - docs/api-reference-cheatsheet.md: v3 API quick reference All scripts tested against the live Devin Enterprise API: - Verified auth as enterprise service user (Devin-PW-Internal-Shared) - Successfully created/deleted git permissions for mirror org - Successfully created a Devin session via API with create_as_user_id - Discovered ACU limit requirement (cycle limit must be > 0) - Full provision workflow tested with --skip-sessions on existing org --- README.md | 190 ++++++++++++++++++++++++++++++- configs/_template.json | 13 +++ configs/dc-april-2026.json | 18 +++ docs/api-reference-cheatsheet.md | 182 +++++++++++++++++++++++++++++ scripts/examples/mirror-repos.sh | 96 ++++++++++++++++ scripts/lib/common.sh | 162 ++++++++++++++++++++++++++ scripts/lib/invoke-setup.sh | 112 ++++++++++++++++++ scripts/lib/manage-org.sh | 98 ++++++++++++++++ scripts/lib/manage-repos.sh | 102 +++++++++++++++++ scripts/provision-workshop.sh | 140 +++++++++++++++++++++++ scripts/teardown-workshop.sh | 87 ++++++++++++++ scripts/verify-auth.sh | 67 +++++++++++ 12 files changed, 1265 insertions(+), 2 deletions(-) create mode 100644 configs/_template.json create mode 100644 configs/dc-april-2026.json create mode 100644 docs/api-reference-cheatsheet.md create mode 100755 scripts/examples/mirror-repos.sh create mode 100644 scripts/lib/common.sh create mode 100644 scripts/lib/invoke-setup.sh create mode 100644 scripts/lib/manage-org.sh create mode 100644 scripts/lib/manage-repos.sh create mode 100755 scripts/provision-workshop.sh create mode 100755 scripts/teardown-workshop.sh create mode 100755 scripts/verify-auth.sh diff --git a/README.md b/README.md index b3a7ea5..b370f57 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,188 @@ -# bootstrap -Mirrors Cognition Partner Workshops so you can run this yourself +# Workshop Operator Guide + +Automate the provisioning and teardown of Devin Enterprise workshops using the Devin v3 API. This repo contains scripts and documentation for workshop hosts who need to stand up isolated participant environments backed by a mirror GitHub org. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Devin Enterprise │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Source Org │ │ Mirror Org │ │ Workshop Org│ │ +│ │ (Demo) │ │ (template) │ │ (per-event) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ Cognition-Partner- Cognition-Partner- Created per │ +│ Workshops (GH org) Workshops-mirror workshop via API │ +│ (GH org) │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Source org** (`Cognition-Partner-Workshops`) — canonical repos with workshop content. +**Mirror org** (`Cognition-Partner-Workshops-mirror`) — GitHub org with forked/mirrored repos that the Devin Enterprise GitHub App is installed on. Repos here are cloned from the source org before each event. +**Workshop org** — a Devin org created per workshop event via API. Participants use this org. It gets git permissions scoped to the mirror GitHub org repos, ACU limits, and environment configs set up by Devin sessions. + +## Prerequisites + +| Requirement | Description | +|---|---| +| **Enterprise Service User API Key** | A `cog_`-prefixed key with enterprise admin permissions (`ManageOrganizations`, `ManageGitIntegrations`, `ManageOrgSessions`, `ImpersonateOrgSessions`) | +| **GitHub App** | The Devin GitHub App installed on `Cognition-Partner-Workshops-mirror` with access to the repos needed for the workshop | +| **Mirror GitHub Org** | Repos from `Cognition-Partner-Workshops` mirrored/forked into `Cognition-Partner-Workshops-mirror` | +| **Workshop metadata** | An event README in `workshop-metadata/events//` listing the repos required | +| **jq** | JSON processor (used by all scripts) | +| **curl** | HTTP client | + +## Quick Start + +```bash +# 1. Set your API key +export DEVIN_API_KEY="cog_your_enterprise_service_user_key" + +# 2. Verify authentication +./scripts/verify-auth.sh + +# 3. Provision a workshop (creates org, sets permissions, invokes setup sessions) +./scripts/provision-workshop.sh \ + --event-name "DC April 2026" \ + --config configs/dc-april-2026.json + +# 4. After the workshop, tear down +./scripts/teardown-workshop.sh --org-id org-xxxxx +``` + +## Directory Structure + +``` +operator/ +├── README.md # This guide +├── configs/ +│ ├── _template.json # Template for workshop event configs +│ └── dc-april-2026.json # Example: DC April 2026 workshop +├── scripts/ +│ ├── verify-auth.sh # Verify API key and list enterprise state +│ ├── provision-workshop.sh # End-to-end: create org → permissions → sessions +│ ├── teardown-workshop.sh # Remove org and clean up permissions +│ ├── lib/ +│ │ ├── common.sh # Shared functions (API calls, logging, error handling) +│ │ ├── manage-org.sh # Create/update/delete/list organizations +│ │ ├── manage-repos.sh # Git permission management (add/replace/clear) +│ │ └── invoke-setup.sh # Create Devin sessions to configure env YAML +│ └── examples/ +│ └── mirror-repos.sh # Helper to mirror repos from source to mirror org +└── docs/ + └── api-reference-cheatsheet.md # Quick reference for all v3 API endpoints used +``` + +## Workflow + +### Phase 1: Pre-Workshop Setup (1-2 days before) + +#### 1.1 Mirror Repos to the Mirror GitHub Org + +Repos from `Cognition-Partner-Workshops` must exist in `Cognition-Partner-Workshops-mirror`. This is a GitHub operation (not a Devin API operation). Use `scripts/examples/mirror-repos.sh` or manually fork/mirror the repos listed in the event's workshop-metadata README. + +#### 1.2 Create a Workshop Config + +Copy `configs/_template.json` and fill in the event details: + +```json +{ + "event_name": "DC April 2026", + "org_name": "DC-April-2026", + "git_connection_id": "git-connection-f76021b797ec4a80a62f8ae9dfc1c45c", + "max_session_acu_limit": 250, + "max_cycle_acu_limit": 250, + "repos": [ + "Cognition-Partner-Workshops-mirror/ts-angular-realworld-example-app", + "Cognition-Partner-Workshops-mirror/uc-framework-upgrade-monolith-to-microservices", + "Cognition-Partner-Workshops-mirror/uc-legacy-modernization-cobol-to-java", + "Cognition-Partner-Workshops-mirror/uc-data-source-migration-legacy-to-modern", + "Cognition-Partner-Workshops-mirror/uc-bdd-test-generation-rest-api", + "Cognition-Partner-Workshops-mirror/app_petclinic-angular", + "Cognition-Partner-Workshops-mirror/app_timesheet" + ], + "setup_as_user_id": "google-oauth2|116326913226854769397", + "setup_prompt_template": "Set up the {repo} repository from scratch: install dependencies, get the build and tests working. Then capture the working setup steps in the .yaml environment configuration.\n\nShould we get the app running: yes" +} +``` + +#### 1.3 Provision the Workshop + +```bash +./scripts/provision-workshop.sh --config configs/dc-april-2026.json +``` + +This script: +1. **Creates a new Devin org** with the configured name and ACU limits +2. **Adds git permissions** for each repo in the config, scoped to the new org +3. **Invokes Devin sessions** (one per repo) to set up the environment config YAML +4. **Outputs** the org ID, session URLs, and a summary + +#### 1.4 Verify Setup Sessions + +The provisioning script prints session URLs. Monitor them in the Devin webapp or poll via API: + +```bash +# Poll a session status +curl -s -H "Authorization: Bearer $DEVIN_API_KEY" \ + "https://api.devin.ai/v3/organizations/$ORG_ID/sessions/$SESSION_ID" | jq .status +``` + +Sessions will create environment config YAMLs that persist for all future sessions in the org. Once complete, participants can start sessions against those repos with working build/test environments. + +### Phase 2: Workshop Day + +Participants log into the Devin Enterprise webapp for the workshop org and start sessions using prompts from the event README. The environment configs created in Phase 1 ensure their sessions start with working build environments. + +### Phase 3: Post-Workshop Teardown + +```bash +./scripts/teardown-workshop.sh --org-id org-xxxxx +``` + +This: +1. **Clears all git permissions** from the org +2. **Optionally deletes the org** (with `--delete-org` flag) + +## API Reference Cheatsheet + +See [docs/api-reference-cheatsheet.md](docs/api-reference-cheatsheet.md) for a complete reference of all Devin v3 API endpoints used by these scripts. + +## Key Findings from API Experimentation + +These notes capture important behaviors discovered during testing: + +1. **ACU limits must be set on org creation.** If `max_cycle_acu_limit` is 0 or null, sessions will be suspended immediately with `status_detail: "org_usage_limit_exceeded"`. Always set both `max_session_acu_limit` and `max_cycle_acu_limit` when creating or updating an org. + +2. **Git permissions are scoped per-org.** Each org needs its own git permissions, even if the git connection (GitHub App) is shared at the enterprise level. The git connection ID is enterprise-wide, but permissions are granted org-by-org. + +3. **`create_as_user_id` requires the user to be an org member.** When creating sessions on behalf of a user, that user must already be a member of the target org with `UseDevinSessions` permission. + +4. **Session creation is async.** The POST returns immediately with `status: "new"`. The session transitions through `claimed` → `running` → `suspended`/`exit`. Poll the GET endpoint to track progress. + +5. **Enterprise service users inherit org permissions.** An enterprise admin service user can call both `/v3/enterprise/*` and `/v3/organizations/{org_id}/*` endpoints across all orgs without additional role assignments. + +6. **Replace (PUT) is idempotent for permissions.** Use `PUT /v3/enterprise/organizations/{org_id}/git-providers/permissions` to set the exact list of repo permissions, replacing any previous state. This is safer than incremental POST for reproducible provisioning. + +## Tested API Calls (Verified Working) + +| Operation | Method | Endpoint | Notes | +|---|---|---|---| +| Verify auth | GET | `/v3/self` | Returns service user identity | +| List orgs | GET | `/v3/enterprise/organizations` | All orgs in enterprise | +| Create org | POST | `/v3/enterprise/organizations` | Set name + ACU limits | +| Update org | PATCH | `/v3/enterprise/organizations/{org_id}` | Update limits/name | +| List git connections | GET | `/v3/enterprise/git-providers/connections` | Find connection IDs | +| List git permissions | GET | `/v3/enterprise/organizations/{org_id}/git-providers/permissions` | Per-org permissions | +| Create git permissions | POST | `/v3/enterprise/organizations/{org_id}/git-providers/permissions` | Bulk add repos | +| Replace git permissions | PUT | `/v3/enterprise/organizations/{org_id}/git-providers/permissions` | Idempotent set | +| Delete git permission | DELETE | `/v3/enterprise/organizations/{org_id}/git-providers/permissions/{id}` | Remove one | +| Clear git permissions | DELETE | `/v3/enterprise/organizations/{org_id}/git-providers/permissions` | Remove all | +| Create session | POST | `/v3/organizations/{org_id}/sessions` | With `create_as_user_id` | +| Get session | GET | `/v3/organizations/{org_id}/sessions/{session_id}` | Poll status | +| List members | GET | `/v3/enterprise/members/users` | Enterprise-wide | +| List org members | GET | `/v3/enterprise/organizations/{org_id}/members/users` | Per-org | +| List service users | GET | `/v3/enterprise/members/service-users` | Enterprise SUs | diff --git a/configs/_template.json b/configs/_template.json new file mode 100644 index 0000000..264314d --- /dev/null +++ b/configs/_template.json @@ -0,0 +1,13 @@ +{ + "event_name": "CHANGEME — Event Name", + "org_name": "CHANGEME-Event-Name", + "git_connection_id": "git-connection-f76021b797ec4a80a62f8ae9dfc1c45c", + "max_session_acu_limit": 250, + "max_cycle_acu_limit": 250, + "repos": [ + "Cognition-Partner-Workshops-mirror/REPO_NAME_1", + "Cognition-Partner-Workshops-mirror/REPO_NAME_2" + ], + "setup_as_user_id": "", + "setup_prompt_template": "Set up the {repo} repository from scratch: install dependencies, get the build and tests working. Then capture the working setup steps in the .yaml environment configuration.\n\nShould we get the app running: yes" +} diff --git a/configs/dc-april-2026.json b/configs/dc-april-2026.json new file mode 100644 index 0000000..8ef55ba --- /dev/null +++ b/configs/dc-april-2026.json @@ -0,0 +1,18 @@ +{ + "event_name": "Hands-on Devin Workshop — Washington, DC (April 2026)", + "org_name": "DC-April-2026", + "git_connection_id": "git-connection-f76021b797ec4a80a62f8ae9dfc1c45c", + "max_session_acu_limit": 250, + "max_cycle_acu_limit": 250, + "repos": [ + "Cognition-Partner-Workshops-mirror/ts-angular-realworld-example-app", + "Cognition-Partner-Workshops-mirror/uc-framework-upgrade-monolith-to-microservices", + "Cognition-Partner-Workshops-mirror/uc-legacy-modernization-cobol-to-java", + "Cognition-Partner-Workshops-mirror/uc-data-source-migration-legacy-to-modern", + "Cognition-Partner-Workshops-mirror/uc-bdd-test-generation-rest-api", + "Cognition-Partner-Workshops-mirror/app_petclinic-angular", + "Cognition-Partner-Workshops-mirror/app_timesheet" + ], + "setup_as_user_id": "google-oauth2|116326913226854769397", + "setup_prompt_template": "Set up the {repo} repository from scratch: install dependencies, get the build and tests working. Then capture the working setup steps in the .yaml environment configuration.\n\nShould we get the app running: yes" +} diff --git a/docs/api-reference-cheatsheet.md b/docs/api-reference-cheatsheet.md new file mode 100644 index 0000000..8814eef --- /dev/null +++ b/docs/api-reference-cheatsheet.md @@ -0,0 +1,182 @@ +# Devin v3 API Reference Cheatsheet + +Quick reference for all API endpoints used by the operator scripts. Full docs: https://docs.devin.ai/api-reference/overview + +## Base URLs + +| Scope | Base URL | +|---|---| +| Enterprise | `https://api.devin.ai/v3/enterprise/*` | +| Organization | `https://api.devin.ai/v3/organizations/{org_id}/*` | + +All requests require `Authorization: Bearer cog_...` header. + +--- + +## Identity & Self + +### Verify credentials +``` +GET /v3/self +``` +Returns: `{principal_type, service_user_id, service_user_name, org_id}` + +Enterprise service users have `org_id: null`. + +--- + +## Organizations + +### List organizations +``` +GET /v3/enterprise/organizations +``` +Returns paginated list of orgs with `{org_id, name, max_session_acu_limit, max_cycle_acu_limit}`. + +### Create organization +``` +POST /v3/enterprise/organizations +``` +Body: +```json +{ + "name": "Workshop-Name", + "max_session_acu_limit": 250, + "max_cycle_acu_limit": 250 +} +``` +**Important:** `max_cycle_acu_limit` must be > 0 or sessions will be suspended with `org_usage_limit_exceeded`. + +### Update organization +``` +PATCH /v3/enterprise/organizations/{org_id} +``` +Body: `{name?, max_session_acu_limit?, max_cycle_acu_limit?}` — all fields optional. + +### Delete organization +``` +DELETE /v3/enterprise/organizations/{org_id} +``` + +--- + +## Git Connections + +### List git connections +``` +GET /v3/enterprise/git-providers/connections +``` +Returns: `{git_connection_id, git_provider_type, name, host}` + +The `git_connection_id` is needed for all permission operations. For the mirror org, this is the GitHub App connection for `Cognition-Partner-Workshops-mirror`. + +--- + +## Git Permissions + +Permissions are per-org and reference repos via the org-wide git connection. + +### List permissions +``` +GET /v3/enterprise/organizations/{org_id}/git-providers/permissions +``` + +### Create permissions (additive) +``` +POST /v3/enterprise/organizations/{org_id}/git-providers/permissions +``` +Body: +```json +{ + "permissions": [ + {"git_connection_id": "git-connection-xxx", "repo_path": "Org/repo-name"}, + {"git_connection_id": "git-connection-xxx", "repo_path": "Org/another-repo"} + ] +} +``` +Max 200 permissions per request. + +### Replace permissions (idempotent) +``` +PUT /v3/enterprise/organizations/{org_id}/git-providers/permissions +``` +Same body format as POST. Replaces all existing permissions with exactly the provided set. Preferred for reproducible provisioning. + +### Delete single permission +``` +DELETE /v3/enterprise/organizations/{org_id}/git-providers/permissions/{git_permission_id} +``` + +### Clear all permissions +``` +DELETE /v3/enterprise/organizations/{org_id}/git-providers/permissions +``` + +--- + +## Sessions + +### Create session +``` +POST /v3/organizations/{org_id}/sessions +``` +Body: +```json +{ + "prompt": "Your task description", + "create_as_user_id": "google-oauth2|...", + "repos": ["Org/repo-name"], + "playbook_id": "playbook-xxx", + "tags": ["setup"], + "max_acu_limit": 50 +} +``` +Only `prompt` is required. `create_as_user_id` requires the `ImpersonateOrgSessions` permission and the target user must be an org member. + +### Get session +``` +GET /v3/organizations/{org_id}/sessions/{session_id} +``` +Returns: `{session_id, url, status, status_detail, acus_consumed, pull_requests, ...}` + +Status values: `new` → `claimed` → `running` → `suspended` / `exit` / `error` + +### List sessions +``` +GET /v3/organizations/{org_id}/sessions +``` + +--- + +## Members + +### List enterprise members +``` +GET /v3/enterprise/members/users +``` + +### List org members +``` +GET /v3/enterprise/organizations/{org_id}/members/users +``` + +### List service users +``` +GET /v3/enterprise/members/service-users +``` + +--- + +## Permissions Reference + +| Permission | Scope | Grants | +|---|---|---| +| `ManageOrganizations` | Enterprise | Create/update/delete orgs | +| `ManageGitIntegrations` | Enterprise | Manage git connections and permissions | +| `ManageOrgSessions` | Org | Create/list sessions | +| `ImpersonateOrgSessions` | Org | Create sessions as another user | +| `UseDevinSessions` | Org | Required for target user of `create_as_user_id` | +| `ViewAccountMetrics` | Enterprise | Read consumption and metrics | +| `ViewAccountAuditLogs` | Enterprise | Read audit logs | + +Enterprise admin service users inherit all org-level permissions across all organizations. diff --git a/scripts/examples/mirror-repos.sh b/scripts/examples/mirror-repos.sh new file mode 100755 index 0000000..7a18c17 --- /dev/null +++ b/scripts/examples/mirror-repos.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# mirror-repos.sh — Mirror repos from source GitHub org to mirror GitHub org +# +# This is a GitHub operation (not a Devin API operation). It requires a GitHub +# PAT with repo creation permissions on the target org. +# +# Usage: +# export GITHUB_TOKEN="ghp_..." +# ./mirror-repos.sh --source-org Cognition-Partner-Workshops \ +# --target-org Cognition-Partner-Workshops-mirror \ +# --repos "repo1,repo2,repo3" +# +# Or with a config file: +# ./mirror-repos.sh --source-org Cognition-Partner-Workshops \ +# --target-org Cognition-Partner-Workshops-mirror \ +# --config ../../configs/dc-april-2026.json +set -euo pipefail + +GITHUB_TOKEN="${GITHUB_TOKEN:?GITHUB_TOKEN must be set}" +GITHUB_API="https://api.github.com" + +SOURCE_ORG="" +TARGET_ORG="" +REPOS=() +CONFIG_FILE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-org) SOURCE_ORG="$2"; shift 2 ;; + --target-org) TARGET_ORG="$2"; shift 2 ;; + --repos) IFS=',' read -ra REPOS <<< "$2"; shift 2 ;; + --config) CONFIG_FILE="$2"; shift 2 ;; + -h|--help) + echo "Usage: $0 --source-org --target-org --repos " + echo " $0 --source-org --target-org --config " + exit 0 + ;; + *) echo "Unknown: $1"; exit 1 ;; + esac +done + +[[ -z "$SOURCE_ORG" ]] && { echo "Error: --source-org required"; exit 1; } +[[ -z "$TARGET_ORG" ]] && { echo "Error: --target-org required"; exit 1; } + +# If config provided, extract repo names (strip the org prefix) +if [[ -n "$CONFIG_FILE" ]]; then + mapfile -t REPOS < <(jq -r '.repos[] | split("/") | .[1]' "$CONFIG_FILE") +fi + +[[ ${#REPOS[@]} -eq 0 ]] && { echo "Error: no repos specified"; exit 1; } + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +gh_api() { + curl -sfS -H "Authorization: token ${GITHUB_TOKEN}" -H "Accept: application/vnd.github+json" "$@" +} + +for repo in "${REPOS[@]}"; do + echo "=== Mirroring ${SOURCE_ORG}/${repo} → ${TARGET_ORG}/${repo} ===" + + # Check if target repo already exists + if gh_api "${GITHUB_API}/repos/${TARGET_ORG}/${repo}" >/dev/null 2>&1; then + echo " Target repo already exists, updating..." + cd "${TMPDIR}" + git clone --mirror "https://github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { + echo " WARNING: Failed to clone source repo"; continue + } + cd "${repo}.git" + git remote set-url --push origin "https://github.com/${TARGET_ORG}/${repo}.git" + git push --mirror 2>/dev/null || echo " WARNING: Push failed (may need force push)" + cd "${TMPDIR}" + rm -rf "${repo}.git" + else + echo " Creating target repo..." + gh_api -X POST "${GITHUB_API}/orgs/${TARGET_ORG}/repos" \ + -d "{\"name\": \"${repo}\", \"private\": false}" >/dev/null 2>&1 || { + echo " WARNING: Failed to create repo"; continue + } + + cd "${TMPDIR}" + git clone --mirror "https://github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { + echo " WARNING: Failed to clone source repo"; continue + } + cd "${repo}.git" + git remote set-url --push origin "https://github.com/${TARGET_ORG}/${repo}.git" + git push --mirror 2>/dev/null || echo " WARNING: Push failed" + cd "${TMPDIR}" + rm -rf "${repo}.git" + fi + + echo " Done" + echo +done + +echo "Mirror complete. ${#REPOS[@]} repo(s) processed." diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..abdac7b --- /dev/null +++ b/scripts/lib/common.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# common.sh — Shared functions for Devin v3 API operator scripts +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +API_BASE="${DEVIN_API_BASE:-https://api.devin.ai}" +DEVIN_API_KEY="${DEVIN_API_KEY:?DEVIN_API_KEY must be set to a cog_ enterprise service user key}" + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +log() { echo "[$(date -u '+%Y-%m-%d %H:%M:%S UTC')] $*"; } +info() { log "INFO $*"; } +warn() { log "WARN $*" >&2; } +err() { log "ERROR $*" >&2; } +die() { err "$@"; exit 1; } + +# --------------------------------------------------------------------------- +# HTTP helpers +# --------------------------------------------------------------------------- + +# Generic GET request. Returns JSON body, dies on HTTP error. +api_get() { + local url="$1" + local response + response=$(curl -sfS -w '\n%{http_code}' \ + -H "Authorization: Bearer ${DEVIN_API_KEY}" \ + -H "Accept: application/json" \ + "${API_BASE}${url}" 2>&1) || true + + local http_code body + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + echo "$body" + else + err "GET ${url} returned HTTP ${http_code}" + err "Response: ${body}" + return 1 + fi +} + +# Generic POST request. Takes URL and JSON body. Returns JSON body. +api_post() { + local url="$1" + local data="$2" + local response + response=$(curl -sfS -w '\n%{http_code}' \ + -X POST \ + -H "Authorization: Bearer ${DEVIN_API_KEY}" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$data" \ + "${API_BASE}${url}" 2>&1) || true + + local http_code body + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + echo "$body" + else + err "POST ${url} returned HTTP ${http_code}" + err "Response: ${body}" + return 1 + fi +} + +# Generic PATCH request. +api_patch() { + local url="$1" + local data="$2" + local response + response=$(curl -sfS -w '\n%{http_code}' \ + -X PATCH \ + -H "Authorization: Bearer ${DEVIN_API_KEY}" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$data" \ + "${API_BASE}${url}" 2>&1) || true + + local http_code body + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + echo "$body" + else + err "PATCH ${url} returned HTTP ${http_code}" + err "Response: ${body}" + return 1 + fi +} + +# Generic PUT request. +api_put() { + local url="$1" + local data="$2" + local response + response=$(curl -sfS -w '\n%{http_code}' \ + -X PUT \ + -H "Authorization: Bearer ${DEVIN_API_KEY}" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$data" \ + "${API_BASE}${url}" 2>&1) || true + + local http_code body + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + echo "$body" + else + err "PUT ${url} returned HTTP ${http_code}" + err "Response: ${body}" + return 1 + fi +} + +# Generic DELETE request. +api_delete() { + local url="$1" + local response + response=$(curl -sfS -w '\n%{http_code}' \ + -X DELETE \ + -H "Authorization: Bearer ${DEVIN_API_KEY}" \ + -H "Accept: application/json" \ + "${API_BASE}${url}" 2>&1) || true + + local http_code body + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + echo "$body" + else + err "DELETE ${url} returned HTTP ${http_code}" + err "Response: ${body}" + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Config helpers +# --------------------------------------------------------------------------- + +# Read a field from a JSON config file. +# Usage: config_get +config_get() { + local file="$1" expr="$2" + jq -r "$expr" "$file" +} + +# Read an array from a JSON config as newline-delimited values. +config_get_array() { + local file="$1" expr="$2" + jq -r "${expr}[]" "$file" +} diff --git a/scripts/lib/invoke-setup.sh b/scripts/lib/invoke-setup.sh new file mode 100644 index 0000000..54262ce --- /dev/null +++ b/scripts/lib/invoke-setup.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# invoke-setup.sh — Create Devin sessions to set up environment configs +# Source common.sh before using these functions. + +# --------------------------------------------------------------------------- +# Create a single Devin session in an organization. +# Usage: create_session [create_as_user_id] +# Returns the session JSON response. +# --------------------------------------------------------------------------- +create_session() { + local org_id="$1" + local prompt="$2" + local user_id="${3:-}" + + local payload + if [[ -n "$user_id" ]]; then + payload=$(jq -n --arg p "$prompt" --arg u "$user_id" \ + '{prompt: $p, create_as_user_id: $u}') + else + payload=$(jq -n --arg p "$prompt" '{prompt: $p}') + fi + + api_post "/v3/organizations/${org_id}/sessions" "$payload" +} + +# --------------------------------------------------------------------------- +# Get session status. +# Usage: get_session +# --------------------------------------------------------------------------- +get_session() { + local org_id="$1" + local session_id="$2" + api_get "/v3/organizations/${org_id}/sessions/${session_id}" +} + +# --------------------------------------------------------------------------- +# Poll session until it reaches a terminal state. +# Usage: poll_session [poll_interval_seconds] +# Returns 0 if session completed (exit), 1 if error/suspended. +# --------------------------------------------------------------------------- +poll_session() { + local org_id="$1" + local session_id="$2" + local interval="${3:-30}" + + info "Polling session ${session_id} every ${interval}s..." + while true; do + local session_json status status_detail + session_json=$(get_session "$org_id" "$session_id") + status=$(echo "$session_json" | jq -r '.status') + status_detail=$(echo "$session_json" | jq -r '.status_detail // empty') + + info "Session ${session_id}: status=${status}${status_detail:+ (${status_detail})}" + + case "$status" in + exit) + info "Session ${session_id} completed successfully" + return 0 + ;; + error|suspended) + warn "Session ${session_id} ended with status=${status}${status_detail:+ (${status_detail})}" + return 1 + ;; + *) + sleep "$interval" + ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Create setup sessions for multiple repos. +# Usage: invoke_setup_sessions [repo2] ... +# +# The prompt_template should contain {repo} as a placeholder, e.g.: +# "Set up the {repo} repository from scratch..." +# +# Returns a JSON array of {repo, session_id, url, status} objects. +# --------------------------------------------------------------------------- +invoke_setup_sessions() { + local org_id="$1" + local prompt_template="$2" + local user_id="$3" + shift 3 + local repos=("$@") + + local results="[]" + + for repo in "${repos[@]}"; do + local prompt="${prompt_template//\{repo\}/$repo}" + info "Creating setup session for ${repo}..." + + local session_json session_id url status + session_json=$(create_session "$org_id" "$prompt" "$user_id") || { + warn "Failed to create session for ${repo}" + results=$(echo "$results" | jq --arg r "$repo" \ + '. + [{repo: $r, session_id: null, url: null, status: "failed_to_create"}]') + continue + } + + session_id=$(echo "$session_json" | jq -r '.session_id') + url=$(echo "$session_json" | jq -r '.url') + status=$(echo "$session_json" | jq -r '.status') + + info " Session: ${session_id} (${url})" + results=$(echo "$results" | jq \ + --arg r "$repo" --arg s "$session_id" --arg u "$url" --arg st "$status" \ + '. + [{repo: $r, session_id: $s, url: $u, status: $st}]') + done + + echo "$results" +} diff --git a/scripts/lib/manage-org.sh b/scripts/lib/manage-org.sh new file mode 100644 index 0000000..0c095d4 --- /dev/null +++ b/scripts/lib/manage-org.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# manage-org.sh — Organization lifecycle functions +# Source common.sh before using these functions. + +# --------------------------------------------------------------------------- +# List all organizations in the enterprise. +# Prints JSON array of orgs. +# --------------------------------------------------------------------------- +list_orgs() { + api_get "/v3/enterprise/organizations" | jq '.items' +} + +# --------------------------------------------------------------------------- +# Get a single organization by ID. +# Usage: get_org +# --------------------------------------------------------------------------- +get_org() { + local org_id="$1" + api_get "/v3/enterprise/organizations" | jq --arg id "$org_id" '.items[] | select(.org_id == $id)' +} + +# --------------------------------------------------------------------------- +# Create a new organization. +# Usage: create_org [max_session_acu_limit] [max_cycle_acu_limit] +# Returns the created org JSON. +# --------------------------------------------------------------------------- +create_org() { + local name="$1" + local max_session="${2:-250}" + local max_cycle="${3:-250}" + + info "Creating organization: ${name} (session_limit=${max_session}, cycle_limit=${max_cycle})" + local result + result=$(api_post "/v3/enterprise/organizations" \ + "$(jq -n \ + --arg name "$name" \ + --argjson session "$max_session" \ + --argjson cycle "$max_cycle" \ + '{name: $name, max_session_acu_limit: $session, max_cycle_acu_limit: $cycle}')") + + local org_id + org_id=$(echo "$result" | jq -r '.org_id') + info "Created org: ${org_id} (${name})" + echo "$result" +} + +# --------------------------------------------------------------------------- +# Update an organization's name and/or ACU limits. +# Usage: update_org [name] [max_session_acu_limit] [max_cycle_acu_limit] +# --------------------------------------------------------------------------- +update_org() { + local org_id="$1" + local name="${2:-}" + local max_session="${3:-}" + local max_cycle="${4:-}" + + local payload="{}" + [[ -n "$name" ]] && payload=$(echo "$payload" | jq --arg n "$name" '. + {name: $n}') + [[ -n "$max_session" ]] && payload=$(echo "$payload" | jq --argjson s "$max_session" '. + {max_session_acu_limit: $s}') + [[ -n "$max_cycle" ]] && payload=$(echo "$payload" | jq --argjson c "$max_cycle" '. + {max_cycle_acu_limit: $c}') + + info "Updating org ${org_id}: ${payload}" + api_patch "/v3/enterprise/organizations/${org_id}" "$payload" +} + +# --------------------------------------------------------------------------- +# Delete an organization. +# Usage: delete_org +# --------------------------------------------------------------------------- +delete_org() { + local org_id="$1" + warn "Deleting organization: ${org_id}" + api_delete "/v3/enterprise/organizations/${org_id}" + info "Deleted org: ${org_id}" +} + +# --------------------------------------------------------------------------- +# List members of an organization. +# Usage: list_org_members +# --------------------------------------------------------------------------- +list_org_members() { + local org_id="$1" + api_get "/v3/enterprise/organizations/${org_id}/members/users" | jq '.items' +} + +# --------------------------------------------------------------------------- +# List all enterprise members. +# --------------------------------------------------------------------------- +list_enterprise_members() { + api_get "/v3/enterprise/members/users" | jq '.items' +} + +# --------------------------------------------------------------------------- +# List enterprise service users. +# --------------------------------------------------------------------------- +list_service_users() { + api_get "/v3/enterprise/members/service-users" | jq '.items' +} diff --git a/scripts/lib/manage-repos.sh b/scripts/lib/manage-repos.sh new file mode 100644 index 0000000..35fffa3 --- /dev/null +++ b/scripts/lib/manage-repos.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# manage-repos.sh — Git connection and permission management functions +# Source common.sh before using these functions. + +# --------------------------------------------------------------------------- +# List all git connections (GitHub Apps, tokens, etc.) in the enterprise. +# --------------------------------------------------------------------------- +list_git_connections() { + api_get "/v3/enterprise/git-providers/connections" | jq '.items' +} + +# --------------------------------------------------------------------------- +# Find a git connection by name (e.g., "Cognition-Partner-Workshops-mirror"). +# Usage: find_git_connection +# Returns the connection JSON object, or empty if not found. +# --------------------------------------------------------------------------- +find_git_connection() { + local name="$1" + api_get "/v3/enterprise/git-providers/connections" | jq --arg n "$name" '.items[] | select(.name == $n)' +} + +# --------------------------------------------------------------------------- +# List git permissions for an organization. +# Usage: list_git_permissions +# --------------------------------------------------------------------------- +list_git_permissions() { + local org_id="$1" + api_get "/v3/enterprise/organizations/${org_id}/git-providers/permissions" | jq '.items' +} + +# --------------------------------------------------------------------------- +# Add git permissions for repos to an organization (incremental). +# Usage: add_git_permissions [repo2] ... +# +# Each repo should be in "org/repo" format, e.g.: +# add_git_permissions org-xxx git-connection-yyy "Org/repo1" "Org/repo2" +# --------------------------------------------------------------------------- +add_git_permissions() { + local org_id="$1" + local conn_id="$2" + shift 2 + local repos=("$@") + + local permissions="[]" + for repo in "${repos[@]}"; do + permissions=$(echo "$permissions" | jq --arg c "$conn_id" --arg r "$repo" \ + '. + [{git_connection_id: $c, repo_path: $r}]') + done + + local payload + payload=$(jq -n --argjson p "$permissions" '{permissions: $p}') + + info "Adding ${#repos[@]} git permission(s) to org ${org_id}" + api_post "/v3/enterprise/organizations/${org_id}/git-providers/permissions" "$payload" +} + +# --------------------------------------------------------------------------- +# Replace all git permissions for an organization (idempotent). +# Usage: replace_git_permissions [repo2] ... +# +# This removes all existing permissions and sets exactly the repos provided. +# --------------------------------------------------------------------------- +replace_git_permissions() { + local org_id="$1" + local conn_id="$2" + shift 2 + local repos=("$@") + + local permissions="[]" + for repo in "${repos[@]}"; do + permissions=$(echo "$permissions" | jq --arg c "$conn_id" --arg r "$repo" \ + '. + [{git_connection_id: $c, repo_path: $r}]') + done + + local payload + payload=$(jq -n --argjson p "$permissions" '{permissions: $p}') + + info "Replacing git permissions for org ${org_id} with ${#repos[@]} repo(s)" + api_put "/v3/enterprise/organizations/${org_id}/git-providers/permissions" "$payload" +} + +# --------------------------------------------------------------------------- +# Delete a single git permission by ID. +# Usage: delete_git_permission +# --------------------------------------------------------------------------- +delete_git_permission() { + local org_id="$1" + local perm_id="$2" + info "Deleting git permission ${perm_id} from org ${org_id}" + api_delete "/v3/enterprise/organizations/${org_id}/git-providers/permissions/${perm_id}" +} + +# --------------------------------------------------------------------------- +# Clear all git permissions from an organization. +# Usage: clear_git_permissions +# --------------------------------------------------------------------------- +clear_git_permissions() { + local org_id="$1" + warn "Clearing ALL git permissions from org ${org_id}" + api_delete "/v3/enterprise/organizations/${org_id}/git-providers/permissions" + info "Cleared all git permissions from org ${org_id}" +} diff --git a/scripts/provision-workshop.sh b/scripts/provision-workshop.sh new file mode 100755 index 0000000..d9dd380 --- /dev/null +++ b/scripts/provision-workshop.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# provision-workshop.sh — End-to-end workshop provisioning +# +# Creates a Devin org, sets git permissions for workshop repos, and invokes +# Devin sessions to set up environment config YAMLs for each repo. +# +# Usage: +# ./provision-workshop.sh --config configs/dc-april-2026.json +# ./provision-workshop.sh --config configs/dc-april-2026.json --skip-sessions +# ./provision-workshop.sh --config configs/dc-april-2026.json --org-id org-existing-id +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" +source "${SCRIPT_DIR}/lib/manage-org.sh" +source "${SCRIPT_DIR}/lib/manage-repos.sh" +source "${SCRIPT_DIR}/lib/invoke-setup.sh" + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +CONFIG_FILE="" +SKIP_SESSIONS=false +EXISTING_ORG_ID="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) CONFIG_FILE="$2"; shift 2 ;; + --skip-sessions) SKIP_SESSIONS=true; shift ;; + --org-id) EXISTING_ORG_ID="$2"; shift 2 ;; + -h|--help) + echo "Usage: $0 --config [--skip-sessions] [--org-id ]" + echo + echo "Options:" + echo " --config Path to workshop config JSON (required)" + echo " --skip-sessions Skip invoking Devin setup sessions" + echo " --org-id Use an existing org instead of creating one" + exit 0 + ;; + *) die "Unknown argument: $1" ;; + esac +done + +[[ -z "$CONFIG_FILE" ]] && die "Missing required --config argument. Run with --help for usage." +[[ ! -f "$CONFIG_FILE" ]] && die "Config file not found: ${CONFIG_FILE}" + +# --------------------------------------------------------------------------- +# Read config +# --------------------------------------------------------------------------- +info "Reading config: ${CONFIG_FILE}" + +ORG_NAME=$(config_get "$CONFIG_FILE" '.org_name') +GIT_CONNECTION_ID=$(config_get "$CONFIG_FILE" '.git_connection_id') +MAX_SESSION_ACU=$(config_get "$CONFIG_FILE" '.max_session_acu_limit // 250') +MAX_CYCLE_ACU=$(config_get "$CONFIG_FILE" '.max_cycle_acu_limit // 250') +SETUP_USER_ID=$(config_get "$CONFIG_FILE" '.setup_as_user_id // empty') +SETUP_PROMPT=$(config_get "$CONFIG_FILE" '.setup_prompt_template') + +mapfile -t REPOS < <(config_get_array "$CONFIG_FILE" '.repos') + +echo +echo "============================================" +echo " Workshop Provisioning" +echo "============================================" +echo " Org name : ${ORG_NAME}" +echo " Git connection : ${GIT_CONNECTION_ID}" +echo " Repos : ${#REPOS[@]}" +echo " ACU limits : session=${MAX_SESSION_ACU}, cycle=${MAX_CYCLE_ACU}" +echo " Setup user : ${SETUP_USER_ID:-none (service user)}" +echo " Skip sessions : ${SKIP_SESSIONS}" +echo "============================================" +echo + +# --------------------------------------------------------------------------- +# Step 1: Create or reuse organization +# --------------------------------------------------------------------------- +ORG_ID="" +if [[ -n "$EXISTING_ORG_ID" ]]; then + info "Using existing org: ${EXISTING_ORG_ID}" + ORG_ID="$EXISTING_ORG_ID" + + info "Updating ACU limits..." + update_org "$ORG_ID" "" "$MAX_SESSION_ACU" "$MAX_CYCLE_ACU" > /dev/null +else + info "Creating new organization..." + org_json=$(create_org "$ORG_NAME" "$MAX_SESSION_ACU" "$MAX_CYCLE_ACU") + ORG_ID=$(echo "$org_json" | jq -r '.org_id') +fi +echo +info "Org ID: ${ORG_ID}" +echo + +# --------------------------------------------------------------------------- +# Step 2: Set git permissions (idempotent replace) +# --------------------------------------------------------------------------- +info "Setting git permissions for ${#REPOS[@]} repo(s)..." +replace_git_permissions "$ORG_ID" "$GIT_CONNECTION_ID" "${REPOS[@]}" > /dev/null +echo + +info "Verifying permissions..." +list_git_permissions "$ORG_ID" | jq -r '.[] | " \(.repo_path)"' +echo + +# --------------------------------------------------------------------------- +# Step 3: Invoke Devin setup sessions (one per repo) +# --------------------------------------------------------------------------- +if [[ "$SKIP_SESSIONS" == "true" ]]; then + info "Skipping session creation (--skip-sessions)" +else + info "Invoking setup sessions for ${#REPOS[@]} repo(s)..." + echo + + sessions_json=$(invoke_setup_sessions "$ORG_ID" "$SETUP_PROMPT" "$SETUP_USER_ID" "${REPOS[@]}") + + echo + echo "============================================" + echo " Setup Sessions" + echo "============================================" + echo "$sessions_json" | jq -r '.[] | " \(.repo)\n Session: \(.session_id)\n URL: \(.url)\n Status: \(.status)\n"' +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo +echo "============================================" +echo " Provisioning Complete" +echo "============================================" +echo " Org ID : ${ORG_ID}" +echo " Org name : ${ORG_NAME}" +echo " Repos : ${#REPOS[@]}" +if [[ "$SKIP_SESSIONS" != "true" ]]; then + echo " Sessions : $(echo "$sessions_json" | jq length)" +fi +echo +echo " Next steps:" +echo " 1. Monitor setup sessions in the Devin webapp or poll via API" +echo " 2. Once sessions complete, env config YAMLs are ready for participants" +echo " 3. Share the workshop org URL with participants" +echo " 4. After the workshop: ./scripts/teardown-workshop.sh --org-id ${ORG_ID}" +echo diff --git a/scripts/teardown-workshop.sh b/scripts/teardown-workshop.sh new file mode 100755 index 0000000..88bd75c --- /dev/null +++ b/scripts/teardown-workshop.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# teardown-workshop.sh — Post-workshop cleanup +# +# Clears git permissions and optionally deletes the workshop organization. +# +# Usage: +# ./teardown-workshop.sh --org-id org-xxxxx +# ./teardown-workshop.sh --org-id org-xxxxx --delete-org +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" +source "${SCRIPT_DIR}/lib/manage-org.sh" +source "${SCRIPT_DIR}/lib/manage-repos.sh" + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +ORG_ID="" +DELETE_ORG=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --org-id) ORG_ID="$2"; shift 2 ;; + --delete-org) DELETE_ORG=true; shift ;; + -h|--help) + echo "Usage: $0 --org-id [--delete-org]" + echo + echo "Options:" + echo " --org-id Organization ID to tear down (required)" + echo " --delete-org Also delete the organization (default: only clear permissions)" + exit 0 + ;; + *) die "Unknown argument: $1" ;; + esac +done + +[[ -z "$ORG_ID" ]] && die "Missing required --org-id argument. Run with --help for usage." + +echo +echo "============================================" +echo " Workshop Teardown" +echo "============================================" +echo " Org ID : ${ORG_ID}" +echo " Delete org : ${DELETE_ORG}" +echo "============================================" +echo + +# --------------------------------------------------------------------------- +# Step 1: Show current state +# --------------------------------------------------------------------------- +info "Current git permissions:" +perms=$(list_git_permissions "$ORG_ID") +perm_count=$(echo "$perms" | jq length) +echo "$perms" | jq -r '.[] | " \(.repo_path)"' +echo +info "Found ${perm_count} permission(s)" +echo + +# --------------------------------------------------------------------------- +# Step 2: Clear git permissions +# --------------------------------------------------------------------------- +if [[ "$perm_count" -gt 0 ]]; then + info "Clearing git permissions..." + clear_git_permissions "$ORG_ID" > /dev/null + info "Cleared ${perm_count} permission(s)" +else + info "No permissions to clear" +fi +echo + +# --------------------------------------------------------------------------- +# Step 3: Optionally delete the organization +# --------------------------------------------------------------------------- +if [[ "$DELETE_ORG" == "true" ]]; then + echo + warn "Deleting organization ${ORG_ID}..." + warn "This is irreversible. Proceeding in 5 seconds (Ctrl+C to abort)..." + sleep 5 + delete_org "$ORG_ID" +else + info "Organization ${ORG_ID} preserved (use --delete-org to remove)" +fi +echo + +echo "============================================" +echo " Teardown Complete" +echo "============================================" diff --git a/scripts/verify-auth.sh b/scripts/verify-auth.sh new file mode 100755 index 0000000..eeb0470 --- /dev/null +++ b/scripts/verify-auth.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# verify-auth.sh — Verify API authentication and display enterprise state +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" +source "${SCRIPT_DIR}/lib/manage-org.sh" +source "${SCRIPT_DIR}/lib/manage-repos.sh" + +echo "============================================" +echo " Devin Enterprise — Auth Verification" +echo "============================================" +echo + +# Step 1: Verify identity +info "Verifying API credentials..." +self_json=$(api_get "/v3/self") +principal_type=$(echo "$self_json" | jq -r '.principal_type') +su_id=$(echo "$self_json" | jq -r '.service_user_id // empty') +su_name=$(echo "$self_json" | jq -r '.service_user_name // empty') +org_id=$(echo "$self_json" | jq -r '.org_id // "enterprise-scoped"') + +echo +echo " Principal type : ${principal_type}" +echo " Service user : ${su_name} (${su_id})" +echo " Scope : ${org_id}" +echo + +if [[ "$principal_type" != "service_user" ]]; then + die "Expected principal_type=service_user, got ${principal_type}. Ensure DEVIN_API_KEY is a service user key." +fi + +# Step 2: List organizations +info "Listing organizations..." +orgs_json=$(api_get "/v3/enterprise/organizations") +org_count=$(echo "$orgs_json" | jq '.total') +echo +echo " Organizations (${org_count}):" +echo "$orgs_json" | jq -r '.items[] | " \(.org_id) \(.name) (session=\(.max_session_acu_limit // "null"), cycle=\(.max_cycle_acu_limit // "null"))"' +echo + +# Step 3: List git connections +info "Listing git connections..." +connections_json=$(list_git_connections) +echo +echo " Git connections:" +echo "$connections_json" | jq -r '.[] | " \(.git_connection_id) \(.git_provider_type) \(.name) (\(.host))"' +echo + +# Step 4: List enterprise members +info "Listing enterprise members..." +members_json=$(list_enterprise_members) +echo +echo " Enterprise members:" +echo "$members_json" | jq -r '.[] | " \(.user_id) \(.email) \(.name)"' +echo + +# Step 5: List service users +info "Listing service users..." +sus_json=$(list_service_users) +echo +echo " Service users:" +echo "$sus_json" | jq -r '.[] | " \(.service_user_id) \(.name) (expires: \(.expires_at // "never"))"' +echo + +echo "============================================" +echo " Verification complete" +echo "============================================" From 55a9cc5c96b15fdc30e5f6bdd19aee164e95a69a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 00:27:32 +0000 Subject: [PATCH 2/7] fix: embed GITHUB_TOKEN in git clone/push URLs for mirror script Without token auth in HTTPS URLs, git push --mirror fails silently. Use x-access-token pattern for both clone and push operations. --- scripts/examples/mirror-repos.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/examples/mirror-repos.sh b/scripts/examples/mirror-repos.sh index 7a18c17..9685c66 100755 --- a/scripts/examples/mirror-repos.sh +++ b/scripts/examples/mirror-repos.sh @@ -63,11 +63,11 @@ for repo in "${REPOS[@]}"; do if gh_api "${GITHUB_API}/repos/${TARGET_ORG}/${repo}" >/dev/null 2>&1; then echo " Target repo already exists, updating..." cd "${TMPDIR}" - git clone --mirror "https://github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { + git clone --mirror "https://x-access-token:${GITHUB_TOKEN}@github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { echo " WARNING: Failed to clone source repo"; continue } cd "${repo}.git" - git remote set-url --push origin "https://github.com/${TARGET_ORG}/${repo}.git" + git remote set-url --push origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${TARGET_ORG}/${repo}.git" git push --mirror 2>/dev/null || echo " WARNING: Push failed (may need force push)" cd "${TMPDIR}" rm -rf "${repo}.git" @@ -79,11 +79,11 @@ for repo in "${REPOS[@]}"; do } cd "${TMPDIR}" - git clone --mirror "https://github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { + git clone --mirror "https://x-access-token:${GITHUB_TOKEN}@github.com/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>/dev/null || { echo " WARNING: Failed to clone source repo"; continue } cd "${repo}.git" - git remote set-url --push origin "https://github.com/${TARGET_ORG}/${repo}.git" + git remote set-url --push origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${TARGET_ORG}/${repo}.git" git push --mirror 2>/dev/null || echo " WARNING: Push failed" cd "${TMPDIR}" rm -rf "${repo}.git" From 616fba50eccb9beebe886b87edea766521764102 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 00:38:45 +0000 Subject: [PATCH 3/7] Integrate cleanup, mirror, invite, and PII enforcement scripts - Add participant invitation (manage-members.sh library + invite-participants.sh) - Add mirror-github-org.sh with workflow stripping, include/exclude, config support - Add post-workshop cleanup suite: sanitize-pr-pii.sh, close-old-prs.sh, delete-stale-branches.sh, cleanup-all.sh orchestrator - Add PII enforcement CI workflow (.github/workflows/pr-pii-check.yml) and deploy-pr-pii-check.sh to roll it out across repos - Update provision-workshop.sh to support --emails-file and --skip-invites - Update config template with emails_file, enterprise_role_id, org_role_id - Remove curl -f flag from common.sh to preserve API error response bodies - Replace scripts/examples/mirror-repos.sh with scripts/mirror-github-org.sh - Update README with comprehensive coverage of all scripts and workflow phases --- .github/workflows/pr-pii-check.yml | 49 +++++++ README.md | 181 ++++++++++++++++++----- configs/_template.json | 5 +- configs/dc-april-2026.json | 5 +- scripts/cleanup-all.sh | 51 +++++++ scripts/close-old-prs.sh | 74 ++++++++++ scripts/delete-stale-branches.sh | 85 +++++++++++ scripts/deploy-pr-pii-check.sh | 164 +++++++++++++++++++++ scripts/examples/mirror-repos.sh | 96 ------------ scripts/invite-participants.sh | 69 +++++++++ scripts/lib/common.sh | 10 +- scripts/lib/invoke-setup.sh | 0 scripts/lib/manage-members.sh | 132 +++++++++++++++++ scripts/lib/manage-org.sh | 0 scripts/lib/manage-repos.sh | 0 scripts/mirror-github-org.sh | 226 +++++++++++++++++++++++++++++ scripts/provision-workshop.sh | 54 ++++++- scripts/sanitize-pr-pii.sh | 141 ++++++++++++++++++ 18 files changed, 1199 insertions(+), 143 deletions(-) create mode 100644 .github/workflows/pr-pii-check.yml create mode 100755 scripts/cleanup-all.sh create mode 100755 scripts/close-old-prs.sh create mode 100755 scripts/delete-stale-branches.sh create mode 100755 scripts/deploy-pr-pii-check.sh delete mode 100755 scripts/examples/mirror-repos.sh create mode 100755 scripts/invite-participants.sh mode change 100644 => 100755 scripts/lib/common.sh mode change 100644 => 100755 scripts/lib/invoke-setup.sh create mode 100755 scripts/lib/manage-members.sh mode change 100644 => 100755 scripts/lib/manage-org.sh mode change 100644 => 100755 scripts/lib/manage-repos.sh create mode 100755 scripts/mirror-github-org.sh create mode 100755 scripts/sanitize-pr-pii.sh diff --git a/.github/workflows/pr-pii-check.yml b/.github/workflows/pr-pii-check.yml new file mode 100644 index 0000000..bab7d4a --- /dev/null +++ b/.github/workflows/pr-pii-check.yml @@ -0,0 +1,49 @@ +name: PR PII Check + +on: + pull_request: + types: [opened, synchronize] + pull_request_review_comment: + types: [created, edited] + +permissions: + pull-requests: read + +jobs: + check-pii: + name: Check for PII in PR + runs-on: ubuntu-latest + if: >- + github.event_name == 'pull_request' || + github.event_name == 'pull_request_review_comment' + steps: + - name: Check PR description for PII + if: github.event_name == 'pull_request' + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "Checking PR description for PII patterns..." + printenv PR_BODY > /tmp/pr_body.txt + sed -i 's/\r$//' /tmp/pr_body.txt + # Strip system-appended footers before scanning + sed -i '/^Link to Devin session:/,$d' /tmp/pr_body.txt + sed -i '/^