Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/import-container-app/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:
description: Project name component of the Container App name (TF_VAR_projname)
env:
required: true
description: Environment name (dev|uat|prod)
description: Environment name (dev|staging|prod)
location_short:
required: true
description: Short location code (TF_VAR_location_short)
Expand Down
6 changes: 3 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
- [ ] No environment/config changes required
- [ ] Environment/config changes required (describe below)

## UAT Toggle (PRs to `main`)
## Staging Toggle (PRs to `main`)

- Add label `run-uat` to this PR to enable UAT deployment (`deploy-uat`).
- Remove label `run-uat` to skip UAT deployment.
- Add label `run-staging` to this PR to enable staging deployment (`deploy-staging`).
- Remove label `run-staging` to skip staging deployment.

## Risk / Rollback

Expand Down
24 changes: 12 additions & 12 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,23 @@ env:

jobs:
plan:
# PR into dev → dev | PR into main + label 'run-uat' → uat | Push to main/workflow_dispatch → prod
# PR into dev → dev | PR into main + label 'run-staging' → staging | Push to main/workflow_dispatch → prod
# Skip plan for PRs from forks (no repo secrets; avoids AADSTS700213)
# Runtime UAT toggle: add PR label 'run-uat' to enable UAT on PRs into main.
# Runtime staging toggle: add PR label 'run-staging' to enable staging on PRs into main.
if: |
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) &&
(
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'workflow_dispatch') ||
(github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-uat'))
(github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-staging'))
)
name: Plan ${{ matrix.environment }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment: ${{ (github.event_name == 'workflow_dispatch' && fromJSON('["prod"]')) || (github.event_name == 'push' && github.ref == 'refs/heads/main' && fromJSON('["prod"]')) || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'dev' && fromJSON('["dev"]')) || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-uat') && fromJSON('["uat"]')) || fromJSON('["prod"]') }}
environment: ${{ (github.event_name == 'workflow_dispatch' && fromJSON('["prod"]')) || (github.event_name == 'push' && github.ref == 'refs/heads/main' && fromJSON('["prod"]')) || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'dev' && fromJSON('["dev"]')) || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-staging') && fromJSON('["staging"]')) || fromJSON('["prod"]') }}
environment: ${{ matrix.environment }}
defaults:
run:
Expand Down Expand Up @@ -324,18 +324,18 @@ jobs:

jq -e '.enabled == true' /tmp/selection-get.json > /dev/null

Comment on lines +154 to +170
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does GitHub Actions allow environment:on a job that calls a reusable workflow viauses:? If not, what is the documented way to bind the called job to dev, staging, or prod?

💡 Result:

No. A job that calls a reusable workflow via jobs.<job_id>.uses: can only use a limited set of keywords, and environment is not one of them (so environment: on the calling job is not supported). [1]

Documented way to bind to dev / staging / prod:

  1. Pass the target environment name as an input from the caller (often via a matrix of dev/stage/prod). [2]
  2. Set environment: inside the reusable workflow’s job, using that input (the environment name can be an expression and can use the inputs context). [3]
  3. This is also why environment secrets can’t be passed from the caller via workflow_call (it doesn’t support environment), so the environment needs to be selected in the called workflow’s job. [2]

Example:

Caller:

jobs:
  deploy:
    strategy:
      matrix:
        target: [dev, staging, prod]
    uses: org/repo/.github/workflows/deploy.yml@main
    with:
      target: ${{ matrix.target }}

[2]

Reusable workflow (deploy.yml):

on:
  workflow_call:
    inputs:
      target:
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.target }}
    steps:
      - run: echo "Deploying to ${{ inputs.target }}"

[3]

Sources: GitHub Docs on reusable-workflow keyword limits [1], reusable workflows + environment-secret note + matrix example [2], and workflow syntax for jobs.<job_id>.environment (name can use inputs) [3].


🏁 Script executed:

cd .github/workflows && head -n 220 deploy.yaml | tail -n 70

Repository: phoenixvc/ai-gateway

Length of output: 2993


Remove environment: from all three reusable-workflow caller jobs.

Jobs that call a reusable workflow via uses: cannot use the environment: key. Currently, deploy-dev, deploy-staging, and deploy-prod all specify environment: at the job level alongside uses:, which violates GitHub Actions job schema. Move environment selection into the called workflow (deploy-environment.yaml) by setting environment: there with the input value (e.g., environment: ${{ inputs.env_name }}). The caller jobs already pass the environment name correctly via env_name input and only need the invalid environment: key removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy.yaml around lines 154 - 170, The job-level
environment key must be removed from the reusable-workflow caller jobs (e.g.,
deploy-dev, deploy-staging, deploy-prod) because jobs that use `uses:` cannot
include `environment:`; update each caller to only pass the env via the
`env_name` input (already present) and then add `environment: ${{
inputs.env_name }}` inside the called workflow `deploy-environment.yaml` (use
the `inputs.env_name` value in that workflow's job definition) so the
environment selection is handled by the reusable workflow rather than the
callers.

deploy-uat:
name: Deploy uat
deploy-staging:
name: Deploy staging
needs: plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-uat')
environment: uat
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && contains(join(github.event.pull_request.labels.*.name, ','), 'run-staging')
environment: staging
defaults:
run:
working-directory: infra/env/uat
working-directory: infra/env/staging

env:
TF_VAR_env: "uat"
TF_VAR_env: "staging"
TF_VAR_projname: "aigateway"
TF_VAR_location: "southafricanorth"
TF_VAR_location_short: "san"
Expand Down Expand Up @@ -407,7 +407,7 @@ jobs:
-backend-config="resource_group_name=${TF_BACKEND_RG}" \
-backend-config="storage_account_name=${TF_BACKEND_SA}" \
-backend-config="container_name=${TF_BACKEND_CONTAINER}" \
-backend-config="key=uat.terraform.tfstate"
-backend-config="key=staging.terraform.tfstate"

- name: Import existing Container App into Terraform state
uses: ./.github/actions/import-container-app
Expand All @@ -416,7 +416,7 @@ jobs:
env: ${{ env.TF_VAR_env }}
location_short: ${{ env.TF_VAR_location_short }}
subscription_id: ${{ env.AZURE_SUBSCRIPTION_ID }}
terraform_working_directory: infra/env/uat
terraform_working_directory: infra/env/staging

- name: Terraform Plan
run: |
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Creates the shared resource group, storage account, and container for Terraform

### 2. Add GitHub secrets

Add these secrets to each GitHub **Environment** (dev, uat, prod): **Settings → Environments → &lt;env&gt; → Environment secrets**.
Add these secrets to each GitHub **Environment** (dev, staging, prod): **Settings → Environments → &lt;env&gt; → Environment secrets**.

| Secret | Description | Example |
| ----------------------- | --------------------------------- | --------------------------------------------- |
Expand All @@ -53,16 +53,16 @@ Bootstrap prints these values. For local runs, copy `infra/.env.local.example` t
**Bash:**

```bash
./infra/scripts/terraform-init.sh dev # or uat, prod
./infra/scripts/terraform-init.sh dev # or staging, prod
```

**PowerShell:**

```powershell
.\infra\scripts\terraform-init.ps1 -Env dev # or uat, prod
.\infra\scripts\terraform-init.ps1 -Env dev # or staging, prod
```

Valid environments: `dev`, `uat`, `prod`.
Valid environments: `dev`, `staging`, `prod`.

### 4. Plan and apply

Expand All @@ -74,11 +74,11 @@ terraform apply

## Environments

| Env | Purpose |
| ---- | --------------- |
| dev | Development |
| uat | User acceptance |
| prod | Production |
| Env | Purpose |
| ------- | ----------- |
| dev | Development |
| staging | Staging |
| prod | Production |

## CI/CD

Expand All @@ -104,6 +104,6 @@ pnpm format

- [PRD](docs/PRD.md) – Product requirements
- [Terraform Blueprint](docs/Terraform_Blueprint.md) – Infrastructure design
- [CI/CD Runbook](docs/CI_CD.md) – workflow behavior, UAT toggle, smoke tests
- [CI/CD Runbook](docs/CI_CD.md) – workflow behavior, staging toggle, smoke tests
- [Azure OIDC Setup](docs/AZURE_OIDC_SETUP.md) – GitHub Actions OIDC configuration
- [Secrets Checklist](docs/SECRETS.md) – Copy/paste setup for GitHub environment secrets
2 changes: 1 addition & 1 deletion dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function escHtml(s) {

function deriveEnv(url) {
if (!url) return null;
const m = url.match(/pvc-(dev|uat|prod)-/);
const m = url.match(/pvc-(dev|staging|prod)-/);
return m ? m[1] : null;
}

Expand Down
20 changes: 10 additions & 10 deletions docs/AZURE_OIDC_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ If you see:
Error: AADSTS700213: No matching federated identity record found for presented assertion subject 'repo:phoenixvc/ai-gateway:environment:dev'
```

**Cause:** The workflow uses `environment: dev` (and uat/prod), so the OIDC subject is `repo:org/repo:environment:dev`. Azure must have a federated credential with that exact subject.
**Cause:** The workflow uses `environment: dev` (and staging/prod), so the OIDC subject is `repo:org/repo:environment:dev`. Azure must have a federated credential with that exact subject.

### Fix: Add environment federated credentials

Expand All @@ -32,21 +32,21 @@ az ad app list --display-name pvc-shared-github-actions-oidc --query "[0].appId"

1. Go to **Azure Portal** → **Microsoft Entra ID** → **App registrations** → your app (e.g. `pvc-shared-github-actions-oidc`)
2. **Certificates & secrets** → **Federated credentials** → **Add credential**
3. For each environment (dev, uat, prod), add:
3. For each environment (dev, staging, prod), add:
- **Federated credential scenario:** GitHub Actions deploying Azure resources
- **Organization:** phoenixvc
- **Repository:** ai-gateway
- **Entity type:** Environment
- **Environment name:** dev (or uat, prod)
- **Name:** github-actions-dev (or uat, prod)
- **Environment name:** dev (or staging, prod)
- **Name:** github-actions-dev (or staging, prod)

### Subject formats

| Workflow config | OIDC subject |
| -------------------- | ----------------------------------------------- |
| `environment: dev` | `repo:phoenixvc/ai-gateway:environment:dev` |
| `environment: uat` | `repo:phoenixvc/ai-gateway:environment:uat` |
| `environment: prod` | `repo:phoenixvc/ai-gateway:environment:prod` |
| Branch only (no env) | `repo:phoenixvc/ai-gateway:ref:refs/heads/main` |
| Workflow config | OIDC subject |
| ---------------------- | ----------------------------------------------- |
| `environment: dev` | `repo:phoenixvc/ai-gateway:environment:dev` |
| `environment: staging` | `repo:phoenixvc/ai-gateway:environment:staging` |
| `environment: prod` | `repo:phoenixvc/ai-gateway:environment:prod` |
| Branch only (no env) | `repo:phoenixvc/ai-gateway:ref:refs/heads/main` |

The federated credential **Subject** in Azure must match exactly.
10 changes: 5 additions & 5 deletions docs/CI_CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ This document describes the current GitHub Actions deployment behavior for `ai-g

- PRs from forks are skipped for deployment-related jobs (no repo secrets).
- PRs targeting `dev` run `plan` + `deploy-dev`.
- PRs targeting `main` run UAT only when the PR has label `run-uat`.
- PRs targeting `main` run staging only when the PR has label `run-staging`.
- Push to `main` and `workflow_dispatch` run `plan` + `deploy-prod`.

## Runtime UAT toggle
## Runtime staging toggle

UAT deployment for PRs to `main` is controlled by PR label:
staging deployment for PRs to `main` is controlled by PR label:

- Add label `run-uat` to enable `deploy-uat` for that PR.
- Remove label `run-uat` to disable UAT for that PR.
- Add label `run-staging` to enable `deploy-staging` for that PR.
- Remove label `run-staging` to disable staging for that PR.

## Smoke test behavior

Expand Down
12 changes: 6 additions & 6 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Roo/Qoder currently struggles with Azure model/operation mismatches. A gateway n
2. Support:
- `POST /v1/responses` routed to Azure **Responses** endpoint for configurable model (default: `gpt-5.3-codex`).
- `POST /v1/embeddings` routed to Azure embeddings deployment.
3. Enable **multiple environments** (dev/uat/prod) and **multiple downstream projects**.
3. Enable **multiple environments** (dev/staging/prod) and **multiple downstream projects**.
4. Infrastructure managed with **Terraform**.
5. CI/CD via **GitHub Actions** using **Azure OIDC** (no long-lived secrets).
6. “Get it working” first; hardening follows.
Expand All @@ -29,7 +29,7 @@ Roo/Qoder currently struggles with Azure model/operation mismatches. A gateway n
## 3) Environments

- `dev`
- `uat`
- `staging`
- `prod`

Each env is independently deployable.
Expand Down Expand Up @@ -150,7 +150,7 @@ Gateway must expose:
- `docs/` - Documentation.
- `infra/`
- `modules/aigateway_aca` - Core Terraform module.
- `env/dev|uat|prod` - Environment-specific configurations.
- `env/dev|staging|prod` - Environment-specific configurations.
- `.github/workflows/` - CI/CD pipelines.
- `scripts/` - Helper scripts (bootstrap).

Expand All @@ -161,13 +161,13 @@ Gateway must expose:
- **Phase 1: Terraform & CI/CD**
- Terraform defines infra.
- GitHub Actions deploys using Azure OIDC.
- Dev auto-apply on merge; UAT/Prod gated with environment approvals.
- Dev auto-apply on merge; Staging/Prod gated with environment approvals.

## 10) Acceptance criteria

1. Roo/Qoder can use gateway for coding with configured model (default `gpt-5.3-codex`) without `chatCompletion operation does not work`.
2. Codebase indexing completes using embeddings through the gateway.
3. Dev/UAT/Prod are reproducible via Terraform + Actions.
3. Dev/staging/Prod are reproducible via Terraform + Actions.
4. No secrets committed.

## 11) Risks & mitigations
Expand All @@ -180,5 +180,5 @@ Gateway must expose:

- M0: Repo setup, Bootstrap scripts (OIDC, State Backend).
- M1: Dev env deployed; smoke tests pass; Roo works.
- M2: UAT + Prod; environment approvals.
- M2: staging + Prod; environment approvals.
- M3: Hardening (Front Door/WAF, Entra auth).
18 changes: 9 additions & 9 deletions docs/SECRETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

Copy this checklist when setting up environments for this repo.

For workflow behavior (dev/uat/prod triggers, PR label `run-uat`, and smoke-test flow), see [CI_CD.md](CI_CD.md).
For workflow behavior (dev/staging/prod triggers, PR label `run-staging`, and smoke-test flow), see [CI_CD.md](CI_CD.md).

## Where to add secrets

Add these as **Environment secrets** in GitHub:

- **Settings → Environments → dev → Environment secrets**
- **Settings → Environments → uat → Environment secrets**
- **Settings → Environments → staging → Environment secrets**
- **Settings → Environments → prod → Environment secrets**

> This workflow is environment-based (`environment: dev|uat|prod`), so each environment should have the full secret set.
> This workflow is environment-based (`environment: dev|staging|prod`), so each environment should have the full secret set.

## Required secrets (all environments)

Expand Down Expand Up @@ -53,7 +53,7 @@ When `STATE_SERVICE_CONTAINER_IMAGE` is set (state-service enabled), set this se

## Copy/paste template

Use this block as a setup checklist when creating/updating `dev`, `uat`, and `prod`:
Use this block as a setup checklist when creating/updating `dev`, `staging`, and `prod`:

```text
AZURE_CLIENT_ID=<GUID>
Expand Down Expand Up @@ -82,13 +82,13 @@ STATE_SERVICE_REGISTRY_PASSWORD=<ghcr-read-packages-token> # required for priv
- [ ] `AIGATEWAY_KEY` matches the key expected by the deployed gateway.
- [ ] OIDC federated credentials exist for each environment subject:
- `repo:phoenixvc/ai-gateway:environment:dev`
- `repo:phoenixvc/ai-gateway:environment:uat`
- `repo:phoenixvc/ai-gateway:environment:staging`
- `repo:phoenixvc/ai-gateway:environment:prod`

## Runtime UAT toggle
## Runtime staging toggle

- UAT deploy on PRs into `main` is controlled by PR label `run-uat`.
- Add label `run-uat` to enable `deploy-uat` for that PR.
- Remove label `run-uat` to skip UAT for that PR.
- Staging deploy on PRs into `main` is controlled by PR label `run-staging`.
- Add label `run-staging` to enable `deploy-staging` for that PR.
- Remove label `run-staging` to skip staging for that PR.

For OIDC troubleshooting, see [AZURE_OIDC_SETUP.md](AZURE_OIDC_SETUP.md).
10 changes: 5 additions & 5 deletions docs/Terraform_Blueprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This canvas includes a working Terraform scaffold:

- `infra/modules/aigateway_aca`
- `infra/env/dev|uat|prod`
- `infra/env/dev|staging|prod`
- Shared state configured via `terraform init -backend-config=...` in GitHub Actions

> Notes:
Expand All @@ -27,7 +27,7 @@ infra/
main.tf
variables.tf
terraform.tfvars
uat/
staging/
main.tf
variables.tf
terraform.tfvars
Expand All @@ -44,7 +44,7 @@ infra/
```hcl
variable "env" {
type = string
description = "Environment name (dev|uat|prod)"
description = "Environment name (dev|staging|prod)"
}

variable "projname" {
Expand Down Expand Up @@ -343,7 +343,7 @@ output "key_vault_name" {

## 5) Env stacks

### 5.1 `infra/env/dev/variables.tf` (repeat for uat/prod)
### 5.1 `infra/env/dev/variables.tf` (repeat for staging/prod)

```hcl
variable "env" { type = string }
Expand Down Expand Up @@ -441,7 +441,7 @@ tags = {
}
```

Repeat the env folders for `uat` and `prod`, changing only `env` and tags.
Repeat the env folders for `staging` and `prod`, changing only `env` and tags.

---

Expand Down
Loading
Loading