feat(backend): add isolated v2 foundation stack and query service scaffold

This commit is contained in:
zouantchaw
2026-02-26 12:26:08 -05:00
parent f316d673d0
commit d6559a0a11
14 changed files with 3304 additions and 1 deletions

View File

@@ -25,6 +25,14 @@ jobs:
make -n backend-smoke-core ENV=dev make -n backend-smoke-core ENV=dev
make -n backend-smoke-commands ENV=dev make -n backend-smoke-commands ENV=dev
make -n backend-logs-core ENV=dev make -n backend-logs-core ENV=dev
make -n backend-bootstrap-v2-dev ENV=dev
make -n backend-deploy-core-v2 ENV=dev
make -n backend-deploy-commands-v2 ENV=dev
make -n backend-deploy-query-v2 ENV=dev
make -n backend-smoke-core-v2 ENV=dev
make -n backend-smoke-commands-v2 ENV=dev
make -n backend-smoke-query-v2 ENV=dev
make -n backend-logs-core-v2 ENV=dev
backend-services-tests: backend-services-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -33,6 +41,7 @@ jobs:
service: service:
- backend/core-api - backend/core-api
- backend/command-api - backend/command-api
- backend/query-api
defaults: defaults:
run: run:
working-directory: ${{ matrix.service }} working-directory: ${{ matrix.service }}

View File

@@ -28,3 +28,4 @@
| 2026-02-25 | 0.1.23 | Updated schema blueprint and reconciliation docs to add `business_memberships` and `vendor_memberships` as first-class data actors. | | 2026-02-25 | 0.1.23 | Updated schema blueprint and reconciliation docs to add `business_memberships` and `vendor_memberships` as first-class data actors. |
| 2026-02-25 | 0.1.24 | Removed stale `m4-discrepencies.md` document from M4 planning docs cleanup. | | 2026-02-25 | 0.1.24 | Removed stale `m4-discrepencies.md` document from M4 planning docs cleanup. |
| 2026-02-25 | 0.1.25 | Added target schema model catalog with keys and domain relationship diagrams for slide/workshop use. | | 2026-02-25 | 0.1.25 | Added target schema model catalog with keys and domain relationship diagrams for slide/workshop use. |
| 2026-02-26 | 0.1.26 | Added isolated v2 backend foundation targets, scaffolded `backend/query-api`, and expanded backend CI dry-runs/tests for v2/query. |

View File

@@ -89,6 +89,18 @@ help:
@echo " make backend-smoke-commands [ENV=dev] Run health smoke test for command service (/health)" @echo " make backend-smoke-commands [ENV=dev] Run health smoke test for command service (/health)"
@echo " make backend-logs-core [ENV=dev] Tail/read logs for core service" @echo " make backend-logs-core [ENV=dev] Tail/read logs for core service"
@echo "" @echo ""
@echo " ☁️ BACKEND FOUNDATION V2 (Isolated Parallel Stack)"
@echo " ────────────────────────────────────────────────────────────────────"
@echo " make backend-bootstrap-v2-dev [ENV=dev] Bootstrap isolated v2 resources + SQL instance"
@echo " make backend-deploy-core-v2 [ENV=dev] Build and deploy core API v2 service"
@echo " make backend-deploy-commands-v2 [ENV=dev] Build and deploy command API v2 service"
@echo " make backend-deploy-query-v2 [ENV=dev] Build and deploy query API v2 scaffold"
@echo " make backend-v2-migrate-idempotency Create/upgrade command idempotency table for v2 DB"
@echo " make backend-smoke-core-v2 [ENV=dev] Run health smoke test for core API v2 (/health)"
@echo " make backend-smoke-commands-v2 [ENV=dev] Run health smoke test for command API v2 (/health)"
@echo " make backend-smoke-query-v2 [ENV=dev] Run health smoke test for query API v2 (/health)"
@echo " make backend-logs-core-v2 [ENV=dev] Tail/read logs for core API v2"
@echo ""
@echo " 🛠️ DEVELOPMENT TOOLS" @echo " 🛠️ DEVELOPMENT TOOLS"
@echo " ────────────────────────────────────────────────────────────────────" @echo " ────────────────────────────────────────────────────────────────────"
@echo " make install-melos Install Melos globally (for mobile dev)" @echo " make install-melos Install Melos globally (for mobile dev)"

