From f677fe1a7478a0e6ad674ba0beb7e79b128a3310 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:10:20 -0700 Subject: [PATCH] feat(ci): trusted-publish workflows for miner + ui-kit, rename mcp workflow Adds packages/gittensory-miner and packages/gittensory-ui-kit to the release-please-managed, OIDC trusted-publishing pipeline that packages/gittensory-mcp and packages/gittensory-engine already had -- these two packages have never had CI publish automation before now. New workflows (publish-miner.yml, publish-ui-kit.yml) mirror publish-engine.yml's exact validate/publish/github-release shape: unprivileged validate job packs + smoke-tests the tarball with no id-token, privileged publish job (behind the environment: release approval gate) downloads that exact tarball and publishes via OIDC with provenance. Per-package adaptations: - miner ships hand-written checked-in JS (node --check syntax validation, no tsc build) with two bin entries; validate reuses the existing test:miner-pack allowlist check (scripts/check-miner- package.mjs), then smoke-tests both installed bins' --help output. - ui-kit builds via tsc like engine; validate typechecks, then smoke-tests by importing the built dist/utils.js and calling its pure cn()/relativeTimeFromNow() exports (no React render needed). release-please-config.json / .release-please-manifest.json gain entries for both new components (miner-v*/ui-kit-v* tags, matching the mcp-v*/engine-v* convention already in place), and mcp-release-please.yml dispatches both new workflows on release, syncs package-lock.json for all four components, and is retitled from "MCP/Engine Release Please" to "Package Release Please" to reflect the broader scope. Also renames npm-publish.yml -> publish-mcp.yml for naming consistency with the other three (publish-engine.yml, publish-miner.yml, publish-ui-kit.yml), updating every internal reference: the release-please dispatch, the three sibling workflows' own header comments, and the MCP release-candidate tooling (scripts/check-mcp-release-candidate.mjs, mcp-release-core.mjs, mcp-release-candidate-core.mjs, their test) that hardcoded the old path. Also fixes an unrelated, pre-existing drift caught by this PR's own gate: apps/gittensory-ui/src/lib/mcp-package.ts's MCP_PACKAGE_KNOWN_LATEST_VERSION constant was stale at "0.7.0" against the actually-published "0.7.1" -- bumped to match, per the file's own documented invariant (never ahead of npm, always equal after a real publish). All four packages (@loopover/engine, @loopover/mcp, @loopover/miner, @loopover/ui-kit) have already been bootstrap-published to npm from an authenticated maintainer session and have their Trusted Publisher configured in npmjs.com's package settings, pointing at these exact workflow filenames -- this PR is what makes those configurations actually resolve to real files on main. ## Test plan - [x] Full local gate (`npm run test:ci` + `npm audit --audit-level=moderate`) green. - [x] Verified both new workflows' validate-job logic end-to-end locally before writing the YAML: typecheck/build, pack, file-list check, install into a scratch dir, and run the actual smoke-test assertions -- both passed, including miner's tarball correctly resolving @loopover/engine as a real dependency from the live npm registry. - [x] actionlint clean across all workflow changes. --- .github/workflows/mcp-release-please.yml | 44 ++-- .github/workflows/orb-beta-release.yml | 2 +- .github/workflows/publish-engine.yml | 17 +- .../{npm-publish.yml => publish-mcp.yml} | 0 .github/workflows/publish-miner.yml | 236 +++++++++++++++++ .github/workflows/publish-ui-kit.yml | 237 ++++++++++++++++++ .release-please-manifest.json | 4 +- apps/gittensory-ui/src/lib/mcp-package.ts | 2 +- release-please-config.json | 10 + scripts/check-mcp-release-candidate.mjs | 2 +- scripts/mcp-release-candidate-core.mjs | 2 +- scripts/mcp-release-core.mjs | 4 +- test/unit/mcp-release-candidate.test.ts | 2 +- 13 files changed, 530 insertions(+), 32 deletions(-) rename .github/workflows/{npm-publish.yml => publish-mcp.yml} (100%) create mode 100644 .github/workflows/publish-miner.yml create mode 100644 .github/workflows/publish-ui-kit.yml diff --git a/.github/workflows/mcp-release-please.yml b/.github/workflows/mcp-release-please.yml index 3ee6fcb423..3d962faa0f 100644 --- a/.github/workflows/mcp-release-please.yml +++ b/.github/workflows/mcp-release-please.yml @@ -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 / -# 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). +# 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 / engine-v / +# miner-v / ui-kit-v, 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 @@ -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 @@ -73,7 +75,7 @@ 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." @@ -81,7 +83,7 @@ jobs: 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 @@ -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 diff --git a/.github/workflows/orb-beta-release.yml b/.github/workflows/orb-beta-release.yml index 977fc28457..6537b463c0 100644 --- a/.github/workflows/orb-beta-release.yml +++ b/.github/workflows/orb-beta-release.yml @@ -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 diff --git a/.github/workflows/publish-engine.yml b/.github/workflows/publish-engine.yml index af9467c208..8b8d6d9598 100644 --- a/.github/workflows/publish-engine.yml +++ b/.github/workflows/publish-engine.yml @@ -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: @@ -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 @@ -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 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/publish-mcp.yml similarity index 100% rename from .github/workflows/npm-publish.yml rename to .github/workflows/publish-mcp.yml diff --git a/.github/workflows/publish-miner.yml b/.github/workflows/publish-miner.yml new file mode 100644 index 0000000000..678f785033 --- /dev/null +++ b/.github/workflows/publish-miner.yml @@ -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" </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 diff --git a/.github/workflows/publish-ui-kit.yml b/.github/workflows/publish-ui-kit.yml new file mode 100644 index 0000000000..aeb47828b5 --- /dev/null +++ b/.github/workflows/publish-ui-kit.yml @@ -0,0 +1,237 @@ +name: Publish UI Kit 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-ui-kit/package.json's version. +# +# @loopover/ui-kit 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-ui-kit-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + # Unprivileged: resolves the version, typechecks, 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::UI Kit 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-ui-kit/package.json').version")" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid package version: $VERSION" + exit 1 + fi + TAG="ui-kit-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 + + - name: UI Kit typecheck + run: npm run typecheck --workspace @loopover/ui-kit + + # 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 + npm run build --workspace @loopover/ui-kit + PACK_JSON="$(npm pack --workspace @loopover/ui-kit --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" + UNEXPECTED_FILES="$(tar -tzf "$TARBALL_PATH" | grep -Ev '^(package/dist/.+|package/src/theme\.css|package/(package.json|README.md|CHANGELOG.md|LICENSE))$' || true)" + if [ -n "$UNEXPECTED_FILES" ]; then + printf '%s\n' "$UNEXPECTED_FILES" + echo "Unexpected file in package tarball" + exit 1 + fi + 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" --no-save >/dev/null + node --input-type=module -e " + import { cn, relativeTimeFromNow } from '$TMP/node_modules/@loopover/ui-kit/dist/utils.js'; + if (cn('a', 'b') !== 'a b') throw new Error('cn smoke test failed'); + if (relativeTimeFromNow(Date.now(), Date.now()) !== 'just now') throw new Error('relativeTimeFromNow smoke test failed'); + " + + - name: Upload package tarball + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: gittensory-ui-kit-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::UI Kit 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/ui-kit 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-ui-kit-tarball + path: ${{ runner.temp }}/gittensory-ui-kit-package + + - name: Publish to npm (OIDC trusted publishing) + env: + NPM_CONFIG_PROVENANCE: "true" + run: | + set -euo pipefail + count=$(find "$RUNNER_TEMP/gittensory-ui-kit-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-ui-kit-package" -maxdepth 1 -type f -name "*.tgz" -print >&2 + exit 1 + fi + tarball=$(find "$RUNNER_TEMP/gittensory-ui-kit-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" </dev/null 2>&1; then + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/ui-kit v${RELEASE_VERSION}" --notes-file "$NOTES_FILE" + else + gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --title "@loopover/ui-kit v${RELEASE_VERSION}" --notes-file "$NOTES_FILE" --verify-tag + fi diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1ca777daf6..946feafe8e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,6 @@ { "packages/gittensory-mcp": "0.7.1", - "packages/gittensory-engine": "1.0.0" + "packages/gittensory-engine": "1.0.0", + "packages/gittensory-miner": "0.1.0", + "packages/gittensory-ui-kit": "0.1.0" } diff --git a/apps/gittensory-ui/src/lib/mcp-package.ts b/apps/gittensory-ui/src/lib/mcp-package.ts index 4478e1946d..678983f0fc 100644 --- a/apps/gittensory-ui/src/lib/mcp-package.ts +++ b/apps/gittensory-ui/src/lib/mcp-package.ts @@ -8,7 +8,7 @@ export const MCP_PACKAGE_REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAG export const MCP_PACKAGE_NPM_URL = `https://www.npmjs.com/package/${MCP_PACKAGE_NAME}`; // Tracks the latest PUBLISHED release: ui:version-audit requires this to equal npm dist-tags.latest, so it is // bumped to a new version only AFTER that version publishes (never ahead of npm). -export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.7.0"; +export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.7.1"; export const MCP_MINIMUM_SUPPORTED_VERSION = "0.5.0"; export type NpmPackageMetadata = { diff --git a/release-please-config.json b/release-please-config.json index 8ee2e4c90e..147bd48c43 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -10,6 +10,16 @@ "release-type": "node", "component": "engine", "package-name": "@loopover/engine" + }, + "packages/gittensory-miner": { + "release-type": "node", + "component": "miner", + "package-name": "@loopover/miner" + }, + "packages/gittensory-ui-kit": { + "release-type": "node", + "component": "ui-kit", + "package-name": "@loopover/ui-kit" } }, "include-component-in-tag": true, diff --git a/scripts/check-mcp-release-candidate.mjs b/scripts/check-mcp-release-candidate.mjs index 8b504c13cb..11bb11046b 100644 --- a/scripts/check-mcp-release-candidate.mjs +++ b/scripts/check-mcp-release-candidate.mjs @@ -14,7 +14,7 @@ import { const PACKAGE_DIR = "packages/gittensory-mcp"; const WORKSPACE = "@loopover/mcp"; -const PUBLISH_WORKFLOW = ".github/workflows/npm-publish.yml"; +const PUBLISH_WORKFLOW = ".github/workflows/publish-mcp.yml"; const onWindows = process.platform === "win32"; function arg(name) { diff --git a/scripts/mcp-release-candidate-core.mjs b/scripts/mcp-release-candidate-core.mjs index 85e6672988..7de99cf004 100644 --- a/scripts/mcp-release-candidate-core.mjs +++ b/scripts/mcp-release-candidate-core.mjs @@ -171,7 +171,7 @@ const REMEDIATION = { changelog_section_missing: "Run npm run changelog:mcp and commit the generated mcp-v changelog section.", tarball_unsafe: "Remove unexpected or secret-bearing files from the package and rerun the dry-run.", cli_smoke_failed: "Fix the packed CLI so `gittensory-mcp --help` exits cleanly before tagging.", - publish_token_risk: "Restore tokenless trusted publishing (id-token: write + --provenance, no npm token) in npm-publish.yml.", + publish_token_risk: "Restore tokenless trusted publishing (id-token: write + --provenance, no npm token) in publish-mcp.yml.", }; /** Aggregate individual check results into a pass/fail report with next steps. */ diff --git a/scripts/mcp-release-core.mjs b/scripts/mcp-release-core.mjs index cbd5288325..d847736a52 100644 --- a/scripts/mcp-release-core.mjs +++ b/scripts/mcp-release-core.mjs @@ -2,7 +2,7 @@ export const MCP_RELEASE_DUE_MARKER = ""; const DIRECT_MCP_PATHS = [ "packages/gittensory-mcp/", - ".github/workflows/npm-publish.yml", + ".github/workflows/publish-mcp.yml", "src/mcp/", "src/services/mcp-compatibility.ts", "src/signals/local-branch.ts", @@ -108,7 +108,7 @@ export function isMcpReleaseRelevantCommit(commit) { if (!parsed.type && !parsed.conventional) return false; const hasDirectMcpPath = hasAnyPath(files, DIRECT_MCP_PATHS); - const hasPackageReleasePath = hasAnyPath(files, ["packages/gittensory-mcp/", ".github/workflows/npm-publish.yml"]); + const hasPackageReleasePath = hasAnyPath(files, ["packages/gittensory-mcp/", ".github/workflows/publish-mcp.yml"]); const hasClientVisiblePath = hasAnyPath(files, CLIENT_VISIBLE_PATHS); const hasOnlySupportingVisiblePath = hasAnyPath(files, SUPPORTING_VISIBLE_PATHS) && !hasDirectMcpPath && !hasClientVisiblePath; diff --git a/test/unit/mcp-release-candidate.test.ts b/test/unit/mcp-release-candidate.test.ts index 466d19c12c..f4b0edf030 100644 --- a/test/unit/mcp-release-candidate.test.ts +++ b/test/unit/mcp-release-candidate.test.ts @@ -19,7 +19,7 @@ const FORBIDDEN_PUBLIC_LANGUAGE = /\b(wallet|hotkey|coldkey|raw trust|trust scor const ALLOWED_FILES = ["bin/gittensory-mcp.js", "lib/local-branch.js", "scripts/gittensor-score-preview.mjs", "package.json", "README.md", "CHANGELOG.md", "LICENSE"]; const CHANGELOG = "# Changelog\n\n## mcp-v0.4.0 - 2026-06-02\n\n### Features\n- Add a thing\n"; -// A tokenless trusted-publishing workflow fixture (same shape as npm-publish.yml). +// A tokenless trusted-publishing workflow fixture (same shape as publish-mcp.yml). const TOKENLESS_WORKFLOW = [ "permissions:", " contents: read",