diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..b3346785d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 build + + typecheck: + name: Typecheck + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 build + - run: pnpm -r typecheck + + test-core: + name: "Test: core" + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 --filter @hyperframes/core test -- --coverage + + test-engine: + name: "Test: engine" + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 --filter @hyperframes/engine test + + test-runtime-contract: + name: "Test: runtime contract" + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 --filter @hyperframes/core test:hyperframe-runtime-ci + + semantic-pr-title: + name: Semantic PR title + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..945ec5536 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,53 @@ +name: Publish to npm + +on: + push: + tags: + - "v*" + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: npm-publish + 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 + - run: pnpm build + + - name: Publish @hyperframes/core + run: pnpm --filter @hyperframes/core publish --access public --provenance --no-git-checks + env: + NPM_CONFIG_PROVENANCE: "true" + + - name: Publish @hyperframes/engine + run: pnpm --filter @hyperframes/engine publish --access public --provenance --no-git-checks + env: + NPM_CONFIG_PROVENANCE: "true" + + - name: Publish @hyperframes/producer + run: pnpm --filter @hyperframes/producer publish --access public --provenance --no-git-checks + env: + NPM_CONFIG_PROVENANCE: "true" + + - name: Publish @hyperframes/studio + run: pnpm --filter @hyperframes/studio publish --access public --provenance --no-git-checks + env: + NPM_CONFIG_PROVENANCE: "true" + + - name: Publish hyperframes (CLI) + run: pnpm --filter hyperframes publish --access public --provenance --no-git-checks + env: + NPM_CONFIG_PROVENANCE: "true" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e8904928..9d949a81b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,67 @@ Thanks for your interest in contributing to Hyperframes! This guide will help yo 3. Install dependencies: `pnpm install` 4. Create a branch: `git checkout -b my-feature` - +## Development + +```bash +pnpm install # Install all dependencies +pnpm dev # Run the studio (composition editor) +pnpm build # Build all packages +pnpm -r typecheck # Type-check all packages +``` + +### Running Tests + +```bash +pnpm --filter @hyperframes/core test # Core unit tests (vitest) +pnpm --filter @hyperframes/engine test # Engine unit tests (vitest) +pnpm --filter @hyperframes/core test:hyperframe-runtime-ci # Runtime contract tests +``` + +## Pull Requests + +- Use [conventional commit](https://www.conventionalcommits.org/) format for PR titles (e.g., `feat: add timeline export`, `fix: resolve seek overflow`) +- CI must pass before merge (build, typecheck, tests, semantic PR title) +- PRs require at least 1 approval + +## Packages + +| Package | Description | +|---|---| +| `@hyperframes/core` | Types, HTML generation, runtime, linter | +| `@hyperframes/engine` | Seekable page-to-video capture engine | +| `@hyperframes/producer` | Full rendering pipeline (capture + encode) | +| `@hyperframes/studio` | Composition editor UI | +| `hyperframes` | CLI for creating, previewing, and rendering | + +## Releasing (Maintainers) + +All packages use **fixed versioning** — every release bumps all packages to the same version. + +### Steps + +```bash +# 1. Bump version, commit, and tag +pnpm set-version 0.1.1 --tag + +# 2. Push to trigger the publish workflow +git push origin main --tags +``` + +The `v*` tag triggers CI, which validates (build + typecheck + tests) then publishes all packages to npm with provenance attestation. + +### Without `--tag` (manual control) + +```bash +# 1. Bump versions only (no commit/tag) +pnpm set-version 0.1.1 + +# 2. Review changes, commit yourself +git add -A +git commit -m "chore: release v0.1.1" +git tag v0.1.1 +git push origin main --tags +``` ## Reporting Issues diff --git a/package.json b/package.json index c9d95fc0d..82dc75f79 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build:producer": "pnpm --filter @hyperframes/producer build", "studio": "pnpm --filter @hyperframes/studio dev", "build:hyperframes-runtime": "pnpm --filter @hyperframes/core build:hyperframes-runtime", - "build:hyperframes-runtime:modular": "pnpm --filter @hyperframes/core build:hyperframes-runtime:modular" + "build:hyperframes-runtime:modular": "pnpm --filter @hyperframes/core build:hyperframes-runtime:modular", + "set-version": "tsx scripts/set-version.ts" }, "devDependencies": { "@types/node": "^25.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd429ba93..016967491 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,7 +128,7 @@ importers: specifier: ^1.13.0 version: 1.19.11(hono@4.12.8) '@hyperframes/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core hono: specifier: ^4.6.0 @@ -192,10 +192,10 @@ importers: specifier: ^1.13.0 version: 1.19.11(hono@4.12.8) '@hyperframes/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@hyperframes/engine': - specifier: workspace:* + specifier: workspace:^ version: link:../engine hono: specifier: ^4.6.0 diff --git a/scripts/set-version.ts b/scripts/set-version.ts new file mode 100644 index 000000000..1473fde69 --- /dev/null +++ b/scripts/set-version.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env tsx +/** + * Set the version across all publishable packages in the monorepo. + * + * Usage: + * pnpm set-version 0.1.1 + * pnpm set-version 0.1.1 --tag # also creates a git commit and tag + * + * All packages share a single version number (fixed versioning). + */ + +import { readFileSync, writeFileSync } from "fs"; +import { join } from "path"; +import { execSync } from "child_process"; + +const PACKAGES = [ + "packages/core", + "packages/engine", + "packages/producer", + "packages/studio", + "packages/cli", +]; + +const ROOT = join(import.meta.dirname, ".."); + +function main() { + const args = process.argv.slice(2); + const version = args.find((a) => !a.startsWith("--")); + const shouldTag = args.includes("--tag"); + + if (!version) { + console.error("Usage: pnpm set-version [--tag]"); + console.error("Example: pnpm set-version 0.1.1 --tag"); + process.exit(1); + } + + if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) { + console.error(`Invalid semver: ${version}`); + process.exit(1); + } + + // Update each package.json + for (const pkg of PACKAGES) { + const pkgPath = join(ROOT, pkg, "package.json"); + const content = JSON.parse(readFileSync(pkgPath, "utf-8")); + const oldVersion = content.version; + content.version = version; + writeFileSync(pkgPath, JSON.stringify(content, null, 2) + "\n"); + console.log(` ${content.name}: ${oldVersion} -> ${version}`); + } + + console.log(`\nSet ${PACKAGES.length} packages to v${version}`); + + if (shouldTag) { + // Verify working tree is clean (aside from the version bumps we just made) + const status = execSync("git status --porcelain", { + cwd: ROOT, + encoding: "utf-8", + }).trim(); + const unexpected = status + .split("\n") + .filter((line) => line && !PACKAGES.some((pkg) => line.includes(pkg))); + if (unexpected.length > 0) { + console.error("\nUnexpected uncommitted changes:"); + unexpected.forEach((line) => console.error(` ${line}`)); + console.error("Commit or stash these before tagging."); + process.exit(1); + } + + execSync(`git add ${PACKAGES.map((p) => join(p, "package.json")).join(" ")}`, { cwd: ROOT, stdio: "inherit" }); + execSync(`git commit -m "chore: release v${version}"`, { cwd: ROOT, stdio: "inherit" }); + execSync(`git tag v${version}`, { cwd: ROOT, stdio: "inherit" }); + console.log(`\nCreated commit and tag v${version}`); + console.log(`Run 'git push origin main --tags' to trigger the publish workflow.`); + } +} + +main();