Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b0c4810
feat: pivot Parallax to a trigger layer over Hermes agents
maxigimenez Aug 30, 2026
cbf2594
fix: let the runner read projects from the cloud
maxigimenez Aug 30, 2026
6f1c4f0
fix: make the compiled CLI entry point executable
maxigimenez Aug 30, 2026
95225d2
fix: actually mirror runs to the cloud, which is what fires Slack
maxigimenez Aug 30, 2026
7e687ed
feat: discoverable init, per-route prompts, live config, agent avatars
maxigimenez Aug 30, 2026
02be4c3
fix: make the parallax command work under any installed Node
maxigimenez Aug 30, 2026
bc3ac2b
feat: pull request routing, transition matching, and a loop guard
maxigimenez Aug 31, 2026
d0535ab
feat: support multi-round PR review via reviewersAdded
maxigimenez Sep 1, 2026
9373605
docs: a route catalog the dashboard can build a picker from
maxigimenez Sep 1, 2026
33b3e6f
test: make the adapter abort test deterministic
maxigimenez Sep 1, 2026
afe9407
refactor: rename packages/cloud to packages/cloud-api
maxigimenez Sep 1, 2026
ead7e83
ci: fix lint, and add deploy and publish pipelines
maxigimenez Sep 1, 2026
81244b4
ci: fix the workflow parse error and the typecheck build order
maxigimenez Sep 1, 2026
c383345
feat: add the cloud-dashboard, a React app over the cloud user API
maxigimenez Sep 1, 2026
76c267b
build: move Railway config to Infrastructure as Code
maxigimenez Sep 1, 2026
8917de0
fix(dashboard): correct the design deviations, and guard the cause
maxigimenez Sep 2, 2026
51b4093
feat: runner health, route editing, and dedicated create pages
maxigimenez Sep 2, 2026
da7c10c
chore: bump to 0.2.0 and wrap the Railway commands
maxigimenez Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
166 changes: 166 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: CI

on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Lint, typecheck and test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
# The floor and the newest. Node 22 needs --experimental-sqlite and 24
# ignores it, so running both is what keeps the supported range honest
# rather than aspirational.
#
# The floor is 22.12 rather than 22.11 because Vite 8, which builds the
# dashboard, declares `^20.19.0 || >=22.12.0`. It is a build tool and
# ships in nothing, so this constrains where the repo can be built, not
# where the runner can run.
node: ['22.12.0', '24.x']

env:
# A no-op on 24; required on 22 for node:sqlite.
NODE_OPTIONS: --experimental-sqlite
PARALLAX_DB_PATH: memory

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

# Build first, for two reasons: cloud-api resolves @parallax/common
# through its built .d.ts rather than a path alias, so a typecheck without
# dist cannot see it; and one test suite imports the built package to
# catch circular imports that only fail in the real ESM graph.
- name: Build
run: pnpm build

- name: Typecheck
run: |
for pkg in @parallax/common @parallax/orchestrator @parallax/cloud-api parallax-cli @parallax/cloud-dashboard; do
echo "::group::$pkg"
pnpm --filter "$pkg" exec tsc --noEmit
echo "::endgroup::"
done

- name: Test
run: pnpm test

workflows:
name: Validate the workflow files
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- uses: actions/checkout@v4

# An invalid workflow shows up as a run with no jobs and an error that
# appears in no job log, which is a genuinely confusing way to find out.
- name: Parse every workflow
run: |
npm install --no-save js-yaml >/dev/null 2>&1
node -e "
const yaml = require('js-yaml'), fs = require('fs');
let bad = 0;
for (const f of fs.readdirSync('.github/workflows')) {
try {
const w = yaml.load(fs.readFileSync('.github/workflows/' + f, 'utf8'));
// Bare \`on\` is YAML 1.1 boolean true, so accept either key.
const on = w.on ?? w[true];
if (!on || !w.jobs) throw new Error('missing on: or jobs:');
console.log(f + ': ' + Object.keys(on).join(', '));
} catch (e) {
console.log('::error file=.github/workflows/' + f + '::' + e.message);
bad++;
}
}
process.exit(bad ? 1 : 0);
"

image:
name: Build the cloud-api image
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

# Built for the architecture Railway deploys on, not the runner's, so a
# build that passes here is one that will pass there.
- name: Build
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: false
load: true
tags: parallax-cloud-api:ci
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Check the entry points resolve
run: |
docker run --rm parallax-cloud-api:ci node dist/org-cli.js | grep -q 'Usage:'
docker run --rm parallax-cloud-api:ci node dist/migrate-cli.js 2>&1 \
| grep -q 'DATABASE_URL is required'

dashboard-image:
name: Build the dashboard image
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

- name: Build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.dashboard
platforms: linux/amd64
push: false
load: true
tags: parallax-dashboard:ci
cache-from: type=gha
cache-to: type=gha,mode=max

# Starts it for real. The three things that only fail at runtime are the
# API URL being injected from the environment, the SPA falling back to
# index.html for a client-side route, and the health check Railway polls.
- name: Check the container actually serves
run: |
docker run -d --name dash -p 8080:8080 \
-e PARALLAX_API_URL=https://api.example.invalid parallax-dashboard:ci
for _ in $(seq 1 30); do
curl -sf http://127.0.0.1:8080/health >/dev/null && break
sleep 1
done
curl -s http://127.0.0.1:8080/health | grep -q '"apiConfigured":true'
curl -s http://127.0.0.1:8080/env.js | grep -q 'api.example.invalid'
curl -sf -o /dev/null http://127.0.0.1:8080/runs/run_1
docker rm -f dash
95 changes: 95 additions & 0 deletions .github/workflows/deploy-cloud-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Deploy cloud-api

