Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 22 additions & 5 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,19 @@ jobs:
- name: Configure GHCR pull + deploy backend image
shell: bash
env:
GHCR_PASSWORD: ${{ secrets.GHCR_READ_TOKEN != '' && secrets.GHCR_READ_TOKEN || secrets.GITHUB_TOKEN }}
GHCR_PASSWORD: ${{ secrets.GHCR_READ_TOKEN }}
run: |
set -euo pipefail
if [[ -z "${GHCR_PASSWORD}" ]]; then
echo "::error::GHCR_READ_TOKEN secret is required. GITHUB_TOKEN expires when CD ends — Azure cannot pull images on scale-from-zero without a PAT (read:packages) or public GHCR packages."
exit 1
fi

az containerapp registry set \
--name "$BACKEND_APP_NAME" \
--resource-group "$AZURE_RESOURCE_GROUP" \
--server ghcr.io \
--username "${{ github.actor }}" \
--username "${{ github.repository_owner }}" \
--password "$GHCR_PASSWORD"

az containerapp update \
Expand Down Expand Up @@ -232,13 +238,24 @@ jobs:
- name: Configure GHCR pull + deploy frontend image
shell: bash
env:
GHCR_PASSWORD: ${{ secrets.GHCR_READ_TOKEN != '' && secrets.GHCR_READ_TOKEN || secrets.GITHUB_TOKEN }}
GHCR_PASSWORD: ${{ secrets.GHCR_READ_TOKEN }}
BACKEND_URL: ${{ needs.deploy-backend.outputs.backend_url }}
run: |
set -euo pipefail
if [[ -z "${GHCR_PASSWORD}" ]]; then
echo "::error::GHCR_READ_TOKEN secret is required. GITHUB_TOKEN expires when CD ends — Azure cannot pull images on scale-from-zero without a PAT (read:packages) or public GHCR packages."
exit 1
fi
if [[ -z "${BACKEND_URL}" || "${BACKEND_URL}" == "https://" ]]; then
echo "::error::backend_url job output is empty — cannot set BACKEND_UPSTREAM"
exit 1
fi

az containerapp registry set \
--name "$FRONTEND_APP_NAME" \
--resource-group "$AZURE_RESOURCE_GROUP" \
--server ghcr.io \
--username "${{ github.actor }}" \
--username "${{ github.repository_owner }}" \
--password "$GHCR_PASSWORD"

az containerapp update \
Expand All @@ -247,7 +264,7 @@ jobs:
--image "$FRONTEND_IMAGE" \
--min-replicas 0 \
--max-replicas 2 \
--set-env-vars "BUILD_SHA=${{ github.sha }},BACKEND_UPSTREAM=${{ needs.deploy-backend.outputs.backend_url }}" \
--set-env-vars "BUILD_SHA=${{ github.sha }}" "BACKEND_UPSTREAM=${BACKEND_URL}" \
--output table

- name: Get frontend URL
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export async function googleSignIn(idToken: string): Promise<TokenResponse> {
}

/** GET /api/auth/me — current user profile + query budget */
export async function getMe(): Promise<UserResponse> {
return apiFetch<UserResponse>("/api/auth/me");
export async function getMe(signal?: AbortSignal): Promise<UserResponse> {
return apiFetch<UserResponse>("/api/auth/me", { signal });
}

/** POST /api/auth/logout */
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
bg-gray-50 dark:bg-gray-950">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-violet-600 border-t-transparent
rounded-full animate-spin" />
rounded-full animate-spin motion-reduce:animate-none" />
<p className="text-sm text-gray-400">Loading InsightIQ…</p>
<p className="text-[11px] text-gray-400 max-w-[240px] text-center leading-relaxed">
Waking from idle can take up to a minute.
</p>
</div>
</div>
);
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
return;
}

const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 45_000);

try {
const profile = await getMe();
const profile = await getMe(controller.signal);
setUser(profile);
} catch {
// Token expired or invalid — clear it
// Token expired, backend asleep, or wake-up timed out — show login
localStorage.removeItem(TOKEN_KEY);
} finally {
window.clearTimeout(timeoutId);
setIsLoading(false);
}
};
Expand Down
55 changes: 36 additions & 19 deletions infra/fix-scale-to-zero.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ echo " Backend app : $BACKEND_APP"
echo " Frontend app : $FRONTEND_APP"
echo ""

