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
44 changes: 29 additions & 15 deletions .github/workflows/mcp-release-please.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
name: MCP/Engine Release Please
name: Package 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<semver> /
# engine-v<semver>, 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).
# Conventional-commit-driven release automation for the four published npm packages in this monorepo
# (packages/gittensory-mcp, packages/gittensory-engine, packages/gittensory-miner,
# packages/gittensory-ui-kit). 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<semver> / engine-v<semver> /
# miner-v<semver> / ui-kit-v<semver>, matching this repo's existing mcp-v* tag convention) and
# creates the GitHub Release. release-please then explicitly dispatches the matching publish
# workflow (publish-mcp.yml / publish-engine.yml / publish-miner.yml / publish-ui-kit.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
# default, these four 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
Expand Down Expand Up @@ -41,7 +43,7 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: write # dispatch npm-publish.yml / publish-engine.yml after a release
actions: write # dispatch publish-mcp.yml / publish-engine.yml / publish-miner.yml / publish-ui-kit.yml after a release
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
Expand Down Expand Up @@ -73,15 +75,15 @@ jobs:
cp scripts/sync-release-lockfile-versions.mjs "$trusted_sync_script"
git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git"
gh auth setup-git
for component in mcp engine; do
for component in mcp engine miner ui-kit; do
branch="release-please--branches--main--components--${component}"
if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "No release branch for $component, skipping."
continue
fi
git fetch origin "$branch"
git checkout -B "sync-check-${component}" "origin/$branch"
node "$trusted_sync_script" packages/gittensory-mcp packages/gittensory-engine
node "$trusted_sync_script" packages/gittensory-mcp packages/gittensory-engine packages/gittensory-miner packages/gittensory-ui-kit
if git diff --quiet package-lock.json; then
echo "package-lock.json already in sync on $branch."
else
Expand All @@ -108,10 +110,22 @@ jobs:
if: ${{ steps.release.outputs['packages/gittensory-mcp--release_created'] == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run npm-publish.yml --ref "${{ steps.release.outputs['packages/gittensory-mcp--tag_name'] }}" -f released_by_release_please=true
run: gh workflow run publish-mcp.yml --ref "${{ steps.release.outputs['packages/gittensory-mcp--tag_name'] }}" -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 "${{ steps.release.outputs['packages/gittensory-engine--tag_name'] }}" -f released_by_release_please=true

- name: Dispatch Miner publish
if: ${{ steps.release.outputs['packages/gittensory-miner--release_created'] == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run publish-miner.yml --ref "${{ steps.release.outputs['packages/gittensory-miner--tag_name'] }}" -f released_by_release_please=true

- name: Dispatch UI Kit publish
if: ${{ steps.release.outputs['packages/gittensory-ui-kit--release_created'] == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run publish-ui-kit.yml --ref "${{ steps.release.outputs['packages/gittensory-ui-kit--tag_name'] }}" -f released_by_release_please=true
2 changes: 1 addition & 1 deletion .github/workflows/orb-beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
# Pushed with the default GITHUB_TOKEN, which does NOT fire release-selfhost.yml's own
# `push: tags:` trigger (GitHub suppresses workflow-triggered-workflow pushes to prevent
# recursion) -- that's why the next step dispatches it explicitly instead of relying on this
# push alone. Mirrors publish-engine.yml / npm-publish.yml's identical reasoning and tagging
# push alone. Mirrors publish-engine.yml / publish-mcp.yml's identical reasoning and tagging
# idiom.
# Exposes created=true/false so the dispatch step below never fires against a tag this run didn't
# actually just create -- a defense-in-depth backstop (independent of orb-release-core.mjs's own
Expand Down
17 changes: 8 additions & 9 deletions .github/workflows/publish-engine.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
name: Publish Engine Package

# workflow_dispatch-only, mirroring npm-publish.yml's design exactly (see that file for the fuller
# workflow_dispatch-only, mirroring publish-mcp.yml's design exactly (see that file for the fuller
# rationale): a GITHUB_TOKEN-created tag doesn't fire push-triggered workflows, so the release
# automation must explicitly dispatch this after tagging. A bare manual dispatch (
# released_by_release_please left false) is the human override path and self-tags HEAD from
# packages/gittensory-engine/package.json's version.
#
# NOTE: this workflow cannot run successfully until @loopover/engine has been bootstrap
# -published to npm once from a maintainer's own authenticated `npm login` session (npm's trusted
# publishing/OIDC cannot create a brand-new package -- the package must already exist before a
# Trusted Publisher can be configured in npmjs.com's package settings). Until that one-time manual
# step happens, `npm publish` below will fail with a 404/permission error. See the gittensory-mcp
# release skill / project memory for the exact bootstrap steps.
# @loopover/engine has already been bootstrap-published to npm once (from a maintainer's own
# authenticated `npm login` session -- npm's trusted publishing/OIDC cannot create a brand-new
# package). This workflow still won't succeed until a Trusted Publisher is configured for it in
# npmjs.com's package settings (GitHub Actions provider, org/repo/workflow-filename matching this
# file) -- that's a one-time npmjs.com dashboard step, separate from the bootstrap publish.
on:
workflow_dispatch:
inputs:
Expand All @@ -29,7 +28,7 @@ concurrency:

jobs:
# Unprivileged: resolves the version, runs the package's own test suite, and packs the tarball --
# all with contents: read only. Same privilege-separation reasoning as npm-publish.yml's validate
# all with contents: read only. Same privilege-separation reasoning as publish-mcp.yml's validate
# job (Superagent P2 / mirrors metagraphed's publish-client.yml).
validate:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -130,7 +129,7 @@ jobs:
retention-days: 7

# Privileged: tags + publishes the EXACT tarball the unprivileged job already tested. environment:
# release requires reviewer approval per repo Settings > Environments, same gate npm-publish.yml
# release requires reviewer approval per repo Settings > Environments, same gate publish-mcp.yml
# and release-selfhost.yml already use.
publish:
runs-on: ubuntu-latest
Expand Down
File renamed without changes.
236 changes: 236 additions & 0 deletions .github/workflows/publish-miner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
name: Publish Miner Package

# workflow_dispatch-only, mirroring publish-engine.yml/publish-mcp.yml's design exactly (see those
# files for the fuller rationale): a GITHUB_TOKEN-created tag doesn't fire push-triggered workflows,
# so the release automation must explicitly dispatch this after tagging. A bare manual dispatch (
# released_by_release_please left false) is the human override path and self-tags HEAD from
# packages/gittensory-miner/package.json's version.
#
# @loopover/miner has already been bootstrap-published to npm once (from a maintainer's own
# authenticated `npm login` session -- npm's trusted publishing/OIDC cannot create a brand-new
# package). This workflow still won't succeed until a Trusted Publisher is configured for it in
# npmjs.com's package settings (GitHub Actions provider, org/repo/workflow-filename matching this
# file) -- that's a one-time npmjs.com dashboard step, separate from the bootstrap publish.
on:
workflow_dispatch:
inputs:
released_by_release_please:
description: "Internal: set by the release automation's dispatch so this run skips re-creating the GitHub release it already made."
type: boolean
default: false

permissions:
contents: read

concurrency:
group: publish-miner-${{ github.ref_name }}
cancel-in-progress: false

jobs:
# Unprivileged: resolves the version, validates the package's own syntax + packed contents, and
# packs the tarball -- all with contents: read only. Same privilege-separation reasoning as
# publish-engine.yml/publish-mcp.yml's validate jobs (Superagent P2 / mirrors metagraphed's
# publish-client.yml).
validate:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Verify release commit is on main
env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$RELEASE_SHA" refs/remotes/origin/main; then
echo "::error::Miner package releases must be cut from a commit reachable from main."
exit 1
fi

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0

- name: Resolve release version
id: version
run: |
set -euo pipefail
VERSION="$(node -p "require('./packages/gittensory-miner/package.json').version")"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid package version: $VERSION"
exit 1
fi
TAG="miner-v${VERSION}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
HEAD_SHA="$(git rev-parse HEAD)"
TAG_SHA="$(git rev-list -n 1 "$TAG")"
if [ "$TAG_SHA" != "$HEAD_SHA" ]; then
echo "::error::Tag $TAG already exists but points at $TAG_SHA, not the dispatched commit $HEAD_SHA"
exit 1
fi
echo "Tag $TAG already exists and matches HEAD."
else
echo "Tag $TAG does not exist yet; the publish job will create it."
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Install dependencies
run: npm ci

# Miner ships hand-written, checked-in JS (no tsc build) -- "build" here is node --check syntax
# validation over every bin/lib file, the same script test:ci already runs on every PR.
- name: Miner syntax validation
run: npm run build --workspace @loopover/miner

# Reuses the exact allowlist/required-files/forbidden-content check test:ci already runs on
# every PR (scripts/check-miner-package.mjs) -- a dry-run pack, so it doesn't produce the real
# tarball this job packs+uploads below.
- name: Validate packed file list
run: npm run test:miner-pack

# Build + pack happen in THIS unprivileged job (no id-token). The privileged publish job below
# never runs npm install/build, so a compromised build dependency can't reach the OIDC token.
- name: Pack and smoke-test the tarball
run: |
set -euo pipefail
PACK_JSON="$(npm pack --workspace @loopover/miner --pack-destination "$RUNNER_TEMP" --json)"
TARBALL="$(node -e 'const fs=require("fs"); const input=fs.readFileSync(0,"utf8"); process.stdout.write(JSON.parse(input)[0].filename)' <<< "$PACK_JSON")"
TARBALL_PATH="$RUNNER_TEMP/$TARBALL"
if tar -xOf "$TARBALL_PATH" | grep -qE '(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_|gh[pousr]_|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=)'; then
echo "Secret-like content found in package tarball"
exit 1
fi
TMP="$(mktemp -d)"
npm --prefix "$TMP" init -y >/dev/null
npm --prefix "$TMP" install "$TARBALL_PATH" >/dev/null
"$TMP/node_modules/.bin/gittensory-miner" --help | grep -q "Foundation CLI for the local Gittensory miner runtime"
"$TMP/node_modules/.bin/gittensory-miner-mcp" --help >/dev/null

- name: Upload package tarball
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gittensory-miner-tarball
path: ${{ runner.temp }}/*.tgz
if-no-files-found: error
retention-days: 7

# Privileged: tags + publishes the EXACT tarball the unprivileged job already tested. environment:
# release requires reviewer approval per repo Settings > Environments, same gate every other
# publish workflow in this repo already uses.
publish:
runs-on: ubuntu-latest
needs: validate
environment: release
timeout-minutes: 15
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Verify release commit is on main
env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$RELEASE_SHA" refs/remotes/origin/main; then
echo "::error::Miner package releases must be cut from a commit reachable from main."
exit 1
fi

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
registry-url: https://registry.npmjs.org

- name: Create or verify release tag
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.validate.outputs.tag }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
set -euo pipefail
HEAD_SHA="$(git rev-parse HEAD)"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists (verified against HEAD by the validate job)."
else
echo "Creating tag $TAG at HEAD ($HEAD_SHA)."
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "@loopover/miner v${VERSION}"
git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git"
gh auth setup-git
git push origin "$TAG"
fi

- name: Download package tarball
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: gittensory-miner-tarball
path: ${{ runner.temp }}/gittensory-miner-package

- name: Publish to npm (OIDC trusted publishing)
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
set -euo pipefail
count=$(find "$RUNNER_TEMP/gittensory-miner-package" -maxdepth 1 -type f -name "*.tgz" | wc -l | tr -d ' ')
if [ "$count" != "1" ]; then
echo "Expected exactly one tarball, found $count" >&2
find "$RUNNER_TEMP/gittensory-miner-package" -maxdepth 1 -type f -name "*.tgz" -print >&2
exit 1
fi
tarball=$(find "$RUNNER_TEMP/gittensory-miner-package" -maxdepth 1 -type f -name "*.tgz" -print -quit)
npx -y npm@11.15.0 publish "$tarball" --access public --provenance

github-release:
runs-on: ubuntu-latest
needs: [validate, publish]
# Skip when the release automation dispatched this run: it already created the GitHub release
# with its own generated changelog notes before dispatching, so running this unconditionally
# would overwrite those richer notes with the generic blurb below.
if: ${{ inputs.released_by_release_please != true }}
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
set -euo pipefail
NOTES_FILE="$(mktemp)"
cat > "$NOTES_FILE" <<EOF
Published [@loopover/miner v${RELEASE_VERSION}](https://www.npmjs.com/package/@loopover/miner/v/${RELEASE_VERSION}) to npm with provenance.

Install:

\`\`\`sh
npm install -g @loopover/miner@${RELEASE_VERSION}
\`\`\`
EOF
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/miner v${RELEASE_VERSION}" --notes-file "$NOTES_FILE"
else
gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/miner v${RELEASE_VERSION}" --notes-file "$NOTES_FILE" --verify-tag
fi
Loading
Loading