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
22 changes: 14 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run lint

test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test

test-docker:
runs-on: ubuntu-latest
Expand All @@ -38,7 +44,7 @@ jobs:
runtime: [node, bun, deno]
include:
- runtime: node
integration_cmd: npx vitest run test/integration
integration_cmd: pnpm vitest run test/integration
- runtime: bun
integration_cmd: bunx vitest run test/integration
- runtime: deno
Expand All @@ -50,4 +56,4 @@ jobs:
- name: Unit tests (${{ matrix.runtime }})
run: docker compose run --rm test-${{ matrix.runtime }}
- name: Integration tests (${{ matrix.runtime }})
run: docker compose run --rm test-${{ matrix.runtime }} ${{ matrix.integration_cmd }}
run: docker compose run --rm test-${{ matrix.runtime }} ${{ matrix.integration_cmd }}
104 changes: 104 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Release

on:
push:
tags:
- 'v*'

concurrency:
group: release
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Verify tag matches package version
id: check-version
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION"
exit 1
fi
echo "version=$PKG_VERSION" >> "$GITHUB_OUTPUT"

test:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm test

- name: Integration tests (Node)
run: docker compose run --build --rm test-node pnpm vitest run test/integration
- name: Integration tests (Bun)
run: docker compose run --build --rm test-bun bunx vitest run test/integration
- name: Integration tests (Deno)
run: docker compose run --build --rm test-deno deno run -A npm:vitest run test/integration

publish-npm:
needs: [validate, test]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- name: Publish to npm
run: pnpm publish --provenance --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-jsr:
needs: [validate, test]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Publish to JSR
run: pnpm dlx jsr publish

github-release:
needs: [validate, publish-npm, publish-jsr]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
41 changes: 24 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] — M2 Boomerang (v1.2.0)
## [Unreleased]

## [1.3.0] — M3 Launchpad (2026-02-06)

### Added
- `CasService.restore()` — reconstruct files from manifests with per-chunk SHA-256 integrity verification.
- `ContentAddressableStore.restoreFile()` — facade method that restores and writes to disk.
- `readTree()` on `GitPersistencePort` / `GitPersistenceAdapter` — parse Git trees via `ls-tree`.
- `STREAM_ERROR` wrapping — stream failures during `store()` surface as `CasError('STREAM_ERROR')` with `{ chunksWritten }` metadata.
- `MISSING_KEY` error code — `restore()` now fails fast when manifest is encrypted but no decryption key is provided.
- CLI: `git cas store`, `git cas tree`, `git cas restore` subcommands via `bin/git-cas.js`.
- Integration test suite (59 tests) running against real Git bare repos inside Docker.
- `commander` dependency for CLI.
- Native Bun support via `BunCryptoAdapter` (uses `Bun.CryptoHasher`).
- Native Deno/Web standard support via `WebCryptoAdapter` (uses `crypto.subtle`).
- Automated, secure release workflow (`.github/workflows/release.yml`) with:
- **NPM OIDC support** including build provenance.
- **JSR support** via `jsr.json` and automated publishing.
- **GitHub Releases** with automated release notes.
- **Idempotency & Version Checks** to prevent failed partial releases.
- Dynamic runtime detection in `ContentAddressableStore` to pick the best adapter automatically.
- Hardened `package.json` with repository metadata, engine constraints, and explicit file inclusion.
- Local quality gates via `pre-push` git hook and `scripts/install-hooks.sh`.

### Changed
- `readBlob()` now normalises `Uint8Array` from plumbing into `Buffer` for codec/crypto compatibility.
- `readTree()` uses `git ls-tree -z` (NUL-delimited output) for safe parsing of filenames with leading/trailing spaces.
- **Breaking Change:** `CasService` cryptographic methods (`sha256`, `encrypt`, `decrypt`, `verifyIntegrity`) are now asynchronous to support Web Crypto and native optimizations.
- `ContentAddressableStore` facade methods are now asynchronous to accommodate lazy service initialization and async crypto.
- Project migrated from `npm` to `pnpm` for faster, more reliable dependency management.
- CI workflow (`.github/workflows/ci.yml`) now runs on all branches but prevents duplicate runs on PRs.
- `Dockerfile` now uses `corepack` for pnpm management.

