Architecture Overview
This guide sets up a GitHub-based change management system with two approval tracks, Azure-backed state storage and secrets, and CI/CD for Terraform.
| Component | Solution |
|---|---|
| Repositories | One repo per managed software/service |
| Business Changes | 3-level approval (Technical → IT Lead → Business Owner) |
| IT Changes | 1 approval (any IT team peer) |
| Who Can Merge | Anyone on the IT team |
| Remote State | Azure Storage Account (blob backend with state locking) |
| Secrets | Azure Key Vault (OIDC — zero secrets stored in GitHub) |
| CI Checks | terraform plan, validate, tfsec on every PR |
Prerequisites
- GitHub Organization (Free, Team, or Enterprise)
- Azure subscription with Owner or Contributor role
- Azure CLI (
az) installed and authenticated - GitHub CLI (
gh) installed and authenticated to your org - Terraform CLI installed locally
- A list of software/services you'll manage (one repo each)
terraform-{service} for repo names (e.g., terraform-azure-networking, terraform-m365). Replace {service} with your actual service names throughout.
Phase 1: Foundation
1.1 Create Repositories
Run this for each managed software/service. Replace the services array with your actual list.
# Define your services - one repo per managed system
SERVICES=(
"azure-networking"
"azure-identity"
"m365"
"servicenow"
)
ORG="your-org-name"
for SERVICE in "${SERVICES[@]}"; do
gh repo create "$ORG/terraform-$SERVICE" \
--private \
--description "Terraform IaC for $SERVICE" \
--clone
cd "terraform-$SERVICE"
# Create directory structure
mkdir -p .github/workflows
mkdir -p .github/PULL_REQUEST_TEMPLATE
mkdir -p environments/{dev,staging,prod}
mkdir -p modules
# Initialize with a README
cat > README.md <<EOF
# terraform-$SERVICE
Terraform configuration for $SERVICE.
## Change Request Process
- **Business Changes**: Use the "Business Change Request" PR template. Requires 3 approvals.
- **IT Changes**: Use the "IT Change Request" PR template. Requires 1 approval.
## Structure
\`\`\`
environments/ # Per-environment configs (dev, staging, prod)
modules/ # Reusable Terraform modules
.github/ # CI/CD workflows, PR templates, CODEOWNERS
\`\`\`
EOF
git add -A
git commit -m "Initial repo scaffold for terraform-$SERVICE"
git push -u origin main
cd ..
done
1.2 Create GitHub Teams
You need three teams to support both approval tracks.
ORG="your-org-name"
# IT team - all IT staff (can merge, can approve IT changes)
gh api orgs/$ORG/teams -f name="it-team" \
-f description="All IT team members" \
-f privacy="closed"
# IT leads - senior IT (L2 approver for business changes)
gh api orgs/$ORG/teams -f name="it-leads" \
-f description="IT leadership for change approval" \
-f privacy="closed"
# Business owners - non-IT stakeholders (L3 approver)
gh api orgs/$ORG/teams -f name="business-owners" \
-f description="Business stakeholders for change approval" \
-f privacy="closed"
Add members to each team:
# Add members to teams
# IT Team - all your IT staff
gh api orgs/$ORG/teams/it-team/memberships/username1 -f role="member"
gh api orgs/$ORG/teams/it-team/memberships/username2 -f role="member"
# IT Leads - your senior IT people
gh api orgs/$ORG/teams/it-leads/memberships/senior-username -f role="member"
# Business Owners - your business stakeholders
gh api orgs/$ORG/teams/business-owners/memberships/biz-username -f role="member"
1.3 Set Repository Permissions
Grant the IT team write access to all repos (allowing merge). Business owners only get read + PR review access.
ORG="your-org-name"
for SERVICE in "${SERVICES[@]}"; do
REPO="terraform-$SERVICE"
# IT team gets Write (can push branches + merge PRs)
gh api orgs/$ORG/teams/it-team/repos/$ORG/$REPO \
-X PUT -f permission="push"
# IT leads also get Write
gh api orgs/$ORG/teams/it-leads/repos/$ORG/$REPO \
-X PUT -f permission="push"
# Business owners get Triage (can review/approve but not push code)
gh api orgs/$ORG/teams/business-owners/repos/$ORG/$REPO \
-X PUT -f permission="triage"
done
Phase 2: Azure Backend Setup
2.1 Create Resource Group
# Login to Azure if not already
az login
# Set your subscription
az account set --subscription "Your-Subscription-Name"
# Create resource group for all Terraform infrastructure
az group create \
--name rg-terraform-state \
--location eastus \
--tags purpose=terraform-state managed-by=it-team
2.2 Create Storage Account for State
This stores your Terraform state files with locking via blob lease. Cost is negligible (pennies/month).
# Storage account name must be globally unique, lowercase, no dashes
STORAGE_ACCOUNT="stterraformstate$(openssl rand -hex 4)"
echo "Storage account name: $STORAGE_ACCOUNT"
echo "SAVE THIS NAME - you'll need it in backend.tf"
az storage account create \
--name "$STORAGE_ACCOUNT" \
--resource-group rg-terraform-state \
--location eastus \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--tags purpose=terraform-state
# Create a container for state files
az storage container create \
--name tfstate \
--account-name "$STORAGE_ACCOUNT" \
--auth-mode login
# Enable versioning (state file history / recovery)
az storage account blob-service-properties update \
--account-name "$STORAGE_ACCOUNT" \
--resource-group rg-terraform-state \
--enable-versioning true
# Enable soft delete (14 day recovery window)
az storage blob service-properties delete-policy update \
--account-name "$STORAGE_ACCOUNT" \
--resource-group rg-terraform-state \
--enable true \
--days-retained 14
2.3 Create Key Vault
# Create Key Vault for Terraform secrets
az keyvault create \
--name "kv-terraform-secrets" \
--resource-group rg-terraform-state \
--location eastus \
--sku standard \
--enable-rbac-authorization true \
--tags purpose=terraform-secrets
# Grant yourself admin access to manage secrets
CURRENT_USER=$(az ad signed-in-user show --query id -o tsv)
az role assignment create \
--role "Key Vault Secrets Officer" \
--assignee "$CURRENT_USER" \
--scope "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/rg-terraform-state/providers/Microsoft.KeyVault/vaults/kv-terraform-secrets"
# Example: add a secret
az keyvault secret set \
--vault-name "kv-terraform-secrets" \
--name "example-api-key" \
--value "your-secret-value-here"
2.4 Set Up Workload Identity Federation (OIDC)
This allows GitHub Actions to authenticate to Azure with zero stored secrets. GitHub presents an OIDC token, Azure validates it.
ORG="your-org-name"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Create a managed identity for GitHub Actions
az identity create \
--name "id-github-terraform" \
--resource-group rg-terraform-state \
--location eastus
IDENTITY_CLIENT_ID=$(az identity show \
--name id-github-terraform \
--resource-group rg-terraform-state \
--query clientId -o tsv)
IDENTITY_PRINCIPAL_ID=$(az identity show \
--name id-github-terraform \
--resource-group rg-terraform-state \
--query principalId -o tsv)
IDENTITY_ID=$(az identity show \
--name id-github-terraform \
--resource-group rg-terraform-state \
--query id -o tsv)
echo "Client ID: $IDENTITY_CLIENT_ID"
echo "Principal ID: $IDENTITY_PRINCIPAL_ID"
echo "SAVE THESE VALUES"
# Grant the identity Contributor on the subscription
# (narrow this to specific RGs in production)
az role assignment create \
--role "Contributor" \
--assignee "$IDENTITY_PRINCIPAL_ID" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
# Grant the identity read access to Key Vault secrets
az role assignment create \
--role "Key Vault Secrets User" \
--assignee "$IDENTITY_PRINCIPAL_ID" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-terraform-state/providers/Microsoft.KeyVault/vaults/kv-terraform-secrets"
# Grant the identity access to the storage account (for state)
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee "$IDENTITY_PRINCIPAL_ID" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-terraform-state/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT"
# Create federated credentials for EACH repo
for SERVICE in "${SERVICES[@]}"; do
REPO="terraform-$SERVICE"
# For main branch (apply)
az identity federated-credential create \
--name "github-$SERVICE-main" \
--identity-name "id-github-terraform" \
--resource-group rg-terraform-state \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:$ORG/$REPO:ref:refs/heads/main" \
--audiences "api://AzureADTokenExchange"
# For pull requests (plan)
az identity federated-credential create \
--name "github-$SERVICE-pr" \
--identity-name "id-github-terraform" \
--resource-group rg-terraform-state \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:$ORG/$REPO:pull_request" \
--audiences "api://AzureADTokenExchange"
echo "Created federated credentials for $REPO"
done
Now store the Azure identifiers as GitHub variables (not secrets — these are non-sensitive IDs):
TENANT_ID=$(az account show --query tenantId -o tsv)
for SERVICE in "${SERVICES[@]}"; do
REPO="$ORG/terraform-$SERVICE"
gh variable set AZURE_CLIENT_ID --repo "$REPO" --body "$IDENTITY_CLIENT_ID"
gh variable set AZURE_TENANT_ID --repo "$REPO" --body "$TENANT_ID"
gh variable set AZURE_SUBSCRIPTION_ID --repo "$REPO" --body "$SUBSCRIPTION_ID"
gh variable set STORAGE_ACCOUNT_NAME --repo "$REPO" --body "$STORAGE_ACCOUNT"
echo "Set variables for $REPO"
done
Phase 3: Repository Configuration
3.1 PR Templates
Create these files in every repo. The script below does it for all repos at once.
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
# Business Change Template
cat > .github/PULL_REQUEST_TEMPLATE/business_change.md <<'TMPL'
---
name: Business Change Request
about: Changes requiring business stakeholder approval
labels: cr:business, approval:pending
---
## Change Request - Business
### Classification
- [ ] This change affects business logic, policy, or end-user experience
- [ ] Business justification documented below
### Business Justification
<!-- Why is this change needed from a business perspective? -->
### Impact Assessment
- **Users affected:**
- **Services affected:**
- **Rollback plan:**
### Approval Chain
> All three approvals required before merge.
- [ ] **L1 - Technical Review** (IT team member)
- [ ] **L2 - IT Leadership** (IT manager/lead)
- [ ] **L3 - Business Owner** (business stakeholder)
### Change Window
- **Requested date:**
- **Environment:** dev / staging / prod
TMPL
# IT Change Template
cat > .github/PULL_REQUEST_TEMPLATE/it_change.md <<'TMPL'
---
name: IT Change Request
about: Technical changes within IT scope only
labels: cr:it, approval:pending
---
## Change Request - IT
### Classification
- [ ] This change is purely technical (no business logic impact)
- [ ] Peer-reviewed and tested in lower environment
### Description
<!-- What is changing and why -->
### Impact Assessment
- **Services affected:**
- **Rollback plan:**
### Approval
- [ ] **IT Peer Review** (any IT team member)
### Change Window
- **Environment:** dev / staging / prod
TMPL
git add -A
git commit -m "Add PR templates for business and IT change requests"
git push
cd ..
done
3.2 CODEOWNERS
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
cat > .github/CODEOWNERS <<EOF
# Default - IT team reviews everything
* @$ORG/it-team
# Production changes require IT leads
/environments/prod/ @$ORG/it-leads
# CI/CD changes require IT leads
/.github/ @$ORG/it-leads
EOF
git add .github/CODEOWNERS
git commit -m "Add CODEOWNERS for review routing"
git push
cd ..
done
3.3 Terraform Backend Configuration
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
# Create backend config for each environment
for ENV in dev staging prod; do
cat > "environments/$ENV/backend.tf" <<EOF
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "$STORAGE_ACCOUNT"
container_name = "tfstate"
key = "$SERVICE/$ENV.tfstate"
use_oidc = true
}
}
EOF
cat > "environments/$ENV/providers.tf" <<EOF
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
use_oidc = true
}
EOF
cat > "environments/$ENV/main.tf" <<EOF
# $SERVICE - $ENV environment
# Add your resources here
EOF
done
git add -A
git commit -m "Add Terraform backend and provider config"
git push
cd ..
done
3.4 .gitignore
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
cat > .gitignore <<'EOF'
# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
!*.tfvars.example
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
EOF
git add .gitignore
git commit -m "Add .gitignore"
git push
cd ..
done
Phase 4: CI/CD Pipelines
4.1 Terraform Plan Workflow (runs on every PR)
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
cat > .github/workflows/terraform-plan.yml <<'EOF'
name: Terraform Plan
on:
pull_request:
branches: [main]
permissions:
id-token: write
contents: read
pull-requests: write
env:
ARM_USE_OIDC: true
ARM_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.changes.outputs.environments }}
steps:
- uses: actions/checkout@v4
- id: changes
uses: dorny/paths-filter@v3
with:
filters: |
dev:
- 'environments/dev/**'
- 'modules/**'
staging:
- 'environments/staging/**'
- 'modules/**'
prod:
- 'environments/prod/**'
- 'modules/**'
plan:
needs: detect-changes
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
working-directory: environments/${{ matrix.environment }}
run: terraform init
- name: Terraform Validate
working-directory: environments/${{ matrix.environment }}
run: terraform validate
- name: Terraform Format Check
working-directory: environments/${{ matrix.environment }}
run: terraform fmt -check -recursive
- name: Terraform Plan
id: plan
working-directory: environments/${{ matrix.environment }}
run: |
terraform plan -no-color -out=tfplan 2>&1 | tee plan-output.txt
echo "plan_text<> $GITHUB_OUTPUT
cat plan-output.txt >> $GITHUB_OUTPUT
echo "PLAN_EOF" >> $GITHUB_OUTPUT
- name: Comment Plan on PR
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.plan_text }}`;
const truncated = plan.length > 60000
? plan.substring(0, 60000) + '\n\n... (truncated)'
: plan;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `### Terraform Plan - \`${{ matrix.environment }}\`\n\`\`\`\n${truncated}\n\`\`\``
});
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: tfsec
uses: aquasecurity/tfsec-action@v1.0.3
with:
soft_fail: false
EOF
git add .github/workflows/terraform-plan.yml
git commit -m "Add Terraform plan workflow"
git push
cd ..
done
4.2 Approval Gate Workflow
This is the key workflow that enforces the two-tier approval system. It counts unique approvals and checks against the CR label.
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
cat > .github/workflows/approval-gate.yml <<'WORKFLOW'
name: Approval Gate
on:
pull_request_review:
types: [submitted]
pull_request:
types: [labeled, unlabeled, opened, synchronize]
jobs:
check-approvals:
runs-on: ubuntu-latest
steps:
- name: Validate approval requirements
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const labels = pr.labels.map(l => l.name);
// Determine CR type
const isBusiness = labels.includes('cr:business');
const isIT = labels.includes('cr:it');
if (!isBusiness && !isIT) {
core.setFailed(
'❌ PR must have either "cr:business" or "cr:it" label. ' +
'Add the appropriate label to indicate the change type.'
);
return;
}
if (isBusiness && isIT) {
core.setFailed(
'❌ PR cannot have both "cr:business" and "cr:it" labels. Remove one.'
);
return;
}
// Get all reviews
const reviews = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// Get latest review per user (handles re-reviews)
const latestReviews = {};
for (const review of reviews.data) {
if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED') {
latestReviews[review.user.login] = review;
}
}
const approvals = Object.values(latestReviews)
.filter(r => r.state === 'APPROVED')
.map(r => r.user.login);
const changesRequested = Object.values(latestReviews)
.filter(r => r.state === 'CHANGES_REQUESTED')
.map(r => r.user.login);
// Block if anyone requested changes
if (changesRequested.length > 0) {
core.setFailed(
`❌ Changes requested by: ${changesRequested.join(', ')}. ` +
`Resolve feedback before merging.`
);
return;
}
const requiredCount = isBusiness ? 3 : 1;
const crType = isBusiness ? 'Business' : 'IT';
if (approvals.length < requiredCount) {
core.setFailed(
`⏳ ${crType} Change requires ${requiredCount} approval(s). ` +
`Currently has ${approvals.length}: ${approvals.join(', ') || 'none yet'}`
);
} else {
core.info(
`✅ ${crType} Change: ${approvals.length}/${requiredCount} approvals met. ` +
`Approved by: ${approvals.join(', ')}`
);
}
WORKFLOW
git add .github/workflows/approval-gate.yml
git commit -m "Add approval gate workflow (business=3, IT=1)"
git push
cd ..
done
4.3 Terraform Apply Workflow (runs after merge to main)
for SERVICE in "${SERVICES[@]}"; do
cd "terraform-$SERVICE"
cat > .github/workflows/terraform-apply.yml <<'EOF'
name: Terraform Apply
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
apply:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
working-directory: environments/${{ matrix.environment }}
run: terraform init
- name: Terraform Apply
working-directory: environments/${{ matrix.environment }}
run: terraform apply -auto-approve
EOF
git add .github/workflows/terraform-apply.yml
git commit -m "Add Terraform apply workflow"
git push
cd ..
done
prod GitHub Environment (Phase 5.2) with required reviewers so prod deploys wait for manual approval even after merge.
Phase 5: Branch Protection
5.1 Configure Branch Rulesets
Apply branch protection via the GitHub API. This makes check-approvals a required status check, which enforces the label-based approval logic from Phase 4.2.
ORG="your-org-name"
for SERVICE in "${SERVICES[@]}"; do
REPO="terraform-$SERVICE"
# Create branch protection rule for main
gh api repos/$ORG/$REPO/branches/main/protection \
-X PUT \
-H "Accept: application/vnd.github+json" \
--input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": [
"check-approvals",
"plan (dev)",
"plan (staging)",
"plan (prod)",
"security-scan"
]
},
"enforce_admins": false,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true
},
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
}
JSON
echo "Branch protection set for $REPO"
done
check-approvals status check. The status check enforces 1 for IT or 3 for business based on the label. The GitHub-level approval satisfies the branch rule; the workflow enforces the real requirement.
5.2 Configure GitHub Environments
Add a manual approval gate for production deployments:
ORG="your-org-name"
for SERVICE in "${SERVICES[@]}"; do
REPO="terraform-$SERVICE"
# Create environments
for ENV in dev staging prod; do
gh api repos/$ORG/$REPO/environments/$ENV -X PUT
done
# Add required reviewers to prod environment
# Get the it-leads team ID
TEAM_ID=$(gh api orgs/$ORG/teams/it-leads --jq '.id')
gh api repos/$ORG/$REPO/environments/prod \
-X PUT \
--input - <<JSON
{
"reviewers": [
{
"type": "Team",
"id": $TEAM_ID
}
],
"deployment_branch_policy": {
"protected_branches": true,
"custom_branch_policies": false
}
}
JSON
echo "Environments configured for $REPO"
done
Phase 6: Validation
6.1 Test Both Approval Flows
Test A: IT Change Flow
- Create a branch:
git checkout -b test/it-change - Make a trivial change (add a comment to a .tf file)
- Push and open a PR using the IT Change template
- Add the
cr:itlabel - Verify:
check-approvalsstatus shows "requires 1 approval" - Have one IT team member approve
- Verify:
check-approvalspasses, merge is enabled - Merge and verify
terraform applyruns
Test B: Business Change Flow
- Create a branch:
git checkout -b test/business-change - Make a trivial change
- Push and open a PR using the Business Change template
- Add the
cr:businesslabel - Verify:
check-approvalsshows "requires 3 approvals" - Have one person approve — verify still blocked (1/3)
- Second approval — verify still blocked (2/3)
- Third approval — verify gate passes, merge is enabled
- Merge and verify deploy
Test C: Missing Label
- Open a PR with no
cr:label - Verify:
check-approvalsfails with "must have either cr:business or cr:it label"
6.2 Go-Live Checklist
- All repos created with correct naming
- GitHub teams created and members added
- Team permissions set on all repos
- Azure resource group created
- Storage account created and verified
- Key Vault created with RBAC
- Managed identity created
- Federated credentials created for all repos
- GitHub variables set on all repos
- PR templates committed to all repos
- CODEOWNERS committed to all repos
- Backend config committed to all repos
-
terraform initsucceeds locally for each env - Plan workflow runs on PR
- Approval gate enforces correct counts
- Apply workflow runs on merge to main
- Prod environment requires manual approval
- IT change flow tested end-to-end
- Business change flow tested end-to-end
- tfsec scan runs and blocks on findings
Reference: Quick Command Cheat Sheet
| Task | Command |
|---|---|
| Add new service repo | gh repo create $ORG/terraform-{name} --private |
| Add federated credential | az identity federated-credential create ... |
| Add Key Vault secret | az keyvault secret set --vault-name kv-terraform-secrets --name {name} --value {val} |
| Read Key Vault secret in TF | data "azurerm_key_vault_secret" "x" { name = "..." } |
| Check state | terraform state list |
| Import existing resource | terraform import {resource.name} {azure-id} |
| Unlock stuck state | terraform force-unlock {lock-id} |
| Add team member | gh api orgs/$ORG/teams/{team}/memberships/{user} -f role="member" |
Reference: DNS Records
After deployment, add these records in Hover for easydatatransfer.accessnarrative.com. The exact values will be provided after Vercel deployment.