diff --git a/.github/workflows/mcp-release-please.yml b/.github/workflows/mcp-release-please.yml new file mode 100644 index 0000000000..2b3b0c17c3 --- /dev/null +++ b/.github/workflows/mcp-release-please.yml @@ -0,0 +1,71 @@ +name: MCP/Engine Release Please + +# Conventional-commit-driven release automation for the two published npm packages in this monorepo +# (packages/gittensory-mcp, packages/gittensory-engine). release-please maintains a Release PR per +# component that bumps its SemVer + regenerates its CHANGELOG.md from conventional commits touching +# that package's own subtree. Merging a Release PR tags the component (mcp-v / +# engine-v, matching this repo's existing mcp-v* tag convention) and creates the GitHub +# Release. release-please then explicitly dispatches the matching publish workflow +# (npm-publish.yml / publish-engine.yml) via workflow_dispatch -- a tag-push trigger can't be used +# here: GITHUB_TOKEN-created tags don't fire push-triggered workflows (proven the hard way cutting +# v0.7.0 manually -- see #4132/#4135/#4154's tag-move dance). +# +# separate-pull-requests: true (release-please-config.json) -- unlike metagraphed's combined-PR +# default, these two packages have genuinely independent release cadences and separate publish +# targets/workflows, and bundling them would work against this repo's small-focused-PR convention. +# +# Runs on a schedule rather than every push to main: release-please is idempotent (it recomputes +# purely from conventional-commit history since each component's last release tag), so batching has +# zero functional impact -- it only reduces how often the Release PR(s) refresh against this repo's +# push volume. workflow_dispatch stays available for an on-demand run. +on: + schedule: + - cron: "0 16 */2 * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mcp-release-please + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + actions: write # dispatch npm-publish.yml / publish-engine.yml after a release + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Run release-please + id: release + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + token: ${{ secrets.GITHUB_TOKEN }} + + # release-please creates the component tag + GitHub Release with GITHUB_TOKEN, which (by + # GitHub's recursion-prevention rule) does NOT fire push/tag-based workflows. workflow_dispatch + # IS exempt from that rule, so dispatch the OIDC publish workflow explicitly, telling it + # release-please already created the tag/release (released_by_release_please=true skips both + # workflows' own tag-creation and GitHub-release steps). Without this, merging a Release PR + # would silently never publish. + - name: Dispatch MCP publish + if: ${{ steps.release.outputs['packages/gittensory-mcp--release_created'] == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run npm-publish.yml --ref main -f released_by_release_please=true + + - name: Dispatch Engine publish + if: ${{ steps.release.outputs['packages/gittensory-engine--release_created'] == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run publish-engine.yml --ref main -f released_by_release_please=true diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000000..d6d7671cec --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + "packages/gittensory-mcp": "0.7.0", + "packages/gittensory-engine": "0.1.0" +} diff --git a/package.json b/package.json index a7bdf30c93..c6c4bf71fe 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "test:smoke:browser": "node scripts/smoke-ui-browser.mjs", "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test --workspace @jsonbored/gittensory-engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run manifest:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", - "test:release:mcp": "npm run test:ci && npm run changelog:check:mcp", + "test:release:mcp": "npm run test:ci", "test:watch": "vitest", "validate": "npm run typecheck && npm run test:coverage" }, diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000000..857a1ef497 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + "packages/gittensory-mcp": { + "release-type": "node", + "component": "mcp", + "package-name": "@jsonbored/gittensory-mcp", + "extra-files": [ + { + "type": "json", + "path": "package-lock.json", + "jsonpath": "$.packages['packages/gittensory-mcp'].version" + } + ] + }, + "packages/gittensory-engine": { + "release-type": "node", + "component": "engine", + "package-name": "@jsonbored/gittensory-engine", + "extra-files": [ + { + "type": "json", + "path": "package-lock.json", + "jsonpath": "$.packages['packages/gittensory-engine'].version" + } + ] + } + }, + "include-component-in-tag": true, + "separate-pull-requests": true, + "pull-request-title-pattern": "chore(release): cut ${component} v${version}", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "deps", "section": "Dependencies" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Docs", "hidden": true }, + { "type": "refactor", "section": "Refactors", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true } + ] +} diff --git a/scripts/check-mcp-release-candidate.mjs b/scripts/check-mcp-release-candidate.mjs index 36cb5b83bc..e71ec46040 100644 --- a/scripts/check-mcp-release-candidate.mjs +++ b/scripts/check-mcp-release-candidate.mjs @@ -5,7 +5,6 @@ import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { buildReleaseCandidateReport, - checkChangelog, checkTag, checkTarball, checkTokenlessPublish, @@ -100,12 +99,11 @@ function main() { const tag = arg("tag") ?? (version ? expectedReleaseTag(version) : "mcp-v"); const tagCheck = { ...checkTag({ tag, packageVersion: version }), tag }; - const changelogCheck = checkChangelog({ changelog: readMaybe(join(PACKAGE_DIR, "CHANGELOG.md")), version: version ?? "" }); const { check: tarball } = tarballFileCheck(); const tokenless = checkTokenlessPublish(readMaybe(PUBLISH_WORKFLOW)); const cliSmoke = packedCliSmoke(); - const report = buildReleaseCandidateReport({ tag: tagCheck, changelog: changelogCheck, tarball, cliSmoke, tokenless }); + const report = buildReleaseCandidateReport({ tag: tagCheck, tarball, cliSmoke, tokenless }); if (wantsJson) { process.stdout.write(`${redactSensitive(JSON.stringify(report, null, 2))}\n`);