Fix non-default-base issue auto-close and misleading link diagnostics (#1895) #3137
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Checks and release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # Do not use workflow-level path filters here. The detect-changes job must | |
| # run for every PR update so non-code-only changes still produce completed | |
| # checks while expensive jobs are skipped by job-level conditions. | |
| # Manual release support - consolidated here to work with npm trusted publishing | |
| # npm only allows ONE workflow file as trusted publisher, so all publishing | |
| # must go through this workflow (release.yml) | |
| workflow_dispatch: | |
| inputs: | |
| release_mode: | |
| description: 'Manual release mode' | |
| required: true | |
| type: choice | |
| default: 'instant' | |
| options: | |
| - instant | |
| - changeset-pr | |
| bump_type: | |
| description: 'Manual release type' | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| description: | |
| description: 'Manual release description (optional)' | |
| required: false | |
| type: string | |
| # Concurrency: Only one workflow run per branch at a time | |
| # - For main branch (releases): cancel older runs to prevent blocking newer releases | |
| # When multiple commits are pushed quickly, we want the latest to release, not wait | |
| # - For PR branches: queue runs to avoid cancelling checks on force-pushes | |
| # IMPORTANT: Docker jobs use !cancelled() instead of always() to ensure they respect | |
| # concurrency cancellation. Using always() prevents workflow cancellation entirely. | |
| # See: docs/case-studies/issue-1274/README.md and docs/case-studies/issue-1278/README.md | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref == 'refs/heads/main' }} | |
| env: # Force Node.js 24 for actions without native support. See issue #1417. | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| # === DETECT CHANGES === | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mjs-changed: ${{ steps.changes.outputs.mjs }} | |
| package-changed: ${{ steps.changes.outputs.package }} | |
| docs-changed: ${{ steps.changes.outputs.docs }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow }} | |
| docker-changed: ${{ steps.changes.outputs.docker }} | |
| any-code-changed: ${{ steps.changes.outputs.code }} | |
| helm-changed: ${{ steps.changes.outputs.helm }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changes | |
| id: changes | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_EVENT_ACTION: ${{ github.event.action }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| GITHUB_BEFORE_SHA: ${{ github.event.before }} | |
| GITHUB_AFTER_SHA: ${{ github.event.after }} | |
| run: node scripts/detect-code-changes.mjs | |
| # === VERSION CHANGE CHECK === | |
| # Prohibit manual version changes in package.json - versions should only be changed by CI/CD | |
| # Only runs when code or package.json changed (Issue #1436: skip for unrelated file changes) | |
| version-check: | |
| name: Check for Manual Version Changes | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.package-changed == 'true') | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for version changes in package.json | |
| env: | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: node scripts/check-version.mjs | |
| # === CHANGESET CHECK - only runs on PRs with code changes === | |
| # Docs-only PRs (./docs folder, markdown files) don't require changesets | |
| changeset-check: | |
| name: Check for Changesets | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '24.x' | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Check for changesets | |
| env: | |
| # Pass PR context to the validation script | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| # Skip changeset check for automated version PRs | |
| if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then | |
| echo "Skipping changeset check for automated release PR" | |
| exit 0 | |
| fi | |
| # Run changeset validation script | |
| # This validates that exactly ONE changeset was ADDED by this PR | |
| # Pre-existing changesets from other merged PRs are ignored | |
| node scripts/validate-changeset.mjs | |
| # === FAST CHECKS (FAIL FAST) === | |
| # These checks run quickly (~7-21 seconds each) and should pass before long-running tests | |
| # === COMPILATION & SYNTAX CHECKS === | |
| test-compilation: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check] | |
| # Run if: push event, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR) | |
| if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Test solve.mjs compilation | |
| run: | | |
| echo "Testing solve.mjs compilation..." | |
| timeout 30s node --check src/solve.mjs | |
| echo "solve.mjs compiles successfully" | |
| - name: Test hive.mjs compilation | |
| run: | | |
| echo "Testing hive.mjs compilation..." | |
| timeout 30s node --check src/hive.mjs | |
| echo "hive.mjs compiles successfully" | |
| - name: Check Node.js syntax for all .mjs files | |
| run: bash scripts/check-mjs-syntax.sh | |
| # === ESLINT CODE QUALITY CHECK === | |
| # Lint runs independently of changeset-check - it's a fast check that should always run | |
| # See: docs/case-studies/issue-1023 for why this dependency was removed | |
| # IMPORTANT: ESLint now includes max-lines rule (1500 lines) to synchronize with check-file-line-limits | |
| # See docs/case-studies/issue-1141 for why fresh merge simulation is critical | |
| lint: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.mjs-changed == 'true' || needs.detect-changes.outputs.docs-changed == 'true' || needs.detect-changes.outputs.package-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| # For PRs, fetch enough history to merge with base branch | |
| fetch-depth: 0 | |
| - name: Simulate fresh merge with base branch (PR only) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: bash scripts/simulate-fresh-merge.sh | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs ci | |
| - name: Run Prettier format check | |
| run: | | |
| echo "Running Prettier format check..." | |
| npm run format:check | |
| echo "Prettier format check passed" | |
| - name: Run ESLint | |
| run: | | |
| echo "Running ESLint code quality checks..." | |
| npm run lint | |
| echo "ESLint checks passed" | |
| - name: Run code duplication check | |
| run: | | |
| echo "Running jscpd code duplication detection..." | |
| npm run check:duplication | |
| echo "Code duplication check passed" | |
| # === FILE LINE LIMIT CHECK === | |
| # IMPORTANT: This check must validate the ACTUAL merge result, not a stale merge preview. | |
| # See docs/case-studies/issue-1141 for why this is critical. | |
| check-file-line-limits: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check] | |
| # Run if: push event, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR) | |
| if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && (needs.detect-changes.outputs.mjs-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| # For PRs, fetch enough history to merge with base branch | |
| fetch-depth: 0 | |
| - name: Simulate fresh merge with base branch (PR only) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: bash scripts/simulate-fresh-merge.sh | |
| - name: Check file line limits (.mjs files and release.yml) | |
| run: bash scripts/check-file-line-limits.sh | |
| # === LONG-RUNNING CHECKS === | |
| # These checks take significant time (30s to 4+ minutes) and only run after all fast checks pass | |
| # === UNIT TESTS === | |
| test-suites: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check, test-compilation, lint, check-file-line-limits] | |
| if: | | |
| always() && | |
| !cancelled() && | |
| !contains(needs.*.result, 'failure') && | |
| (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && | |
| needs.test-compilation.result == 'success' && | |
| (needs.lint.result == 'success' || needs.lint.result == 'skipped') && | |
| (needs.check-file-line-limits.result == 'success' || needs.check-file-line-limits.result == 'skipped') && | |
| (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Use Node.js 20.x | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Pre-install use-m packages (issue #1724) | |
| run: node scripts/preinstall-use-m-packages.mjs | |
| - name: Run default test suite | |
| run: npm test | |
| - name: Run GitHub integration test suite | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.TEST_GITHUB_USER_TOKEN || secrets.GITHUB_TOKEN }} | |
| TEST_GITHUB_USERNAME: ${{ secrets.TEST_GITHUB_USERNAME }} | |
| TEST_GITHUB_USER_TOKEN: ${{ secrets.TEST_GITHUB_USER_TOKEN }} | |
| run: node scripts/run-tests.mjs --suite github-integration | |
| # === EXECUTION TESTS === | |
| test-execution: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check, test-compilation, lint, check-file-line-limits] | |
| if: | | |
| always() && | |
| !cancelled() && | |
| !contains(needs.*.result, 'failure') && | |
| (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && | |
| needs.test-compilation.result == 'success' && | |
| (needs.lint.result == 'success' || needs.lint.result == 'skipped') && | |
| (needs.check-file-line-limits.result == 'success' || needs.check-file-line-limits.result == 'skipped') && | |
| (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Use Node.js 20.x | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Pre-install use-m packages (issue #1724) | |
| run: node scripts/preinstall-use-m-packages.mjs | |
| - name: Test solve.mjs execution | |
| run: | | |
| echo "Testing solve.mjs basic execution..." | |
| timeout 10s ./src/solve.mjs --help || echo "Help command completed" | |
| echo "solve.mjs executes without critical errors" | |
| timeout 10s ./src/solve.mjs --version || true | |
| - name: Verify log files contain version and command (Issue #517) | |
| run: bash scripts/verify-log-file-contents.sh | |
| - name: Test hive.mjs execution | |
| run: | | |
| echo "Testing hive.mjs basic execution..." | |
| timeout 10s ./src/hive.mjs --help || echo "Help command completed" | |
| echo "hive.mjs executes without critical errors" | |
| timeout 10s ./src/hive.mjs --version || true | |
| - name: Test telegram-bot.mjs execution | |
| run: | | |
| echo "Testing telegram-bot.mjs basic execution..." | |
| timeout 10s ./src/telegram-bot.mjs --help || echo "Help command completed" | |
| echo "telegram-bot.mjs executes without critical errors" | |
| echo "" | |
| echo "Testing telegram-bot.mjs --dry-run with issue #487 command..." | |
| timeout 10s ./src/telegram-bot.mjs \ | |
| --token "test_token_123" \ | |
| --allowed-chats "(-1002975819706 -1002861722681)" \ | |
| --no-hive \ | |
| --solve-overrides $'( \n --auto-continue\n --attach-logs\n --verbose\n --no-tool-check\n)' \ | |
| --dry-run | |
| echo "Issue #487 command passes with --dry-run" | |
| - name: Test memory-check.mjs execution | |
| run: | | |
| ./src/memory-check.mjs --help | |
| ./src/memory-check.mjs --min-memory 10 --min-disk-space 100 --json | |
| - name: Test global command installation | |
| run: bash scripts/test-global-commands.sh | |
| - name: Test hive dry-run with solve integration | |
| run: | | |
| echo "Testing hive dry-run mode with solve command integration..." | |
| timeout 30s ./src/hive.mjs https://github.com/test/repo --dry-run --skip-claude-check --once --max-issues 1 2>&1 | tee hive_dry_run.log || true | |
| if grep -q "solve.*--dry-run.*--skip-claude-check" hive_dry_run.log; then | |
| echo "hive correctly passes --dry-run and --skip-claude-check flags to solve command" | |
| else | |
| echo "Could not verify flag propagation in dry-run mode (may be due to no issues found)" | |
| fi | |
| echo "" | |
| echo "Testing solve.mjs with --dry-run and --skip-claude-check flags..." | |
| timeout 10s ./src/solve.mjs https://github.com/test/repo/issues/1 --dry-run --skip-claude-check 2>&1 | head -20 || true | |
| echo "solve.mjs accepts --dry-run and --skip-claude-check flags" | |
| - name: Test --auto-fork option | |
| run: bash scripts/test-auto-fork-option.sh | |
| # === MEMORY CHECKS - LINUX === | |
| memory-check-linux: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check, test-compilation, lint, check-file-line-limits] | |
| if: | | |
| always() && | |
| !cancelled() && | |
| !contains(needs.*.result, 'failure') && | |
| (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && | |
| needs.test-compilation.result == 'success' && | |
| (needs.lint.result == 'success' || needs.lint.result == 'skipped') && | |
| (needs.check-file-line-limits.result == 'success' || needs.check-file-line-limits.result == 'skipped') && | |
| (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Use Node.js 20.x | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: System info | |
| run: | | |
| echo "=== System Information ===" | |
| uname -a | |
| echo "" | |
| echo "=== Memory Information ===" | |
| free -h | |
| echo "" | |
| echo "=== Disk Information ===" | |
| df -h | |
| echo "" | |
| echo "=== CPU Information ===" | |
| lscpu | head -20 | |
| - name: Run memory-check tests | |
| run: | | |
| chmod +x tests/test-memory-check.mjs | |
| node tests/test-memory-check.mjs | |
| - name: Test memory-check with various thresholds | |
| run: | | |
| echo "Testing with low thresholds (should pass)..." | |
| ./src/memory-check.mjs --min-memory 10 --min-disk-space 100 --json | |
| echo "" | |
| echo "Testing verbose output..." | |
| ./src/memory-check.mjs --min-memory 10 --min-disk-space 100 | |
| echo "" | |
| echo "Testing quiet mode..." | |
| ./src/memory-check.mjs --min-memory 10 --min-disk-space 100 --quiet --json | |
| - name: Test memory-check failure conditions | |
| run: | | |
| echo "Testing with impossible memory requirement (should fail)..." | |
| if ./src/memory-check.mjs --min-memory 999999 --exit-on-failure --quiet --json; then | |
| echo "ERROR: Should have failed with impossible memory requirement" | |
| exit 1 | |
| else | |
| echo "Correctly failed with impossible memory requirement" | |
| fi | |
| echo "" | |
| echo "Testing with impossible disk requirement (should fail)..." | |
| if ./src/memory-check.mjs --min-disk-space 999999999 --exit-on-failure --quiet --json; then | |
| echo "ERROR: Should have failed with impossible disk requirement" | |
| exit 1 | |
| else | |
| echo "Correctly failed with impossible disk requirement" | |
| fi | |
| # === DOCUMENTATION VALIDATION (FAST) === | |
| validate-docs: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check] | |
| # Run if: push event, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR) | |
| if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && needs.detect-changes.outputs.docs-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Use Node.js 20.x | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Validate documentation files | |
| env: | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| GITHUB_BEFORE_SHA: ${{ github.event.before }} | |
| GITHUB_AFTER_SHA: ${{ github.event.after }} | |
| run: | | |
| node tests/docs-validation.mjs | |
| node tests/test-docs-language-sync.mjs | |
| # === DOCKER PR CHECK === | |
| docker-pr-check: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check, test-compilation, lint, check-file-line-limits] | |
| # Run if: changeset-check succeeded OR was skipped (docs-only PR), and required checks passed | |
| if: | | |
| always() && | |
| !cancelled() && | |
| !contains(needs.*.result, 'failure') && | |
| github.event_name == 'pull_request' && | |
| (needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && | |
| (needs.test-compilation.result == 'success' || needs.test-compilation.result == 'skipped') && | |
| (needs.lint.result == 'success' || needs.lint.result == 'skipped') && | |
| (needs.check-file-line-limits.result == 'success' || needs.check-file-line-limits.result == 'skipped') && | |
| (needs.detect-changes.outputs.docker-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: konard/hive-mind | |
| DIND_IMAGE_NAME: konard/hive-mind-dind | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24.x | |
| - name: Verify Docker release-order contract | |
| run: | | |
| echo "Statically verifying: release Docker publish waits for npm availability" | |
| echo "and passes the exact published version into Docker builds." | |
| node tests/test-docker-release-order.mjs | |
| - name: Free up disk space | |
| run: node scripts/free-disk-space.mjs | |
| - name: Show Box base image versions | |
| run: | | |
| BOX_VERSION=$(grep '^FROM konard/box:' Dockerfile | sed 's/FROM konard\/box://') | |
| BOX_DIND_VERSION=$(grep '^FROM konard/box-dind:' Dockerfile.dind | sed 's/FROM konard\/box-dind://') | |
| echo "Box base image version: konard/box:${BOX_VERSION}" | |
| echo "Box DinD base image version: konard/box-dind:${BOX_DIND_VERSION}" | |
| echo "This pinned version ensures stable, reproducible builds." | |
| - name: Build Docker images | |
| # PR builds install @link-assistant/hive-mind@latest (the currently | |
| # published version on npm, which may pre-date this PR). The Dockerfile | |
| # tolerates a missing configure-claude bin in that case. Release builds | |
| # install the exact version just published and enforce the bin strictly. | |
| run: | | |
| BOX_VERSION=$(grep '^FROM konard/box:' Dockerfile | sed 's/FROM konard\/box://') | |
| BOX_DIND_VERSION=$(grep '^FROM konard/box-dind:' Dockerfile.dind | sed 's/FROM konard\/box-dind://') | |
| echo "Building Docker image from konard/box:${BOX_VERSION} base..." | |
| echo "Note: General-purpose tools are inherited from pinned konard/box:${BOX_VERSION}" | |
| echo "This image adds AI-specific tools on top of the Box base." | |
| echo "Installing @link-assistant/hive-mind@latest; configure-claude may not yet be present in the published package." | |
| echo "" | |
| # Multi-platform builds (amd64+arm64) are tested in docker-publish during release. | |
| # PR checks only validate amd64 using standard docker build (not buildx) so the | |
| # image is loaded into the Docker daemon for container verification. | |
| docker build --progress=plain -t ${{ env.IMAGE_NAME }}:test . 2>&1 | tee build-output.log | |
| echo "" | |
| echo "Building Docker-in-Docker image from konard/box-dind:${BOX_DIND_VERSION} base..." | |
| docker build --progress=plain -f Dockerfile.dind -t ${{ env.DIND_IMAGE_NAME }}:test . 2>&1 | tee build-dind-output.log | |
| echo "" | |
| echo "Docker images built successfully" | |
| docker images | grep -E "${{ env.IMAGE_NAME }}|${{ env.DIND_IMAGE_NAME }}|REPOSITORY" | |
| - name: Verify build completed without errors | |
| run: | | |
| echo "Checking build logs for critical errors..." | |
| if grep -E 'unbound variable' build-output.log build-dind-output.log; then | |
| echo "ERROR: Unbound variable error detected in Docker build" | |
| grep -E 'unbound variable' build-output.log build-dind-output.log || true | |
| exit 1 | |
| fi | |
| echo "Build log check completed" | |
| - name: Verify system & development tools in running containers | |
| run: | | |
| BOX_VERSION=$(grep '^FROM konard/box:' Dockerfile | sed 's/FROM konard\/box://') | |
| BOX_DIND_VERSION=$(grep '^FROM konard/box-dind:' Dockerfile.dind | sed 's/FROM konard\/box-dind://') | |
| echo "=== Verifying hive-mind Docker image ===" | |
| echo "Base: konard/box:${BOX_VERSION} (pinned) + AI-specific tools" | |
| echo "" | |
| # Run verification script inside container (mounts script as read-only volume) | |
| # Verifies box user setup (/home/box access), dev tools, AI tools, | |
| # and configure-claude (tolerantly for PR builds). | |
| docker run --rm \ | |
| -v "$(pwd)/scripts/verify-docker-image.sh:/verify-docker-image.sh:ro" \ | |
| ${{ env.IMAGE_NAME }}:test \ | |
| bash /verify-docker-image.sh | |
| echo "" | |
| echo "=== Verifying hive-mind Docker-in-Docker image ===" | |
| echo "Base: konard/box-dind:${BOX_DIND_VERSION} (pinned) + AI-specific tools" | |
| docker run --rm --privileged \ | |
| -v "$(pwd)/scripts/verify-docker-image.sh:/verify-docker-image.sh:ro" \ | |
| ${{ env.DIND_IMAGE_NAME }}:test \ | |
| bash /verify-docker-image.sh | |
| bash scripts/verify-dind-exec-defaults.sh "${{ env.DIND_IMAGE_NAME }}:test" | |
| echo "" | |
| echo "All system, development, and nested Docker verification checks passed!" | |
| # === HELM PR CHECK (FAST) === | |
| helm-pr-check: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changeset-check] | |
| # Only runs when helm chart, package.json, or workflow files changed (Issue #1436) | |
| if: always() && github.event_name == 'pull_request' && (needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && (needs.detect-changes.outputs.helm-changed == 'true' || needs.detect-changes.outputs.package-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Get package version | |
| id: package-version | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Package version: $VERSION" | |
| - name: Lint Helm chart | |
| run: | | |
| echo "Linting Helm chart..." | |
| helm lint helm/hive-mind | |
| echo "Helm chart validation passed" | |
| - name: Verify Chart.yaml structure | |
| run: bash scripts/verify-chart-yaml.sh | |
| # === RELEASE - only runs on main after tests pass === | |
| release: | |
| name: Release | |
| needs: [detect-changes, lint, test-suites, test-execution, memory-check-linux] | |
| if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' && !contains(needs.*.result, 'failure') && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.test-suites.result == 'success' || needs.test-suites.result == 'skipped') && (needs.test-execution.result == 'success' || needs.test-execution.result == 'skipped') && (needs.memory-check-linux.result == 'success' || needs.memory-check-linux.result == 'skipped') && (needs.detect-changes.outputs.any-code-changed == 'true' || needs.detect-changes.outputs.docker-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| published: ${{ steps.publish.outputs.published }} | |
| published_version: ${{ steps.publish.outputs.published_version }} | |
| # Permissions required for npm OIDC trusted publishing | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '24.x' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Update npm for OIDC trusted publishing | |
| run: node scripts/setup-npm.mjs | |
| - name: Check for changesets | |
| id: check_changesets | |
| run: | | |
| # Count changeset files (excluding README.md and config.json) | |
| CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l) | |
| echo "Found $CHANGESET_COUNT changeset file(s)" | |
| echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT | |
| echo "changeset_count=$CHANGESET_COUNT" >> $GITHUB_OUTPUT | |
| - name: Harmonize changeset bump types | |
| if: steps.check_changesets.outputs.has_changesets == 'true' && steps.check_changesets.outputs.changeset_count > 1 | |
| run: | | |
| echo "Multiple changesets detected, harmonizing bump types..." | |
| node scripts/merge-changesets.mjs | |
| - name: Version packages and commit to main | |
| if: steps.check_changesets.outputs.has_changesets == 'true' | |
| id: version | |
| run: node scripts/version-and-commit.mjs --mode changeset | |
| - name: Publish to npm | |
| # Run if version was committed OR if a previous attempt already committed (for re-runs) | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| id: publish_npm | |
| run: node scripts/publish-to-npm.mjs --should-pull | |
| - name: Set publish outputs | |
| # This step explicitly sets the outputs using shell (more reliable than Node.js appendFileSync) | |
| # It runs after npm publish and reads the script's outputs to pass them along | |
| id: publish | |
| if: steps.publish_npm.outputs.published == 'true' || steps.publish_npm.outputs.already_published == 'true' | |
| run: | | |
| echo "published=true" >> $GITHUB_OUTPUT | |
| echo "published_version=${{ steps.publish_npm.outputs.published_version }}" >> $GITHUB_OUTPUT | |
| echo "Docker publish will be triggered with version: ${{ steps.publish_npm.outputs.published_version }}" | |
| - name: Create GitHub Release | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" | |
| - name: Format GitHub release notes | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" | |
| - name: Post-publish - Upload Source Maps to Sentry | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| echo "Uploading source maps to Sentry for published packages..." | |
| chmod +x scripts/upload-sourcemaps.mjs | |
| node scripts/upload-sourcemaps.mjs | |
| # === DOCKER PUBLISH === | |
| # Multi-platform builds using native runners (no QEMU). See: docs/case-studies/issue-982/ | |
| # ARM64 may be slow: actions/runner-images#11790, actions/partner-runner-images#101 | |
| # See also: docs/case-studies/issue-998/README.md | |
| docker-publish: | |
| name: Docker Publish (${{ matrix.platform }}) | |
| needs: [release] | |
| # !cancelled() allows concurrency cancellation. See: docs/case-studies/issue-1278/ | |
| if: "!cancelled() && needs.release.result == 'success' && needs.release.outputs.published == 'true'" | |
| # Timeout 60 min: arm64 can take 26+ min. See: docs/case-studies/issue-1415/README.md | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| cache_suffix: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm # Native ARM64, no QEMU. See: docs/case-studies/issue-998/ | |
| cache_suffix: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: konard/hive-mind | |
| outputs: | |
| digest-amd64: ${{ steps.build.outputs.digest }} | |
| digest-arm64: ${{ steps.build.outputs.digest }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Free up disk space | |
| run: node scripts/free-disk-space.mjs | |
| - name: Wait for NPM package availability | |
| run: node scripts/wait-for-npm.mjs --release-version "${{ needs.release.outputs.published_version }}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| # Set version label to actual release version, not Git ref "main" (issue #1419) | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.release.outputs.published_version }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| HIVE_MIND_VERSION=${{ needs.release.outputs.published_version }} | |
| # Registry cache for faster parallel export. See: docs/case-studies/issue-1415/README.md | |
| cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-${{ matrix.cache_suffix }} | |
| cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-${{ matrix.cache_suffix }},mode=max,image-manifest=true,ignore-error=true | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| echo "Digest for ${{ matrix.platform }}: $digest" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hive-mind-digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Merge multi-platform manifests after all platform builds complete | |
| docker-publish-merge: | |
| name: Docker Publish (Merge) | |
| needs: [release, docker-publish] | |
| # Use !cancelled() instead of always() to allow concurrency cancellation. | |
| # See: docs/case-studies/issue-1278/README.md for why this change was necessary. | |
| if: "!cancelled() && needs.release.result == 'success' && needs.docker-publish.result == 'success' && needs.release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| IMAGE_NAME: konard/hive-mind | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: hive-mind-digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ needs.release.outputs.published_version }} | |
| type=raw,value=latest | |
| # Set version label to actual release version, not Git ref "main" (issue #1419) | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.release.outputs.published_version }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| echo "Creating multi-platform manifest..." | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| echo "Digests:" | |
| ls -la | |
| docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: | | |
| echo "Inspecting multi-platform image..." | |
| docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ needs.release.outputs.published_version }} | |
| - name: Verify published image | |
| run: | | |
| echo "Docker image published successfully" | |
| echo "Image: ${{ env.IMAGE_NAME }}" | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| docker-publish-dind: | |
| name: Docker Publish DinD (${{ matrix.platform }}) | |
| needs: [release] | |
| if: "!cancelled() && needs.release.result == 'success' && needs.release.outputs.published == 'true'" | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| cache_suffix: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| cache_suffix: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: konard/hive-mind-dind | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Free up disk space | |
| run: node scripts/free-disk-space.mjs | |
| - name: Wait for NPM package availability | |
| run: node scripts/wait-for-npm.mjs --release-version "${{ needs.release.outputs.published_version }}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.release.outputs.published_version }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile.dind | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| HIVE_MIND_VERSION=${{ needs.release.outputs.published_version }} | |
| cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-dind-${{ matrix.cache_suffix }} | |
| cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-dind-${{ matrix.cache_suffix }},mode=max,image-manifest=true,ignore-error=true | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| echo "Digest for ${{ matrix.platform }}: $digest" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hive-mind-dind-digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| docker-publish-dind-merge: | |
| name: Docker Publish DinD (Merge) | |
| needs: [release, docker-publish-dind] | |
| if: "!cancelled() && needs.release.result == 'success' && needs.docker-publish-dind.result == 'success' && needs.release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| IMAGE_NAME: konard/hive-mind-dind | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: hive-mind-dind-digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ needs.release.outputs.published_version }} | |
| type=raw,value=latest | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.release.outputs.published_version }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| echo "Creating multi-platform manifest..." | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| echo "Digests:" | |
| ls -la | |
| docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ needs.release.outputs.published_version }} | |
| - name: Verify published image | |
| run: | | |
| echo "Docker DinD image published successfully" | |
| echo "Image: ${{ env.IMAGE_NAME }}" | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| # === HELM CHART RELEASE === | |
| helm-release: | |
| name: Helm Release | |
| needs: [release, docker-publish-merge, docker-publish-dind-merge] | |
| # Use !cancelled() instead of always() to allow concurrency cancellation. | |
| # See: docs/case-studies/issue-1278/README.md for why this change was necessary. | |
| if: "!cancelled() && needs.release.result == 'success' && needs.docker-publish-merge.result == 'success' && needs.docker-publish-dind-merge.result == 'success' && needs.release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| pages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Release Helm chart | |
| run: node scripts/helm-release.mjs --release-version "${{ needs.release.outputs.published_version }}" | |
| - name: Upload chart to release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.release.outputs.published_version }} | |
| files: .helm-packages/*.tgz | |
| # === MANUAL INSTANT RELEASE === | |
| instant-release: | |
| name: Instant Release | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| published: ${{ steps.publish.outputs.published }} | |
| published_version: ${{ steps.publish.outputs.published_version }} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '24.x' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Update npm for OIDC trusted publishing | |
| run: node scripts/setup-npm.mjs | |
| - name: Version packages and commit to main | |
| id: version | |
| run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" | |
| - name: Publish to npm | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| id: publish_npm | |
| run: node scripts/publish-to-npm.mjs | |
| - name: Set publish outputs | |
| # This step explicitly sets the outputs using shell (more reliable than Node.js appendFileSync) | |
| id: publish | |
| if: steps.publish_npm.outputs.published == 'true' || steps.publish_npm.outputs.already_published == 'true' | |
| run: | | |
| echo "published=true" >> $GITHUB_OUTPUT | |
| echo "published_version=${{ steps.publish_npm.outputs.published_version }}" >> $GITHUB_OUTPUT | |
| echo "Docker publish will be triggered with version: ${{ steps.publish_npm.outputs.published_version }}" | |
| - name: Create GitHub Release | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" | |
| - name: Format GitHub release notes | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" | |
| # === DOCKER PUBLISH FOR INSTANT RELEASE === | |
| # Same multi-platform setup as docker-publish above (native runners, no QEMU). | |
| # See: docs/case-studies/issue-982/README.md and docs/case-studies/issue-998/README.md | |
| docker-publish-instant: | |
| name: Docker Publish Instant (${{ matrix.platform }}) | |
| needs: [instant-release] | |
| # Use !cancelled() instead of always() to allow concurrency cancellation. | |
| # See: docs/case-studies/issue-1278/README.md for why this change was necessary. | |
| if: "!cancelled() && needs.instant-release.result == 'success' && needs.instant-release.outputs.published == 'true'" | |
| # Timeout 60 min: arm64 can take 26+ min. See: docs/case-studies/issue-1415/README.md | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| cache_suffix: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm # Native ARM64, no QEMU. See: docs/case-studies/issue-998/ | |
| cache_suffix: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: konard/hive-mind | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Free up disk space | |
| run: node scripts/free-disk-space.mjs | |
| - name: Wait for NPM package availability | |
| run: node scripts/wait-for-npm.mjs --release-version "${{ needs.instant-release.outputs.published_version }}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| # Set version label to actual release version, not Git ref "main" (issue #1419) | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.instant-release.outputs.published_version }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| HIVE_MIND_VERSION=${{ needs.instant-release.outputs.published_version }} | |
| # Registry cache for faster parallel export. See: docs/case-studies/issue-1415/README.md | |
| cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-${{ matrix.cache_suffix }} | |
| cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-${{ matrix.cache_suffix }},mode=max,image-manifest=true,ignore-error=true | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| echo "Digest for ${{ matrix.platform }}: $digest" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hive-mind-instant-digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Merge multi-platform manifests after all platform builds complete (instant release) | |
| docker-publish-instant-merge: | |
| name: Docker Publish Instant (Merge) | |
| needs: [instant-release, docker-publish-instant] | |
| # Use !cancelled() instead of always() to allow concurrency cancellation. | |
| # See: docs/case-studies/issue-1278/README.md for why this change was necessary. | |
| if: "!cancelled() && needs.instant-release.result == 'success' && needs.docker-publish-instant.result == 'success' && needs.instant-release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| IMAGE_NAME: konard/hive-mind | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: hive-mind-instant-digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ needs.instant-release.outputs.published_version }} | |
| type=raw,value=latest | |
| # Set version label to actual release version, not Git ref "main" (issue #1419) | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.instant-release.outputs.published_version }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| echo "Creating multi-platform manifest..." | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| echo "Digests:" | |
| ls -la | |
| docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: | | |
| echo "Inspecting multi-platform image..." | |
| docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ needs.instant-release.outputs.published_version }} | |
| - name: Verify published image | |
| run: | | |
| echo "Docker image published successfully" | |
| echo "Image: ${{ env.IMAGE_NAME }}" | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| docker-publish-dind-instant: | |
| name: Docker Publish DinD Instant (${{ matrix.platform }}) | |
| needs: [instant-release] | |
| if: "!cancelled() && needs.instant-release.result == 'success' && needs.instant-release.outputs.published == 'true'" | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| cache_suffix: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| cache_suffix: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: konard/hive-mind-dind | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Free up disk space | |
| run: node scripts/free-disk-space.mjs | |
| - name: Wait for NPM package availability | |
| run: node scripts/wait-for-npm.mjs --release-version "${{ needs.instant-release.outputs.published_version }}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.instant-release.outputs.published_version }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile.dind | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| HIVE_MIND_VERSION=${{ needs.instant-release.outputs.published_version }} | |
| cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-dind-${{ matrix.cache_suffix }} | |
| cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache-dind-${{ matrix.cache_suffix }},mode=max,image-manifest=true,ignore-error=true | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| echo "Digest for ${{ matrix.platform }}: $digest" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hive-mind-dind-instant-digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| docker-publish-dind-instant-merge: | |
| name: Docker Publish DinD Instant (Merge) | |
| needs: [instant-release, docker-publish-dind-instant] | |
| if: "!cancelled() && needs.instant-release.result == 'success' && needs.docker-publish-dind-instant.result == 'success' && needs.instant-release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| IMAGE_NAME: konard/hive-mind-dind | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: hive-mind-dind-instant-digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ needs.instant-release.outputs.published_version }} | |
| type=raw,value=latest | |
| labels: | | |
| org.opencontainers.image.version=${{ needs.instant-release.outputs.published_version }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| echo "Creating multi-platform manifest..." | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| echo "Digests:" | |
| ls -la | |
| docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ needs.instant-release.outputs.published_version }} | |
| - name: Verify published image | |
| run: | | |
| echo "Docker DinD image published successfully" | |
| echo "Image: ${{ env.IMAGE_NAME }}" | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" | |
| # === HELM CHART RELEASE FOR INSTANT RELEASE === | |
| helm-release-instant: | |
| name: Helm Release (Instant) | |
| needs: [instant-release, docker-publish-instant-merge, docker-publish-dind-instant-merge] | |
| # Use !cancelled() instead of always() to allow concurrency cancellation. | |
| # See: docs/case-studies/issue-1278/README.md for why this change was necessary. | |
| if: "!cancelled() && needs.instant-release.result == 'success' && needs.docker-publish-instant-merge.result == 'success' && needs.docker-publish-dind-instant-merge.result == 'success' && needs.instant-release.outputs.published == 'true'" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| pages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Release Helm chart | |
| run: node scripts/helm-release.mjs --release-version "${{ needs.instant-release.outputs.published_version }}" | |
| - name: Upload chart to release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.instant-release.outputs.published_version }} | |
| files: .helm-packages/*.tgz | |
| # === MANUAL CHANGESET PR === | |
| changeset-pr: | |
| name: Create Changeset PR | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '24.x' | |
| - name: Install dependencies | |
| run: node scripts/npm-install-with-retry.mjs install | |
| - name: Create changeset file | |
| run: node scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" | |
| - name: Format changeset with Prettier | |
| run: | | |
| npx prettier --write ".changeset/*.md" || true | |
| echo "Formatted changeset files" | |
| - name: Create Pull Request | |
| uses: peter-evans/create-pull-request@v8 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release' | |
| branch: changeset-manual-release-${{ github.run_id }} | |
| delete-branch: true | |
| title: 'chore: manual ${{ github.event.inputs.bump_type }} release' | |
| body: | | |
| ## Manual Release Request | |
| This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release. | |
| ### Release Details | |
| - **Type:** ${{ github.event.inputs.bump_type }} | |
| - **Description:** ${{ github.event.inputs.description || 'Manual release' }} | |
| - **Triggered by:** @${{ github.actor }} | |
| ### Next Steps | |
| 1. Review the changeset in this PR | |
| 2. Merge this PR to main | |
| 3. The automated release workflow will create a version PR | |
| 4. Merge the version PR to publish to npm and create a GitHub release |