Skip to content
Open
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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm check-types
- run: pnpm test
- run: pnpm check-boundaries
- run: pnpm lint
- run: npm pack --dry-run
working-directory: packages/dynamic-apps-builder
- run: npm pack --dry-run
working-directory: packages/dynamic-apps
- run: pnpm test:packed
100 changes: 100 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Publish

on:
workflow_dispatch:
inputs:
version:
description: Semver to publish, or "legacy" for the matching legacy baseline
required: true
default: legacy
type: string
dist_tag:
description: npm dist-tag (auto derives latest/rc/next)
required: true
default: auto
type: choice
options: [auto, latest, rc, next, preview]

permissions:
contents: write
id-token: write

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

jobs:
publish:
runs-on: ubuntu-latest
environment: npm
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
registry-url: https://registry.npmjs.org
- name: Install OIDC-capable npm
run: npm install --global npm@11.16.0
- run: pnpm install --frozen-lockfile
- id: release
run: >-
node scripts/resolve-release.mjs
--version=${{ inputs.version }}
--tag=${{ inputs.dist_tag }}
--branch=${{ github.ref_name }}
- name: Verify npm authentication and unused versions
run: |
set -euo pipefail
npm ping
for package in @rivet-dev/dynamic-apps-builder @rivet-dev/dynamic-apps; do
if npm view "$package@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then
echo "$package@${{ steps.release.outputs.version }} already exists" >&2
exit 1
fi
done
- run: node scripts/set-release-version.mjs ${{ steps.release.outputs.version }}
- run: pnpm build
- run: pnpm check-types
- run: pnpm test
- run: pnpm check-boundaries
- run: pnpm lint
- run: pnpm test:packed
- name: Publish builder
run: >-
npm publish
.pack/rivet-dev-dynamic-apps-builder-${{ steps.release.outputs.version }}.tgz
--access public
--provenance
--tag ${{ steps.release.outputs.npm_tag }}
- name: Wait for builder registry visibility
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if npm view "@rivet-dev/dynamic-apps-builder@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then
exit 0
fi
sleep 10
done
echo "builder version did not become visible" >&2
exit 1
- name: Publish main package
run: >-
npm publish
.pack/rivet-dev-dynamic-apps-${{ steps.release.outputs.version }}.tgz
--access public
--provenance
--tag ${{ steps.release.outputs.npm_tag }}
- name: Create release tag
if: steps.release.outputs.real_release == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git tag "v${{ steps.release.outputs.version }}"
git push origin "v${{ steps.release.outputs.version }}"
gh release create "v${{ steps.release.outputs.version }}" --generate-notes
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
dist/
*.tsbuildinfo
*.aospkg
.pack/
.agent/
.DS_Store
16 changes: 16 additions & 0 deletions benchmarks/dynamic-apps/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@rivet-dev/dynamic-apps-benchmarks",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"load": "node --import tsx src/load.ts",
"check-types": "tsc --noEmit",
"test": "node --import tsx --test src/load.test.ts"
},
"devDependencies": {
"@types/node": "^22.19.15",
"tsx": "^4.20.6",
"typescript": "^5.7.3"
}
}
80 changes: 80 additions & 0 deletions benchmarks/dynamic-apps/src/load.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { strict as assert } from "node:assert";
import { createServer } from "node:http";
import { describe, it } from "node:test";
import { readLoadConfig, runLoadTest } from "./load.js";

describe("Dynamic Apps load driver", () => {
it("rejects an impossible success-rate gate", () => {
assert.throws(
() => readLoadConfig({ LOAD_TEST_MIN_SUCCESS_RATE: "1.1" }),
/LOAD_TEST_MIN_SUCCESS_RATE must be a number between 0 and 1/,
);
});

it("records cold and warm latency with a hard request bound", async () => {
let requestCount = 0;
const server = createServer((_request, response) => {
requestCount += 1;
response.setHeader(
"x-agentos-app-cold-start",
requestCount % 2 === 1 ? "1" : "0",
);
response.setHeader(
"x-agentos-app-replica",
`replica-${requestCount % 2}`,
);
response.setHeader("x-agentos-app-replica-count", "2");
response.setHeader("x-agentos-app-queue-delay-ms", "3");
response.end("hello");
});
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", resolve),
);

try {
const address = server.address();
assert(address && typeof address !== "string");
const result = await runLoadTest({
...readLoadConfig({}),
target: `http://127.0.0.1:${address.port}`,
concurrency: 2,
durationSeconds: 1,
maxRequests: 4,
});

assert.equal(result.completed, 4);
assert.equal(result.successRate, 1);
assert.equal(result.coldStarts, 2);
assert.equal(result.warmRequests, 2);
assert.equal(result.unclassifiedRequests, 0);
assert.equal(result.warmHitRate, 0.5);
assert.equal(result.replicaHeaderCoverage, 1);
assert.equal(result.maximumReplicaCount, 2);
assert.equal(result.stoppedBy, "request-limit");
assert(result.coldLatencyMs.p50 > 0);
assert(result.warmLatencyMs.p50 > 0);
} finally {
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});

it("fails a request instead of buffering an oversized response", async () => {
const result = await runLoadTest(
{
...readLoadConfig({}),
target: "http://load.test",
concurrency: 1,
durationSeconds: 1,
maxRequests: 1,
maxResponseBytes: 4,
},
async () => new Response("too large"),
);

assert.equal(result.completed, 1);
assert.equal(result.successRate, 0);
assert.deepEqual(result.statuses, { ResponseBodyLimitError: 1 });
});
});
Loading
Loading