Skip to content

Resolve AuthClientId from the shared-auth stack, not a hardcoded value #283

Resolve AuthClientId from the shared-auth stack, not a hardcoded value

Resolve AuthClientId from the shared-auth stack, not a hardcoded value #283

name: Deploy DataOps V1 Lambda
on:
push:
branches:
- main
paths:
- ".github/workflows/deploy-dataops-v1.yml"
- "backend/**"
- "content/**"
- "frontend/**"
- "infra/**"
- "Makefile"
- "package.json"
- "package-lock.json"
- "scripts/test-backend-transactions.sh"
- "samconfig.toml"
- "scripts/deploy/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: dataops-v1-deploy
cancel-in-progress: false
env:
AWS_REGION: eu-west-1
AWS_DEFAULT_REGION: eu-west-1
AWS_ROLE_ARN: arn:aws:iam::817685572750:role/dataops-github-actions-deploy
DOCS_GITHUB_OWNER: DataTalksClub
DOCS_GITHUB_REPO: dataops-knowledge
DOCS_GITHUB_TOKEN_SECRET: dataops-v1/knowledge/github-token
DOCS_GITHUB_BRANCH: main
jobs:
checks:
name: Check DataOps v1 app
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
cache-dependency-path: package-lock.json
- name: Install Node workspace dependencies
run: npm ci
- name: Run frontend unit coverage
run: npm run test:frontend:coverage
- name: Run backend tests
run: npm --prefix backend test
- name: Gate all backend DynamoDB transactions
run: npm run test:backend:transactions
- name: Typecheck backend
run: npm --prefix backend run typecheck
- name: Build backend
run: npm --prefix backend run build
- name: Set up SAM
uses: aws-actions/setup-sam@v3
with:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Validate SAM template
run: make sam-validate
deploy:
name: Deploy DataOps v1 app
runs-on: ubuntu-latest
needs: checks
permissions:
contents: read
id-token: write
env:
AUTH_BASE_URL: https://auth.dtcdev.click
AUTH_USER_POOL_ID: us-east-1_H7nJu52Bs
# AUTH_ISSUER, AUTH_JWKS_URL, and AUTH_CLIENT_ID are resolved from the
# shared-auth stack's own outputs in the "Resolve shared auth client"
# step below, not hardcoded here — a hardcoded client_id goes stale
# silently whenever the Cognito app client is recreated.
AUTH_CALLBACK_URL: https://ops.dtcdev.click/auth/callback
AUTH_LOGOUT_URL: https://ops.dtcdev.click/
AUTH_SESSION_LIFETIME_SECONDS: 28800
EMAIL_DOCUMENT_INTAKE_SECRET_ARN: ${{ secrets.EMAIL_DOCUMENT_INTAKE_SECRET_ARN }}
EMAIL_DOCUMENT_EXTERNAL_SOURCE_BUCKET: ${{ vars.EMAIL_DOCUMENT_EXTERNAL_SOURCE_BUCKET }}
EMAIL_DOCUMENT_EXTERNAL_SOURCE_PREFIX: ${{ vars.EMAIL_DOCUMENT_EXTERNAL_SOURCE_PREFIX }}
EMAIL_DOCUMENT_EXTERNAL_SOURCE_KMS_KEY_ARN: ${{ secrets.EMAIL_DOCUMENT_EXTERNAL_SOURCE_KMS_KEY_ARN }}
DAPIER_CREDENTIALS_TABLE_NAME: ${{ vars.DAPIER_CREDENTIALS_TABLE_NAME }}
DAPIER_CREDENTIALS_TABLE_ARN: ${{ vars.DAPIER_CREDENTIALS_TABLE_ARN }}
MAILING_EXPORTS_CONFIG: ${{ vars.MAILING_EXPORTS_CONFIG }}
CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED: ${{ vars.CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED }}
SPONSOR_FINANCE_ENABLED: ${{ vars.SPONSOR_FINANCE_ENABLED }}
CONVERSATIONAL_EXECUTION_ENABLED: ${{ vars.CONVERSATIONAL_EXECUTION_ENABLED }}
CONVERSATIONAL_ENABLED_PLUGINS: ${{ vars.CONVERSATIONAL_ENABLED_PLUGINS }}
CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED: ${{ vars.CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED }}
CONVERSATIONAL_TYPEFULLY_ACCOUNT_CONFIG_DIGEST: ${{ vars.CONVERSATIONAL_TYPEFULLY_ACCOUNT_CONFIG_DIGEST }}
TYPEFULLY_ACCOUNT_CONFIG_REVISION: ${{ vars.TYPEFULLY_ACCOUNT_CONFIG_REVISION }}
TYPEFULLY_BASE_URL: ${{ vars.TYPEFULLY_BASE_URL }}
WORK_ENGINE_TYPEFULLY_API_TOKEN_SECRET_ARN: ${{ vars.WORK_ENGINE_TYPEFULLY_API_TOKEN_SECRET_ARN }}
TYPEFULLY_SOCIAL_SET_ALEXEY: ${{ vars.TYPEFULLY_SOCIAL_SET_ALEXEY }}
TYPEFULLY_SOCIAL_SET_DATATALKSCLUB: ${{ vars.TYPEFULLY_SOCIAL_SET_DATATALKSCLUB }}
ZAI_CONVERSATIONAL_API_KEY_SECRET_ARN: ${{ vars.ZAI_CONVERSATIONAL_API_KEY_SECRET_ARN }}
CONVERSATIONAL_TELEGRAM_VOICE_ENABLED: ${{ vars.CONVERSATIONAL_TELEGRAM_VOICE_ENABLED }}
CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED: ${{ vars.CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED }}
CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED: ${{ vars.CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED }}
GROQ_TRANSCRIPTION_API_KEY_SECRET_ARN: ${{ vars.GROQ_TRANSCRIPTION_API_KEY_SECRET_ARN }}
ZAI_VISION_API_KEY_SECRET_ARN: ${{ vars.ZAI_VISION_API_KEY_SECRET_ARN }}
ZAI_VISION_MODEL: ${{ vars.ZAI_VISION_MODEL }}
ZAI_VISION_BASE_URL: ${{ vars.ZAI_VISION_BASE_URL }}
TELEGRAM_HANDLER_DEADLINE_MS: ${{ vars.TELEGRAM_HANDLER_DEADLINE_MS }}
TELEGRAM_API_TIMEOUT_MS: ${{ vars.TELEGRAM_API_TIMEOUT_MS }}
TELEGRAM_API_MAX_RESPONSE_BYTES: ${{ vars.TELEGRAM_API_MAX_RESPONSE_BYTES }}
TELEGRAM_VOICE_MAX_BYTES: ${{ vars.TELEGRAM_VOICE_MAX_BYTES }}
TELEGRAM_VOICE_MAX_SECONDS: ${{ vars.TELEGRAM_VOICE_MAX_SECONDS }}
TELEGRAM_PHOTO_MAX_BYTES: ${{ vars.TELEGRAM_PHOTO_MAX_BYTES }}
TELEGRAM_PHOTO_MAX_PIXELS: ${{ vars.TELEGRAM_PHOTO_MAX_PIXELS }}
TELEGRAM_MEDIA_DOWNLOAD_TIMEOUT_MS: ${{ vars.TELEGRAM_MEDIA_DOWNLOAD_TIMEOUT_MS }}
TELEGRAM_MEDIA_PROVIDER_TIMEOUT_MS: ${{ vars.TELEGRAM_MEDIA_PROVIDER_TIMEOUT_MS }}
TELEGRAM_MEDIA_PROVIDER_MAX_RESPONSE_BYTES: ${{ vars.TELEGRAM_MEDIA_PROVIDER_MAX_RESPONSE_BYTES }}
TELEGRAM_MEDIA_MAX_TEXT_BYTES: ${{ vars.TELEGRAM_MEDIA_MAX_TEXT_BYTES }}
SPONSOR_COMMUNICATION_SEND_ENABLED: ${{ vars.SPONSOR_COMMUNICATION_SEND_ENABLED }}
SPONSOR_COMMUNICATION_TEMPLATE_SECRET_ARN: ${{ vars.SPONSOR_COMMUNICATION_TEMPLATE_SECRET_ARN }}
SPONSOR_COMMUNICATION_HMAC_SECRET_ARN: ${{ vars.SPONSOR_COMMUNICATION_HMAC_SECRET_ARN }}
SPONSOR_SES_IDENTITY_ARN: ${{ vars.SPONSOR_SES_IDENTITY_ARN }}
SPONSOR_SES_FROM_ADDRESS: ${{ vars.SPONSOR_SES_FROM_ADDRESS }}
SPONSOR_SES_CONFIGURATION_SET_NAME: ${{ vars.SPONSOR_SES_CONFIGURATION_SET_NAME }}
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
cache-dependency-path: package-lock.json
- name: Set up SAM
uses: aws-actions/setup-sam@v3
with:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
role-session-name: dataops-v1-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Resolve shared auth client
run: |
auth_output() {
aws cloudformation describe-stacks --region us-east-1 --stack-name dtcdev-shared-auth \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text
}
{
echo "AUTH_CLIENT_ID=$(auth_output DataOpsClientId)"
echo "AUTH_ISSUER=$(auth_output IssuerUrl)"
echo "AUTH_JWKS_URL=$(auth_output JwksUrl)"
} >> "$GITHUB_ENV"
- name: Build SAM artifact
run: make sam-build
- name: Verify packaged canonical frontend
run: make verify-sam-frontend
- name: Verify packaged runtime infrastructure boundary
run: make verify-sam-runtime-boundary
- name: Test packaged frontend isolation
run: make test-sam-frontend-isolation
- name: Deploy DataOps v1 stack
run: |
set -euo pipefail
if [ -z "$EMAIL_DOCUMENT_INTAKE_SECRET_ARN" ]; then
echo "Required email-document intake secret ARN is not configured" >&2
exit 1
fi
if [ -z "$DAPIER_CREDENTIALS_TABLE_NAME" ] || [ -z "$DAPIER_CREDENTIALS_TABLE_ARN" ]; then
echo "Required Dapier credentials table name/ARN are not configured" >&2
exit 1
fi
: "${EMAIL_DOCUMENT_EXTERNAL_SOURCE_PREFIX:=transfer/}"
: "${MAILING_EXPORTS_CONFIG:=[]}"
: "${CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED:=false}"
: "${SPONSOR_FINANCE_ENABLED:=false}"
: "${CONVERSATIONAL_EXECUTION_ENABLED:=false}"
: "${CONVERSATIONAL_ENABLED_PLUGINS:=none}"
: "${CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED:=false}"
: "${CONVERSATIONAL_TYPEFULLY_ACCOUNT_CONFIG_DIGEST:=sha256:0000000000000000000000000000000000000000000000000000000000000000}"
: "${TYPEFULLY_ACCOUNT_CONFIG_REVISION:=unconfigured}"
: "${TYPEFULLY_BASE_URL:=https://api.typefully.com}"
: "${CONVERSATIONAL_TELEGRAM_VOICE_ENABLED:=false}"
: "${CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED:=false}"
: "${CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED:=false}"
: "${ZAI_VISION_MODEL:=glm-4.6v}"
: "${ZAI_VISION_BASE_URL:=https://api.z.ai/api/paas/v4/chat/completions}"
: "${TELEGRAM_HANDLER_DEADLINE_MS:=28000}"
: "${TELEGRAM_API_TIMEOUT_MS:=5000}"
: "${TELEGRAM_API_MAX_RESPONSE_BYTES:=65536}"
: "${TELEGRAM_VOICE_MAX_BYTES:=20971520}"
: "${TELEGRAM_VOICE_MAX_SECONDS:=300}"
: "${TELEGRAM_PHOTO_MAX_BYTES:=10485760}"
: "${TELEGRAM_PHOTO_MAX_PIXELS:=20000000}"
: "${TELEGRAM_MEDIA_DOWNLOAD_TIMEOUT_MS:=8000}"
: "${TELEGRAM_MEDIA_PROVIDER_TIMEOUT_MS:=18000}"
: "${TELEGRAM_MEDIA_PROVIDER_MAX_RESPONSE_BYTES:=65536}"
: "${TELEGRAM_MEDIA_MAX_TEXT_BYTES:=16384}"
: "${SPONSOR_COMMUNICATION_SEND_ENABLED:=false}"
: "${SPONSOR_SES_IDENTITY_ARN:=arn:aws:ses:eu-west-1:000000000000:identity/example.invalid}"
: "${SPONSOR_SES_FROM_ADDRESS:=no-reply@example.invalid}"
: "${SPONSOR_SES_CONFIGURATION_SET_NAME:=dataops-sponsor-reviewed-send-disabled}"
strict_boolean() {
case "$2" in
true|false) ;;
*) echo "$1 must be exactly true or false" >&2; exit 1 ;;
esac
}
strict_boolean CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED "$CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED"
strict_boolean CONVERSATIONAL_EXECUTION_ENABLED "$CONVERSATIONAL_EXECUTION_ENABLED"
strict_boolean CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED "$CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED"
strict_boolean CONVERSATIONAL_TELEGRAM_VOICE_ENABLED "$CONVERSATIONAL_TELEGRAM_VOICE_ENABLED"
strict_boolean CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED "$CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED"
strict_boolean CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED "$CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED"
case "$TELEGRAM_API_TIMEOUT_MS" in
''|*[!0-9]*) echo "TELEGRAM_API_TIMEOUT_MS must be an integer from 100 through 5000" >&2; exit 1 ;;
esac
if [ "$TELEGRAM_API_TIMEOUT_MS" -lt 100 ] || [ "$TELEGRAM_API_TIMEOUT_MS" -gt 5000 ]; then
echo "TELEGRAM_API_TIMEOUT_MS must be an integer from 100 through 5000" >&2
exit 1
fi
case "$CONVERSATIONAL_ENABLED_PLUGINS" in
none|todo|typefully|todo,typefully) ;;
*) echo "CONVERSATIONAL_ENABLED_PLUGINS must be a canonical plugin subset" >&2; exit 1 ;;
esac
if [ "$CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED" = "true" ] && [ -z "$ZAI_CONVERSATIONAL_API_KEY_SECRET_ARN" ]; then
echo "Telegram ingress cannot be enabled without its z.ai secret ARN" >&2
exit 1
fi
if [ "$CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED" = "true" ] && { \
[ "$CONVERSATIONAL_EXECUTION_ENABLED" != "true" ] \
|| { [ "$CONVERSATIONAL_ENABLED_PLUGINS" != "typefully" ] && [ "$CONVERSATIONAL_ENABLED_PLUGINS" != "todo,typefully" ]; } \
|| [ -z "$WORK_ENGINE_TYPEFULLY_API_TOKEN_SECRET_ARN" ] \
|| [ -z "$TYPEFULLY_SOCIAL_SET_ALEXEY" ] \
|| [ -z "$TYPEFULLY_SOCIAL_SET_DATATALKSCLUB" ] \
|| [ "$TYPEFULLY_ACCOUNT_CONFIG_REVISION" = "unconfigured" ]; \
}; then
echo "Conversational Typefully execution requires the exact secret ARN, both account mappings, and a configuration revision" >&2
exit 1
fi
if [ "$CONVERSATIONAL_TELEGRAM_VOICE_ENABLED" = "true" ] && { [ "$CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED" != "true" ] || [ -z "$GROQ_TRANSCRIPTION_API_KEY_SECRET_ARN" ]; }; then
echo "Telegram voice requires the global adapter and Groq secret ARN" >&2
exit 1
fi
if [ "$CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED" = "true" ] && { [ "$CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED" != "true" ] || [ -z "$ZAI_VISION_API_KEY_SECRET_ARN" ]; }; then
echo "Telegram photo requires the global adapter and z.ai vision secret ARN" >&2
exit 1
fi
if [ "$SPONSOR_COMMUNICATION_SEND_ENABLED" = "true" ] && { [ -z "$SPONSOR_COMMUNICATION_TEMPLATE_SECRET_ARN" ] || [ -z "$SPONSOR_COMMUNICATION_HMAC_SECRET_ARN" ]; }; then
echo "Sponsor reviewed sending cannot be enabled without separate template and HMAC secret ARNs" >&2
exit 1
fi
sam deploy \
--config-env full-sandbox \
--parameter-overrides \
"ParameterKey=GitHubOwner,ParameterValue=$DOCS_GITHUB_OWNER" \
"ParameterKey=GitHubRepo,ParameterValue=$DOCS_GITHUB_REPO" \
"ParameterKey=GitHubTokenSecretName,ParameterValue=$DOCS_GITHUB_TOKEN_SECRET" \
"ParameterKey=GitHubBranch,ParameterValue=$DOCS_GITHUB_BRANCH" \
"ParameterKey=AuthBaseUrl,ParameterValue=$AUTH_BASE_URL" \
"ParameterKey=AuthUserPoolId,ParameterValue=$AUTH_USER_POOL_ID" \
"ParameterKey=AuthIssuer,ParameterValue=$AUTH_ISSUER" \
"ParameterKey=AuthJwksUrl,ParameterValue=$AUTH_JWKS_URL" \
"ParameterKey=AuthClientId,ParameterValue=$AUTH_CLIENT_ID" \
"ParameterKey=AuthCallbackUrl,ParameterValue=$AUTH_CALLBACK_URL" \
"ParameterKey=AuthLogoutUrl,ParameterValue=$AUTH_LOGOUT_URL" \
"ParameterKey=AuthSessionLifetimeSeconds,ParameterValue=$AUTH_SESSION_LIFETIME_SECONDS" \
"ParameterKey=EmailDocumentIntakeSecretArn,ParameterValue=$EMAIL_DOCUMENT_INTAKE_SECRET_ARN" \
"ParameterKey=EmailDocumentExternalSourceBucketName,ParameterValue=$EMAIL_DOCUMENT_EXTERNAL_SOURCE_BUCKET" \
"ParameterKey=EmailDocumentExternalSourcePrefix,ParameterValue=$EMAIL_DOCUMENT_EXTERNAL_SOURCE_PREFIX" \
"ParameterKey=EmailDocumentExternalSourceKmsKeyArn,ParameterValue=$EMAIL_DOCUMENT_EXTERNAL_SOURCE_KMS_KEY_ARN" \
"ParameterKey=DapierCredentialsTableName,ParameterValue=$DAPIER_CREDENTIALS_TABLE_NAME" \
"ParameterKey=DapierCredentialsTableArn,ParameterValue=$DAPIER_CREDENTIALS_TABLE_ARN" \
"ParameterKey=MailingExportsConfig,ParameterValue=$MAILING_EXPORTS_CONFIG" \
"ParameterKey=ConversationalTelegramIngressEnabled,ParameterValue=$CONVERSATIONAL_TELEGRAM_INGRESS_ENABLED" \
"ParameterKey=SponsorFinanceEnabled,ParameterValue=$SPONSOR_FINANCE_ENABLED" \
"ParameterKey=ConversationalExecutionEnabled,ParameterValue=$CONVERSATIONAL_EXECUTION_ENABLED" \
"ParameterKey=ConversationalEnabledPlugins,ParameterValue=$CONVERSATIONAL_ENABLED_PLUGINS" \
"ParameterKey=ConversationalTypefullyExternalExecutionEnabled,ParameterValue=$CONVERSATIONAL_TYPEFULLY_EXTERNAL_EXECUTION_ENABLED" \
"ParameterKey=ConversationalTypefullyAccountConfigDigest,ParameterValue=$CONVERSATIONAL_TYPEFULLY_ACCOUNT_CONFIG_DIGEST" \
"ParameterKey=TypefullyAccountConfigRevision,ParameterValue=$TYPEFULLY_ACCOUNT_CONFIG_REVISION" \
"ParameterKey=TypefullyBaseUrl,ParameterValue=$TYPEFULLY_BASE_URL" \
"ParameterKey=WorkEngineTypefullyApiTokenSecretArn,ParameterValue=$WORK_ENGINE_TYPEFULLY_API_TOKEN_SECRET_ARN" \
"ParameterKey=TypefullySocialSetAlexey,ParameterValue=$TYPEFULLY_SOCIAL_SET_ALEXEY" \
"ParameterKey=TypefullySocialSetDataTalksClub,ParameterValue=$TYPEFULLY_SOCIAL_SET_DATATALKSCLUB" \
"ParameterKey=ZaiConversationalApiKeySecretArn,ParameterValue=$ZAI_CONVERSATIONAL_API_KEY_SECRET_ARN" \
"ParameterKey=ConversationalTelegramVoiceEnabled,ParameterValue=$CONVERSATIONAL_TELEGRAM_VOICE_ENABLED" \
"ParameterKey=ConversationalTelegramPhotoEnabled,ParameterValue=$CONVERSATIONAL_TELEGRAM_PHOTO_ENABLED" \
"ParameterKey=ConversationalAlarmSubscriptionConfirmed,ParameterValue=$CONVERSATIONAL_ALARM_SUBSCRIPTION_CONFIRMED" \
"ParameterKey=DataOpsDeploymentId,ParameterValue=$GITHUB_SHA" \
"ParameterKey=GroqTranscriptionApiKeySecretArn,ParameterValue=$GROQ_TRANSCRIPTION_API_KEY_SECRET_ARN" \
"ParameterKey=ZaiVisionApiKeySecretArn,ParameterValue=$ZAI_VISION_API_KEY_SECRET_ARN" \
"ParameterKey=ZaiVisionModel,ParameterValue=$ZAI_VISION_MODEL" \
"ParameterKey=ZaiVisionBaseUrl,ParameterValue=$ZAI_VISION_BASE_URL" \
"ParameterKey=TelegramHandlerDeadlineMs,ParameterValue=$TELEGRAM_HANDLER_DEADLINE_MS" \
"ParameterKey=TelegramApiTimeoutMs,ParameterValue=$TELEGRAM_API_TIMEOUT_MS" \
"ParameterKey=TelegramApiMaxResponseBytes,ParameterValue=$TELEGRAM_API_MAX_RESPONSE_BYTES" \
"ParameterKey=TelegramVoiceMaxBytes,ParameterValue=$TELEGRAM_VOICE_MAX_BYTES" \
"ParameterKey=TelegramVoiceMaxSeconds,ParameterValue=$TELEGRAM_VOICE_MAX_SECONDS" \
"ParameterKey=TelegramPhotoMaxBytes,ParameterValue=$TELEGRAM_PHOTO_MAX_BYTES" \
"ParameterKey=TelegramPhotoMaxPixels,ParameterValue=$TELEGRAM_PHOTO_MAX_PIXELS" \
"ParameterKey=TelegramMediaDownloadTimeoutMs,ParameterValue=$TELEGRAM_MEDIA_DOWNLOAD_TIMEOUT_MS" \
"ParameterKey=TelegramMediaProviderTimeoutMs,ParameterValue=$TELEGRAM_MEDIA_PROVIDER_TIMEOUT_MS" \
"ParameterKey=TelegramMediaProviderMaxResponseBytes,ParameterValue=$TELEGRAM_MEDIA_PROVIDER_MAX_RESPONSE_BYTES" \
"ParameterKey=TelegramMediaMaxTextBytes,ParameterValue=$TELEGRAM_MEDIA_MAX_TEXT_BYTES" \
"ParameterKey=SponsorCommunicationSendEnabled,ParameterValue=$SPONSOR_COMMUNICATION_SEND_ENABLED" \
"ParameterKey=SponsorCommunicationTemplateSecretArn,ParameterValue=$SPONSOR_COMMUNICATION_TEMPLATE_SECRET_ARN" \
"ParameterKey=SponsorCommunicationHmacSecretArn,ParameterValue=$SPONSOR_COMMUNICATION_HMAC_SECRET_ARN" \
"ParameterKey=SponsorSesIdentityArn,ParameterValue=$SPONSOR_SES_IDENTITY_ARN" \
"ParameterKey=SponsorSesFromAddress,ParameterValue=$SPONSOR_SES_FROM_ADDRESS" \
"ParameterKey=SponsorSesConfigurationSetName,ParameterValue=$SPONSOR_SES_CONFIGURATION_SET_NAME"
- name: Seed runtime users, workflow templates, and recurring configs
run: |
set -euo pipefail
backend_function="$(aws cloudformation describe-stack-resource \
--stack-name dataops-v1 \
--logical-resource-id BackendFunction \
--query "StackResourceDetail.PhysicalResourceId" \
--output text)"
seed_response="$RUNNER_TEMP/dataops-runtime-seeds.json"
seed_error="$(aws lambda invoke \
--function-name "$backend_function" \
--cli-binary-format raw-in-base64-out \
--payload '{"source":"dataops.deploy","detail-type":"Runtime Seed","detail":{"dataopsAction":"sync-runtime-seeds"}}' \
--query FunctionError \
--output text \
"$seed_response")"
if [ "$seed_error" != "None" ]; then
echo "Runtime seed failed inside the deployed Lambda" >&2
exit 1
fi
if ! jq -e '
def nonnegative_integer: type == "number" and floor == . and . >= 0;
.statusCode == 200
and ((.body | fromjson) as $body
| ($body | keys == ["recurring", "templates", "users"])
and ($body.users | keys == ["created", "processed", "unchanged", "updated"])
and ($body.users.processed == 3)
and ([$body.users.created, $body.users.updated, $body.users.unchanged] | all(.[]; nonnegative_integer))
and ($body.users.created + $body.users.updated + $body.users.unchanged == 3)
and ($body.templates | keys == ["created", "total", "unchanged", "updated"])
and ($body.templates.total == 11)
and ([$body.templates.created, $body.templates.updated, $body.templates.unchanged] | all(.[]; nonnegative_integer))
and ($body.templates.created + $body.templates.updated + $body.templates.unchanged == 11)
and ($body.recurring | keys == ["created", "repairedTasks", "skipped", "total", "updated"])
and ($body.recurring.total == 7)
and ([$body.recurring.created, $body.recurring.updated, $body.recurring.skipped, $body.recurring.repairedTasks] | all(.[]; nonnegative_integer))
and ($body.recurring.created + $body.recurring.updated + $body.recurring.skipped == 7)
)
' "$seed_response" >/dev/null; then
echo "Runtime seed did not confirm the exact users, Templates, and recurring-config contract" >&2
exit 1
fi
echo "Runtime users, workflow templates, and recurring configs synchronized."
- name: Smoke test deployed single-origin backend
run: |
backend_url="$(aws cloudformation describe-stacks \
--stack-name dataops-v1 \
--query "Stacks[0].Outputs[?OutputKey=='BackendUrl'].OutputValue" \
--output text)"
# The Function URL output ends with a trailing slash; strip it so we hit
# "/" and "/login" instead of malformed double-slash paths ("//", "//login").
backend_url="${backend_url%/}"
# /login redirects to the shared Cognito authorize endpoint and root redirects locally.
# The Function URL event normalization in handler.ts must work for this.
# Retry a few times: immediately after a stack update the Function URL can
# briefly return a cold-start/propagation blip before the new code settles.
code=""
login_code=""
for attempt in 1 2 3 4 5; do
code=$(curl -sS -o /dev/null -w "%{http_code}" "$backend_url/")
login_code=$(curl -sS -o /dev/null -w "%{http_code}" "$backend_url/login")
if [ "$code" = "302" ] && [ "$login_code" = "302" ]; then
echo "Backend smoke OK on attempt $attempt: root=$code login=$login_code"
exit 0
fi
echo "Smoke attempt $attempt: root=$code login=$login_code (retrying)"
sleep 10
done
echo "Backend smoke test failed after retries: root expected 302 got $code; /login expected 302 got $login_code"
exit 1