View File

@@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV PORT=8080
EXPOSE 8080
CMD ["node", "src/server.js"]

2901
backend/query-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"name": "@krow/query-api",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"start": "node src/server.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.21.2",
"firebase-admin": "^13.0.2",
"pino": "^9.6.0",
"pino-http": "^10.3.0"
},
"devDependencies": {
"supertest": "^7.0.0"
}
}

View File

@@ -0,0 +1,28 @@
import express from 'express';
import pino from 'pino';
import pinoHttp from 'pino-http';
import { requestContext } from './middleware/request-context.js';
import { errorHandler, notFoundHandler } from './middleware/error-handler.js';
import { healthRouter } from './routes/health.js';
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
export function createApp() {
const app = express();
app.use(requestContext);
app.use(
pinoHttp({
logger,
customProps: (req) => ({ requestId: req.requestId }),
})
);
app.use(express.json({ limit: '2mb' }));
app.use(healthRouter);
app.use(notFoundHandler);
app.use(errorHandler);
return app;
}

View File

@@ -0,0 +1,26 @@
export class AppError extends Error {
constructor(code, message, status = 400, details = {}) {
super(message);
this.name = 'AppError';
this.code = code;
this.status = status;
this.details = details;
}
}
export function toErrorEnvelope(error, requestId) {
const status = error?.status && Number.isInteger(error.status) ? error.status : 500;
const code = error?.code || 'INTERNAL_ERROR';
const message = error?.message || 'Unexpected error';
const details = error?.details || {};
return {
status,
body: {
code,
message,
details,
requestId,
},
};
}

View File

@@ -0,0 +1,25 @@
import { toErrorEnvelope } from '../lib/errors.js';
export function notFoundHandler(req, res) {
res.status(404).json({
code: 'NOT_FOUND',
message: `Route not found: ${req.method} ${req.path}`,
details: {},
requestId: req.requestId,
});
}
export function errorHandler(error, req, res, _next) {
const envelope = toErrorEnvelope(error, req.requestId);
if (req.log) {
req.log.error(
{
errCode: envelope.body.code,
status: envelope.status,
details: envelope.body.details,
},
envelope.body.message
);
}
res.status(envelope.status).json(envelope.body);
}

View File

@@ -0,0 +1,9 @@
import { randomUUID } from 'node:crypto';
export function requestContext(req, res, next) {
const incoming = req.get('X-Request-Id');
req.requestId = incoming || randomUUID();
res.setHeader('X-Request-Id', req.requestId);
res.locals.startedAt = Date.now();
next();
}

View File

@@ -0,0 +1,15 @@
import { Router } from 'express';
export const healthRouter = Router();
function healthHandler(req, res) {
res.status(200).json({
ok: true,
service: 'krow-query-api',
version: process.env.SERVICE_VERSION || 'dev',
requestId: req.requestId,
});
}
healthRouter.get('/health', healthHandler);
healthRouter.get('/healthz', healthHandler);

View File

@@ -0,0 +1,9 @@
import { createApp } from './app.js';
const port = Number(process.env.PORT || 8080);
const app = createApp();
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`krow-query-api listening on port ${port}`);
});

View File

@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { createApp } from '../src/app.js';
test('GET /healthz returns healthy response', async () => {
const app = createApp();
const res = await request(app).get('/healthz');
assert.equal(res.status, 200);
assert.equal(res.body.ok, true);
assert.equal(res.body.service, 'krow-query-api');
assert.equal(typeof res.body.requestId, 'string');
assert.equal(typeof res.headers['x-request-id'], 'string');
});
test('GET unknown route returns not found envelope', async () => {
const app = createApp();
const res = await request(app).get('/query/unknown');
assert.equal(res.status, 404);
assert.equal(res.body.code, 'NOT_FOUND');
assert.equal(typeof res.body.requestId, 'string');
});

