Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
168 changes: 168 additions & 0 deletions .github/workflows/azure-pr-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Azure Container Apps PR Deployment

on:
pull_request:
types: [opened, reopened, synchronize, closed]

Comment on lines +3 to +6
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

Unrestricted PR trigger exposes Azure resources to untrusted fork code
Running this workflow for every pull_request event allows code from forks to execute arbitrary az * commands with the federated service-principal, giving write access to the subscription. Restrict deployments to trusted actors, e.g.

-on:
-  pull_request:
-    types: [opened, reopened, synchronize, closed]
+on:
+  pull_request:
+    types: [opened, reopened, synchronize, closed]
+    # Only run when the PR originates from this repo (not forks)
+    branches: [main]            # adjust to default branch

or gate the jobs behind a protected environment that requires manual approval.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
pull_request:
types: [opened, reopened, synchronize, closed]
on:
pull_request:
types: [opened, reopened, synchronize, closed]
# Only run when the PR originates from this repo (not forks)
branches: [main] # adjust to default branch
🤖 Prompt for AI Agents
In .github/workflows/azure-pr-deploy.yml around lines 3 to 6, the workflow
triggers on all pull_request events, which allows untrusted fork code to run
Azure commands with elevated permissions. To fix this, restrict the workflow
trigger to trusted actors only by adding a condition that checks the pull
request author or use a protected environment with manual approval to gate
deployment jobs. This prevents unauthorized code from executing sensitive Azure
commands.

permissions:
id-token: write
contents: read
pull-requests: write

env:
AZURE_CONTAINER_REGISTRY: dotinc.azurecr.io
AZURE_RESOURCE_GROUP: rg-brease-pr-${{ github.event.number }}
AZURE_LOCATION: westeurope
API_APP_NAME: brease-api-pr-${{ github.event.number }}

jobs:
deploy:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event.number }}
cancel-in-progress: true
outputs:
api-url: ${{ steps.get-api-url.outputs.url }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Debug OIDC token
run: |
echo "GitHub context:"
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"

- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZ_SP_CLIENT_ID }}
tenant-id: ${{ secrets.AZ_SP_TENANT_ID }}
subscription-id: ${{ secrets.AZ_SUBSCRIPTION_ID }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Azure Container Registry
run: az acr login --name dotinc

- name: Build and push API image
uses: docker/build-push-action@v6
with:
context: ./apps/api
push: true
file: ./apps/api/Dockerfile
tags: ${{ env.AZURE_CONTAINER_REGISTRY }}/brease-api:pr-${{ github.event.number }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Create resource group
run: |
az group create \
--name ${{ env.AZURE_RESOURCE_GROUP }} \
--location ${{ env.AZURE_LOCATION }}

- name: Deploy API service
run: |
az containerapp up \
--name ${{ env.API_APP_NAME }} \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--location ${{ env.AZURE_LOCATION }} \
--image ${{ env.AZURE_CONTAINER_REGISTRY }}/brease-api:pr-${{ github.event.number }} \
--registry-server ${{ env.AZURE_CONTAINER_REGISTRY }} \
--ingress external \
--target-port 4400 \
--secrets \
infisicalClientId=${{ secrets.INFISICAL_CLIENT_ID }} \
infisicalClientSecret=${{ secrets.INFISICAL_CLIENT_SECRET }} \
--env-vars \
PORT=4400 \
NODE_ENV=staging \
INFISICAL_PROJECT_ID=642ed4939db25595ac7eb9cd \
INFISICAL_ENVIRONMENT=staging \
INFISICAL_CLIENT_ID=secretref:infisicalClientId \
INFISICAL_CLIENT_SECRET=secretref:infisicalClientSecret

- name: Get API URL
id: get-api-url
run: |
URL=$(az containerapp show \
--name ${{ env.API_APP_NAME }} \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--query properties.configuration.ingress.fqdn \
--output tsv)
echo "url=https://$URL" >> $GITHUB_OUTPUT

Comment on lines +97 to +107
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

az containerapp show can race with provisioning – add retry
Immediately querying the FQDN sometimes returns an empty string while DNS & ingress settle. A short retry loop (or az rest --retry-interval) prevents posting an empty URL to the PR comment.

-  URL=$(az containerapp show \
+  for i in {1..10}; do
+    URL=$(az containerapp show \
       --name ${{ env.API_APP_NAME }} \
       --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
       --query properties.configuration.ingress.fqdn \
       --output tsv)
+    [ -n "$URL" ] && break
+    sleep 15
+  done
   echo "url=https://$URL" >> $GITHUB_OUTPUT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Get API URL
id: get-api-url
run: |
URL=$(az containerapp show \
--name ${{ env.API_APP_NAME }} \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--query properties.configuration.ingress.fqdn \
--output tsv)
echo "url=https://$URL" >> $GITHUB_OUTPUT
- name: Get API URL
id: get-api-url
run: |
for i in {1..10}; do
URL=$(az containerapp show \
--name ${{ env.API_APP_NAME }} \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--query properties.configuration.ingress.fqdn \
--output tsv)
[ -n "$URL" ] && break
sleep 15
done
echo "url=https://$URL" >> $GITHUB_OUTPUT
🤖 Prompt for AI Agents
In .github/workflows/azure-pr-deploy.yml around lines 100 to 110, the az
containerapp show command can return an empty FQDN due to provisioning delays.
To fix this, wrap the command in a retry loop that attempts the query multiple
times with a short delay between tries, ensuring the URL is not empty before
echoing it to GITHUB_OUTPUT. This prevents posting an empty URL to the PR
comment.

- name: Comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-deployment
message: |
## 🚀 Brease PR Environment Deployed Successfully!

**API Service**: ${{ steps.get-api-url.outputs.url }}
**OpenAPI Docs**: ${{ steps.get-api-url.outputs.url }}/docs
**Health Check**: ${{ steps.get-api-url.outputs.url }}/health

**Resources Created:**
- Resource Group: `${{ env.AZURE_RESOURCE_GROUP }}`
- API Service: `${{ env.API_APP_NAME }}` (with managed environment)

**API Testing:**
```bash
# Test the API endpoint
curl ${{ steps.get-api-url.outputs.url }}/health

# View OpenAPI documentation
open ${{ steps.get-api-url.outputs.url }}/docs
```

> 💡 This environment will be automatically cleaned up when the PR is merged or closed.

cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZ_SP_CLIENT_ID }}
tenant-id: ${{ secrets.AZ_SP_TENANT_ID }}
subscription-id: ${{ secrets.AZ_SUBSCRIPTION_ID }}

- name: Delete resource group
run: |
if az group exists --name ${{ env.AZURE_RESOURCE_GROUP }}; then
echo "Deleting resource group: ${{ env.AZURE_RESOURCE_GROUP }}"
az group delete \
--name ${{ env.AZURE_RESOURCE_GROUP }} \
--yes \
--no-wait
else
echo "Resource group ${{ env.AZURE_RESOURCE_GROUP }} does not exist"
fi

- name: Clean up container images
run: |
# Delete PR-specific images from ACR
az acr repository delete \
--name dotinc \
--repository brease-api \
--tag pr-${{ github.event.number }} \
--yes || true

- name: Comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-deployment
message: |
## 🧹 Brease PR Environment Cleaned Up

All Azure resources for this PR have been deleted:
- Resource Group: `${{ env.AZURE_RESOURCE_GROUP }}`
- Container image: `brease-api:pr-${{ github.event.number }}`

> ✅ Cleanup completed successfully.
12 changes: 10 additions & 2 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG GO_VERSION=1
FROM golang:${GO_VERSION}-bookworm as builder

Check warning on line 2 in apps/api/Dockerfile

View workflow job for this annotation

GitHub Actions / deploy

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/
# Update package lists and install ca-certificates

WORKDIR /usr/src/app
Expand All @@ -10,11 +10,19 @@


FROM debian:bookworm
# Update package lists and install ca-certificates
# Update package lists and install ca-certificates and Infisical CLI
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
apt-get update --fix-missing && apt-get install -y \
ca-certificates \
curl \
bash \
&& curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /brease /usr/local/bin/
CMD ["brease"]
COPY start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh

CMD ["/usr/local/bin/start.sh"]
5 changes: 5 additions & 0 deletions apps/api/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden the script with set -euo pipefail

Fail fast on unset variables and pipe errors for more predictable behaviour.

-set -e
+set -euo pipefail
🤖 Prompt for AI Agents
In apps/api/start.sh at line 2, replace the current 'set -e' command with 'set
-euo pipefail' to make the script fail fast on unset variables and pipe errors,
improving its robustness and predictability.

echo "Fetching secrets from Infisical..."
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --plain --silent)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix SC2155 and quote sensitive expansions

