diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cecc14e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + opencode-plugin-sdks: + patterns: ["@opencode-ai/plugin", "@opencode/plugin"] + dev-dependencies: + dependency-type: development + ignore: + # TypeScript 7 ships no tsserver.js, which editors and typescript-language-server still need. + - dependency-name: typescript + update-types: ["version-update:semver-major"] + # @types/node tracks the oldest Node version CI tests (22). + - dependency-name: "@types/node" + update-types: ["version-update:semver-major"] + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + actions: + patterns: ["*"] diff --git a/.github/scripts/changelog-section.mjs b/.github/scripts/changelog-section.mjs new file mode 100644 index 0000000..e1006f7 --- /dev/null +++ b/.github/scripts/changelog-section.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// Prints the CHANGELOG.md section for a version (used as GitHub Release notes). +// Fails when the section is missing or still marked "Unreleased", so a release cannot ship undated notes. +import { readFileSync } from "node:fs"; + +const version = process.argv[2]; +if (!version) { + console.error("Usage: changelog-section.mjs "); + process.exit(2); +} + +const lines = readFileSync(new URL("../../CHANGELOG.md", import.meta.url), "utf8").split("\n"); +const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const start = lines.findIndex((line) => new RegExp(`^## ${escaped}(\\s|$)`).test(line)); + +if (start === -1) { + console.error(`CHANGELOG.md has no "## ${version}" section.`); + process.exit(1); +} +if (/unreleased/i.test(lines[start] ?? "")) { + console.error(`CHANGELOG.md section "${lines[start]}" is still marked Unreleased; replace it with the release date.`); + process.exit(1); +} + +const end = lines.findIndex((line, index) => index > start && line.startsWith("## ")); +const body = lines.slice(start + 1, end === -1 ? undefined : end).join("\n").trim(); +process.stdout.write(`${body}\n`); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ab33d7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_call: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test (Node ${{ matrix.node }}) + # GitHub only: the Gitea origin also runs .github/workflows. + if: github.server_url == 'https://github.com' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [22, 24, 26] + env: + HUSKY: "0" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm test + - name: Check package contents + run: npm pack --dry-run + + e2e: + name: E2E (OpenCode ${{ matrix.opencode1 }} + ${{ matrix.opencode2 }}) + # GitHub only: the Gitea origin also runs .github/workflows. + if: github.server_url == 'https://github.com' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - opencode1: 1.18.31 + opencode2: 2.0.32 + - opencode1: 1.19.3 + opencode2: 2.1.0 + env: + HUSKY: "0" + E2E_REQUIRE_ALL: "1" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - run: npm ci + - name: Install OpenCode 1 and OpenCode 2 side by side + run: | + npm install -g --prefix "$RUNNER_TEMP/opencode1" "opencode-ai@${{ matrix.opencode1 }}" + npm install -g --prefix "$RUNNER_TEMP/opencode2" "@opencode/cli@${{ matrix.opencode2 }}" + echo "OPENCODE_V1_BIN=$RUNNER_TEMP/opencode1/bin/opencode" >> "$GITHUB_ENV" + echo "OPENCODE_V2_BIN=$RUNNER_TEMP/opencode2/bin/opencode2" >> "$GITHUB_ENV" + - run: npm run test:e2e diff --git a/.github/workflows/e2e-latest.yml b/.github/workflows/e2e-latest.yml new file mode 100644 index 0000000..ed1ce20 --- /dev/null +++ b/.github/workflows/e2e-latest.yml @@ -0,0 +1,33 @@ +name: E2E (latest OpenCode) + +on: + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e: + name: E2E (latest OpenCode 1 + 2) + # GitHub only: the Gitea origin also runs .github/workflows. + if: github.server_url == 'https://github.com' + runs-on: ubuntu-latest + env: + HUSKY: "0" + E2E_REQUIRE_ALL: "1" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - run: npm ci + - name: Install the latest OpenCode 1 and OpenCode 2 side by side + run: | + npm install -g --prefix "$RUNNER_TEMP/opencode1" "opencode-ai@latest" + npm install -g --prefix "$RUNNER_TEMP/opencode2" "@opencode/cli@latest" + echo "OPENCODE_V1_BIN=$RUNNER_TEMP/opencode1/bin/opencode" >> "$GITHUB_ENV" + echo "OPENCODE_V2_BIN=$RUNNER_TEMP/opencode2/bin/opencode2" >> "$GITHUB_ENV" + - run: npm run test:e2e diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a0d18dd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,79 @@ +name: Release + +# Push a vX.Y.Z tag after bumping package.json and dating the CHANGELOG section. +# The Verify job reruns CI, including the OpenCode 1 + 2 end-to-end check, before anything is published. +# Publishing uses npm trusted publishing (OIDC): no npm token is stored in this repository. + +on: + push: + tags: ["v*.*.*"] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + verify: + name: Verify + # GitHub only: npm trusted publishing needs GitHub's OIDC token; later jobs skip with this one. + if: github.server_url == 'https://github.com' + uses: ./.github/workflows/ci.yml + + publish: + name: Publish to npm + needs: verify + runs-on: ubuntu-latest + environment: npm + permissions: + contents: read + id-token: write + env: + HUSKY: "0" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Use npm 12 (trusted publishing needs 11.5.1 or later) + run: npm install -g npm@12 + - name: Check tag, package.json, and CHANGELOG agree + run: | + version="${GITHUB_REF_NAME#v}" + package_version="$(node -p "require('./package.json').version")" + if [ "$version" != "$package_version" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $package_version" + exit 1 + fi + node .github/scripts/changelog-section.mjs "$version" > /dev/null + - run: npm ci + - name: Publish + run: | + version="${GITHUB_REF_NAME#v}" + if [[ "$version" == *-* ]]; then + npm publish --access public --tag next + else + npm publish --access public + fi + + github-release: + name: GitHub Release + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + - name: Create release from CHANGELOG + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${GITHUB_REF_NAME#v}" + node .github/scripts/changelog-section.mjs "$version" > release-notes.md + prerelease="" + if [[ "$version" == *-* ]]; then prerelease="--prerelease"; fi + gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" --notes-file release-notes.md $prerelease