check_pull_errors() {
local app="$1"
local revision
revision=$(az containerapp show --name "$app" --resource-group "$RESOURCE_GROUP" \
--query "properties.latestRevisionName" -o tsv)
local details
details=$(az containerapp replica list --name "$app" --resource-group "$RESOURCE_GROUP" \
--revision "$revision" -o json 2>/dev/null \
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r[0]['properties']['containers'][0].get('runningStateDetails','')) if r else print('')" 2>/dev/null || true)
if [[ "$details" == *ImagePullBackOff* ]]; then
echo " ⚠ ${app}: ${details}"
return 1
fi
return 0
}

for app in "$BACKEND_APP" "$FRONTEND_APP"; do
echo "--- $app ---"
az containerapp show \
Expand All @@ -38,29 +54,30 @@ for app in "$BACKEND_APP" "$FRONTEND_APP"; do
az containerapp revision list \
--name "$app" \
--resource-group "$RESOURCE_GROUP" \
--query "[].{name:name,active:properties.active,traffic:properties.trafficWeight,health:properties.healthState,replicas:properties.replicas}" \
--query "[].{name:name,active:properties.active,traffic:properties.trafficWeight,health:properties.healthState,replicas:properties.replicas,running:properties.runningState}" \
-o table
check_pull_errors "$app" || PULL_ERROR=1
echo ""
done

echo "[1/3] Enforcing min-replicas=0 (idle cost) on both apps..."
az containerapp update --name "$BACKEND_APP" --resource-group "$RESOURCE_GROUP" --min-replicas 0 --output none
az containerapp update --name "$FRONTEND_APP" --resource-group "$RESOURCE_GROUP" --min-replicas 0 --output none
echo " ✓ min-replicas=0"

echo ""
echo "[2/3] Checklist before redeploy:"
echo " • GitHub Packages → insightiq-frontend / insightiq-backend → Package settings → Public"
echo " (or set GHCR_READ_TOKEN secret with read:packages scope)"
echo " • GitHub Secrets: NEON_DATABASE_URL, JWT_SECRET_KEY, ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID"
echo " • GitHub → Actions → CD → Run workflow on main"
echo "=================================================="
echo " ROOT CAUSE (most common): ImagePullBackOff"
echo "=================================================="
echo " Azure stores GHCR credentials to pull container images."
echo " If CD used GITHUB_TOKEN, it EXPIRES when the workflow ends."
echo " Scale-from-zero then cannot pull → Activation failed → hang forever."
echo ""
echo "[3/3] After CD succeeds, test wake-up (first click may take 30–90s):"
BACKEND_FQDN=$(az containerapp show --name "$BACKEND_APP" --resource-group "$RESOURCE_GROUP" --query "properties.configuration.ingress.fqdn" -o tsv)
FRONTEND_FQDN=$(az containerapp show --name "$FRONTEND_APP" --resource-group "$RESOURCE_GROUP" --query "properties.configuration.ingress.fqdn" -o tsv)
echo " Frontend : https://${FRONTEND_FQDN}/"
echo " Backend : https://${BACKEND_FQDN}/health"
echo " FIX (pick one):"
echo " A) GitHub → Packages → insightiq-frontend + insightiq-backend"
echo " → Package settings → Change visibility to PUBLIC"
echo " B) GitHub → Settings → Developer settings → PAT (classic)"
echo " → read:packages → add as repo secret GHCR_READ_TOKEN"
echo " → re-run CD workflow on main"
echo ""
echo "If a revision still shows 'Activation failed' with 100% traffic,"
echo "open Portal → Revisions → deactivate the failed revision, then re-run CD."
echo " After fixing GHCR access, re-run CD (Actions → CD → Run workflow)."
echo " First visit after idle: allow 30–90s cold start."
echo "=================================================="

if [[ "${PULL_ERROR:-0}" == 1 ]]; then
exit 1
fi
Loading