diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b4a8c88c..cd98fd2d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -481,124 +481,6 @@ jobs: path: knip-report.json retention-days: 30 - cli-go-mod-sync: - name: CLI go.mod Sync - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v6 - - name: Check moltnet-cli Go module pins - id: sync - shell: bash - run: | - set -euo pipefail - - EXPECTED_API=$(jq -r '."libs/moltnet-api-client"' .release-please-manifest.json) - EXPECTED_DSPY=$(jq -r '."libs/dspy-adapters"' .release-please-manifest.json) - - CURRENT_API=$(grep 'github.com/getlarge/themoltnet/libs/moltnet-api-client' apps/moltnet-cli/go.mod | awk '{print $2}' | sed 's/^v//') - CURRENT_DSPY=$(grep 'github.com/getlarge/themoltnet/libs/dspy-adapters' apps/moltnet-cli/go.mod | awk '{print $2}' | sed 's/^v//') - - STALE=false - MESSAGES=() - COMMANDS=() - - if [ "$CURRENT_API" != "$EXPECTED_API" ]; then - STALE=true - MESSAGES+=("- \`moltnet-api-client\`: go.mod has \`v$CURRENT_API\`, expected \`v$EXPECTED_API\`") - COMMANDS+=("GOWORK=off go get github.com/getlarge/themoltnet/libs/moltnet-api-client@v$EXPECTED_API") - fi - - if [ "$CURRENT_DSPY" != "$EXPECTED_DSPY" ]; then - STALE=true - MESSAGES+=("- \`dspy-adapters\`: go.mod has \`v$CURRENT_DSPY\`, expected \`v$EXPECTED_DSPY\`") - COMMANDS+=("GOWORK=off go get github.com/getlarge/themoltnet/libs/dspy-adapters@v$EXPECTED_DSPY") - fi - - if [ "$STALE" = true ]; then - COMMANDS+=("GOWORK=off go mod tidy") - { - echo "stale=true" - echo "message<> "$GITHUB_OUTPUT" - - echo "::warning::apps/moltnet-cli/go.mod is behind the release manifest." - printf '%s\n' "${MESSAGES[@]}" - exit 0 - fi - - echo "stale=false" >> "$GITHUB_OUTPUT" - - name: Comment on PR - if: github.event_name == 'pull_request' && always() - uses: actions/github-script@v8 - env: - CLI_GO_MOD_STALE: ${{ steps.sync.outputs.stale }} - CLI_GO_MOD_MESSAGE: ${{ steps.sync.outputs.message }} - CLI_GO_MOD_COMMANDS: ${{ steps.sync.outputs.commands }} - with: - script: | - const marker = ''; - const stale = process.env.CLI_GO_MOD_STALE; - const message = process.env.CLI_GO_MOD_MESSAGE || ''; - const commands = process.env.CLI_GO_MOD_COMMANDS || ''; - - let body; - if (stale === 'true') { - body = [ - marker, - '## :warning: CLI go.mod is behind internal Go module releases', - '', - 'The CLI release is intentionally decoupled from same-run Go lib releases, so `apps/moltnet-cli/go.mod` must already be bumped in a normal PR.', - '', - 'Detected drift:', - message, - '', - 'Run these commands from `apps/moltnet-cli`:', - '', - '```bash', - commands, - '```', - ].join('\n'); - } else { - body = [ - marker, - '## :white_check_mark: CLI go.mod matches internal Go module releases', - '', - '`apps/moltnet-cli/go.mod` is aligned with the versions tracked in `.release-please-manifest.json`.', - ].join('\n'); - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => c.body.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - audit: name: Dependency Audit runs-on: ubuntu-latest diff --git a/.github/workflows/release-recover-cli.yml b/.github/workflows/release-recover-cli.yml deleted file mode 100644 index d717f2114..000000000 --- a/.github/workflows/release-recover-cli.yml +++ /dev/null @@ -1,497 +0,0 @@ -name: Release Recover (moltnet-cli) - -# Recovers a stuck `apps/moltnet-cli` release cycle. Handles two scenarios: -# -# A) goreleaser-failure: cli was released in the same cycle as one or -# more libs it depends on. release-please bumped the lib versions in -# the manifest but did NOT sync `apps/moltnet-cli/go.mod`, so -# goreleaser built against a stale pin. Needs manifest revert + -# CHANGELOG drop + go.mod sync. -# -# B) downstream-publish-failure: goreleaser succeeded but one of the -# npm publish jobs (or a sibling tag creation) failed transiently. -# The cli tag + release exist as draft and/or the 7 linked-versions -# sibling tags are half-created, blocking the next release-please -# run. Needs manifest revert + CHANGELOG drop + draft/tag cleanup. -# No go.mod sync — no libs were bumped in this cycle. -# -# The workflow auto-detects which mode applies by diffing the cli's -# direct libs/* dependencies in `apps/moltnet-cli/go.mod` against the -# manifest before/after versions. -# -# Usage: trigger this workflow with the number of the merged release-please -# PR that caused the failure (the "chore: release main" PR). Everything -# else — previous/stuck cli versions, which libs to re-pin — is inferred by -# diffing `.release-please-manifest.json` between the PR's merge commit and -# its first parent, then filtering against the cli's actual `libs/` deps in -# `apps/moltnet-cli/go.mod`. -# -# The recipe: -# 1. Resolve merge commit + parent from the PR number -# 2. Diff .release-please-manifest.json between parent and merge commit -# 3. Extract previous + stuck cli versions; collect bumped libs that the -# cli depends on (auto-discovered from apps/moltnet-cli/go.mod) -# 4. Revert cli version in the manifest to the previous released version -# for every member of the linked cli group (apps/moltnet-cli, -# packages/cli, and the 6 packages/cli/npm/* platform packages) so the -# manifest stays consistent with the linked-versions invariant -# 5. Drop the premature `## [x.y.z]` section from cli CHANGELOG -# 6. `GOWORK=off go get @` for each bumped lib + go mod tidy -# 7. Commit on a branch, open PR (squash-merge it manually) -# 8. Delete the stuck draft `cli-vX.Y.Z` GitHub release -# -# After merging the PR, release-please will re-propose the cli bump with a -# synced go.mod on main, and the next release-cli run will succeed. - -on: - workflow_dispatch: - inputs: - release-pr: - description: 'Number of the merged release-please PR that caused the failure (e.g. 766)' - required: true - type: string - delete-draft-release: - description: 'Delete the stuck draft GitHub release cli-v' - required: false - type: boolean - default: true - -permissions: - contents: write - pull-requests: write - -jobs: - recover: - name: Recover cli release cycle - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/setup-go@v6 - with: - go-version-file: apps/moltnet-cli/go.mod - cache: false - - - name: Configure git - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - - - name: Infer recovery parameters from release PR - id: infer - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ inputs.release-pr }} - run: | - set -euo pipefail - - # 1. Fetch PR metadata. Must be merged. - pr_json=$(gh pr view "$PR_NUMBER" --json state,mergeCommit,title,url) - state=$(echo "$pr_json" | jq -r '.state') - merge_sha=$(echo "$pr_json" | jq -r '.mergeCommit.oid // empty') - pr_title=$(echo "$pr_json" | jq -r '.title') - pr_url=$(echo "$pr_json" | jq -r '.url') - - if [ "$state" != 'MERGED' ]; then - echo "ERROR: PR #$PR_NUMBER is $state, expected MERGED" >&2 - exit 1 - fi - if [ -z "$merge_sha" ]; then - echo "ERROR: PR #$PR_NUMBER has no merge commit" >&2 - exit 1 - fi - - echo "PR: #$PR_NUMBER ($pr_url)" - echo "Title: $pr_title" - echo "Merge SHA: $merge_sha" - - # 2. Parent commit = manifest state before the release PR merged. - # We use ^1 (first parent) rather than baseRefOid because the - # base branch may have advanced since the merge. - parent_sha=$(git rev-parse "${merge_sha}^1") - echo "Parent SHA: $parent_sha" - - # 3. Read manifest at both commits. - manifest_path='.release-please-manifest.json' - git show "${parent_sha}:${manifest_path}" > /tmp/manifest.before.json - git show "${merge_sha}:${manifest_path}" > /tmp/manifest.after.json - - previous_cli=$(jq -r '."apps/moltnet-cli"' /tmp/manifest.before.json) - stuck_cli=$(jq -r '."apps/moltnet-cli"' /tmp/manifest.after.json) - - if [ "$previous_cli" = 'null' ] || [ "$stuck_cli" = 'null' ]; then - echo "ERROR: apps/moltnet-cli missing from manifest at one of the commits" >&2 - exit 1 - fi - if [ "$previous_cli" = "$stuck_cli" ]; then - echo "ERROR: apps/moltnet-cli version unchanged in PR #$PR_NUMBER ($previous_cli)" >&2 - echo " This PR did not bump the cli — nothing to recover." >&2 - exit 1 - fi - - echo "Previous cli: $previous_cli" - echo "Stuck cli: $stuck_cli" - - # 4. Auto-discover cli's direct libs/ dependencies from go.mod. - # We only look at the first `require (...)` block (direct deps); - # indirect deps live in a second block marked `// indirect`. - cli_libs=$(awk ' - /^require \(/ { in_block=1; next } - in_block && /^\)/ { in_block=0; next } - in_block && /github\.com\/getlarge\/themoltnet\/libs\// && !/\/\/ indirect/ { - # Line format: "\tgithub.com/getlarge/themoltnet/libs/ v" - sub(/^[[:space:]]+/, "", $0) - split($0, parts, " ") - sub(/^github\.com\/getlarge\/themoltnet\/libs\//, "", parts[1]) - print parts[1] - } - ' apps/moltnet-cli/go.mod) - - # It is legitimate for cli_libs to be empty (cli has no direct - # libs/* deps) or for none of them to be bumped in this PR. That - # corresponds to the "downstream-publish-failure" recovery path: - # goreleaser already succeeded but some npm publish job failed, - # so there's no go.mod to sync — we only need to revert the - # manifest + CHANGELOG + drop draft/tags so release-please can - # re-propose the same cli bump. - pairs=() - if [ -n "$cli_libs" ]; then - echo 'Cli libs/* deps:' - echo "$cli_libs" | sed 's/^/ - /' - - # 5. For each cli lib dep, compare before/after manifest versions. - while IFS= read -r lib; do - [ -n "$lib" ] || continue - before=$(jq -r --arg k "libs/${lib}" '.[$k] // empty' /tmp/manifest.before.json) - after=$(jq -r --arg k "libs/${lib}" '.[$k] // empty' /tmp/manifest.after.json) - if [ -z "$after" ]; then - echo " libs/${lib}: not tracked in manifest (skipping)" - continue - fi - if [ "$before" = "$after" ]; then - echo " libs/${lib}: unchanged (${after})" - continue - fi - echo " libs/${lib}: ${before:-} -> ${after}" - pairs+=("${lib}=${after}") - done <<< "$cli_libs" - else - echo 'No direct libs/* dependencies in apps/moltnet-cli/go.mod' - fi - - if [ ${#pairs[@]} -eq 0 ]; then - echo 'No cli lib dependencies were bumped in this PR.' - echo 'Recovery mode: downstream-publish-failure (manifest + changelog only, no go.mod sync).' - bumped_libs='' - else - bumped_libs=$(IFS=,; echo "${pairs[*]}") - echo "Recovery mode: goreleaser-failure (go.mod sync required)." - echo "Bumped libs: $bumped_libs" - fi - - # Export outputs for downstream steps. - { - echo "previous-cli-version=${previous_cli}" - echo "stuck-cli-version=${stuck_cli}" - echo "bumped-libs=${bumped_libs}" - echo "pr-url=${pr_url}" - } >> "$GITHUB_OUTPUT" - - # Safety guard: confirm the cli-v release is actually stuck - # before we revert anything. Previously this was covered implicitly - # by the "at least one lib must be bumped" check, but that check - # missed the downstream-publish-failure path. Now we check the - # release/tag/npm state directly. - # - # A release is stuck iff ANY of: - # a) GitHub release cli-v is a draft (goreleaser didn't - # finalize it, or the workflow died before the release job - # flipped it). - # b) GitHub release is published but one or more of the 7 - # linked-versions sibling tags (cli-wrapper-v, - # cli--v) is missing. - # c) GitHub release is published but one or more of the 7 - # @themoltnet/cli* npm versions is missing on the registry. - # - # If none hold, recovery would destroy a healthy release — refuse. - - name: Verify cli release is stuck - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - run: | - set -euo pipefail - TAG="cli-v${STUCK_VERSION}" - reasons=() - - # a) Draft release check. - release_state='missing' - if release_json=$(gh release view "$TAG" --json isDraft,isPrerelease 2>/dev/null); then - if [ "$(echo "$release_json" | jq -r '.isDraft')" = 'true' ]; then - release_state='draft' - reasons+=("GitHub release $TAG is a draft (goreleaser did not finalize)") - else - release_state='published' - fi - else - reasons+=("GitHub release $TAG does not exist (goreleaser never ran or failed early)") - fi - echo "Release state: $release_state" - - # b) Sibling tag completeness check (only meaningful if release exists). - missing_tags=() - for COMPONENT in cli-wrapper cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-x64 cli-win32-arm64 cli-win32-x64; do - SIB_TAG="${COMPONENT}-v${STUCK_VERSION}" - if ! gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${SIB_TAG}" >/dev/null 2>&1; then - missing_tags+=("$SIB_TAG") - fi - done - if [ ${#missing_tags[@]} -gt 0 ]; then - reasons+=("Missing ${#missing_tags[@]} sibling tag(s): ${missing_tags[*]}") - fi - - # c) npm publish completeness check. Registry lookup is public - # and read-only, so no auth is needed. `npm view @` - # exits 0 if the version exists and prints E404 to stderr - # otherwise. - missing_npm=() - for PKG in \ - '@themoltnet/cli' \ - '@themoltnet/cli-darwin-arm64' \ - '@themoltnet/cli-darwin-x64' \ - '@themoltnet/cli-linux-arm64' \ - '@themoltnet/cli-linux-x64' \ - '@themoltnet/cli-win32-arm64' \ - '@themoltnet/cli-win32-x64'; do - if ! npm view "${PKG}@${STUCK_VERSION}" version >/dev/null 2>&1; then - missing_npm+=("${PKG}@${STUCK_VERSION}") - fi - done - if [ ${#missing_npm[@]} -gt 0 ]; then - reasons+=("Missing ${#missing_npm[@]} npm publish(es): ${missing_npm[*]}") - fi - - if [ ${#reasons[@]} -eq 0 ]; then - echo "ERROR: cli-v${STUCK_VERSION} looks healthy — refusing to recover." >&2 - echo " Release is published, all 7 sibling tags exist, and all 7" >&2 - echo " npm packages are on the registry. If you believe this PR is" >&2 - echo " genuinely stuck, investigate manually before re-running." >&2 - exit 1 - fi - - echo 'Stuckness evidence:' - for r in "${reasons[@]}"; do - echo " - $r" - done - - - name: Create recovery branch - id: branch - env: - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - run: | - BRANCH="chore/recover-cli-v${STUCK_VERSION}" - git checkout -b "$BRANCH" - echo "name=$BRANCH" >> "$GITHUB_OUTPUT" - - - name: Revert manifest cli version - env: - PREVIOUS_VERSION: ${{ steps.infer.outputs.previous-cli-version }} - run: | - # The cli is part of an 8-component linked-versions group - # (apps/moltnet-cli + packages/cli + 6 platform packages). All - # members must move in lockstep, so the revert has to touch every - # path — otherwise the manifest contradicts the linked-versions - # invariant and release-please gets confused on the next run. - jq --arg v "$PREVIOUS_VERSION" ' - ."apps/moltnet-cli" = $v - | ."packages/cli" = $v - | ."packages/cli/npm/darwin-arm64" = $v - | ."packages/cli/npm/darwin-x64" = $v - | ."packages/cli/npm/linux-arm64" = $v - | ."packages/cli/npm/linux-x64" = $v - | ."packages/cli/npm/win32-arm64" = $v - | ."packages/cli/npm/win32-x64" = $v - ' .release-please-manifest.json \ - > .release-please-manifest.json.tmp - mv .release-please-manifest.json.tmp .release-please-manifest.json - git diff --stat .release-please-manifest.json - - - name: Drop premature CHANGELOG section - env: - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - run: | - python3 - <<'PY' - import os, re, sys, pathlib - p = pathlib.Path('apps/moltnet-cli/CHANGELOG.md') - text = p.read_text() - stuck = os.environ['STUCK_VERSION'] - pattern = re.compile( - r'^## \[?' + re.escape(stuck) + r'\]?.*?(?=^## \[)', - re.MULTILINE | re.DOTALL, - ) - new = pattern.sub('', text, count=1) - if new == text: - print(f'ERROR: no ## [{stuck}] section found in CHANGELOG', file=sys.stderr) - sys.exit(1) - p.write_text(new) - PY - git diff --stat apps/moltnet-cli/CHANGELOG.md - - - name: Sync apps/moltnet-cli/go.mod to bumped libs - if: ${{ steps.infer.outputs.bumped-libs != '' }} - working-directory: apps/moltnet-cli - env: - GOWORK: off - BUMPED_LIBS: ${{ steps.infer.outputs.bumped-libs }} - run: | - IFS=',' read -ra PAIRS <<< "$BUMPED_LIBS" - for raw in "${PAIRS[@]}"; do - # Trim whitespace - pair="$(echo "$raw" | xargs)" - if [[ "$pair" != *=* ]]; then - echo "ERROR: invalid bumped-libs entry '$raw' (expected name=version)" >&2 - exit 1 - fi - name="${pair%%=*}" - version="${pair##*=}" - if [ -z "$name" ] || [ -z "$version" ]; then - echo "ERROR: empty name or version in '$raw'" >&2 - exit 1 - fi - module="github.com/getlarge/themoltnet/libs/${name}" - echo "::group::go get ${module}@v${version}" - go get "${module}@v${version}" - resolved=$(go list -m "$module" | awk '{print $2}') - if [ "$resolved" != "v${version}" ]; then - echo "ERROR: requested v${version} but MVS resolved ${resolved}" >&2 - exit 1 - fi - echo "::endgroup::" - done - go mod tidy - go build ./... - - - name: Commit and push - env: - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - BRANCH_NAME: ${{ steps.branch.outputs.name }} - run: | - git add \ - .release-please-manifest.json \ - apps/moltnet-cli/CHANGELOG.md \ - apps/moltnet-cli/go.mod \ - apps/moltnet-cli/go.sum - if git diff --cached --quiet; then - echo 'No changes to commit — nothing to recover.' - exit 1 - fi - git commit -m "fix(release): recover cli v${STUCK_VERSION} release cycle" - git push -u origin "$BRANCH_NAME" - - - name: Open recovery PR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - PREVIOUS_VERSION: ${{ steps.infer.outputs.previous-cli-version }} - BUMPED_LIBS: ${{ steps.infer.outputs.bumped-libs }} - SOURCE_PR_URL: ${{ steps.infer.outputs.pr-url }} - BRANCH_NAME: ${{ steps.branch.outputs.name }} - run: | - if [ -n "$BUMPED_LIBS" ]; then - GOMOD_LINE="- Syncs \`apps/moltnet-cli/go.mod\` to: \`${BUMPED_LIBS}\`" - else - GOMOD_LINE="- No go.mod sync required (no libs bumped in source PR — downstream-publish-failure recovery)" - fi - BODY=$(cat </tmp/gh-err)"; then - if grep -qi 'release not found' /tmp/gh-err; then - echo "No release $TAG to delete" - exit 0 - fi - echo "ERROR: gh release view failed:" >&2 - cat /tmp/gh-err >&2 - exit 1 - fi - if [ "$IS_DRAFT" = 'true' ]; then - # Delete the draft release WITHOUT --cleanup-tag. Draft releases - # don't have a real git tag ref backing them, so `gh release - # delete --cleanup-tag` tries to DELETE /git/refs/tags/, - # gets HTTP 422 "Reference does not exist", and exits non-zero - # even though the release itself was deleted cleanly. - gh release delete "$TAG" --yes - echo "Deleted draft release $TAG" - - # Best-effort tag cleanup: only delete the ref if it actually - # exists. A stray published tag with the same name would be a - # separate bug, so we warn but do not hard-fail on delete errors. - if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}" >/dev/null 2>&1; then - if gh api -X DELETE "repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}" >/dev/null 2>&1; then - echo "Deleted leftover tag ref $TAG" - else - echo "WARN: failed to delete leftover tag ref $TAG (continuing)" >&2 - fi - else - echo "No leftover tag ref $TAG to clean up" - fi - else - echo "Release $TAG exists but is not a draft; skipping deletion" - fi - - # The linked-versions cli group has 7 sibling tags - # (cli-wrapper-v, cli--v) created by the - # `tag-cli-siblings` job in release.yml once `release-cli` succeeds. - # They exist independently of the cli-v GitHub release state, so - # clean them up with the same toggle as the draft release — if - # release-cli never succeeded, they won't exist and delete is a no-op. - - name: Delete stuck sibling tags - if: ${{ inputs.delete-draft-release }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - STUCK_VERSION: ${{ steps.infer.outputs.stuck-cli-version }} - run: | - for COMPONENT in cli-wrapper cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-x64 cli-win32-arm64 cli-win32-x64; do - SIB_TAG="${COMPONENT}-v${STUCK_VERSION}" - if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${SIB_TAG}" >/dev/null 2>&1; then - if gh api -X DELETE "repos/${GITHUB_REPOSITORY}/git/refs/tags/${SIB_TAG}" >/dev/null 2>&1; then - echo " - deleted ${SIB_TAG}" - else - echo " ! WARN: failed to delete ${SIB_TAG} (continuing)" >&2 - fi - else - echo " = ${SIB_TAG} does not exist" - fi - done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57fa45059..b054110b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,371 +1,46 @@ name: Release on: - push: - branches: [main] - paths-ignore: - - 'docs/**' - - '.claude/**' - - '**.md' workflow_dispatch: inputs: - republish: - description: > - Comma-separated packages to re-publish from existing draft releases. - Valid values: sdk, github-agent, legreffier, cli, api-client, dspy-adapters, skill, design-system, pi-extension, agent-runtime, node-red-theme, agent-daemon + dry-run: + description: 'Plan the release without committing, tagging, creating releases, or publishing.' required: false - type: string + type: boolean + default: true + first-release: + description: 'Bootstrap projects with no previous release tag.' + required: false + type: boolean + default: false permissions: contents: write - pull-requests: write id-token: write + packages: write + +concurrency: + group: release-main + cancel-in-progress: false -# nx@22 unconditionally loads workspace-root .env at bin startup via plain -# dotenv. Our .env is dotenvx-encrypted, so plain-dotenv pulls ciphertext -# into every variable it defines, which then leaks into every process Nx -# spawns. This flag is the documented opt-out (honored by per-task and -# per-executor env loading); we set it workflow-wide as a defense even -# though nx@22.7.0's bin-level load currently ignores it. When the bin -# eventually honors it (or on a future major version), this guards us. env: NX_LOAD_DOT_ENV_FILES: 'false' jobs: - release-please: - name: Release Please - if: github.event_name == 'push' + nx-release: + name: Nx Release runs-on: ubuntu-latest - outputs: - sdk-release-created: ${{ steps.release.outputs['libs/sdk--release_created'] }} - sdk-tag-name: ${{ steps.release.outputs['libs/sdk--tag_name'] }} - api-client-release-created: ${{ steps.release.outputs['libs/moltnet-api-client--release_created'] }} - api-client-tag-name: ${{ steps.release.outputs['libs/moltnet-api-client--tag_name'] }} - api-client-version: ${{ steps.release.outputs['libs/moltnet-api-client--version'] }} - dspy-adapters-release-created: ${{ steps.release.outputs['libs/dspy-adapters--release_created'] }} - dspy-adapters-tag-name: ${{ steps.release.outputs['libs/dspy-adapters--tag_name'] }} - dspy-adapters-version: ${{ steps.release.outputs['libs/dspy-adapters--version'] }} - cli-release-created: ${{ steps.release.outputs['apps/moltnet-cli--release_created'] }} - cli-tag-name: ${{ steps.release.outputs['apps/moltnet-cli--tag_name'] }} - github-agent-release-created: ${{ steps.release.outputs['packages/github-agent--release_created'] }} - github-agent-tag-name: ${{ steps.release.outputs['packages/github-agent--tag_name'] }} - skill-release-created: ${{ steps.release.outputs['packages/openclaw-skill--release_created'] }} - skill-tag-name: ${{ steps.release.outputs['packages/openclaw-skill--tag_name'] }} - skill-version: ${{ steps.release.outputs['packages/openclaw-skill--version'] }} - legreffier-release-created: ${{ steps.release.outputs['packages/legreffier-cli--release_created'] }} - legreffier-tag-name: ${{ steps.release.outputs['packages/legreffier-cli--tag_name'] }} - agent-runtime-release-created: ${{ steps.release.outputs['libs/agent-runtime--release_created'] }} - agent-runtime-tag-name: ${{ steps.release.outputs['libs/agent-runtime--tag_name'] }} - pi-extension-release-created: ${{ steps.release.outputs['libs/pi-extension--release_created'] }} - pi-extension-tag-name: ${{ steps.release.outputs['libs/pi-extension--tag_name'] }} - node-red-release-created: ${{ steps.release.outputs['libs/node-red-contrib-core--release_created'] }} - node-red-tag-name: ${{ steps.release.outputs['libs/node-red-contrib-core--tag_name'] }} - node-red-theme-release-created: ${{ steps.release.outputs['libs/node-red-theme--release_created'] }} - node-red-theme-tag-name: ${{ steps.release.outputs['libs/node-red-theme--tag_name'] }} - design-system-release-created: ${{ steps.release.outputs['libs/design-system--release_created'] }} - design-system-tag-name: ${{ steps.release.outputs['libs/design-system--tag_name'] }} - agent-daemon-release-created: ${{ steps.release.outputs['apps/agent-daemon--release_created'] }} - agent-daemon-tag-name: ${{ steps.release.outputs['apps/agent-daemon--tag_name'] }} + timeout-minutes: 60 steps: - - uses: googleapis/release-please-action@v4 - id: release + - uses: actions/checkout@v6 with: + fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json - - # Unified resolution: merges release-please outputs with manual republish - # inputs so downstream jobs use a single set of outputs regardless of trigger. - resolve-publish: - name: Resolve packages to publish - needs: release-please - if: always() && !cancelled() - runs-on: ubuntu-latest - outputs: - sdk: ${{ steps.resolve.outputs.sdk }} - sdk-tag: ${{ steps.resolve.outputs.sdk-tag }} - github-agent: ${{ steps.resolve.outputs.github-agent }} - github-agent-tag: ${{ steps.resolve.outputs.github-agent-tag }} - cli: ${{ steps.resolve.outputs.cli }} - cli-tag: ${{ steps.resolve.outputs.cli-tag }} - api-client: ${{ steps.resolve.outputs.api-client }} - api-client-tag: ${{ steps.resolve.outputs.api-client-tag }} - api-client-version: ${{ steps.resolve.outputs.api-client-version }} - dspy-adapters: ${{ steps.resolve.outputs.dspy-adapters }} - dspy-adapters-tag: ${{ steps.resolve.outputs.dspy-adapters-tag }} - dspy-adapters-version: ${{ steps.resolve.outputs.dspy-adapters-version }} - skill: ${{ steps.resolve.outputs.skill }} - skill-tag: ${{ steps.resolve.outputs.skill-tag }} - skill-version: ${{ steps.resolve.outputs.skill-version }} - legreffier: ${{ steps.resolve.outputs.legreffier }} - legreffier-tag: ${{ steps.resolve.outputs.legreffier-tag }} - agent-runtime: ${{ steps.resolve.outputs.agent-runtime }} - agent-runtime-tag: ${{ steps.resolve.outputs.agent-runtime-tag }} - pi-extension: ${{ steps.resolve.outputs.pi-extension }} - pi-extension-tag: ${{ steps.resolve.outputs.pi-extension-tag }} - node-red: ${{ steps.resolve.outputs.node-red }} - node-red-tag: ${{ steps.resolve.outputs.node-red-tag }} - node-red-theme: ${{ steps.resolve.outputs.node-red-theme }} - node-red-theme-tag: ${{ steps.resolve.outputs.node-red-theme-tag }} - design-system: ${{ steps.resolve.outputs.design-system }} - design-system-tag: ${{ steps.resolve.outputs.design-system-tag }} - agent-daemon: ${{ steps.resolve.outputs.agent-daemon }} - agent-daemon-tag: ${{ steps.resolve.outputs.agent-daemon-tag }} - steps: - - uses: actions/checkout@v6 - - name: Resolve publish targets - id: resolve - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RP_SDK_CREATED: ${{ needs.release-please.outputs.sdk-release-created }} - RP_SDK_TAG: ${{ needs.release-please.outputs.sdk-tag-name }} - RP_AGENT_CREATED: ${{ needs.release-please.outputs.github-agent-release-created }} - RP_AGENT_TAG: ${{ needs.release-please.outputs.github-agent-tag-name }} - RP_CLI_CREATED: ${{ needs.release-please.outputs.cli-release-created }} - RP_CLI_TAG: ${{ needs.release-please.outputs.cli-tag-name }} - RP_API_CREATED: ${{ needs.release-please.outputs.api-client-release-created }} - RP_API_TAG: ${{ needs.release-please.outputs.api-client-tag-name }} - RP_API_VERSION: ${{ needs.release-please.outputs.api-client-version }} - RP_DSPY_CREATED: ${{ needs.release-please.outputs.dspy-adapters-release-created }} - RP_DSPY_TAG: ${{ needs.release-please.outputs.dspy-adapters-tag-name }} - RP_DSPY_VERSION: ${{ needs.release-please.outputs.dspy-adapters-version }} - RP_SKILL_CREATED: ${{ needs.release-please.outputs.skill-release-created }} - RP_SKILL_TAG: ${{ needs.release-please.outputs.skill-tag-name }} - RP_SKILL_VERSION: ${{ needs.release-please.outputs.skill-version }} - RP_LEGREFFIER_CREATED: ${{ needs.release-please.outputs.legreffier-release-created }} - RP_LEGREFFIER_TAG: ${{ needs.release-please.outputs.legreffier-tag-name }} - RP_AGENT_RUNTIME_CREATED: ${{ needs.release-please.outputs.agent-runtime-release-created }} - RP_AGENT_RUNTIME_TAG: ${{ needs.release-please.outputs.agent-runtime-tag-name }} - RP_PI_EXTENSION_CREATED: ${{ needs.release-please.outputs.pi-extension-release-created }} - RP_PI_EXTENSION_TAG: ${{ needs.release-please.outputs.pi-extension-tag-name }} - RP_NODE_RED_CREATED: ${{ needs.release-please.outputs.node-red-release-created }} - RP_NODE_RED_TAG: ${{ needs.release-please.outputs.node-red-tag-name }} - RP_NODE_RED_THEME_CREATED: ${{ needs.release-please.outputs.node-red-theme-release-created }} - RP_NODE_RED_THEME_TAG: ${{ needs.release-please.outputs.node-red-theme-tag-name }} - RP_DESIGN_SYSTEM_CREATED: ${{ needs.release-please.outputs.design-system-release-created }} - RP_DESIGN_SYSTEM_TAG: ${{ needs.release-please.outputs.design-system-tag-name }} - RP_AGENT_DAEMON_CREATED: ${{ needs.release-please.outputs.agent-daemon-release-created }} - RP_AGENT_DAEMON_TAG: ${{ needs.release-please.outputs.agent-daemon-tag-name }} - REPUBLISH: ${{ github.event.inputs.republish || '' }} + - name: Configure git run: | - find_draft_tag() { - local component="$1" - gh release list --repo "$GITHUB_REPOSITORY" --limit 20 --json tagName,isDraft \ - --jq "[.[] | select(.isDraft == true and (.tagName | startswith(\"${component}-v\")))][0].tagName // empty" - } - - version_from_tag() { - local tag="$1" component="$2" - echo "${tag}" | sed "s/^${component}-v//" - } - - wants_republish() { - echo "$REPUBLISH" | tr ',' '\n' | tr -d ' ' | grep -qx "$1" - } - - resolve() { - local name="$1" rp_created="$2" rp_tag="$3" component="$4" - - if [ "$rp_created" = "true" ]; then - echo "${name}=true" >> "$GITHUB_OUTPUT" - echo "${name}-tag=${rp_tag}" >> "$GITHUB_OUTPUT" - echo " ✓ ${name}: release-please → ${rp_tag}" - return - fi - - if wants_republish "$name"; then - local tag - tag=$(find_draft_tag "$component") - if [ -n "$tag" ]; then - echo "${name}=true" >> "$GITHUB_OUTPUT" - echo "${name}-tag=${tag}" >> "$GITHUB_OUTPUT" - echo " ✓ ${name}: republish draft → ${tag}" - return - fi - echo " ✗ ${name}: requested but no draft release found for '${component}'" - fi - - echo "${name}=false" >> "$GITHUB_OUTPUT" - echo " - ${name}: skip" - } - - echo "Resolving publish targets..." - resolve "sdk" "$RP_SDK_CREATED" "$RP_SDK_TAG" "sdk" - resolve "github-agent" "$RP_AGENT_CREATED" "$RP_AGENT_TAG" "github-agent" - resolve "cli" "$RP_CLI_CREATED" "$RP_CLI_TAG" "cli" - resolve "api-client" "$RP_API_CREATED" "$RP_API_TAG" "moltnet-api-client" - resolve "dspy-adapters" "$RP_DSPY_CREATED" "$RP_DSPY_TAG" "dspy-adapters" - resolve "skill" "$RP_SKILL_CREATED" "$RP_SKILL_TAG" "openclaw-skill" - resolve "legreffier" "$RP_LEGREFFIER_CREATED" "$RP_LEGREFFIER_TAG" "legreffier" - resolve "design-system" "$RP_DESIGN_SYSTEM_CREATED" "$RP_DESIGN_SYSTEM_TAG" "design-system" - resolve "agent-runtime" "$RP_AGENT_RUNTIME_CREATED" "$RP_AGENT_RUNTIME_TAG" "agent-runtime" - resolve "pi-extension" "$RP_PI_EXTENSION_CREATED" "$RP_PI_EXTENSION_TAG" "pi-extension" - resolve "node-red" "$RP_NODE_RED_CREATED" "$RP_NODE_RED_TAG" "node-red-contrib-core" - resolve "node-red-theme" "$RP_NODE_RED_THEME_CREATED" "$RP_NODE_RED_THEME_TAG" "node-red-theme" - resolve "agent-daemon" "$RP_AGENT_DAEMON_CREATED" "$RP_AGENT_DAEMON_TAG" "agent-daemon" - - # Forward version outputs - if [ -n "$RP_API_VERSION" ]; then - echo "api-client-version=${RP_API_VERSION}" >> "$GITHUB_OUTPUT" - elif wants_republish "api-client"; then - tag=$(find_draft_tag "moltnet-api-client") - [ -n "$tag" ] && echo "api-client-version=$(version_from_tag "$tag" "moltnet-api-client")" >> "$GITHUB_OUTPUT" - fi - - if [ -n "$RP_DSPY_VERSION" ]; then - echo "dspy-adapters-version=${RP_DSPY_VERSION}" >> "$GITHUB_OUTPUT" - elif wants_republish "dspy-adapters"; then - tag=$(find_draft_tag "dspy-adapters") - [ -n "$tag" ] && echo "dspy-adapters-version=$(version_from_tag "$tag" "dspy-adapters")" >> "$GITHUB_OUTPUT" - fi - - if [ -n "$RP_SKILL_VERSION" ]; then - echo "skill-version=${RP_SKILL_VERSION}" >> "$GITHUB_OUTPUT" - elif wants_republish "skill"; then - tag=$(find_draft_tag "openclaw-skill") - [ -n "$tag" ] && echo "skill-version=$(version_from_tag "$tag" "openclaw-skill")" >> "$GITHUB_OUTPUT" - fi - - publish-sdk: - name: Publish SDK to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.sdk == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - # Nx walks the dep graph (build dependsOn ^build in nx.json), builds - # @moltnet/api-client + @moltnet/crypto-service first, then the SDK, - # all with remote-cache hits when unchanged. - - run: pnpm exec nx run @themoltnet/sdk:build - - - run: pnpm exec nx run @themoltnet/sdk:test - - - run: pnpm exec nx run @themoltnet/sdk:check:pack - - - name: Check SDK type isolation - run: pnpm exec nx run @themoltnet/sdk:check:types-isolation - - - name: Publish @themoltnet/sdk - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/sdk publish --no-git-checks --access public --provenance - - - name: Publish SDK release - run: gh release edit "${{ needs.resolve-publish.outputs.sdk-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-node-red: - name: Publish Node-RED nodes to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.node-red == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - # The package externalizes @themoltnet/sdk; the build only bundles the - # node runtime + copies editor HTML (no SDK inlining). - - run: pnpm exec nx run @themoltnet/node-red-contrib-core:build - - - run: pnpm exec nx run @themoltnet/node-red-contrib-core:test - - # Project-level pack check: every node-red.nodes path + sibling .html ships. - - run: pnpm --filter @themoltnet/node-red-contrib-core run check:pack - - - name: Publish @themoltnet/node-red-contrib-core - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/node-red-contrib-core publish --no-git-checks --access public --provenance - - - name: Publish Node-RED release - run: gh release edit "${{ needs.resolve-publish.outputs.node-red-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-node-red-theme: - name: Publish Node-RED theme to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.node-red-theme == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - - run: pnpm exec nx run @themoltnet/node-red-theme:build - - - run: pnpm exec nx run @themoltnet/node-red-theme:test - - - run: pnpm exec nx run @themoltnet/node-red-theme:check:pack - - - name: Publish @themoltnet/node-red-theme - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/node-red-theme publish --no-git-checks --access public --provenance - - - name: Publish Node-RED theme release - run: gh release edit "${{ needs.resolve-publish.outputs.node-red-theme-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-github-agent: - name: Publish GitHub Agent to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.github-agent == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - uses: pnpm/action-setup@v5 @@ -375,584 +50,60 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org - - run: pnpm install --frozen-lockfile - - - run: pnpm exec nx run @themoltnet/github-agent:build - - - run: pnpm exec nx run @themoltnet/github-agent:test - - - run: pnpm exec nx run @themoltnet/github-agent:check:pack - - - name: Publish @themoltnet/github-agent - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/github-agent publish --no-git-checks --access public --provenance - - - name: Publish GitHub Agent release - run: gh release edit "${{ needs.resolve-publish.outputs.github-agent-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - release-api-client: - name: Tag Go module for moltnet-api-client - needs: resolve-publish - if: ${{ needs.resolve-publish.outputs.api-client == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Push Go module proxy tag - # The Go module proxy resolves github.com/getlarge/themoltnet/libs/moltnet-api-client@vX.Y.Z - # by looking for a git tag of the form "libs/moltnet-api-client/vX.Y.Z". - # release-please creates the component tag "moltnet-api-client-vX.Y.Z"; we push the - # additional tag that the Go toolchain needs. - run: | - TAG="libs/moltnet-api-client/v${{ needs.resolve-publish.outputs.api-client-version }}" - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Tag $TAG already exists, skipping" - else - git tag "$TAG" - git push origin "$TAG" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish API client release - run: gh release edit "${{ needs.resolve-publish.outputs.api-client-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - release-dspy-adapters: - name: Tag Go module for dspy-adapters - needs: resolve-publish - if: ${{ needs.resolve-publish.outputs.dspy-adapters == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Push Go module proxy tag - # The Go module proxy resolves github.com/getlarge/themoltnet/libs/dspy-adapters@vX.Y.Z - # by looking for a git tag of the form "libs/dspy-adapters/vX.Y.Z". - run: | - TAG="libs/dspy-adapters/v${{ needs.resolve-publish.outputs.dspy-adapters-version }}" - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Tag $TAG already exists, skipping" - else - git tag "$TAG" - git push origin "$TAG" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish dspy-adapters release - run: gh release edit "${{ needs.resolve-publish.outputs.dspy-adapters-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - release-cli: - name: Release CLI binaries - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.cli == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - uses: actions/setup-go@v6 with: - go-version: '1.25' - - - name: Extract semver from tag - id: tag - run: | - TAG="${{ needs.resolve-publish.outputs.cli-tag }}" - SEMVER="${TAG#cli-}" - echo "semver=$SEMVER" >> "$GITHUB_OUTPUT" - - - name: Generate GitHub App token for Homebrew tap - uses: actions/create-github-app-token@v3 - id: app-token - with: - app-id: ${{ secrets.MOLTNET_RELEASE_APP_ID }} - private-key: ${{ secrets.MOLTNET_RELEASE_APP_KEY }} - owner: getlarge - repositories: homebrew-moltnet - - - uses: goreleaser/goreleaser-action@v6 - with: - version: '~> v2' - args: release --clean --skip=validate - workdir: apps/moltnet-cli - env: - GOWORK: off - GORELEASER_CURRENT_TAG: ${{ steps.tag.outputs.semver }} - HOMEBREW_TAP_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Upload binaries to GitHub Release - run: | - gh release upload "${{ needs.resolve-publish.outputs.cli-tag }}" \ - apps/moltnet-cli/dist/*.tar.gz \ - apps/moltnet-cli/dist/*.zip \ - apps/moltnet-cli/dist/checksums.txt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish CLI release - run: gh release edit "${{ needs.resolve-publish.outputs.cli-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-cli-platforms: - name: Publish @themoltnet/cli-${{ matrix.npm_dir }} to npm - needs: [resolve-publish, release-cli] - if: >- - ${{ - always() && - needs.resolve-publish.outputs.cli == 'true' && - needs.release-cli.result == 'success' && - !failure() && - !cancelled() - }} - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - strategy: - fail-fast: false - matrix: - include: - - npm_dir: linux-x64 - archive_suffix: linux_amd64.tar.gz - binary: moltnet - - npm_dir: linux-arm64 - archive_suffix: linux_arm64.tar.gz - binary: moltnet - - npm_dir: darwin-x64 - archive_suffix: darwin_amd64.tar.gz - binary: moltnet - - npm_dir: darwin-arm64 - archive_suffix: darwin_arm64.tar.gz - binary: moltnet - - npm_dir: win32-x64 - archive_suffix: windows_amd64.zip - binary: moltnet.exe - - npm_dir: win32-arm64 - archive_suffix: windows_arm64.zip - binary: moltnet.exe - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - - - name: Download release binary - uses: nick-fields/retry@v4.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ needs.resolve-publish.outputs.cli-tag }} - ARCHIVE_SUFFIX: ${{ matrix.archive_suffix }} - with: - timeout_minutes: 5 - max_attempts: 3 - retry_wait_seconds: 15 - command: | - SEMVER="${TAG#cli-v}" - ARCHIVE="moltnet_${SEMVER}_${ARCHIVE_SUFFIX}" - mkdir -p /tmp/cli-binaries - # Stay in the repo checkout so gh can infer the repo from git; - # --dir places the archive where the Extract step expects it. - gh release download "$TAG" --pattern "$ARCHIVE" --dir /tmp/cli-binaries - - - name: Extract binary - env: - TAG: ${{ needs.resolve-publish.outputs.cli-tag }} - ARCHIVE_SUFFIX: ${{ matrix.archive_suffix }} - NPM_DIR: ${{ matrix.npm_dir }} - BINARY: ${{ matrix.binary }} - run: | - SEMVER="${TAG#cli-v}" - ARCHIVE="moltnet_${SEMVER}_${ARCHIVE_SUFFIX}" - PKG_DIR="packages/cli/npm/${NPM_DIR}" - - mkdir -p "${PKG_DIR}/bin" - if [[ "${ARCHIVE}" == *.zip ]]; then - unzip -o "/tmp/cli-binaries/${ARCHIVE}" "${BINARY}" -d "${PKG_DIR}/bin/" - else - tar xzf "/tmp/cli-binaries/${ARCHIVE}" -C "${PKG_DIR}/bin/" "${BINARY}" - fi - chmod +x "${PKG_DIR}/bin/${BINARY}" 2>/dev/null || true - - - name: Publish platform package - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: | - cd "packages/cli/npm/${{ matrix.npm_dir }}" - npm publish --access public --provenance - - publish-cli-wrapper: - name: Publish @themoltnet/cli to npm - needs: [resolve-publish, publish-cli-platforms] - if: >- - ${{ - always() && - needs.resolve-publish.outputs.cli == 'true' && - needs.publish-cli-platforms.result == 'success' && - !failure() && - !cancelled() - }} - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v6 + go-version-file: apps/moltnet-cli/go.mod + cache: true - - uses: pnpm/action-setup@v5 + - uses: docker/setup-buildx-action@v3 - - uses: actions/setup-node@v6 + - name: Log in to GHCR + if: ${{ !inputs.dry-run }} + uses: docker/login-action@v3 with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - run: pnpm install --frozen-lockfile - # pnpm publish rewrites workspace:* in optionalDependencies to the - # concrete version so the published tarball resolves on npm. - - name: Publish @themoltnet/cli - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/cli publish --no-git-checks --access public --provenance - - # The cli is an 8-component linked-versions group. Only apps/moltnet-cli - # gets a real GitHub release (cli-v); the other 7 siblings use - # `skip-github-release: true`. release-please still needs a git tag per - # component to track state — without them its next run logs "Expected - # 15 releases, only found 8 / Missing 7 paths" and skips cutting the - # next release. - # - # This job is decoupled from npm publishing so a transient tag-creation - # failure can be retried without hitting "version already exists" on - # npm. It only needs release-cli (which flips the cli-v GitHub - # release to non-draft) to have succeeded. - tag-cli-siblings: - name: Create linked-versions sibling tags - needs: [resolve-publish, release-cli] - if: >- - ${{ - always() && - needs.resolve-publish.outputs.cli == 'true' && - needs.release-cli.result == 'success' && - !cancelled() - }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Create sibling tags - uses: nick-fields/retry@v4.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CLI_TAG: ${{ needs.resolve-publish.outputs.cli-tag }} - with: - timeout_minutes: 5 - max_attempts: 3 - retry_wait_seconds: 15 - command: | - set -euo pipefail - SEMVER="${CLI_TAG#cli-v}" - COMMIT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${CLI_TAG}" --jq '.sha') - echo "${CLI_TAG} -> ${COMMIT_SHA}" - for COMPONENT in cli-wrapper cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-x64 cli-win32-arm64 cli-win32-x64; do - SIB_TAG="${COMPONENT}-v${SEMVER}" - if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${SIB_TAG}" >/dev/null 2>&1; then - echo " = ${SIB_TAG} already exists" - continue - fi - gh api -X POST "repos/${GITHUB_REPOSITORY}/git/refs" \ - -f ref="refs/tags/${SIB_TAG}" \ - -f sha="${COMMIT_SHA}" - echo " + ${SIB_TAG}" - done - - release-skill: - name: Release OpenClaw Skill - needs: resolve-publish - if: ${{ needs.resolve-publish.outputs.skill == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - - - name: Package skill tarball - run: packages/openclaw-skill/scripts/package.sh /tmp - - - name: Upload tarball to GitHub Release + - name: Guard production ref + if: ${{ !inputs.dry-run && github.ref_name != 'main' }} run: | - gh release upload "${{ needs.resolve-publish.outputs.skill-tag }}" \ - "/tmp/moltnet-skill-v${{ needs.resolve-publish.outputs.skill-version }}.tar.gz" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish release - run: gh release edit "${{ needs.resolve-publish.outputs.skill-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-skill-clawhub: - name: Publish Skill to ClawHub - needs: [resolve-publish, release-skill] - if: ${{ always() && needs.resolve-publish.outputs.skill == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 22.19.0 - - - name: Authenticate with ClawHub - env: - CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} - run: npx clawhub auth login --token "$CLAWHUB_TOKEN" --no-browser - - - name: Publish to ClawHub - env: - CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} - run: | - packages/openclaw-skill/scripts/publish-clawhub.sh \ - --changelog "Release ${{ needs.resolve-publish.outputs.skill-tag }}" - - publish-legreffier: - name: Publish LeGreffier to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.legreffier == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - - run: pnpm exec nx run @themoltnet/legreffier:build - - - run: pnpm exec nx run @themoltnet/legreffier:test - - - run: pnpm exec nx run @themoltnet/legreffier:check:pack - - - name: Publish @themoltnet/legreffier - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/legreffier publish --no-git-checks --access public --provenance - - - name: Publish LeGreffier release - run: gh release edit "${{ needs.resolve-publish.outputs.legreffier-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-agent-runtime: - name: Publish Agent Runtime to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.agent-runtime == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 + echo 'Non-dry-run releases must run from main.' + exit 1 - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - - run: pnpm exec nx run @themoltnet/agent-runtime:build - - - run: pnpm exec nx run @themoltnet/agent-runtime:test - - - run: pnpm exec nx run @themoltnet/agent-runtime:check:pack - - - name: Publish @themoltnet/agent-runtime - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/agent-runtime publish --no-git-checks --access public --provenance - - - name: Publish Agent Runtime release - run: gh release edit "${{ needs.resolve-publish.outputs.agent-runtime-tag }}" --draft=false + - name: Run Nx release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-pi-extension: - name: Publish Pi Extension to npm - needs: [resolve-publish, publish-agent-runtime] - if: ${{ always() && needs.resolve-publish.outputs.pi-extension == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - - run: pnpm exec nx run @themoltnet/pi-extension:build - - - run: pnpm exec nx run @themoltnet/pi-extension:test - - - name: Install Pi for extension loader smoke + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_ACCESS: public + NPM_CONFIG_PROVENANCE: 'true' + GOPROXY: https://proxy.golang.org,direct + GONOSUMDB: github.com/getlarge/themoltnet run: | - npm install -g @earendil-works/pi-coding-agent - echo "MOLTNET_PI_BIN=$(npm prefix -g)/bin/pi" >> "$GITHUB_ENV" - - - run: pnpm exec nx run @themoltnet/pi-extension:check:pack - env: - MOLTNET_PI_LOADER_SMOKE: required - - - name: Publish @themoltnet/pi-extension - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/pi-extension publish --no-git-checks --access public --provenance + set -euo pipefail - - name: Publish Pi Extension release - run: gh release edit "${{ needs.resolve-publish.outputs.pi-extension-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-agent-daemon: - name: Publish Agent Daemon to npm - needs: [resolve-publish, publish-agent-runtime, publish-pi-extension] - # Always run (even if upstream publish jobs were skipped because they - # had no release this run); only publish if release-please cut a tag - # for the daemon itself. - if: ${{ always() && needs.resolve-publish.outputs.agent-daemon == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile - - # Nx walks the dep graph (build dependsOn ^build in nx.json) and - # builds @themoltnet/{sdk,agent-runtime,pi-extension} before the - # daemon, all with remote-cache hits when unchanged. - - run: pnpm exec nx run @themoltnet/agent-daemon:build - - - run: pnpm exec nx run @themoltnet/agent-daemon:check:pack - - - name: Publish @themoltnet/agent-daemon - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/agent-daemon publish --no-git-checks --access public --provenance - - - name: Publish Agent Daemon release - run: gh release edit "${{ needs.resolve-publish.outputs.agent-daemon-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-design-system: - name: Publish Design System to npm - needs: resolve-publish - if: ${{ always() && needs.resolve-publish.outputs.design-system == 'true' && !failure() && !cancelled() }} - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org - - - run: pnpm install --frozen-lockfile + RELEASE_ARGS=(--verbose --skip-publish) + PUBLISH_ARGS=(--verbose) + if [ '${{ inputs.first-release || false }}' = 'true' ]; then + RELEASE_ARGS+=(--first-release) + PUBLISH_ARGS+=(--first-release) + fi - - run: pnpm exec nx run @themoltnet/design-system:build + if [ '${{ inputs.dry-run }}' = 'true' ]; then + pnpm exec nx release --dry-run "${RELEASE_ARGS[@]}" + exit 0 + fi - - run: pnpm exec nx run @themoltnet/design-system:check:pack + BEFORE_RELEASE="$(git rev-parse HEAD)" + pnpm exec nx release "${RELEASE_ARGS[@]}" + AFTER_RELEASE="$(git rev-parse HEAD)" - - name: Publish @themoltnet/design-system - uses: nick-fields/retry@v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: pnpm --filter @themoltnet/design-system publish --no-git-checks --access public --provenance + if [ "$AFTER_RELEASE" = "$BEFORE_RELEASE" ]; then + echo 'No release commit was created.' + exit 0 + fi - - name: Publish Design System release - run: gh release edit "${{ needs.resolve-publish.outputs.design-system-tag }}" --draft=false - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + git push origin HEAD:main --follow-tags --no-verify --atomic + pnpm exec nx release publish "${PUBLISH_ARGS[@]}" diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 0abe62f42..000000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "apps/agent-daemon": "0.29.5", - "apps/mcp-server": "0.14.0", - "apps/moltnet-cli": "1.62.0", - "apps/rest-api": "0.26.0", - "libs/agent-runtime": "0.35.0", - "libs/design-system": "0.9.0", - "libs/dspy-adapters": "0.9.4", - "libs/moltnet-api-client": "1.44.1", - "libs/node-red-contrib-core": "0.10.0", - "libs/node-red-theme": "0.3.1", - "libs/pi-extension": "0.33.0", - "libs/sdk": "0.119.0", - "packages/cli": "1.62.0", - "packages/cli/npm/darwin-arm64": "1.62.0", - "packages/cli/npm/darwin-x64": "1.62.0", - "packages/cli/npm/linux-arm64": "1.62.0", - "packages/cli/npm/linux-x64": "1.62.0", - "packages/cli/npm/win32-arm64": "1.62.0", - "packages/cli/npm/win32-x64": "1.62.0", - "packages/github-agent": "0.24.12", - "packages/legreffier-cli": "0.34.6", - "packages/openclaw-skill": "0.28.0" -} diff --git a/apps/moltnet-cli/README.md b/apps/moltnet-cli/README.md index f9e006899..ea0706ce8 100644 --- a/apps/moltnet-cli/README.md +++ b/apps/moltnet-cli/README.md @@ -95,13 +95,13 @@ All API commands accept `--api-url` to override the default (`https://api.themol ## Versioning & Release Coupling -The CLI depends on the generated Go API client (`libs/moltnet-api-client`, module `github.com/getlarge/themoltnet/libs/moltnet-api-client`). Both are versioned independently via release-please. +The CLI depends on the generated Go API client (`libs/moltnet-api-client`, module `github.com/getlarge/themoltnet/libs/moltnet-api-client`). Both are versioned through Nx release groups. **Local dev:** `go.work` at the repo root ties both modules together — `go.work` supersedes the `replace` directive during development. Run `go test ./apps/moltnet-cli/...` from the repo root. -**Release:** goreleaser runs with `GOWORK=off`. The `before.hooks` step in `.goreleaser.yml` drops the `replace` directive and pins the proxy version before building. **Do not remove the `replace` directive from `go.mod`** — it is the anchor that goreleaser strips at release time. Removing it will make the hook a no-op and break releases. +**Release:** Nx release runs the Go artifact publisher with `GOWORK=off`. During release validation, the publisher can use local `replace` directives only for local rehearsals; production releases verify tagged Go modules through GOPROXY. -**Updating the api-client pin:** after a new `libs/moltnet-api-client` tag is published, update the `require github.com/getlarge/themoltnet/libs/moltnet-api-client vX.Y.Z` line in `go.mod` and run `go mod tidy`. The `replace` directive remains; goreleaser drops it transiently at build time. +**Updating the api-client pin:** prefer Nx release versioning so `tools/src/release/go-version-actions.ts` updates Go module dependencies consistently. For manual maintenance, update the `require github.com/getlarge/themoltnet/libs/moltnet-api-client vX.Y.Z` line in `go.mod`, keep the local `replace` directive for development, and run `go mod tidy`. ## See Also diff --git a/apps/moltnet-cli/nx-release-artifacts.json b/apps/moltnet-cli/nx-release-artifacts.json index 63aa6cc17..fc37ece47 100644 --- a/apps/moltnet-cli/nx-release-artifacts.json +++ b/apps/moltnet-cli/nx-release-artifacts.json @@ -3,9 +3,9 @@ "nameTemplate": "{binary}_{version}_{goos}_{goarch}" }, "artifactStore": { - "create": false, + "create": true, "draft": true, - "finalize": false, + "finalize": true, "provider": "github", "releaseTag": "cli-v{version}", "upload": true diff --git a/apps/rest-api/scripts/generate-openapi.ts b/apps/rest-api/scripts/generate-openapi.ts index e4b7188b7..25b8ecf18 100644 --- a/apps/rest-api/scripts/generate-openapi.ts +++ b/apps/rest-api/scripts/generate-openapi.ts @@ -19,16 +19,15 @@ const defaultOutputPath = resolve(__dirname, '..', 'public', 'openapi.json'); /** * Recursively sort object keys so the serialized spec is deterministic and - * matches release-please's `extra-files` JSON updater output byte-for-byte. + * matches the committed release serializer output byte-for-byte. * - * release-please bumps `$.info.version` by reparsing and re-serializing the - * whole file with `JSON.stringify(data, null, 2)` (no key reordering, no array + * Release versioning rewrites `$.info.version` by reparsing and + * re-serializing the whole file with `JSON.stringify(data, null, 2)` (no array * collapse). If `generate` emitted a different shape, the committed spec would * drift from the release commit and CI's OpenAPI gate would fail on every - * release. By emitting the same sorted `JSON.stringify(_, 2)` here — and NOT - * running Prettier on the file (it is in `.prettierignore`) — the three - * writers (this script, release-please, the committed file) agree exactly. - * See diary incident c72108c8. + * release. By emitting the same sorted `JSON.stringify(_, 2)` here and NOT + * running Prettier on the file (it is in `.prettierignore`), this script and + * the committed file agree exactly. See diary incident c72108c8. */ function sortKeysDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortKeysDeep); @@ -127,7 +126,7 @@ async function main() { await app.ready(); const spec = app.swagger(); - // Sort keys + trailing newline so output matches release-please's serializer + // Sort keys + trailing newline so output matches the release serializer // exactly. Do NOT post-process with Prettier (see sortKeysDeep + .prettierignore). const json = JSON.stringify(sortKeysDeep(spec), null, 2) + '\n'; mkdirSync(dirname(outputPath), { recursive: true }); diff --git a/docs/reference/mcp-server.md b/docs/reference/mcp-server.md index 0b9129676..a08a67f72 100644 --- a/docs/reference/mcp-server.md +++ b/docs/reference/mcp-server.md @@ -8,7 +8,7 @@ Authentication is `X-Client-Id` / `X-Client-Secret` on the initial connection; t The MCP server exposes its application version through MCP `serverInfo.version`. That version comes from `apps/mcp-server/package.json` -and is managed by release-please as the `mcp-server` component. +and is managed by the Nx release workflow. The public endpoint stays stable at `https://mcp.themolt.net/mcp`. Tool names are not path-versioned or suffixed by default. diff --git a/docs/reference/nx-release-workflow.md b/docs/reference/nx-release-workflow.md index 22b0b6329..d7627812e 100644 --- a/docs/reference/nx-release-workflow.md +++ b/docs/reference/nx-release-workflow.md @@ -5,14 +5,12 @@ It records how this branch expects Nx release to be used. ## Intent -Nx release should replace release-please as the release orchestration layer. -The goal is to let the Nx project graph and release groups decide ordering, -dependency bumps, changelog/tag generation, Docker image tagging, and Go module -dependency propagation. +Nx release is the release orchestration layer. The goal is to let the Nx project +graph and release groups decide ordering, dependency bumps, changelog/tag +generation, Docker image tagging, and Go module dependency propagation. -Release-please remains the historical workflow until the migration lands. Do -not update release-please configuration for new release behavior on this branch -unless the migration is being rolled back. +Do not add new release-please configuration. The release-please workflow and +manifest were removed when this workflow became authoritative. ## Release Groups @@ -30,11 +28,77 @@ Configured in `nx.json` under `release.groups`: Use groups when invoking release commands. Avoid hand-ordering projects; Nx should derive project ordering and dependent updates. +## Production Workflow + +Production releases run from `.github/workflows/release.yml` by manual +`workflow_dispatch` only. This preserves the operator gate we had with +release-please PR merges: a release is deliberate, reviewed, and triggered by a +human. + +The workflow does not preselect release groups with `nx affected`. Nx release +reads conventional commits since each project's previous release tag and decides +which projects need a new version. This avoids tying release selection to the +latest push range, which can miss unreleased changes from earlier failed or +skipped release attempts. + +```bash +pnpm exec nx release --verbose --skip-publish +git push origin HEAD:main --follow-tags --no-verify --atomic +pnpm exec nx release publish --verbose +``` + +The workflow exposes `dry-run` and `first-release` inputs. `dry-run` defaults to +`true` so the safest manual trigger only plans the release. Dry-runs use the same +Nx release detection, but skip release commits, tags, GitHub Releases, Docker +pushes, and package publishes. `first-release` passes Nx's `--first-release` +flag for bootstrap cases where selected projects do not yet have release tags. + +```bash +pnpm exec nx release --dry-run --verbose --skip-publish +pnpm exec nx release --first-release --verbose --skip-publish +``` + +Important production details: + +- The explicit git push happens between versioning and publishing so Go module + tags are visible to GOPROXY before publish targets run. +- Go library modules and the Go CLI release in the same top-level Nx release + invocation. This keeps Nx's dependency propagation in one versioning pass, so + `apps/moltnet-cli/go.mod` consumes the just-released `moltnet-api-client` and + `dspy-adapters` versions when those modules move. +- The Go CLI artifact publisher creates the draft `cli-v{version}` GitHub + Release, uploads archives and checksums, then undrafts it. +- Go module publish targets verify the pushed module tags through + `GOPROXY=https://proxy.golang.org,direct` with `GOWORK=off`. +- Project changelogs are enabled, but the workspace changelog is disabled. + `automaticFromRef` stays enabled because Nx 22.7 still resolves a workspace + changelog range internally before it skips writing the workspace changelog. + Author sections stay enabled, but GitHub username lookup is disabled so + changelog rendering does not depend on rate-limited GitHub email search calls. +- Current versions resolve from git tags, with a temporary disk fallback for + projects that do not yet have Nx-shaped release tags. After their first Nx + release, the generated tags become the source of truth. +- npm packages publish with public access and provenance through npm config + environment variables plus GitHub Actions OIDC/trusted publishing. The + workflow must keep `permissions.id-token: write`. +- Docker image publish targets push to GHCR. The workflow must keep + `permissions.packages: write` and the GHCR login step. +- Docker release dry-runs still need a working Docker daemon. Nx Docker retags + local images during versioning even when `--dry-run` is set, so the Docker + pre-version hook builds local images before Nx applies release tags. +- Nx release commits include `[skip ci]` so the generated release commit does + not fan out into unrelated CI workflows. + Keep the skip token at the end of the subject. Nx appends fixed release group + and independent project versions to the commit body for top-level multi-group + releases. +- The GitHub Action release target moves the stable major tag, for example + `v0`, after its bundled `dist/main.js` has been committed by the release. + ## Full Rehearsal A dry-run is useful for a quick config sanity check, but it is not enough to -prove that Nx can replace release-please. The migration needs at least one full -release rehearsal with cleanup prepared before running the command. +prove the production release path. This workflow needs at least one full release +rehearsal with cleanup prepared before running the command. Prefer two rehearsal stages: @@ -46,8 +110,8 @@ and npm publish failures without publishing public artifacts. It does not prove GitHub release creation, GitHub release asset upload, npm provenance/OIDC, or GHCR package permissions. -Run the rehearsal from a disposable worktree on a dedicated branch. Do not run -it from the main development worktree: +Run rehearsals from a disposable worktree on a dedicated branch. Do not run them +from the main development worktree: ```bash git worktree add --detach .worktrees/nx-release-rehearsal origin/main @@ -399,12 +463,16 @@ NX_RELEASE_DOCKER_PROJECTS=@moltnet/rest-api pnpm exec nx release version patch ## Expected Operator Flow 1. Confirm release groups and tag patterns in `nx.json`. -2. Run a dry-run for the affected group or groups. -3. Inspect planned file changes, tags, and Go validation commands. -4. Run focused tests for any changed release helpers. -5. Prepare the rehearsal cleanup values and permissions. -6. Run the full non-dry release rehearsal from a disposable worktree. -7. Clean up rehearsal side effects before merging release automation. +2. Trigger the `Release` workflow manually on `main` with `dry-run: true`. +3. Inspect planned file changes, tags, artifact operations, and Go validation + commands. +4. Run focused tests for any changed release helpers if the dry-run exposes a + release-helper change. +5. Trigger the `Release` workflow manually on `main` with `dry-run: false` when + the plan is acceptable. + +For release automation changes, also run the full non-dry rehearsal from a +disposable worktree and clean up rehearsal side effects before merging. Do not manually edit generated changelogs, release tags, or dependent version bumps unless Nx release output is wrong and the release helper/config is being diff --git a/docs/understand/infrastructure.md b/docs/understand/infrastructure.md index 4726be453..8289b7851 100644 --- a/docs/understand/infrastructure.md +++ b/docs/understand/infrastructure.md @@ -240,8 +240,8 @@ Both call the reusable `_deploy.yml` workflow (build Docker image, push to GHCR ### Deployable app versions -Server apps that expose a public contract are release-please components. The -package version in each app's `package.json` is the source of truth and must be +Server apps that expose a public contract are Nx release projects. The package +version in each app's `package.json` is the source of truth and must be propagated into public metadata and OpenTelemetry: - REST API: OpenAPI `info.version` and OTel `service.version` @@ -357,24 +357,33 @@ The e5-small-v2 ONNX model (~33MB) is lazy-loaded on first embedding request. Fi ## Release Pipeline -Releases are automated via [release-please](https://github.com/googleapis/release-please) + GitHub Actions (`.github/workflows/release.yml`). A push to `main` triggers the pipeline: - -1. **Release Please** — creates/updates a release PR. The config uses the `node-workspace` plugin so Node packages that depend on other workspace packages (for example `apps/agent-daemon` bundling `@themoltnet/pi-extension`, `@themoltnet/agent-runtime`, and `@themoltnet/sdk`) are pulled into the same release round when those deps bump. The CLI packages remain in their own `linked-versions` group. -2. **Publish SDK to npm** — builds, tests, publishes `@themoltnet/sdk` with provenance, then publishes the draft release -3. **Release CLI binaries** — cross-compiles Go binaries via GoReleaser, pushes Homebrew formula, uploads assets to the draft release, then publishes it -4. **Publish CLI to npm** — publishes the `@themoltnet/cli` npm wrapper (thin binary downloader) -5. **Publish bundled Node apps/libs** — jobs such as `publish-agent-daemon`, `publish-agent-runtime`, and `publish-pi-extension` publish the packages selected by the release PR - -Releases are created as drafts (`"draft": true` in `release-please-config.json`) to support [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). Assets are uploaded while the release is still a draft, then each job publishes its release as the final step. Once published, the release and its assets become immutable. +Releases are automated via Nx Release + GitHub Actions +(`.github/workflows/release.yml`). A push to `main` uses conventional commits +and Nx affected projects to select release groups, pushes each release commit +and its tags, then runs `nx release publish` for the same groups. Nx uses the +project graph and release groups in `nx.json` to decide version ordering, +changelog generation, git tags, dependency updates, Docker image tags, and +publish targets. + +The Go artifact publisher creates the draft `cli-v{version}` GitHub Release, +cross-compiles the CLI, uploads the archives/checksums to that release, then +publishes the release. Go library module releases are git tags and run in the +same release job as the Go CLI when a Go module changes, so the CLI's `go.mod` +is updated to the latest module tags before CLI artifacts are built. Go modules +are verified through the public Go proxy during publish. ### Release configuration files -| File | Purpose | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `release-please-config.json` | Defines releasable packages and plugins (`node-workspace` for workspace-dep propagation, `linked-versions` for the CLI family) | -| `.release-please-manifest.json` | Tracks current versions | -| `apps/moltnet-cli/.goreleaser.yml` | Cross-compilation targets, archive format, Homebrew formula publisher | -| `packages/cli/` | npm wrapper — postinstall downloads the correct Go binary | +| File | Purpose | +| ---------------------------------------------- | ---------------------------------------------------------------------------- | +| `nx.json` | Release groups, tag patterns, version actions, Docker release settings | +| `.github/workflows/release.yml` | Production Nx release workflow | +| `apps/moltnet-cli/nx-release-artifacts.json` | Go CLI build matrix, archive/checksum settings, GitHub Release upload target | +| `tools/src/release/go-version-actions.ts` | Go module version/dependency propagation during Nx versioning | +| `tools/src/release/go-artifact-publisher.ts` | Go CLI cross-compile/archive/upload publisher | +| `tools/src/release/go-module-publisher.cli.ts` | Go module publish verification through GOPROXY | +| `tools/src/release/github-action-publisher.ts` | GitHub Action release publisher that moves the stable major tag | +| `packages/cli/` | npm wrapper — postinstall downloads the correct Go binary | ### npm trusted publishing (OIDC) @@ -391,51 +400,18 @@ The SDK and CLI npm packages use [npm trusted publishing](https://docs.npmjs.com The workflow uses `permissions: id-token: write` so GitHub Actions can mint OIDC tokens, and `actions/setup-node` with `registry-url` to configure the `.npmrc`. -### Homebrew tap (GitHub App) - -The CLI is distributed via `brew install --cask getlarge/moltnet/moltnet`. GoReleaser pushes the cask to the [getlarge/homebrew-moltnet](https://github.com/getlarge/homebrew-moltnet) repository using a short-lived token from a GitHub App. - -**GitHub App setup:** - -1. Create a GitHub App (org or personal) with **Repository permissions > Contents: Read and write** -2. **Install the app** on the `getlarge` organization — select **"Only select repositories"** and choose `homebrew-moltnet` -3. Store the app credentials as repository secrets on `getlarge/themoltnet`: - -| Secret | Value | -| ------------------------- | ----------------------------------------- | -| `MOLTNET_RELEASE_APP_ID` | The GitHub App's numeric App ID | -| `MOLTNET_RELEASE_APP_KEY` | The GitHub App's private key (PEM format) | - -The workflow uses `actions/create-github-app-token@v1` to mint a scoped installation token at runtime, passed to GoReleaser as `HOMEBREW_TAP_TOKEN`. The token is short-lived and limited to the `homebrew-moltnet` repository. - -> **Troubleshooting:** If the token step fails with `404 Not Found` on `/repos/getlarge/homebrew-moltnet/installation`, the app is **not installed** on the repository. Go to the app's settings page > **Install App** and grant it access to `homebrew-moltnet`. - ### CI secrets summary -| Secret | Used by | Purpose | -| ------------------------- | ----------------------- | ----------------------------------- | -| `MOLTNET_RELEASE_APP_ID` | `release-cli` job | GitHub App ID for Homebrew tap push | -| `MOLTNET_RELEASE_APP_KEY` | `release-cli` job | GitHub App private key (PEM) | -| `CLAWHUB_TOKEN` | `publish-skill-clawhub` | ClawHub CLI auth for skill publish | -| `FLY_API_TOKEN` | Deploy workflows | Fly.io deployment | +| Secret | Used by | Purpose | +| --------------- | ---------------- | ----------------- | +| `FLY_API_TOKEN` | Deploy workflows | Fly.io deployment | npm publishing requires no secrets — it uses OIDC trusted publishing. ### OpenClaw skill publishing -The MoltNet OpenClaw skill (`packages/openclaw-skill/`) is a markdown bundle — not an npm package. It's distributed through two channels: - -| Channel | Installation | Automated by | -| -------------------- | --------------------------------------------------------- | --------------------------- | -| **ClawHub registry** | `clawhub install moltnet` | `publish-skill-clawhub` job | -| **GitHub Release** | `tar -xzf moltnet-skill-v*.tar.gz -C ~/.openclaw/skills/` | `release-skill` job | - -Both are triggered by the same Release Please cycle. The skill uses `release-type: simple` with a `version.txt` file (not `package.json`). - -**CI jobs in `release.yml`:** - -1. **`release-skill`** — runs `packages/openclaw-skill/scripts/package.sh` to create a tarball, uploads it to the GitHub Release, then undrafts -2. **`publish-skill-clawhub`** — installs `clawhub` CLI, authenticates with `CLAWHUB_TOKEN`, runs `packages/openclaw-skill/scripts/publish-clawhub.sh` +The MoltNet OpenClaw skill (`packages/openclaw-skill/`) is a legacy markdown +bundle and is not part of the Nx release workflow. **CI validation in `ci.yml`:** @@ -446,12 +422,6 @@ The `skill-check` job validates on every PR: - `version.txt` contains valid semver - Tarball packaging succeeds -**Required secret:** - -| Secret | Used by | Purpose | How to obtain | -| --------------- | ----------------------- | ---------------------------------- | -------------------------------------------------------- | -| `CLAWHUB_TOKEN` | `publish-skill-clawhub` | ClawHub CLI auth for CI publishing | Run `clawhub login` locally, copy token from config file | - **Manual usage:** ```bash diff --git a/nx.json b/nx.json index 5c5698c98..6aac8ec8c 100644 --- a/nx.json +++ b/nx.json @@ -109,13 +109,13 @@ ], "release": { "changelog": { - "projectChangelogs": true, - "workspaceChangelog": { - "createRelease": false, + "automaticFromRef": true, + "projectChangelogs": { "renderOptions": { - "applyUsernameToAuthors": true + "applyUsernameToAuthors": false } - } + }, + "workspaceChangelog": false }, "conventionalCommits": { "types": { @@ -177,6 +177,9 @@ "production": "{versionActionsVersion}" } }, + "git": { + "commitMessage": "chore(release): publish {version} [skip ci]" + }, "groups": { "cli": { "projects": [ @@ -248,8 +251,8 @@ } }, "version": { - "currentVersionResolver": "disk", - "deleteVersionPlans": true, + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk", "specifierSource": "conventional-commits", "updateDependents": "auto", "versionActionsOptions": { @@ -262,8 +265,7 @@ ], "goReleaseValidationRoots": ["apps/moltnet-cli"] } - }, - "versionPlans": true + } }, "sync": { "applyChanges": true diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index ce49d4a1b..000000000 --- a/release-please-config.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "commit-batch-size": 25, - "commit-search-depth": 250, - "draft": true, - "force-tag-creation": true, - "include-component-in-tag": true, - "include-v-in-tag": true, - "packages": { - "apps/agent-daemon": { - "bump-minor-pre-major": true, - "component": "agent-daemon", - "release-type": "node" - }, - "apps/mcp-server": { - "bump-minor-pre-major": true, - "component": "mcp-server", - "release-type": "node" - }, - "apps/moltnet-cli": { - "bump-minor-pre-major": true, - "component": "cli", - "release-type": "go" - }, - "apps/rest-api": { - "bump-minor-pre-major": true, - "component": "rest-api", - "extra-files": [ - { - "jsonpath": "$.info.version", - "path": "public/openapi.json", - "type": "json" - } - ], - "release-type": "node" - }, - "libs/agent-runtime": { - "component": "agent-runtime", - "release-type": "node" - }, - "libs/design-system": { - "component": "design-system", - "release-type": "node" - }, - "libs/dspy-adapters": { - "bump-minor-pre-major": true, - "component": "dspy-adapters", - "release-type": "go" - }, - "libs/moltnet-api-client": { - "bump-minor-pre-major": true, - "changelog-sections": [ - { - "section": "Features", - "type": "feat" - }, - { - "section": "Bug Fixes", - "type": "fix" - }, - { - "section": "Codegen", - "type": "chore" - } - ], - "component": "moltnet-api-client", - "release-type": "go" - }, - "libs/node-red-contrib-core": { - "bump-minor-pre-major": true, - "component": "node-red-contrib-core", - "release-type": "node" - }, - "libs/node-red-theme": { - "component": "node-red-theme", - "release-type": "node" - }, - "libs/pi-extension": { - "component": "pi-extension", - "release-type": "node" - }, - "libs/sdk": { - "component": "sdk", - "release-type": "node" - }, - "packages/cli": { - "component": "cli-wrapper", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/darwin-arm64": { - "component": "cli-darwin-arm64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/darwin-x64": { - "component": "cli-darwin-x64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/linux-arm64": { - "component": "cli-linux-arm64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/linux-x64": { - "component": "cli-linux-x64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/win32-arm64": { - "component": "cli-win32-arm64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/cli/npm/win32-x64": { - "component": "cli-win32-x64", - "release-type": "node", - "skip-changelog": true, - "skip-github-release": true - }, - "packages/github-agent": { - "component": "github-agent", - "release-type": "node" - }, - "packages/legreffier-cli": { - "component": "legreffier", - "release-type": "node" - }, - "packages/openclaw-skill": { - "component": "openclaw-skill", - "release-type": "simple" - } - }, - "plugins": [ - { - "type": "node-workspace" - }, - { - "components": [ - "cli", - "cli-wrapper", - "cli-darwin-arm64", - "cli-darwin-x64", - "cli-linux-arm64", - "cli-linux-x64", - "cli-win32-arm64", - "cli-win32-x64" - ], - "groupName": "cli", - "type": "linked-versions" - } - ], - "release-search-depth": 80, - "separate-pull-requests": false, - "tag-separator": "-" -} diff --git a/tools/release/docker-preversion.mjs b/tools/release/docker-preversion.mjs index 433dfe183..4626c11e7 100644 --- a/tools/release/docker-preversion.mjs +++ b/tools/release/docker-preversion.mjs @@ -2,13 +2,6 @@ import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import process from 'node:process'; -if (process.env.NX_DRY_RUN === 'true') { - process.stdout.write( - 'Skipping Docker pre-version build during nx release dry-run.\n', - ); - process.exit(0); -} - const nxConfig = JSON.parse(readFileSync('nx.json', 'utf8')); const releaseDockerProjects = nxConfig.release?.groups?.['docker-images']?.projects ?? []; @@ -29,7 +22,7 @@ const projects = process.stdout.write( `Building Docker release images for ${projects.join( ', ', - )} before Nx retags them.\n`, + )} before Nx retags them${process.env.NX_DRY_RUN === 'true' ? ' during dry-run' : ''}.\n`, ); execFileSync( diff --git a/tools/src/release/github-action-publisher.ts b/tools/src/release/github-action-publisher.ts index 234330626..c9ee84596 100644 --- a/tools/src/release/github-action-publisher.ts +++ b/tools/src/release/github-action-publisher.ts @@ -24,6 +24,7 @@ function normalizeBooleanOptionValues(args: string[]) { 'dryRun', 'firstRelease', 'first-release', + 'provenance', 'verbose', 'yes', ]); @@ -58,6 +59,9 @@ function parsePublisherArgs(argv = process.argv.slice(2)): Options { project: { type: 'string', }, + provenance: { + type: 'boolean', + }, registry: { type: 'string', }, diff --git a/tools/src/release/go-artifact-publisher.cli.ts b/tools/src/release/go-artifact-publisher.cli.ts index 7be866e30..680aab1db 100644 --- a/tools/src/release/go-artifact-publisher.cli.ts +++ b/tools/src/release/go-artifact-publisher.cli.ts @@ -20,6 +20,7 @@ function normalizeBooleanOptionValues(args: string[]) { 'dryRun', 'skip-upload', 'skipUpload', + 'provenance', 'verbose', 'yes', ]); @@ -67,6 +68,9 @@ export function createCliRunOptions(argv = process.argv, env = process.env) { otp: { type: 'string', }, + provenance: { + type: 'boolean', + }, userconfig: { type: 'string', }, diff --git a/tools/src/release/go-artifact-publisher.test.ts b/tools/src/release/go-artifact-publisher.test.ts index 12b0e7927..56b222958 100644 --- a/tools/src/release/go-artifact-publisher.test.ts +++ b/tools/src/release/go-artifact-publisher.test.ts @@ -406,6 +406,7 @@ describe('go artifact publisher', () => { dryRun: true, verbose: true, skipUpload: false, + useLocalReplaces: false, }); expect( createCliRunOptions( @@ -417,6 +418,7 @@ describe('go artifact publisher', () => { dryRun: false, verbose: false, skipUpload: true, + useLocalReplaces: false, }); expect( createCliRunOptions( @@ -428,6 +430,7 @@ describe('go artifact publisher', () => { '--dryRun=true', '--tag=next', '--access=public', + '--provenance', '--skipUpload', ], {}, @@ -437,6 +440,7 @@ describe('go artifact publisher', () => { dryRun: true, verbose: false, skipUpload: true, + useLocalReplaces: false, }); }); diff --git a/tools/src/release/go-module-publisher.cli.ts b/tools/src/release/go-module-publisher.cli.ts index 3a12da98e..9b5720721 100644 --- a/tools/src/release/go-module-publisher.cli.ts +++ b/tools/src/release/go-module-publisher.cli.ts @@ -16,6 +16,7 @@ function normalizeBooleanOptionValues(args: string[]) { 'dryRun', 'firstRelease', 'first-release', + 'provenance', 'verbose', 'yes', ]); @@ -40,6 +41,7 @@ export function createCliOptions(argv = process.argv, env = process.env) { firstRelease: { type: 'boolean' }, goproxy: { type: 'string' }, otp: { type: 'string' }, + provenance: { type: 'boolean' }, 'project-root': { type: 'string' }, registry: { type: 'string' }, tag: { type: 'string' },