View File

@@ -36,7 +36,41 @@ BACKEND_VERIFICATION_PROVIDER_TIMEOUT_MS ?= 8000
BACKEND_MAX_SIGNED_URL_SECONDS ?= 900 BACKEND_MAX_SIGNED_URL_SECONDS ?= 900
BACKEND_LLM_RATE_LIMIT_PER_MINUTE ?= 20 BACKEND_LLM_RATE_LIMIT_PER_MINUTE ?= 20
.PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core BACKEND_V2_ARTIFACT_REPO ?= krow-backend-v2
BACKEND_V2_CORE_SERVICE_NAME ?= krow-core-api-v2
BACKEND_V2_COMMAND_SERVICE_NAME ?= krow-command-api-v2
BACKEND_V2_QUERY_SERVICE_NAME ?= krow-query-api-v2
BACKEND_V2_RUNTIME_SA_NAME ?= krow-backend-v2-runtime
BACKEND_V2_RUNTIME_SA_EMAIL := $(BACKEND_V2_RUNTIME_SA_NAME)@$(GCP_PROJECT_ID).iam.gserviceaccount.com
BACKEND_V2_CORE_DIR ?= backend/core-api
BACKEND_V2_COMMAND_DIR ?= backend/command-api
BACKEND_V2_QUERY_DIR ?= backend/query-api
BACKEND_V2_SQL_INSTANCE ?= krow-sql-v2
BACKEND_V2_SQL_DATABASE ?= krow_v2_db
BACKEND_V2_SQL_TIER ?= $(SQL_TIER)
BACKEND_V2_DEV_PUBLIC_BUCKET ?= krow-workforce-dev-v2-public
BACKEND_V2_DEV_PRIVATE_BUCKET ?= krow-workforce-dev-v2-private
BACKEND_V2_STAGING_PUBLIC_BUCKET ?= krow-workforce-staging-v2-public
BACKEND_V2_STAGING_PRIVATE_BUCKET ?= krow-workforce-staging-v2-private
ifeq ($(ENV),staging)
BACKEND_V2_PUBLIC_BUCKET := $(BACKEND_V2_STAGING_PUBLIC_BUCKET)
BACKEND_V2_PRIVATE_BUCKET := $(BACKEND_V2_STAGING_PRIVATE_BUCKET)
BACKEND_V2_RUN_AUTH_FLAG := --no-allow-unauthenticated
else
BACKEND_V2_PUBLIC_BUCKET := $(BACKEND_V2_DEV_PUBLIC_BUCKET)
BACKEND_V2_PRIVATE_BUCKET := $(BACKEND_V2_DEV_PRIVATE_BUCKET)
BACKEND_V2_RUN_AUTH_FLAG := --allow-unauthenticated
endif
BACKEND_V2_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/core-api-v2:latest
BACKEND_V2_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/command-api-v2:latest
BACKEND_V2_QUERY_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/query-api-v2:latest
.PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core backend-bootstrap-v2-dev backend-deploy-core-v2 backend-deploy-commands-v2 backend-deploy-query-v2 backend-smoke-core-v2 backend-smoke-commands-v2 backend-smoke-query-v2 backend-logs-core-v2 backend-v2-migrate-idempotency
backend-help: backend-help:
@echo "--> Backend Foundation Commands" @echo "--> Backend Foundation Commands"
@@ -49,6 +83,17 @@ backend-help:
@echo " make backend-smoke-core [ENV=dev] Smoke test core /health" @echo " make backend-smoke-core [ENV=dev] Smoke test core /health"
@echo " make backend-smoke-commands [ENV=dev] Smoke test commands /health" @echo " make backend-smoke-commands [ENV=dev] Smoke test commands /health"
@echo " make backend-logs-core [ENV=dev] Read core service logs" @echo " make backend-logs-core [ENV=dev] Read core service logs"
@echo ""
@echo "--> Backend Foundation Commands (isolated v2 stack)"
@echo " make backend-bootstrap-v2-dev [ENV=dev] Bootstrap isolated v2 resources and SQL instance"
@echo " make backend-deploy-core-v2 [ENV=dev] Build + deploy core API v2 service"
@echo " make backend-deploy-commands-v2 [ENV=dev] Build + deploy command API v2 service"
@echo " make backend-deploy-query-v2 [ENV=dev] Build + deploy query API v2 scaffold service"
@echo " make backend-v2-migrate-idempotency Apply command idempotency migration against v2 DB"
@echo " make backend-smoke-core-v2 [ENV=dev] Smoke test core API v2 /health"
@echo " make backend-smoke-commands-v2 [ENV=dev] Smoke test command API v2 /health"
@echo " make backend-smoke-query-v2 [ENV=dev] Smoke test query API v2 /health"
@echo " make backend-logs-core-v2 [ENV=dev] Read core API v2 logs"
backend-enable-apis: backend-enable-apis:
@echo "--> Enabling backend APIs on project [$(GCP_PROJECT_ID)]..." @echo "--> Enabling backend APIs on project [$(GCP_PROJECT_ID)]..."
@@ -190,3 +235,167 @@ backend-logs-core:
--region=$(BACKEND_REGION) \ --region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \ --project=$(GCP_PROJECT_ID) \
--limit=$(BACKEND_LOG_LIMIT) --limit=$(BACKEND_LOG_LIMIT)
backend-bootstrap-v2-dev: backend-enable-apis
@echo "--> Bootstrapping isolated backend v2 foundation for [$(ENV)] on project [$(GCP_PROJECT_ID)]..."
@echo "--> Ensuring Artifact Registry repo [$(BACKEND_V2_ARTIFACT_REPO)] exists..."
@if ! gcloud artifacts repositories describe $(BACKEND_V2_ARTIFACT_REPO) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud artifacts repositories create $(BACKEND_V2_ARTIFACT_REPO) \
--repository-format=docker \
--location=$(BACKEND_REGION) \
--description="KROW backend v2 services" \
--project=$(GCP_PROJECT_ID); \
else \
echo " - Artifact Registry repo already exists."; \
fi
@echo "--> Ensuring v2 runtime service account [$(BACKEND_V2_RUNTIME_SA_NAME)] exists..."
@if ! gcloud iam service-accounts describe $(BACKEND_V2_RUNTIME_SA_EMAIL) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud iam service-accounts create $(BACKEND_V2_RUNTIME_SA_NAME) \
--display-name="KROW Backend Runtime V2" \
--project=$(GCP_PROJECT_ID); \
else \
echo " - Runtime service account already exists."; \
fi
@echo "--> Ensuring v2 runtime service account IAM roles..."
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \
--role="roles/storage.objectAdmin" \
--quiet >/dev/null
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \
--role="roles/aiplatform.user" \
--quiet >/dev/null
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \
--role="roles/cloudsql.client" \
--quiet >/dev/null
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \
--role="roles/secretmanager.secretAccessor" \
--quiet >/dev/null
@gcloud iam service-accounts add-iam-policy-binding $(BACKEND_V2_RUNTIME_SA_EMAIL) \
--member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=$(GCP_PROJECT_ID) \
--quiet >/dev/null
@echo "--> Ensuring v2 storage buckets exist..."
@if ! gcloud storage buckets describe gs://$(BACKEND_V2_PUBLIC_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud storage buckets create gs://$(BACKEND_V2_PUBLIC_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \
else \
echo " - Public bucket already exists: $(BACKEND_V2_PUBLIC_BUCKET)"; \
fi
@if ! gcloud storage buckets describe gs://$(BACKEND_V2_PRIVATE_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud storage buckets create gs://$(BACKEND_V2_PRIVATE_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \
else \
echo " - Private bucket already exists: $(BACKEND_V2_PRIVATE_BUCKET)"; \
fi
@echo "--> Ensuring v2 Cloud SQL instance [$(BACKEND_V2_SQL_INSTANCE)] exists..."
@if ! gcloud sql instances describe $(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud sql instances create $(BACKEND_V2_SQL_INSTANCE) \
--database-version=POSTGRES_15 \
--tier=$(BACKEND_V2_SQL_TIER) \
--region=$(BACKEND_REGION) \
--storage-size=10 \
--storage-auto-increase \
--availability-type=zonal \
--backup-start-time=03:00 \
--project=$(GCP_PROJECT_ID); \
else \
echo " - Cloud SQL instance already exists: $(BACKEND_V2_SQL_INSTANCE)"; \
fi
@echo "--> Ensuring v2 Cloud SQL database [$(BACKEND_V2_SQL_DATABASE)] exists..."
@if ! gcloud sql databases describe $(BACKEND_V2_SQL_DATABASE) --instance=$(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud sql databases create $(BACKEND_V2_SQL_DATABASE) --instance=$(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID); \
else \
echo " - Cloud SQL database already exists: $(BACKEND_V2_SQL_DATABASE)"; \
fi
@echo "✅ Backend v2 foundation bootstrap complete for [$(ENV)]."
backend-deploy-core-v2:
@echo "--> Deploying core backend v2 service [$(BACKEND_V2_CORE_SERVICE_NAME)] to [$(ENV)]..."
@test -d $(BACKEND_V2_CORE_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_CORE_DIR)" && exit 1)
@test -f $(BACKEND_V2_CORE_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_CORE_DIR)/Dockerfile" && exit 1)
@gcloud builds submit $(BACKEND_V2_CORE_DIR) --tag $(BACKEND_V2_CORE_IMAGE) --project=$(GCP_PROJECT_ID)
@gcloud run deploy $(BACKEND_V2_CORE_SERVICE_NAME) \
--image=$(BACKEND_V2_CORE_IMAGE) \
--region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \
--service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \
--set-env-vars=APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),UPLOAD_MOCK=false,SIGNED_URL_MOCK=false,LLM_MOCK=false,LLM_LOCATION=$(BACKEND_REGION),LLM_MODEL=$(BACKEND_LLM_MODEL),LLM_TIMEOUT_MS=20000,MAX_SIGNED_URL_SECONDS=$(BACKEND_MAX_SIGNED_URL_SECONDS),LLM_RATE_LIMIT_PER_MINUTE=$(BACKEND_LLM_RATE_LIMIT_PER_MINUTE),VERIFICATION_ACCESS_MODE=authenticated,VERIFICATION_REQUIRE_FILE_EXISTS=true,VERIFICATION_ATTIRE_PROVIDER=vertex,VERIFICATION_ATTIRE_MODEL=$(BACKEND_VERIFICATION_ATTIRE_MODEL),VERIFICATION_PROVIDER_TIMEOUT_MS=$(BACKEND_VERIFICATION_PROVIDER_TIMEOUT_MS) \
$(BACKEND_V2_RUN_AUTH_FLAG)
@echo "✅ Core backend v2 service deployed."
backend-deploy-commands-v2:
@echo "--> Deploying command backend v2 service [$(BACKEND_V2_COMMAND_SERVICE_NAME)] to [$(ENV)]..."
@test -d $(BACKEND_V2_COMMAND_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_COMMAND_DIR)" && exit 1)
@test -f $(BACKEND_V2_COMMAND_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_COMMAND_DIR)/Dockerfile" && exit 1)
@gcloud builds submit $(BACKEND_V2_COMMAND_DIR) --tag $(BACKEND_V2_COMMAND_IMAGE) --project=$(GCP_PROJECT_ID)
@EXTRA_ENV="APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),IDEMPOTENCY_STORE=memory"; \
if [ -n "$(IDEMPOTENCY_DATABASE_URL)" ]; then \
EXTRA_ENV="APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),IDEMPOTENCY_STORE=sql,IDEMPOTENCY_DATABASE_URL=$(IDEMPOTENCY_DATABASE_URL)"; \
fi; \
gcloud run deploy $(BACKEND_V2_COMMAND_SERVICE_NAME) \
--image=$(BACKEND_V2_COMMAND_IMAGE) \
--region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \
--service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \
--set-env-vars=$$EXTRA_ENV \
$(BACKEND_V2_RUN_AUTH_FLAG)
@echo "✅ Command backend v2 service deployed."
backend-deploy-query-v2:
@echo "--> Deploying query backend v2 service [$(BACKEND_V2_QUERY_SERVICE_NAME)] to [$(ENV)]..."
@test -d $(BACKEND_V2_QUERY_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_QUERY_DIR)" && exit 1)
@test -f $(BACKEND_V2_QUERY_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_QUERY_DIR)/Dockerfile" && exit 1)
@gcloud builds submit $(BACKEND_V2_QUERY_DIR) --tag $(BACKEND_V2_QUERY_IMAGE) --project=$(GCP_PROJECT_ID)
@gcloud run deploy $(BACKEND_V2_QUERY_SERVICE_NAME) \
--image=$(BACKEND_V2_QUERY_IMAGE) \
--region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \
--service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \
--set-env-vars=APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID) \
$(BACKEND_V2_RUN_AUTH_FLAG)
@echo "✅ Query backend v2 service deployed."
backend-v2-migrate-idempotency:
@echo "--> Applying idempotency table migration for command API v2..."
@test -n "$(IDEMPOTENCY_DATABASE_URL)" || (echo "❌ IDEMPOTENCY_DATABASE_URL is required" && exit 1)
@cd $(BACKEND_V2_COMMAND_DIR) && IDEMPOTENCY_DATABASE_URL="$(IDEMPOTENCY_DATABASE_URL)" npm run migrate:idempotency
@echo "✅ Idempotency migration applied for command API v2."
backend-smoke-core-v2:
@echo "--> Running core v2 smoke check..."
@URL=$$(gcloud run services describe $(BACKEND_V2_CORE_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \
if [ -z "$$URL" ]; then \
echo "❌ Could not resolve URL for service $(BACKEND_V2_CORE_SERVICE_NAME)"; \
exit 1; \
fi; \
TOKEN=$$(gcloud auth print-identity-token); \
curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/health" >/dev/null && echo "✅ Core v2 smoke check passed: $$URL/health"
backend-smoke-commands-v2:
@echo "--> Running command v2 smoke check..."
@URL=$$(gcloud run services describe $(BACKEND_V2_COMMAND_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \
if [ -z "$$URL" ]; then \
echo "❌ Could not resolve URL for service $(BACKEND_V2_COMMAND_SERVICE_NAME)"; \
exit 1; \
fi; \
TOKEN=$$(gcloud auth print-identity-token); \
curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/health" >/dev/null && echo "✅ Command v2 smoke check passed: $$URL/health"
backend-smoke-query-v2:
@echo "--> Running query v2 smoke check..."
@URL=$$(gcloud run services describe $(BACKEND_V2_QUERY_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \
if [ -z "$$URL" ]; then \
echo "❌ Could not resolve URL for service $(BACKEND_V2_QUERY_SERVICE_NAME)"; \
exit 1; \
fi; \
TOKEN=$$(gcloud auth print-identity-token); \
curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/health" >/dev/null && echo "✅ Query v2 smoke check passed: $$URL/health"
backend-logs-core-v2:
@echo "--> Reading logs for core backend v2 service [$(BACKEND_V2_CORE_SERVICE_NAME)]..."
@gcloud run services logs read $(BACKEND_V2_CORE_SERVICE_NAME) \
--region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \
--limit=$(BACKEND_LOG_LIMIT)