Declare then export and always quote to avoid word-splitting / injection.

-export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --plain --silent)
+INFISICAL_TOKEN=$(infisical login \
+  --method=universal-auth \
+  --client-id="$INFISICAL_CLIENT_ID" \
+  --client-secret="$INFISICAL_CLIENT_SECRET" \
+  --plain --silent)
+export INFISICAL_TOKEN
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --plain --silent)
INFISICAL_TOKEN=$(infisical login \
--method=universal-auth \
--client-id="$INFISICAL_CLIENT_ID" \
--client-secret="$INFISICAL_CLIENT_SECRET" \
--plain --silent)
export INFISICAL_TOKEN
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 4-4: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
In apps/api/start.sh at line 4, the export statement combines declaration and
export, which triggers SC2155, and the variable expansion is unquoted, risking
word-splitting or injection. Fix this by first declaring the variable on a
separate line, quoting the command substitution to prevent word-splitting, and
then exporting the variable on the next line.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Quote variables & split assignment to avoid masked return value

Capturing the token in the same export statement both masks infisical login’s exit status (SC2155) and leaves expansions un-quoted, which will break if any variable contains whitespace. Recommend:

-export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --plain --silent)
+INFISICAL_TOKEN="$(infisical login \
+  --method=universal-auth \
+  --client-id="$INFISICAL_CLIENT_ID" \
+  --client-secret="$INFISICAL_CLIENT_SECRET" \
+  --plain --silent)"
+export INFISICAL_TOKEN

Adds quoting, preserves exit-code, and keeps the script safety guarantees already implied by set -e.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --plain --silent)
INFISICAL_TOKEN="$(infisical login \
--method=universal-auth \
--client-id="$INFISICAL_CLIENT_ID" \
--client-secret="$INFISICAL_CLIENT_SECRET" \
--plain --silent)"
export INFISICAL_TOKEN
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 4-4: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
In apps/api/start.sh at line 4, the export statement combines variable
assignment and command substitution without quoting, which masks the exit status
and risks word splitting. To fix this, split the assignment and export into two
separate commands, assign the output of the infisical login command to the
variable with proper double quotes around the command substitution to preserve
whitespace, then export the variable on the next line. This preserves the exit
code and ensures safe handling of the token value.

infisical run --projectId=$INFISICAL_PROJECT_ID --env=staging brease
Loading