on:
# Two ways in. Manual dispatch lets you pick the branch, so a change can be
# deployed before it merges. The push trigger is path-filtered rather than
# firing on every merge to main, because redeploying the control plane
# interrupts a runner's long poll for no reason. Delete the `push:` block
# below if you would rather every deploy be deliberate.
workflow_dispatch:
inputs:
service:
description: Railway service name
required: true
default: api
environment:
description: Railway environment
required: false
default: production

push:
branches: [main]
paths:
- 'packages/cloud-api/**'
- 'packages/common/**'
- 'Dockerfile'
- '.railway/railway.ts'
- '.github/workflows/deploy-cloud-api.yml'

concurrency:
# Never two deploys at once: the pre-deploy step runs migrations.
group: deploy-cloud-api
cancel-in-progress: false

jobs:
deploy:
name: Deploy to Railway
runs-on: ubuntu-latest
timeout-minutes: 30
environment: production

env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
SERVICE: ${{ inputs.service || 'api' }}

steps:
- uses: actions/checkout@v4

- name: Require a Railway token
run: |
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set. Create a project token in"\
"Railway (Project Settings -> Tokens) and add it as a repository secret."
exit 1
fi

- name: Install the Railway CLI
run: npm install -g @railway/cli@4

# Deploys from the checkout, so no git remote needs to be connected on the
# Railway side. The root is the Docker build context.
- name: Deploy
run: railway up --service "$SERVICE" --environment "${{ inputs.environment || 'production' }}" --ci

- name: Wait for health
run: |
# `railway domain` output has changed shape between CLI versions, so
# the hostname is scraped rather than parsed from a flag that may not
# exist. An explicit CLOUD_HEALTH_URL wins over both.
DOMAIN="${{ vars.CLOUD_HEALTH_URL }}"
if [ -z "$DOMAIN" ]; then
DOMAIN=$(railway domain --service "$SERVICE" 2>/dev/null \
| grep -oE '[a-z0-9.-]+\.up\.railway\.app' | head -1)
fi

if [ -z "$DOMAIN" ]; then
echo "::warning::Could not resolve the service domain; skipping the health check."
exit 0
fi

URL="$DOMAIN"
case "$URL" in https://*) ;; *) URL="https://$DOMAIN" ;; esac
echo "Checking $URL/health"
for attempt in $(seq 1 30); do
code=$(curl -sS -o /dev/null -w '%{http_code}' "$URL/health" || true)
if [ "$code" = "200" ]; then
echo "Healthy after ${attempt} attempt(s)."
exit 0
fi
sleep 10
done

# The deploy itself may have succeeded and the check raced it, so this
# fails loudly rather than silently passing.
echo "::error::/health did not return 200 within 5 minutes (last: $code)."
exit 1
99 changes: 99 additions & 0 deletions .github/workflows/deploy-dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Deploy dashboard

on:
# Same two ways in as the control plane. Manual dispatch picks the branch, so
# a change can be deployed before it merges; the push trigger is path-filtered
# rather than firing on every merge to main.
workflow_dispatch:
inputs:
service:
description: Railway service name
required: true
default: dashboard
environment:
description: Railway environment
required: false
default: production

push:
branches: [main]
paths:
- 'packages/cloud-dashboard/**'
- 'Dockerfile.dashboard'
- '.railway/railway.ts'
- '.github/workflows/deploy-dashboard.yml'

concurrency:
# Serialized for the same reason as the API: two deploys racing on one
# service leaves the winner ambiguous.
group: deploy-dashboard
cancel-in-progress: false

jobs:
deploy:
name: Deploy to Railway
runs-on: ubuntu-latest
timeout-minutes: 30
environment: production

env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
SERVICE: ${{ inputs.service || 'dashboard' }}

steps:
- uses: actions/checkout@v4

- name: Require a Railway token
run: |
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set. Create a project token in"\
"Railway (Project Settings -> Tokens) and add it as a repository secret."
exit 1
fi

- name: Install the Railway CLI
run: npm install -g @railway/cli@4

# Which Dockerfile this service builds comes from .railway/railway.ts.
# Run `railway config apply` after changing that file — `up` deploys the
# source, it does not reconcile the project's configuration.
- name: Deploy
run: railway up --service "$SERVICE" --environment "${{ inputs.environment || 'production' }}" --ci

- name: Wait for health
run: |
DOMAIN="${{ vars.DASHBOARD_HEALTH_URL }}"
if [ -z "$DOMAIN" ]; then
DOMAIN=$(railway domain --service "$SERVICE" 2>/dev/null \
| grep -oE '[a-z0-9.-]+\.up\.railway\.app' | head -1)
fi

if [ -z "$DOMAIN" ]; then
echo "::warning::Could not resolve the service domain; skipping the health check."
exit 0
fi

URL="$DOMAIN"
case "$URL" in https://*) ;; *) URL="https://$DOMAIN" ;; esac
echo "Checking $URL/health"
for attempt in $(seq 1 30); do
body=$(curl -sS "$URL/health" || true)
case "$body" in
*'"status":"ok"'*)
echo "Healthy after ${attempt} attempt(s): $body"
# A dashboard that cannot reach an API serves a page that can do
# nothing, so this is called out rather than passed silently.
case "$body" in
*'"apiConfigured":false'*)
echo "::warning::The dashboard is up but PARALLAX_API_URL is unset."\
"Set it on the service, or sign-in will fail."
;;
esac
exit 0
;;
esac
sleep 10
done

echo "::error::/health did not report ok within 5 minutes (last: ${body:-no response})."
exit 1
Loading
Loading