diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d15106f..4d888ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,125 +1,22 @@ -name: CI +name: Nix CI on: push: branches: [main] workflow_dispatch: -jobs: - determinism: - runs-on: ${{ fromJSON(vars.CI_RUNNER_LABELS) }} - strategy: - fail-fast: false - matrix: - node-version: ["20", "22"] - - steps: - - uses: actions/checkout@v4 - - - run: corepack enable - - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "pnpm" - - - run: pnpm install --frozen-lockfile - - run: npm run verify:golden +permissions: + contents: read + id-token: write - test: - runs-on: ${{ fromJSON(vars.CI_RUNNER_LABELS) }} - env: - BIOFLOW_DATABASE_URL: postgres://postgres@127.0.0.1:55432/postgres +concurrency: + group: nix-ci-${{ github.ref }} + cancel-in-progress: true - steps: - - uses: actions/checkout@v4 - - - run: corepack enable - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "pnpm" - - - run: pnpm install --frozen-lockfile - - name: Start local Postgres - env: - PGDATA: ${{ runner.temp }}/bioflow-pgdata - PGSOCKET_DIR: ${{ runner.temp }}/bioflow-pgsocket - PGPORT: "55432" - PGLOG: ${{ runner.temp }}/bioflow-postgres.log - run: nix shell nixpkgs#postgresql_16 --command bash scripts/ci-local-postgres.sh start - - run: npm run check:ci - - run: npm run demo:killer:ci - - run: npm run build - - name: Detect hosted-only scripts - id: hosted-scripts - run: | - node - <<'NODE' - const scripts = require('./package.json').scripts ?? {}; - const enabled = Boolean(scripts['db:migrate'] && scripts['service:api'] && scripts['test:rls']); - const fs = require('node:fs'); - fs.appendFileSync(process.env.GITHUB_OUTPUT, `enabled=${enabled}\n`); - NODE - - name: CLI autopilot flow (mock hosted) - env: - BIOFLOW_AUTOPILOT_FLOW_OUT: .bioflow_smoke_service/cli-autopilot-run-flow-summary.json - run: | - set -euo pipefail - npm run test -- test/cli-autopilot-run-flow.test.ts - - name: Upload CLI autopilot flow summary - if: always() - uses: actions/upload-artifact@v4 - with: - name: cli-autopilot-run-flow-summary - path: .bioflow_smoke_service/cli-autopilot-run-flow-summary.json - if-no-files-found: warn - - name: Self-serve smoke (signup -> run -> verify -> share) - if: ${{ steps.hosted-scripts.outputs.enabled == 'true' }} - env: - BIOFLOW_SERVICE_DATA_DIR: .bioflow_smoke_service - BIOFLOW_RUNNER_MODE: inline - BIOFLOW_API_KEY_PEPPER: 0123456789abcdef0123456789abcdef - BIOFLOW_DEV_ALLOW_ORG_HEADER: "false" - BIOFLOW_HTTP_RATE_LIMIT_POINTS: "0" - BIOFLOW_SIGNUP_RATE_LIMIT_POINTS: "0" - BIOFLOW_REMOTE_URL: http://127.0.0.1:8080 - BIOFLOW_AUTOPILOT_OUT: .bioflow_smoke_service/self-serve-autopilot.json - run: | - set -euo pipefail - npm run db:migrate - npm run service:api >/tmp/bioflow-api.log 2>&1 & - API_PID=$! - trap 'status=$?; kill "$API_PID" >/dev/null 2>&1 || true; wait "$API_PID" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "=== /tmp/bioflow-api.log ==="; cat /tmp/bioflow-api.log || true; fi' EXIT - for _ in {1..60}; do - if curl -fsS http://127.0.0.1:8080/readyz >/dev/null; then - break - fi - sleep 1 - done - curl -fsS http://127.0.0.1:8080/readyz >/dev/null - npm run bioflow -- autopilot:run \ - --remote-url "$BIOFLOW_REMOTE_URL" \ - --enforce-policy \ - --out "$BIOFLOW_AUTOPILOT_OUT" - echo "=== self-serve autopilot summary ===" - cat "$BIOFLOW_AUTOPILOT_OUT" - npm run bioflow -- autopilot:gate --json - - name: Upload self-serve autopilot summary - if: always() - uses: actions/upload-artifact@v4 - with: - name: self-serve-autopilot-summary - path: .bioflow_smoke_service/self-serve-autopilot.json - if-no-files-found: warn - - name: RLS integration - if: ${{ steps.hosted-scripts.outputs.enabled == 'true' }} - run: npm run test:rls - - name: Stop local Postgres - if: always() - env: - PGDATA: ${{ runner.temp }}/bioflow-pgdata - PGSOCKET_DIR: ${{ runner.temp }}/bioflow-pgsocket - PGPORT: "55432" - PGLOG: ${{ runner.temp }}/bioflow-postgres.log - run: nix shell nixpkgs#postgresql_16 --command bash scripts/ci-local-postgres.sh stop +jobs: + nix-ci: + uses: OmnisGenomics/.github/.github/workflows/nix-ci.yml@main + with: + publish_cache: ${{ github.ref == 'refs/heads/main' }} + secrets: + MONARCHIC_GITHUB_PAT: ${{ secrets.MONARCHIC_GITHUB_PAT }} diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 469bc6e..cc11edf 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -5,48 +5,22 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + jobs: conventional-commits: - name: Conventional Commits - runs-on: ${{ fromJSON(vars.CI_RUNNER_LABELS) }} - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Determine Revision Range - id: range - shell: bash - run: | - set -euo pipefail - if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then - range="HEAD~1..HEAD" - else - range="HEAD" - fi - echo "range=$range" >> "$GITHUB_OUTPUT" - - - name: Validate Commit Subjects - run: | - set -euo pipefail - ./scripts/conventional-commit-check.sh \ - --rev-range "${{ steps.range.outputs.range }}" \ - --output out/conventional-commits-summary.json \ - --allow-merge-commits \ - --allow-git-revert - - - name: Upload Conventional Commit Summary - if: always() - uses: actions/upload-artifact@v4 - with: - name: conventional-commits-${{ github.run_id }}-${{ github.run_attempt }} - path: out/conventional-commits-summary.json - if-no-files-found: warn + uses: OmnisGenomics/.github/.github/workflows/maintenance.yml@main + with: + check_flake: false + report_artifact_name: conventional-commits-${{ github.run_id }}-${{ github.run_attempt }} + maintenance_command: | + git fetch --no-tags --depth=2 origin "${GITHUB_REF_NAME}" + if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then + rev_range=HEAD~1..HEAD + else + rev_range=HEAD + fi + nix run .#conventional-commits -- "$rev_range" + secrets: + MONARCHIC_GITHUB_PAT: ${{ secrets.MONARCHIC_GITHUB_PAT }} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 7fed0e4..20cc1fc 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -5,35 +5,19 @@ on: tags: - "cli-v*" +permissions: + actions: read + contents: read + id-token: write + jobs: publish: - runs-on: ${{ fromJSON(vars.CI_RUNNER_LABELS) }} - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v4 - - - run: corepack enable - - - uses: actions/setup-node@v4 - with: - node-version: "20" - registry-url: "https://registry.npmjs.org" - cache: "pnpm" - - - name: Validate tag matches package version - run: | - TAG_VERSION="${GITHUB_REF_NAME#cli-v}" - PKG_VERSION="$(node -e 'const fs=require(\"fs\");process.stdout.write(JSON.parse(fs.readFileSync(\"package.json\",\"utf8\")).version)')" - if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "tag version ($TAG_VERSION) must match package.json version ($PKG_VERSION)" - exit 1 - fi - - - run: pnpm install --frozen-lockfile - - run: npm run build - - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + uses: OmnisGenomics/.github/.github/workflows/npm-publish.yml@main + with: + tag_pattern: '^cli-v[0-9]+[.][0-9]+[.][0-9]+$' + tag_prefix: cli-v + install_command: pnpm install --frozen-lockfile + required_workflow: Nix CI + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + MONARCHIC_GITHUB_PAT: ${{ secrets.MONARCHIC_GITHUB_PAT }} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8b1d51a --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1785571196, + "narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-node20": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-node20": "nixpkgs-node20" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f71e28a --- /dev/null +++ b/flake.nix @@ -0,0 +1,209 @@ +{ + description = "BioFlow CLI and MCP"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs-node20.url = "github:NixOS/nixpkgs/nixos-25.05"; + }; + + outputs = { self, nixpkgs, nixpkgs-node20 }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + pkgsFor = system: nixpkgs.legacyPackages.${system}; + node20PkgsFor = system: nixpkgs-node20.legacyPackages.${system}; + in + { + packages = forAllSystems (system: + let + pkgs = pkgsFor system; + packageJson = builtins.fromJSON (builtins.readFile ./package.json); + pname = "bioflow"; + version = packageJson.version; + pnpmDeps = pkgs.fetchPnpmDeps { + inherit pname version; + src = ./.; + hash = "sha256-rxMM+o786sMQgjtUMvPIBjDnz/LArxMl3Eahu4bXT9Q="; + fetcherVersion = 4; + }; + bioflow = pkgs.stdenv.mkDerivation { + inherit pname version pnpmDeps; + src = ./.; + + nativeBuildInputs = [ + pkgs.nodejs_22 + pkgs.pnpm + pkgs.pnpmConfigHook + ]; + + npm_config_manage_package_manager_versions = "false"; + + buildPhase = '' + runHook preBuild + pnpm --config.manage-package-manager-versions=false run build + patchShebangs dist/cli/main.js dist/mcp/main.js + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p "$out/lib/bioflow" + cp -R dist package.json pnpm-lock.yaml "$out/lib/bioflow/" + runHook postInstall + ''; + + meta = { + description = packageJson.description or "BioFlow CLI and MCP"; + homepage = "https://github.com/OmnisGenomics/BioFlow"; + license = pkgs.lib.licenses.asl20; + }; + }; + in + { + default = bioflow; + bioflow = bioflow; + }); + + checks = forAllSystems (system: + let + pkgs = pkgsFor system; + node20Pkgs = node20PkgsFor system; + packageJson = builtins.fromJSON (builtins.readFile ./package.json); + pname = "bioflow"; + version = packageJson.version; + pnpmDeps = pkgs.fetchPnpmDeps { + inherit pname version; + src = ./.; + hash = "sha256-rxMM+o786sMQgjtUMvPIBjDnz/LArxMl3Eahu4bXT9Q="; + fetcherVersion = 4; + }; + + mkCheck = { name, command, nodejs ? pkgs.nodejs_22, extraInputs ? [ ] }: + pkgs.stdenv.mkDerivation { + inherit pname version pnpmDeps; + name = "${pname}-${name}-${version}"; + src = ./.; + + nativeBuildInputs = [ + nodejs + pkgs.pnpm + pkgs.pnpmConfigHook + ] ++ extraInputs; + + npm_config_manage_package_manager_versions = "false"; + + dontBuild = true; + installPhase = '' + runHook preInstall + export HOME="$(mktemp -d)" + export TMPDIR="$(mktemp -d)" + ${command} + mkdir -p "$out" + touch "$out/${name}" + runHook postInstall + ''; + }; + in + { + default = self.checks.${system}.ci; + package = self.packages.${system}.bioflow; + golden-node20 = mkCheck { + name = "golden-node20"; + nodejs = node20Pkgs.nodejs_20; + command = "npm run verify:golden"; + }; + golden-node22 = mkCheck { + name = "golden-node22"; + command = "npm run verify:golden"; + }; + ci = mkCheck { + name = "ci"; + command = "npm run check"; + }; + package-smoke = mkCheck { + name = "package-smoke"; + command = '' + npm run build + patchShebangs dist/cli/main.js dist/mcp/main.js + + pack_json="$TMPDIR/npm-pack.json" + npm pack --json > "$pack_json" + tarball="$(node -e 'const fs = require("fs"); const packed = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(packed[0].filename);' "$pack_json")" + + package_dir="$TMPDIR/package-smoke" + mkdir -p "$package_dir" + tar -xzf "$tarball" -C "$package_dir" + test -f "$package_dir/package/dist/cli/main.js" + test -f "$package_dir/package/dist/mcp/main.js" + ln -s "$PWD/node_modules" "$package_dir/package/node_modules" + + node "$package_dir/package/dist/cli/main.js" --help | grep -F "Usage:" + autopilot_status=0 + autopilot_help="$(node "$package_dir/package/dist/cli/main.js" autopilot:run --help)" || autopilot_status=$? + test "$autopilot_status" -eq 2 + printf '%s\n' "$autopilot_help" | grep -F "bioflow autopilot:run" + + node --input-type=module \ + -e 'const modulePath = process.argv[1]; process.argv[1] = ""; await import(modulePath);' \ + "$package_dir/package/dist/mcp/main.js" + ''; + }; + demo-killer = mkCheck { + name = "demo-killer"; + command = "npm run demo:killer:ci"; + }; + cli-autopilot-flow = mkCheck { + name = "cli-autopilot-flow"; + command = '' + export BIOFLOW_AUTOPILOT_FLOW_OUT=.bioflow_smoke_service/cli-autopilot-run-flow-summary.json + npm run test -- test/cli-autopilot-run-flow.test.ts + test -s "$BIOFLOW_AUTOPILOT_FLOW_OUT" + ''; + }; + }); + + apps = forAllSystems (system: + let + pkgs = pkgsFor system; + app = pkgs.writeShellApplication { + name = "bioflow-conventional-commits"; + runtimeInputs = [ + pkgs.git + pkgs.python312 + ]; + text = '' + mkdir -p out + rev_range="''${1:-HEAD~1..HEAD}" + exec bash scripts/conventional-commit-check.sh \ + --rev-range "$rev_range" \ + --output out/conventional-commits-summary.json \ + --allow-merge-commits \ + --allow-git-revert + ''; + }; + in + { + conventional-commits = { + type = "app"; + program = "${app}/bin/bioflow-conventional-commits"; + }; + }); + + devShells = forAllSystems (system: + let + pkgs = pkgsFor system; + in + { + default = pkgs.mkShell { + packages = [ + pkgs.nodejs_22 + pkgs.pnpm + pkgs.python312 + ]; + }; + }); + }; +}