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
8 changes: 8 additions & 0 deletions .github/workflows/aimock-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ on:
# lands recorded fixtures. Re-enable a `schedule:` trigger here at that
# point (weekly cron is a reasonable starting cadence).

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
issues: write

jobs:
drift:
runs-on: ubuntu-latest
Expand Down
105 changes: 85 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ on:
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
library:
name: Library — lint / test / build
Expand All @@ -27,15 +34,32 @@ jobs:
website:
name: Website — lint / build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.sha }}
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx lint website
- run: npm run generate-api-docs
- name: Commit generated API docs to same-repo PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
if git diff --quiet -- apps/website/content/docs/*/api/api-docs.json; then
echo "Generated API docs are already committed."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/website/content/docs/*/api/api-docs.json
git commit -m "chore(docs): regenerate api docs"
git push origin "HEAD:${{ github.head_ref }}"
- name: Verify generated API docs are committed
run: git diff --exit-code -- apps/website/content/docs/*/api/api-docs.json
- run: npx nx build website
Expand Down Expand Up @@ -362,32 +386,31 @@ jobs:
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }}

# ── Canonical demo deploy ────────────────────────────────────────────
- name: Check if demo changed
id: demo_changed
demo-deploy:
name: Canonical demo → Vercel
needs: [examples-chat-smoke, examples-chat-e2e]
runs-on: ubuntu-latest
if: ${{ always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' }}
steps:
- name: Require demo prerequisite jobs
run: |
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
demo_changed=false
if printf '%s\n' "$changed_files" | grep -E '^examples/chat/(angular|python)/' >/dev/null; then
demo_changed=true
fi
if printf '%s\n' "$changed_files" | grep -E '^(vercel\.demo\.json|scripts/(assemble-demo|demo-middleware|langgraph-proxy|rate-limit)\.ts)$' >/dev/null; then
demo_changed=true
if [ "${{ needs.examples-chat-smoke.result }}" != "success" ]; then
echo "::error::examples/chat — python smoke finished with ${{ needs.examples-chat-smoke.result }}; refusing to deploy the canonical demo."
exit 1
fi
if printf '%s\n' "$changed_files" | grep -E '^libs/' >/dev/null; then
demo_changed=true
if [ "${{ needs.examples-chat-e2e.result }}" != "success" ]; then
echo "::error::examples/chat — e2e finished with ${{ needs.examples-chat-e2e.result }}; refusing to deploy the canonical demo."
exit 1
fi
echo "changed=$demo_changed" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build and assemble canonical demo
if: steps.demo_changed.outputs.changed == 'true'
run: npx tsx scripts/assemble-demo.ts
- name: Deploy canonical demo to Vercel (production)
if: steps.demo_changed.outputs.changed == 'true'
working-directory: deploy/demo
run: |
mkdir -p .vercel
Expand All @@ -396,6 +419,48 @@ jobs:
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }}
- name: Verify canonical demo build stamp
env:
DEMO_URL: https://demo.cacheplane.ai
EXPECTED_SHA: ${{ github.sha }}
run: |
node <<'NODE'
const { setTimeout: sleep } = require('node:timers/promises');

async function main() {
const demoUrl = process.env.DEMO_URL;
const expectedSha = process.env.EXPECTED_SHA;
let last = 'no response yet';

for (let attempt = 1; attempt <= 20; attempt += 1) {
try {
const response = await fetch(`${demoUrl}/__build.json?t=${Date.now()}`);
last = `HTTP ${response.status}`;

if (response.ok) {
const metadata = await response.json();
last = JSON.stringify(metadata);
if (metadata.sha === expectedSha) {
console.log(`Canonical demo is serving ${expectedSha}.`);
return;
}
}
} catch (error) {
last = error instanceof Error ? error.message : String(error);
}

console.log(`Waiting for canonical demo stamp ${expectedSha}; attempt ${attempt}/20. Last: ${last}`);
await sleep(5000);
}

throw new Error(`Canonical demo did not serve build stamp ${expectedSha}. Last: ${last}`);
}

main().catch((error) => {
console.error(`::error::${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
NODE

production-smoke:
name: Production smoke
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/deploy-langgraph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ on:
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
name: Deploy shared cockpit-dev to LangGraph Cloud
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
cockpit:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
type: boolean
default: true

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
Expand Down
20 changes: 20 additions & 0 deletions scripts/assemble-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ const root = resolve(__dirname, '..');
const deployDir = resolve(root, 'deploy/demo');
const skipBuild = process.argv.includes('--skip-build');

function resolveBuildSha(): string {
return process.env['GITHUB_SHA'] ?? execSync('git rev-parse HEAD', {
cwd: root,
encoding: 'utf8',
}).trim();
}

const buildMetadata = {
sha: resolveBuildSha(),
runId: process.env['GITHUB_RUN_ID'] ?? null,
runAttempt: process.env['GITHUB_RUN_ATTEMPT'] ?? null,
builtAt: new Date().toISOString(),
};

function writeBuildMetadata(outDir: string): void {
writeFileSync(resolve(outDir, '__build.json'), JSON.stringify(buildMetadata, null, 2) + '\n');
}

if (!skipBuild) {
console.log('Building examples-chat-angular (production)...');
execSync('npx nx build examples-chat-angular --configuration=production --skip-nx-cache', {
Expand All @@ -44,6 +62,7 @@ if (!existsSync(src)) {

mkdirSync(deployDir, { recursive: true });
cpSync(src, deployDir, { recursive: true });
writeBuildMetadata(deployDir);
console.log(`✅ Copied SPA to ${deployDir}`);

const outputDir = resolve(deployDir, '.vercel/output');
Expand All @@ -54,6 +73,7 @@ mkdirSync(staticDir, { recursive: true });
// Copy from the original dist (not deployDir) — Node's cpSync rejects
// copying a directory to a subdirectory of itself, filter or no filter.
cpSync(src, staticDir, { recursive: true });
writeBuildMetadata(staticDir);

mkdirSync(funcDir, { recursive: true });
execSync(`npx esbuild scripts/demo-middleware.ts --bundle --format=cjs --platform=node --outfile=${funcDir}/index.js`, {
Expand Down
Loading