### Fixed
- Fuzz tests in stream-error suite now fail explicitly if `store()` does not throw.
- ROADMAP: resolved inconsistent CLI signatures for `git cas tree` (`--slug` vs `--manifest`).

### Security
- None.
- Fixed recursion bug in `BunCryptoAdapter` where `randomBytes` shadowed the imported function.
- Resolved lazy-initialization race condition in `ContentAddressableStore` via promise caching.
- Fixed state leak in `WebCryptoAdapter` streaming encryption.
- Consolidated double decrypt calls in integrity tests for better performance.
- Hardened adapter-level key validation with type checks.

## [1.1.0] — M1 Bedrock
## [1.2.0] — M2 Boomerang (v1.2.0)

### Added
- `CryptoPort` interface and `NodeCryptoAdapter` — extracted all `node:crypto` usage from the domain layer.
Expand Down
18 changes: 15 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
- **Domain Purity**: Keep crypto and chunking logic independent of Git implementation details.
- **Portability**: The `GitPersistencePort` allows swapping the storage backend.

## Testing
- Use `npm test`.
- All domain logic should be tested with mocks for the persistence layer.
## Development Workflow

1. **Install Dependencies**: Use `pnpm install` to ensure consistent dependency management.
2. **Install Git Hooks**: Run `bash scripts/install-hooks.sh` to set up local quality gates. This will ensure that linting and unit tests pass before every push.
3. **Run Tests Locally**:
- `pnpm test` for unit tests.
- `pnpm run test:integration` for integration tests (requires Docker).

## Quality Gates
We enforce high standards for code quality:
- **Linting**: Must pass `pnpm run lint`.
- **Unit Tests**: All unit tests must pass.
- **Integration Tests**: Must pass across Node, Bun, and Deno runtimes.

These gates are enforced both locally via git hooks and in CI/CD.
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# --- Node ---
FROM node:22-slim AS node
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10 --activate
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
ENV GIT_STUNTS_DOCKER=1
CMD ["npx", "vitest", "run", "test/unit"]
CMD ["pnpm", "vitest", "run", "test/unit"]

# --- Bun ---
FROM oven/bun:1-slim AS bun
Expand All @@ -26,4 +27,4 @@ WORKDIR /app
COPY . .
RUN deno install --allow-scripts
ENV GIT_STUNTS_DOCKER=1
CMD ["deno", "run", "-A", "npm:vitest", "run", "test/unit"]
CMD ["deno", "run", "-A", "npm:vitest", "run", "test/unit"]
11 changes: 6 additions & 5 deletions bin/git-cas.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Manifest from '../src/domain/value-objects/Manifest.js';
program
.name('git-cas')
.description('Content Addressable Storage backed by Git')
.version('1.2.0');
.version('1.3.0');

/**
* Read a 32-byte raw encryption key from a file.
Expand Down Expand Up @@ -96,9 +96,10 @@ program
.action(async (treeOid, opts) => {
try {
const cas = createCas(opts.cwd);
const service = await cas.getService();

// Read the tree to find the manifest
const entries = await cas.service.persistence.readTree(treeOid);
const entries = await service.persistence.readTree(treeOid);
const manifestEntry = entries.find(
(e) => e.name.startsWith('manifest.'),
);
Expand All @@ -107,11 +108,11 @@ program
process.exit(1);
}

const manifestBlob = await cas.service.persistence.readBlob(
const manifestBlob = await service.persistence.readBlob(
manifestEntry.oid,
);
const manifest = new Manifest(
cas.service.codec.decode(manifestBlob),
service.codec.decode(manifestBlob),
);

const restoreOpts = { manifest };
Expand All @@ -131,4 +132,4 @@ program
}
});

program.parse();
program.parse();
Loading