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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
*.cmd text eol=crlf
*.bat text eol=crlf
*.sln text eol=crlf
*.cs text eol=crlf
*.csproj text eol=crlf
*.props text eol=crlf
*.targets text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
Expand Down
49 changes: 37 additions & 12 deletions .github/branch-protection.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# Branch protection

> **Status:** not yet enabled. This document records the ruleset that Wave C
> will apply to `main` once continuous integration produces checks worth
> requiring.
> **Status:** The required checks below are now real and verified green.
> Enablement to GitHub is pending explicit approval from the repository owner.

## Why it is deferred
## Enablement is pending repository owner approval

`CONTRIBUTING.md` states that direct implementation commits to `main` are not
allowed. That rule is currently enforced by process, not by GitHub. A
protection rule cannot require status checks that do not exist, so enabling
protection before Wave C would either require nothing or block every merge.
Originally, protection was deferred because a protection rule cannot require
status checks that do not exist, and the CI workflow did not exist yet. That
prerequisite has been satisfied: the required checks are now defined, verified
green, and documented in the section below.

Until Wave C, independent review is performed locally with `codex exec` after
implementation and before integration.
The remaining gate is explicit approval from the repository owner to enable
GitHub branch protection. Until that approval is given, `main` remains
protected by process: `CONTRIBUTING.md` states that direct implementation
commits to `main` are not allowed. That rule is currently enforced by policy
and code review, not by GitHub. Independent review is performed locally with
`codex exec` after implementation and before integration, and this remains
the operative control.

## Ruleset to enable in Wave C

Expand All @@ -31,5 +35,26 @@ Applied to `main`:

## Required checks

To be filled in by Wave C with the exact job names from the CI workflow.
Wave C is not complete until this section names real, passing checks.
These are the job names from [`ci.yml`](workflows/ci.yml). Branch protection
matches required checks by name, so **renaming a job here or in the workflow
without updating the other blocks merging instead of un-enforcing anything**
— GitHub fails closed: a required context that never reports leaves the pull
request waiting on it indefinitely, blocking the merge rather than silently
letting it through. This differs from a job that reports as *skipped*, which
GitHub treats as passing; a renamed job's old context reports nothing at
all, so the two behave oppositely. Any job rename must update both files
together to avoid a permanently blocked pull request.

| Check | What it verifies |
|---|---|
| `build-and-test` | C# formatting, restore, build, tests, and the minimum discovered-test count |
| `plugin` | The Obsidian plugin installs from the committed lockfile and type-checks |
| `docs` | Every repository-internal Markdown link resolves |
| `secret-scan` | gitleaks finds no secret in the full history |
| `dependency-review` | No known-vulnerable dependency is introduced by the pull request |
| `artifact` | Publish, checksum, and SBOM generation succeed |

Licence policy is not configured for `dependency-review`: the action runs
without `allow-licenses`/`deny-licenses` inputs, so it checks vulnerabilities
only. Configuring licence enforcement requires first deciding which licences
are acceptable — a product decision that has not been made.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
commit-message:
prefix: "ci"
labels:
- dependencies
186 changes: 186 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

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

jobs:
build-and-test:
name: build-and-test
runs-on: windows-latest
steps:
- name: Check out
uses: actions/checkout@v7

- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: Verify formatting
run: dotnet format OpenMemory.sln --verify-no-changes

- name: Restore
run: dotnet restore OpenMemory.sln

- name: Build
run: dotnet build OpenMemory.sln --no-restore --nologo

- name: Run tests
run: dotnet test OpenMemory.sln --no-build --nologo

- name: Assert minimum discovered test count
shell: pwsh
run: |
$expected = 4
$listed = dotnet test OpenMemory.sln --no-build --nologo --list-tests
$count = ($listed | Where-Object { $_ -match '^\s+\S+\.\S+\.\S+$' }).Count
Write-Host "Discovered $count tests (floor $expected)"
if ($count -lt $expected) {
throw "Discovered $count tests, expected at least $expected. Either tests were removed, or test discovery is broken. dotnet test exits 0 in both cases, which is why this assertion exists."
}

plugin:
name: plugin
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/OpenMemory.ObsidianPlugin
steps:
- name: Check out
uses: actions/checkout@v7

- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: 24

- name: Enable corepack
run: corepack enable

- name: Install
run: pnpm install --frozen-lockfile

- name: Type-check
run: pnpm run typecheck

docs:
name: docs
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v7

- name: Check repository-internal links
run: bash tools/check-links.sh

secret-scan:
name: secret-scan
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Install gitleaks
run: |
curl -sSfL -o gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz"
tar -xzf gitleaks.tar.gz gitleaks
chmod +x gitleaks

- name: Scan repository history
run: ./gitleaks git --no-banner --redact --exit-code 1 .

dependency-review:
name: dependency-review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Check out
uses: actions/checkout@v7

- name: Review dependencies
uses: actions/dependency-review-action@v5

artifact:
name: artifact
runs-on: windows-latest
steps:
- name: Check out
uses: actions/checkout@v7

- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: Publish executables
shell: pwsh
run: |
foreach ($p in "Service", "Cli", "McpBridge") {
dotnet publish "src/OpenMemory.$p/OpenMemory.$p.csproj" `
--configuration Release `
--output "artifacts/publish/OpenMemory.$p"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}

- name: Package and checksum
shell: pwsh
run: |
$expectedDirs = @(
"artifacts/publish/OpenMemory.Service",
"artifacts/publish/OpenMemory.Cli",
"artifacts/publish/OpenMemory.McpBridge"
)
$problems = @()
foreach ($dir in $expectedDirs) {
if (-not (Test-Path $dir)) {
$problems += "missing: $dir"
} elseif (-not (Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue)) {
$problems += "empty: $dir"
}
}
if ($problems.Count -gt 0) {
throw "Publish output incomplete, refusing to package:`n$($problems -join "`n")"
}

New-Item -ItemType Directory -Force -Path artifacts/out | Out-Null
Compress-Archive -Path artifacts/publish/* -DestinationPath artifacts/out/openmemory-dev.zip
$hash = Get-FileHash -Algorithm SHA256 -Path artifacts/out/openmemory-dev.zip
$line = "$($hash.Hash.ToLower()) openmemory-dev.zip"
# sha256sum expects LF-only line endings; Out-File/Set-Content emit
# CRLF on Windows, which corrupts the filename field (trailing `r`)
# and breaks `sha256sum -c` on Linux/macOS/Git Bash consumers.
[System.IO.File]::WriteAllText(
"artifacts/out/openmemory-dev.zip.sha256",
"$line`n",
[System.Text.Encoding]::ASCII)
Get-Content artifacts/out/openmemory-dev.zip.sha256

- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: artifacts/out/openmemory-sbom.cyclonedx.json
upload-artifact: false
syft-version: "v1.51.0"

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: openmemory-dev
path: artifacts/out/
if-no-files-found: error
Loading