43949b20a0
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 10m38s
Dockerfile.stage COPYs dist/soul-demo-image.tar so k3s can import soul-demo:local at container startup. Stage CI now compiles soul-demo from source on the host runner and packages it as an OCI image before the main Docker build runs.
269 lines
12 KiB
YAML
269 lines
12 KiB
YAML
name: Stage — Build, push & deploy to marketing-stage
|
|
|
|
# Pipeline: build → push → deploy marketing-stage → smoke test.
|
|
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
|
|
# Triggered: 2026-05-05 (promote fix/gallery-layout-account-otp)
|
|
|
|
on:
|
|
push:
|
|
branches: [stage]
|
|
paths:
|
|
- 'src/**'
|
|
- 'dist/**'
|
|
- 'runtime/**'
|
|
- 'Dockerfile.stage'
|
|
- 'build-stage.sh'
|
|
- '.gitea/workflows/stage.yaml'
|
|
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Image tag to build and deploy (defaults to short SHA)'
|
|
required: false
|
|
type: string
|
|
|
|
jobs:
|
|
deploy-stage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 2
|
|
|
|
- name: Enforce dev-only source
|
|
# stage only accepts merges from dev. Any PR from another branch fails
|
|
# here before a single build step runs.
|
|
# workflow_dispatch is exempt (allows manual redeploy of current stage).
|
|
# Must run AFTER checkout — git commands require a cloned workspace.
|
|
if: github.event_name != 'workflow_dispatch'
|
|
run: |
|
|
set -euo pipefail
|
|
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || true)
|
|
echo "Merge commit: $COMMIT_MSG"
|
|
# Gitea merge commits: "Merge pull request '...' (#N) from dev into stage"
|
|
# Direct branch merges: "Merge branch 'dev' into stage"
|
|
# tea pr merge with custom title: any subject line is possible, so
|
|
# fall back to checking git parents — if the second parent is on dev
|
|
# the merge came from dev regardless of the commit subject.
|
|
SECOND_PARENT=$(git log -1 --pretty=format:"%P" HEAD | awk '{print $2}')
|
|
FROM_DEV=""
|
|
if [ -n "$SECOND_PARENT" ]; then
|
|
if git merge-base --is-ancestor "$SECOND_PARENT" origin/dev 2>/dev/null; then
|
|
FROM_DEV=1
|
|
fi
|
|
fi
|
|
if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$" || [ -n "$FROM_DEV" ]; then
|
|
echo "Source branch check: OK (merged from dev)"
|
|
else
|
|
echo "ERROR: stage only accepts merges from dev."
|
|
echo "Commit message was: $COMMIT_MSG"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Detect change type
|
|
id: changetype
|
|
run: |
|
|
set -euo pipefail
|
|
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || echo "unknown")
|
|
echo "Changed files:"
|
|
echo "$CHANGED"
|
|
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^$' || true)
|
|
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
|
|
echo "asset_only=true" >> "$GITHUB_OUTPUT"
|
|
echo "=> Asset-only change detected, will use fast path"
|
|
else
|
|
echo "asset_only=false" >> "$GITHUB_OUTPUT"
|
|
echo "=> Full build required"
|
|
fi
|
|
|
|
- name: Authenticate to GCP
|
|
uses: google-github-actions/auth@v2
|
|
with:
|
|
credentials_json: ${{ secrets.GCP_SA_KEY }}
|
|
|
|
- name: Set up gcloud SDK
|
|
uses: google-github-actions/setup-gcloud@v2
|
|
with:
|
|
project_id: neuron-785695
|
|
|
|
- name: Configure docker auth for Artifact Registry
|
|
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
|
|
|
- name: Compute image tag
|
|
id: tag
|
|
run: |
|
|
TAG="${{ inputs.tag }}"
|
|
if [ -z "$TAG" ]; then TAG="stage-${GITHUB_SHA:0:8}"; fi
|
|
IMAGE="us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:${TAG}"
|
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
|
echo "Will build and push: ${IMAGE}"
|
|
|
|
- name: Touch HTML placeholder files
|
|
# El binary regenerates these at startup via fs_write. They must exist
|
|
# in the build context for Dockerfile COPY to succeed.
|
|
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
|
|
|
- name: Build soul-demo image tar
|
|
# Dockerfile.stage COPYs dist/soul-demo-image.tar so k3s can import
|
|
# soul-demo:local at runtime. We compile soul-demo from source on the
|
|
# host runner (ci-base has gcc), build a minimal OCI image, and save it.
|
|
# Runs before the main Docker build so the COPY succeeds.
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
# Compile el_runtime.o and soul-demo on the host runner
|
|
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime.o
|
|
cc -O2 -rdynamic -DEL_SOUL_DEMO_BUILD \
|
|
-o dist/soul-demo \
|
|
dist/soul-demo.c dist/vessel_stubs.c /tmp/el_runtime.o \
|
|
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
|
echo "soul-demo compiled: $(ls -lh dist/soul-demo)"
|
|
# Package as minimal OCI image for k3s import
|
|
docker build -f dist/Dockerfile.soul-demo -t soul-demo:local dist/
|
|
docker save soul-demo:local -o dist/soul-demo-image.tar
|
|
echo "soul-demo-image.tar: $(du -sh dist/soul-demo-image.tar | cut -f1)"
|
|
docker rmi soul-demo:local 2>/dev/null || true
|
|
|
|
# ── El SDK setup ──────────────────────────────────────────────────────
|
|
|
|
- name: Extract El SDK from ci-base
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev
|
|
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev)
|
|
sudo mkdir -p /opt/el
|
|
docker cp "$CID:/opt/el" /opt/
|
|
docker rm "$CID"
|
|
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
|
|
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
|
|
echo "EL_RUNTIME=$GITHUB_WORKSPACE/runtime" >> "$GITHUB_ENV"
|
|
|
|
# ── Build neuron-web binary ───────────────────────────────────────────
|
|
|
|
- name: Build neuron-web with elb
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
"$ELB" \
|
|
--elc="$ELC" \
|
|
--runtime="$EL_RUNTIME"
|
|
echo "Binary: $(ls -lh dist/neuron-landing)"
|
|
|
|
# ── Compile JS client sources ─────────────────────────────────────────
|
|
|
|
- name: Compile JS El sources
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
cp "$EL_RUNTIME/el_runtime.js" src/js/
|
|
mkdir -p dist/js
|
|
for f in src/js/*.el; do
|
|
[ -f "$f" ] || continue
|
|
name=$(basename "$f" .el)
|
|
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
|
echo " compiled: $f -> dist/js/${name}.js"
|
|
done
|
|
rm -f src/js/el_runtime.js
|
|
|
|
# ── Docker build + push ───────────────────────────────────────────────
|
|
|
|
- name: Build and tag image
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
docker build \
|
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest \
|
|
-f Dockerfile.stage \
|
|
-t "marketing:${{ steps.tag.outputs.tag }}" \
|
|
.
|
|
docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}"
|
|
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
|
|
|
|
- name: Push image
|
|
if: steps.changetype.outputs.asset_only != 'true'
|
|
run: |
|
|
docker push "${{ steps.tag.outputs.image }}"
|
|
docker push "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
|
|
|
|
- name: Asset-only fast build
|
|
if: steps.changetype.outputs.asset_only == 'true'
|
|
env:
|
|
IMAGE: ${{ steps.tag.outputs.image }}
|
|
run: |
|
|
set -euo pipefail
|
|
LATEST="us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
|
|
docker pull "$LATEST"
|
|
cat > /tmp/Dockerfile.assets <<'EOF'
|
|
ARG BASE
|
|
FROM ${BASE}
|
|
COPY src/assets /srv/landing/assets
|
|
COPY src/shares /srv/landing/shares
|
|
EOF
|
|
docker build \
|
|
--build-arg BASE="$LATEST" \
|
|
-f /tmp/Dockerfile.assets \
|
|
-t "marketing:${{ steps.tag.outputs.tag }}" \
|
|
-t "$IMAGE" \
|
|
-t "${LATEST%:*}:stage-latest" \
|
|
.
|
|
docker push "$IMAGE"
|
|
docker push "${LATEST%:*}:stage-latest"
|
|
echo "Fast asset build complete"
|
|
|
|
- name: Deploy to marketing-stage
|
|
id: deploy-stage
|
|
env:
|
|
IMAGE: ${{ steps.tag.outputs.image }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
gcloud run deploy marketing-stage \
|
|
--image "$IMAGE" \
|
|
--region us-central1 \
|
|
--project neuron-785695 \
|
|
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
|
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
|
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
|
--allow-unauthenticated \
|
|
--quiet
|
|
|
|
STAGE_URL=$(gcloud run services describe marketing-stage \
|
|
--region us-central1 --project neuron-785695 \
|
|
--format 'value(status.url)')
|
|
echo "stage_url=${STAGE_URL}" >> "$GITHUB_OUTPUT"
|
|
echo "Stage URL: ${STAGE_URL}"
|
|
|
|
gcloud run services update marketing-stage \
|
|
--region us-central1 --project neuron-785695 \
|
|
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
|
|
--quiet
|
|
|
|
- name: Smoke test stage
|
|
run: |
|
|
set -euo pipefail
|
|
STAGE_URL="${{ steps.deploy-stage.outputs.stage_url }}"
|
|
echo "Smoke testing stage: ${STAGE_URL}"
|
|
|
|
for i in $(seq 1 18); do
|
|
STATUS=$(curl -sSo /dev/null -w "%{http_code}" --max-time 15 "${STAGE_URL}/" || echo "000")
|
|
echo "Attempt ${i}/18: HTTP ${STATUS}"
|
|
if [ "$STATUS" = "200" ]; then
|
|
echo "Stage smoke test PASSED — merge to main when ready"
|
|
exit 0
|
|
fi
|
|
sleep 5
|
|
done
|
|
|
|
echo "Stage smoke test FAILED"
|
|
exit 1
|