Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
da29617
Initial diagrammatic morphisms + validation
Oct 6, 2025
5d55650
CLEANUP: Reimplement notebook keyboard shortcuts more simply. (#757)
epatters Oct 14, 2025
da8a838
ENH: Add model library, a reactive cache of elaborated models.
epatters Oct 13, 2025
44e104d
ENH: Adapt model library to support Patchwork module.
epatters Oct 15, 2025
5139ece
TST: Models in library do and don't re-elaborate as expected.
epatters Oct 15, 2025
17f8898
REFACTOR: Pass notebook, not just formal cells, to model elaborator.
epatters Oct 16, 2025
617e96e
FIX: Invalid leading "." in nix store paths for secrets
Oct 16, 2025
112c534
Use Cachix in CI for deploying to catcolab-next
Oct 16, 2025
f6199ac
REFACTOR: move nix integration tests to a separate file
Oct 16, 2025
32757d4
Disable nix integration tests
Oct 16, 2025
b57a720
ENH: Notebook types for instantiating models (#761)
epatters Oct 17, 2025
2936abf
BUILD: Run frontend-only tests in CI action. (#763)
epatters Oct 18, 2025
128f7aa
REFACTOR: Consistently migrate doc after fetching from Automerge repo.
epatters Oct 17, 2025
86f2ed8
REFACTOR: Make `RefId` type in `ModelLibrary` be generic.
epatters Oct 17, 2025
ef35184
ENH: Recursively elaborate instantiated models in model library.
epatters Oct 17, 2025
c40d8dd
BUG: Detect cycles in model document instead of looping infinitely.
epatters Oct 18, 2025
a960ecc
BUG: Don't take ownership of model in wasm bindings for migrations.
epatters Oct 18, 2025
1b6fa9a
REFACTOR: Wrap boxed models in `Rc` in Wasm bindings.
epatters Oct 18, 2025
b0e83a7
DOC: Help page for Petri nets (#737)
tim-at-topos Oct 21, 2025
523241b
BUG: Fix failing integration test and improve error handling (#765)
jmoggr Oct 21, 2025
9fd16dd
FIX: notebook-types package missing from backend nix package (#769)
jmoggr Oct 22, 2025
65fde8f
REFACTOR: Move Nix build job for catcolab-next to CI workflow (#771)
jmoggr Oct 22, 2025
dceadcd
using elaborator
Dec 4, 2025
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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ jobs:
run: |
RUSTDOCFLAGS='--deny warnings' cargo doc --no-deps

frontend_tests:
name: frontend tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install Rust toolchain from file
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
# Don't override flags in cargo config files.
rustflags: ""

- name: Build frontend
run: |
pnpm install
pnpm --filter ./packages/frontend run build -- --mode development

- name: Run frontend tests
run: |
pnpm --filter ./packages/frontend run test:no-backend

npm_checks:
name: npm checks
runs-on: ubuntu-latest
Expand All @@ -98,3 +129,26 @@ jobs:
- name: Format/linting/import sorting
run: |
pnpm --filter "./packages/*" run ci

build_nixos_system:
name: build NixOS system
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v25
with:
nix_path: nixpkgs=channel:25.05

- name: Configure Cachix
uses: cachix/cachix-action@v14
with:
name: catcolab-jmoggr
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

- name: Build the NixOS system for catcolab-next
run: nix build .#nixosConfigurations.catcolab-next.config.system.build.toplevel

49 changes: 32 additions & 17 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,36 @@ jobs:
name: Deploy backend to AWS
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' && github.ref_name == 'main'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.CATCOLAB_NEXT_DEPLOYUSER_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan backend-next.catcolab.org >> ~/.ssh/known_hosts

- name: Rsync the repo to remote host
run: |
rsync -az --delete ./ catcolab@backend-next.catcolab.org:~/catcolab

- name: Run nixos-rebuild switch remotely
run:
ssh catcolab@backend-next.catcolab.org "sudo nixos-rebuild switch --flake ./catcolab#catcolab-next"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v25
with:
nix_path: nixpkgs=channel:25.05

- name: Configure Cachix
uses: cachix/cachix-action@v14
with:
name: catcolab-jmoggr
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

- name: Set the SSH key for deployment to catcolab-next
run: |
mkdir -p ~/.ssh
echo "${{ secrets.CATCOLAB_NEXT_DEPLOYUSER_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan backend-next.catcolab.org >> ~/.ssh/known_hosts

- name: Deploy to catcolab-next
run: |
nix run github:serokell/deploy-rs .#catcolab-next

# Ensure that a copy of the deployed repository is on the machine that it was deployed to. This is
# a nice-to-have which enables checking the configuration of the currently deployed system and could
# make recovery slightly less aweful in the event nix commands need to be run on the remote.
- name: Rsync the repo to remote host
run: |
rsync -az --delete ./ catcolab@backend-next.catcolab.org:~/catcolab
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 11 additions & 52 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -320,57 +320,16 @@
};
};

# The backend relies on Firebase, so tests require VM internet access. Enable networking by running
# with --no-sandbox.
# Docs for nixos tests: https://nixos.org/manual/nixos/stable/index.html#sec-nixos-test-nodes
# (google and LLMs are useless)
checks.x86_64-linux.integrationTests = nixpkgs.legacyPackages.x86_64-linux.testers.runNixOSTest {
name = "Integration Tests";

skipTypeCheck = true;
nodes = {
catcolab = import ./infrastructure/hosts/catcolab-vm;
};

node.specialArgs = {
inherit inputs self;
rustToolchain = rustToolchainLinux;
};

# NOTE: This only checks if the services "start" from systemds perspective, not if they are not
# failed immediately after starting...
testScript = ''
def dump_logs(machine, *units):
for u in units:
print(f"\n===== journal for {u} =====")
print(machine.succeed(f"journalctl -u {u} --no-pager"))

def test_service(machine, service):
try:
machine.wait_for_unit(service)
except:
dump_logs(machine, service)
raise

def test_oneshot_service(machine, service):
try:
machine.wait_until_succeeds(
f"test $(systemctl is-active {service}) = inactive"
)
except:
dump_logs(machine, service)
raise

test_oneshot_service(catcolab, "database-setup.service")
test_oneshot_service(catcolab, "migrations.service")

test_service(catcolab, "automerge.service");
test_service(catcolab, "backend.service");
test_service(catcolab, "caddy.service");

catcolab.start_job("backupdb.service")
test_oneshot_service(catcolab, "backupdb.service")
'';
};
# Temporarily disabled until more meaningful tests are developed. Keeping the frontend dependecies
# up to date is currently not worth the hassle.
# checks.x86_64-linux.integrationTests = import ./infrastructure/tests/integration.nix {
# inherit
# nixpkgs
# inputs
# self
# linuxSystem
# ;
# rustToolchain = rustToolchainLinux;
# };
};
}
2 changes: 1 addition & 1 deletion infrastructure/hosts/catcolab-next/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ in
};

catcolabSecrets = {
file = ../../secrets/.env.next.age;
file = ../../secrets/env.next.age;
mode = "400";
owner = "catcolab";
};
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/hosts/catcolab/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ in
owner = "catcolab";
};
catcolabSecrets = {
file = ../../secrets/.env.prod.age;
file = ../../secrets/env.prod.age;
owner = "catcolab";
};
};
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/secrets/secrets.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ let
catcolab-next-deployuser = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM7AYg1fZM0zMxb/BuZTSwK4O3ycUIHruApr1tKoO8nJ deployuser@next.catcolab.org";
in
builtins.mapAttrs (_: publicKeys: { inherit publicKeys; }) ({
".env.next.age" = [
"env.next.age" = [
catcolab-next
owen
epatters
jmoggr
catcolab-next-deployuser
];
".env.prod.age" = [
"env.prod.age" = [
catcolab
owen
epatters
Expand Down
59 changes: 59 additions & 0 deletions infrastructure/tests/integration.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# The backend relies on Firebase, so tests require VM internet access. Enable networking by running
# with --no-sandbox.
# Docs for nixos tests: https://nixos.org/manual/nixos/stable/index.html#sec-nixos-test-nodes
# (google and LLMs are useless)
{
nixpkgs,
inputs,
self,
rustToolchain,
linuxSystem,
}:
nixpkgs.legacyPackages.${linuxSystem}.testers.runNixOSTest {
name = "Integration Tests";

skipTypeCheck = true;

nodes = {
catcolab = import ../hosts/catcolab-vm;
};

node.specialArgs = {
inherit inputs self rustToolchain;
};

# NOTE: This only checks if the services "start" from systemds perspective, not if they are not
# failed immediately after starting...
testScript = ''
def dump_logs(machine, *units):
for u in units:
print(f"\n===== journal for {u} =====")
print(machine.succeed(f"journalctl -u {u} --no-pager"))

def test_service(machine, service):
try:
machine.wait_for_unit(service)
except:
dump_logs(machine, service)
raise

def test_oneshot_service(machine, service):
try:
machine.wait_until_succeeds(
f"test $(systemctl is-active {service}) = inactive"
)
except:
dump_logs(machine, service)
raise

test_oneshot_service(catcolab, "database-setup.service")
test_oneshot_service(catcolab, "migrations.service")

test_service(catcolab, "automerge.service");
test_service(catcolab, "backend.service");
test_service(catcolab, "caddy.service");

catcolab.start_job("backupdb.service")
test_oneshot_service(catcolab, "backupdb.service")
'';
}
25 changes: 22 additions & 3 deletions packages/automerge-doc-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ import type { Pool as PoolType } from "pg";
import { PostgresStorageAdapter } from "./postgres_storage_adapter.js";
import type { NewDocSocketResponse, StartListeningSocketResponse } from "./types.js";
import type { SocketIOHandlers } from "./socket.js";
import { serializeError } from "./socket.js";
import jsonpatch from "fast-json-patch";

// Load environment variables from .env
dotenv.config();

/** Attempt to migrate a document, returning the migrated document or an error message. */
function migrateDocument(doc: unknown): { Ok: any } | { Err: string } {
try {
return { Ok: notbookTypes.migrateDocument(doc) };
} catch (e) {
return { Err: `Failed to migrate document: ${serializeError(e)}` };
}
}

export class AutomergeServer implements SocketIOHandlers {
private docMap: Map<string, DocHandle<unknown>>;

Expand Down Expand Up @@ -66,7 +76,12 @@ export class AutomergeServer implements SocketIOHandlers {
}

async createDoc(content: unknown): Promise<NewDocSocketResponse> {
const handle = this.repo.create(content);
const migrateResult = migrateDocument(content);
if ("Err" in migrateResult) {
return migrateResult;
}

const handle = this.repo.create(migrateResult.Ok);
if (!handle) {
return {
Err: "Failed to create a new document",
Expand Down Expand Up @@ -133,14 +148,18 @@ export class AutomergeServer implements SocketIOHandlers {
//
// XXX: frontend/src/api/document.ts needs to be kept up to date with this
const docBefore = handle.doc();
const docAfter = notbookTypes.migrateDocument(docBefore);
const migrateResult = migrateDocument(docBefore);
if ("Err" in migrateResult) {
return migrateResult;
}
const docAfter = migrateResult.Ok;

if ((docBefore as any).version !== docAfter.version) {
const patches = jsonpatch.compare(docBefore as any, docAfter);
handle.change((doc: any) => {
jsonpatch.applyPatch(doc, patches);
});
}

this.docMap.set(refId, handle);

return { Ok: null };
Expand Down
Loading