Skip to content
Merged
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
52af0ca
fix: pin analyzer test Pkcs dependency to patched version (#728)
thomasluizon Oct 17, 2026
7b60cc8
fix: shard pull request domain mutation testing (#729)
thomasluizon Oct 17, 2026
71e975f
Promote the API redesign with production compatibility checks (#723)
thomasluizon Oct 17, 2026
4cfe1ca
Add per-account feature flag targeting and admin controls (#724)
thomasluizon Oct 17, 2026
7cebaf2
Add header API versioning for v1 and v2 (#733)
thomasluizon Oct 18, 2026
1e432b5
fix: expose the config response in OpenAPI (#734)
thomasluizon Oct 18, 2026
0992cb3
Route production redesign accounts through Cloudflare (#735)
thomasluizon Oct 18, 2026
20384f7
Fix habit edit gating, end dates, and reminder updates (#736)
thomasluizon Oct 18, 2026
c481e0e
Reduce mutation and guard CI jobs (#737)
thomasluizon Oct 18, 2026
ad60dc7
test: strengthen habit and goal ownership coverage (#739)
thomasluizon Oct 18, 2026
fffcd22
test: verify checkout session user metadata (#740)
thomasluizon Oct 18, 2026
4e32a4c
fix: protect OAuth code delivery with Turnstile (#741)
thomasluizon Oct 18, 2026
5f6fa7d
fix: enforce chat interaction scope before loading context (#743)
thomasluizon Oct 18, 2026
7ec3746
fix: erase accountability pairs during account deletion and reset (#745)
thomasluizon Oct 18, 2026
7229647
Fix goal sync dates and deadline reminder counts (#747)
thomasluizon Oct 18, 2026
2e5439b
Manage staging landing routing in Terraform (#742)
thomasluizon Oct 18, 2026
d7edd2c
Restrict Web Push endpoints to supported push services (#1503) (#751)
thomasluizon Oct 18, 2026
cf8489d
Erase account uploads before permanent deletion (#1507) (#750)
thomasluizon Oct 18, 2026
cd6931a
fix: limit anonymous auth and waitlist by email and source IP (ORB-10…
thomasluizon Oct 18, 2026
a1207aa
Limit access tokens to fifteen minutes (#738)
thomasluizon Oct 18, 2026
e2dd228
fix(auth): equalize account provisioning work (ORB-96) (#744)
thomasluizon Oct 18, 2026
5d22617
fix: expose redesign controllers in both API versions
thomasluizon Oct 18, 2026
b34d010
fix: honor account flags in proactive engine
thomasluizon Oct 18, 2026
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
46 changes: 46 additions & 0 deletions .github/scripts/mutation-matrix.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";

const shards = [
{
project: "domain",
shard: "domain-habit-freeze",
mutate: ["**/Entities/Habit.cs", "**/Entities/StreakFreeze.cs"],
},
{
project: "domain",
shard: "domain-user-invariants",
mutate: ["**/Entities/User.cs", "**/Entities/HabitInvariants.cs"],
},
{ project: "application" },
{ project: "infrastructure" },
];

const projects = {
domain: "Orbit.Domain",
application: "Orbit.Application",
infrastructure: "Orbit.Infrastructure",
};

let selected = shards;

if (process.argv[2] === "--stdin") {
const changed = readFileSync(0, "utf8")
.split("\0")
.filter(Boolean);
for (const path of changed) process.stderr.write(`Changed: ${JSON.stringify(path)}\n`);
const shared = changed.some((path) =>
/^Directory\..*\.(props|targets)$/.test(path)
|| /^[^/]+\.slnx?$/.test(path)
|| /^(global\.json|[Nn]u[Gg]et\.[Cc]onfig|\.config\/dotnet-tools\.json)$/.test(path)
|| path === ".github/workflows/mutation.yml"
|| path.startsWith(".github/scripts/mutation-matrix.")
);
selected = shards.filter(({ project }) => shared || changed.some((path) => !/\.(md|mdx|rst|adoc)$/i.test(path) && (
path.startsWith(`src/${projects[project]}/`)
|| path.startsWith(`tests/${projects[project]}.Tests/`)
|| path === `stryker-config.${project}.json`
)));
}

process.stdout.write(`matrix=${JSON.stringify({ include: selected })}\n`);
process.stdout.write(`has-shards=${selected.length > 0}\n`);
142 changes: 142 additions & 0 deletions .github/scripts/mutation-matrix.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

const selector = fileURLToPath(new URL("./mutation-matrix.mjs", import.meta.url));
const allShards = ["domain-habit-freeze", "domain-user-invariants", "application", "infrastructure"];

function select(changed) {
const output = execFileSync(process.execPath, [selector, ...(changed === undefined ? [] : ["--stdin"])], {
input: changed,
encoding: "utf8",
});
const values = Object.fromEntries(output.trim().split("\n").map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}));
const matrix = JSON.parse(values.matrix);
assert.equal(values["has-shards"], String(matrix.include.length > 0));
return matrix.include;
}

function selectPaths(paths) {
return select(paths.map((path) => `${path}\0`).join(""));
}

function repository(t) {
const root = mkdtempSync(join(tmpdir(), "orbit-mutation-matrix-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
const git = (...args) => execFileSync("git", args, { cwd: root, encoding: "utf8" });
git("init", "--quiet");
git("-c", "user.name=Mutation test", "-c", "user.email=mutation@example.com", "commit", "--quiet", "--allow-empty", "-m", "Base");
git("branch", "base");
const paths = new Set();
const write = (path) => {
mkdirSync(dirname(join(root, path)), { recursive: true });
writeFileSync(join(root, path), "changed\n");
paths.add(path);
};
const commit = (...extraPaths) => {
git("--literal-pathspecs", "add", "--", ...new Set([...paths, ...extraPaths]));
git("-c", "user.name=Mutation test", "-c", "user.email=mutation@example.com", "commit", "--quiet", "-m", "Change");
paths.clear();
};
const changed = () => git("diff", "--name-only", "--no-renames", "-z", "--end-of-options", "base...HEAD");
return { root, git, write, commit, changed };
}

function names(matrix) {
return matrix.map((entry) => entry.shard ?? entry.project);
}

test("an empty changed-file list selects no shards", () => {
assert.deepEqual(selectPaths([]), []);
});

test("documentation-only changes select no shards", () => {
assert.deepEqual(selectPaths([
"README.md",
"docs/mutation notes.md",
"src/Orbit.Domain/CLAUDE.md",
"tests/Orbit.Application.Tests/README.md",
"src/Orbit.Infrastructure/docs/design.MD",
]), []);
});

test("unrelated projects select no shards", () => {
assert.deepEqual(selectPaths(["src/Orbit.Api/Controllers/AuthController.cs", "tests/Orbit.Analyzers.Tests/RuleTests.cs"]), []);
});

for (const [project, expected] of [
["Domain", allShards.slice(0, 2)],
["Application", ["application"]],
["Infrastructure", ["infrastructure"]],
]) {
for (const path of [`src/Orbit.${project}/Entities/Changed.cs`, `tests/Orbit.${project}.Tests/ChangedTests.cs`]) {
test(`${path} selects only its project's shards`, () => {
assert.deepEqual(names(selectPaths([path])), expected);
});
}
}

test("changes across projects select their union without duplicate shards", () => {
assert.deepEqual(names(selectPaths([
"src/Orbit.Domain/Entities/Habit.cs",
"tests/Orbit.Domain.Tests/HabitTests.cs",
"src/Orbit.Infrastructure/Services/UserStreakService.cs",
])), [allShards[0], allShards[1], "infrastructure"]);
});

test("NUL separators preserve spaces and newlines within paths", () => {
assert.deepEqual(names(selectPaths(["src/Orbit.Domain/Entities/Habit notes\nREADME.md"])), []);
assert.deepEqual(names(selectPaths(["src/Orbit.Domain/Entities/Habit notes\nChanged.cs"])), allShards.slice(0, 2));
});

test("renames select both the old and new project, and deletions select the old project", (t) => {
const repo = repository(t);
const oldPath = "src/Orbit.Domain/Old.cs";
const newPath = "src/Orbit.Application/Moved.cs";
const deletedPath = "tests/Orbit.Infrastructure.Tests/RemovedTests.cs";
repo.write(oldPath);
repo.write(deletedPath);
repo.commit();
repo.git("branch", "--force", "base");
mkdirSync(dirname(join(repo.root, newPath)), { recursive: true });
renameSync(join(repo.root, oldPath), join(repo.root, newPath));
rmSync(join(repo.root, deletedPath));
repo.commit(oldPath, newPath, deletedPath);
assert.deepEqual(names(select(repo.changed())), allShards);
});

test("the diff uses the merge base rather than unrelated changes on the base branch", (t) => {
const repo = repository(t);
const head = repo.git("branch", "--show-current").trim();
repo.git("checkout", "--quiet", "base");
repo.write("src/Orbit.Infrastructure/BaseOnly.cs");
repo.commit();
repo.git("checkout", "--quiet", head);
repo.write("README.md");
repo.commit();
assert.deepEqual(select(repo.changed()), []);
});

for (const path of ["Directory.Build.props", ".config/dotnet-tools.json", ".github/workflows/mutation.yml", ".github/scripts/mutation-matrix.mjs"]) {
test(`${path} selects every shard`, () => {
assert.deepEqual(names(selectPaths([path])), allShards);
});
}

test("a project-specific Stryker config selects only that project", () => {
assert.deepEqual(names(selectPaths(["stryker-config.application.json"])), ["application"]);
});

test("nightly and manual runs keep all shards and their mutation scopes without input", () => {
const matrix = select();
assert.deepEqual(names(matrix), allShards);
assert.deepEqual(matrix[0].mutate, ["**/Entities/Habit.cs", "**/Entities/StreakFreeze.cs"]);
assert.deepEqual(matrix[1].mutate, ["**/Entities/User.cs", "**/Entities/HabitInvariants.cs"]);
});
12 changes: 12 additions & 0 deletions .github/workflows/arch-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@ name: arch-map

on:
pull_request:
paths:
- 'src/**/*.cs'
- 'src/**/*.csproj'
- 'tests/**/*.cs'
- 'tools/arch-map.mjs'
- '.github/workflows/arch-map.yml'
push:
branches: [main]
paths:
- 'src/**/*.cs'
- 'src/**/*.csproj'
- 'tests/**/*.cs'
- 'tools/arch-map.mjs'
- '.github/workflows/arch-map.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/gating-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ name: Gating matrix
on:
pull_request:
branches: [main]
paths:
- 'src/**/*.cs'
- 'tools/gating-matrix.mjs'
- 'tools/__tests__/gating-matrix.test.mjs'
- '.github/workflows/gating-matrix.yml'
push:
branches: [main]
paths:
- 'src/**/*.cs'
- 'tools/gating-matrix.mjs'
- 'tools/__tests__/gating-matrix.test.mjs'
- '.github/workflows/gating-matrix.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -25,9 +35,6 @@ jobs:
with:
node-version: 22

- name: Test the generator
run: node --test tools/__tests__/gating-matrix.test.mjs

- name: Generate the gating matrix
run: node tools/gating-matrix.mjs

Expand Down
104 changes: 44 additions & 60 deletions .github/workflows/guards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,63 @@ permissions:
pull-requests: read

jobs:
workflows:
name: Workflow Syntax
guards:
name: Guards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check workflow files
defaults:
run:
shell: bash
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency documents queue, which actionlint does not recognize.
run: |
lint_dir="$(mktemp -d)"
curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash -o "$lint_dir/download-actionlint.bash"
bash "$lint_dir/download-actionlint.bash" 1.7.12 "$lint_dir"
"$lint_dir/actionlint" -color -ignore '^unexpected key "queue" for "concurrency" section\. expected one of "cancel-in-progress", "group"$'

timeless:
name: Timeless Text
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Check changed text
if: github.event_name == 'pull_request'
run: node tools/check-timeless.mjs --base origin/${{ github.base_ref }}
- name: Check tracked text
if: github.event_name == 'push'
run: node tools/check-timeless.mjs --all

dashes:
name: Dash Ban
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Changed files carry no new dashes
if: github.event_name == 'pull_request'
- name: Workflow Syntax
if: ${{ !cancelled() }}
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency documents queue, which actionlint does not recognize.
run: |
lint_dir="$(mktemp -d)"
curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash -o "$lint_dir/download-actionlint.bash"
bash "$lint_dir/download-actionlint.bash" 1.7.12 "$lint_dir"
"$lint_dir/actionlint" -color -ignore '^unexpected key "queue" for "concurrency" section\. expected one of "cancel-in-progress", "group"$'

- name: Timeless Text
if: ${{ !cancelled() }}
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
run: |
git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -z \
| xargs -0 --no-run-if-empty node tools/check-dashes.mjs --files
- name: Baseline only shrinks
run: node tools/check-dashes.mjs --check-baseline
- name: PR title and body carry no dashes
# Bot PR bodies quote upstream text that Orbit cannot edit.
# The file-level dash check still covers bot changes.
if: github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot'
if [ "$EVENT_NAME" = pull_request ]; then
node tools/check-timeless.mjs --base "origin/$BASE_REF"
else
node tools/check-timeless.mjs --all
fi

- name: Dash Ban
if: ${{ !cancelled() }}
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: node tools/check-dashes.mjs --text "$PR_TITLE $PR_BODY"
run: |
if [ "$EVENT_NAME" = pull_request ]; then
git diff --name-only --diff-filter=ACMR "origin/$BASE_REF...HEAD" -z \
| xargs -0 --no-run-if-empty node tools/check-dashes.mjs --files
fi
node tools/check-dashes.mjs --check-baseline
if [ "$EVENT_NAME" = pull_request ] && [ "$PR_AUTHOR_TYPE" != Bot ]; then
node tools/check-dashes.mjs --text "$PR_TITLE $PR_BODY"
fi

frontmatter:
name: Skill and Agent Frontmatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- run: node tools/check-frontmatter.mjs
- name: Skill and Agent Frontmatter
if: ${{ !cancelled() }}
run: node tools/check-frontmatter.mjs

root-allowlist:
name: Root Allowlist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- run: node tools/check-root-allowlist.mjs
- name: Root Allowlist
if: ${{ !cancelled() }}
run: node tools/check-root-allowlist.mjs
Loading
Loading