ci(release): mirror npm publish to @shep.bot scope #2174
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
| # ============================================================================= | |
| # CI/CD Pipeline | |
| # ============================================================================= | |
| # All jobs run in parallel. Release only runs on push to main. | |
| # | |
| # Pipeline Structure: | |
| # ┌──────┬───────────┬─────────────────┬─────────────────┬─────────┬─────────┬───────────┬─────────────────────┐ | |
| # │ Lint │ Typecheck │ Unit (lin+win) │ E2E CLI (l+w) │ E2E TUI │ E2E Web │ Storybook │ Electron (m+w+l) │ | |
| # └──────┴───────────┴─────────────────┴─────────────────┴─────────┴─────────┴───────────┴─────────────────────┘ | |
| # ┌──────────┬─────────┐ | |
| # │ Gitleaks │ Semgrep │ | |
| # └──────────┴─────────┘ | |
| # (all parallel) | |
| # | |
| # On push to main only: | |
| # ┌───────────┐ | |
| # │ Release │ (after ALL jobs pass) | |
| # └───────────┘ | |
| # | |
| # On pull requests (after ALL jobs pass): | |
| # ┌───────────────┐ | |
| # │ Dev Release │ npm dev tag | |
| # └───────────────┘ | |
| # | |
| # @see https://docs.github.com/en/actions | |
| name: CI/CD | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| env: | |
| NODE_VERSION: '22' | |
| jobs: | |
| # =========================================================================== | |
| # Code Quality | |
| # =========================================================================== | |
| lint: | |
| name: Lint & Format | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run lint | |
| - run: pnpm run format:check | |
| - run: pnpm run tsp:compile | |
| typecheck: | |
| name: Type Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run generate | |
| - run: pnpm run typecheck | |
| # =========================================================================== | |
| # Tests | |
| # =========================================================================== | |
| test-unit: | |
| name: Unit Tests (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run tsp:compile | |
| - run: pnpm run test:unit --passWithNoTests | |
| - run: pnpm run test:int --passWithNoTests | |
| test-e2e-cli: | |
| name: E2E CLI (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - run: pnpm run test:e2e:cli | |
| test-e2e-tui: | |
| name: E2E (TUI) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - run: pnpm run test:e2e:tui | |
| test-e2e-web: | |
| name: E2E (Web) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - run: pnpm exec playwright install --with-deps chromium | |
| - run: pnpm run test:e2e:web | |
| # =========================================================================== | |
| # Storybook Build (catches missing mocks, broken imports in stories) | |
| # =========================================================================== | |
| storybook-build: | |
| name: Storybook Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:storybook | |
| # =========================================================================== | |
| # Electron Desktop App Build | |
| # Builds Electron installers for macOS, Windows, and Linux in parallel. | |
| # Artifacts are uploaded for PR inspection; release job publishes them. | |
| # =========================================================================== | |
| build-electron: | |
| name: Electron (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest, windows-latest, ubuntu-latest] | |
| include: | |
| - os: macos-latest | |
| build_cmd: 'build:mac' | |
| artifact_pattern: 'packages/electron/release/*.dmg' | |
| artifact_name: 'electron-mac' | |
| - os: windows-latest | |
| build_cmd: 'build:win' | |
| artifact_pattern: 'packages/electron/release/*.exe' | |
| artifact_name: 'electron-win' | |
| - os: ubuntu-latest | |
| build_cmd: 'build:linux' | |
| artifact_pattern: 'packages/electron/release/*.AppImage' | |
| artifact_name: 'electron-linux' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build web UI and CLI | |
| run: pnpm run build:release | |
| - name: Build Electron installer | |
| run: pnpm --filter @shepai/electron ${{ matrix.build_cmd }} | |
| - name: Upload Electron artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: ${{ matrix.artifact_pattern }} | |
| if-no-files-found: error | |
| retention-days: 7 | |
| # =========================================================================== | |
| # Electron Apps-Only Desktop Build | |
| # Builds the focused "Shep Apps" desktop binary for macOS, Windows, and | |
| # Linux in parallel. Runs as a sibling of `build-electron` (no `needs:` | |
| # dependency) so all 6 platform builds execute concurrently — total | |
| # wall time is max(full, apps-only) per platform, not the sum. | |
| # The build script bakes `SHEP_SHELL_VARIANT=apps-only` into the | |
| # compiled main bundle via esbuild `define`, so the packaged binary | |
| # launches into /applications without any runtime env configuration. | |
| # =========================================================================== | |
| build-electron-apps-only: | |
| name: Electron Apps-Only (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest, windows-latest, ubuntu-latest] | |
| include: | |
| - os: macos-latest | |
| build_cmd: 'build:apps-only:mac' | |
| artifact_pattern: 'packages/electron/release-apps-only/*.dmg' | |
| artifact_name: 'electron-apps-only-mac' | |
| - os: windows-latest | |
| build_cmd: 'build:apps-only:win' | |
| artifact_pattern: 'packages/electron/release-apps-only/*.exe' | |
| artifact_name: 'electron-apps-only-win' | |
| - os: ubuntu-latest | |
| build_cmd: 'build:apps-only:linux' | |
| artifact_pattern: 'packages/electron/release-apps-only/*.AppImage' | |
| artifact_name: 'electron-apps-only-linux' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build web UI and CLI | |
| run: pnpm run build:release | |
| - name: Build Apps-Only Electron installer | |
| run: pnpm --filter @shepai/electron ${{ matrix.build_cmd }} | |
| - name: Upload Apps-Only Electron artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: ${{ matrix.artifact_pattern }} | |
| if-no-files-found: error | |
| retention-days: 7 | |
| # =========================================================================== | |
| # Security: Gitleaks Secret Detection | |
| # Uses gitleaks CLI directly (free, open source) instead of gitleaks-action | |
| # which requires a paid license for organizations | |
| # =========================================================================== | |
| security-gitleaks: | |
| name: Gitleaks | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install gitleaks | |
| run: | | |
| GITLEAKS_VERSION="8.30.1" | |
| curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz | |
| sudo mv gitleaks /usr/local/bin/ | |
| - name: Gitleaks secret scan | |
| run: gitleaks detect --source . --verbose --redact --config .gitleaks.toml --gitleaks-ignore-path .gitleaksignore | |
| # =========================================================================== | |
| # Security: Semgrep SAST | |
| # =========================================================================== | |
| security-semgrep: | |
| name: Semgrep | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Semgrep SAST scan | |
| uses: returntocorp/semgrep-action@v1 | |
| with: | |
| config: >- | |
| p/typescript | |
| p/javascript | |
| p/security-audit | |
| # =========================================================================== | |
| # Security Summary Comment (PRs only) | |
| # Posts aggregated security scan results as a PR comment | |
| # =========================================================================== | |
| security-summary: | |
| name: Security Summary | |
| runs-on: ubuntu-latest | |
| needs: | |
| - security-gitleaks | |
| - security-semgrep | |
| # Only post comment when security issues are found | |
| if: >- | |
| github.event_name == 'pull_request' && always() && | |
| (needs.security-gitleaks.result == 'failure' || | |
| needs.security-semgrep.result == 'failure') | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Post security summary comment | |
| uses: peter-evans/create-or-update-comment@v5 | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| comment-tag: security-summary | |
| body: | | |
| ## 🔒 Security Issues Detected | |
| | Scanner | Status | Details | | |
| |---------|--------|---------| | |
| | **Gitleaks** | ${{ needs.security-gitleaks.result == 'success' && '✅ Pass' || needs.security-gitleaks.result == 'failure' && '❌ Failed' || needs.security-gitleaks.result == 'skipped' && '⏭️ Skipped' || '⚠️ Cancelled' }} | Secret detection in git history | | |
| | **Semgrep** | ${{ needs.security-semgrep.result == 'success' && '✅ Pass' || needs.security-semgrep.result == 'failure' && '❌ Failed' || needs.security-semgrep.result == 'skipped' && '⏭️ Skipped' || '⚠️ Cancelled' }} | SAST for TypeScript/JavaScript | | |
| > ⚠️ **Please review the failed checks above before merging.** | |
| --- | |
| <sub>🤖 Generated by CI Security Scan | [View Details](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})</sub> | |
| # =========================================================================== | |
| # Release (main only, after ALL jobs pass) | |
| # =========================================================================== | |
| release: | |
| name: Release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - lint | |
| - typecheck | |
| - test-unit | |
| - test-e2e-cli | |
| - test-e2e-tui | |
| - test-e2e-web | |
| - storybook-build | |
| - build-electron | |
| - build-electron-apps-only | |
| - security-gitleaks | |
| - security-semgrep | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]') | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # Use PAT token to allow pushing to protected main branch | |
| # persist-credentials must be true for semantic-release to push commits | |
| persist-credentials: true | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - name: Download Electron artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: electron-artifacts | |
| pattern: electron-* | |
| merge-multiple: true | |
| - name: Capture pre-release version | |
| id: pre | |
| run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT | |
| - name: Run semantic-release | |
| id: semantic | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
| run: npx semantic-release | |
| # =========================================================================== | |
| # Mirror publish to @shep.bot scope (additive, fully backward compatible) | |
| # | |
| # semantic-release bumps package.json's version in-place when it publishes; | |
| # if the version is unchanged, no release happened and we skip the mirror. | |
| # continue-on-error keeps the canonical @shepai/cli release authoritative | |
| # — a failed mirror publish is reported but does not fail the release job. | |
| # =========================================================================== | |
| - name: Mirror publish to @shep.bot scope | |
| continue-on-error: true | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_SHEPBOT }} | |
| PRE_VERSION: ${{ steps.pre.outputs.version }} | |
| run: | | |
| if [ -z "${NODE_AUTH_TOKEN}" ]; then | |
| echo "::warning::NPM_TOKEN_SHEPBOT secret not set — skipping @shep.bot mirror publish" | |
| exit 0 | |
| fi | |
| POST_VERSION=$(node -p "require('./package.json').version") | |
| if [ "${POST_VERSION}" = "${PRE_VERSION}" ]; then | |
| echo "No version bump (pre=${PRE_VERSION} post=${POST_VERSION}) — semantic-release did not publish. Skipping mirror." | |
| exit 0 | |
| fi | |
| echo "Mirror-publishing @shep.bot/cli@${POST_VERSION} (was ${PRE_VERSION})" | |
| npm pkg set name='@shep.bot/cli' | |
| npm publish --access public | |
| # =========================================================================== | |
| # Dev Release (PRs only, after ALL jobs pass) | |
| # Publishes npm prerelease with dev tag | |
| # =========================================================================== | |
| dev-release: | |
| name: Dev Release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - lint | |
| - typecheck | |
| - test-unit | |
| - test-e2e-cli | |
| - test-e2e-tui | |
| - test-e2e-web | |
| - storybook-build | |
| - build-electron | |
| - security-gitleaks | |
| - security-semgrep | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Compute dev version | |
| id: devver | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| SHORT_SHA=$(git rev-parse --short=7 HEAD) | |
| BASE_VERSION=$(node -p "require('./package.json').version") | |
| DEV_VERSION="${BASE_VERSION}-pr${PR_NUMBER}.${SHORT_SHA}" | |
| echo "version=$DEV_VERSION" >> $GITHUB_OUTPUT | |
| echo "Dev version: $DEV_VERSION" | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - name: Publish npm dev release | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| DEV_VERSION: ${{ steps.devver.outputs.version }} | |
| run: | | |
| npm version "$DEV_VERSION" --no-git-tag-version | |
| npm publish --tag dev --access public | |
| - name: Post PR comment with dev versions | |
| uses: peter-evans/create-or-update-comment@v5 | |
| env: | |
| DEV_VERSION: ${{ steps.devver.outputs.version }} | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| comment-tag: dev-release | |
| body: | | |
| ## Dev Release Published | |
| | Artifact | Version | Install | | |
| |----------|---------|---------| | |
| | **npm** | `${{ steps.devver.outputs.version }}` | `npm install -g @shepai/cli@${{ steps.devver.outputs.version }}` | | |
| --- | |
| <sub>Published from commit ${{ github.event.pull_request.head.sha }} | [View CI](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})</sub> | |
| # =========================================================================== | |
| # Dev Release Mirror — @shep.bot scope (PRs only, parallel to dev-release) | |
| # Independent job so a misconfigured/missing NPM_TOKEN_SHEPBOT cannot block | |
| # PRs while the new org's publishing stabilizes. continue-on-error keeps it | |
| # additive: failure here is reported but does not fail the PR check. | |
| # =========================================================================== | |
| dev-release-shepbot: | |
| name: Dev Release (shep.bot mirror) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - lint | |
| - typecheck | |
| - test-unit | |
| - test-e2e-cli | |
| - test-e2e-tui | |
| - test-e2e-web | |
| - storybook-build | |
| - build-electron | |
| - security-gitleaks | |
| - security-semgrep | |
| if: github.event_name == 'pull_request' | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Compute dev version | |
| id: devver | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| SHORT_SHA=$(git rev-parse --short=7 HEAD) | |
| BASE_VERSION=$(node -p "require('./package.json').version") | |
| DEV_VERSION="${BASE_VERSION}-pr${PR_NUMBER}.${SHORT_SHA}" | |
| echo "version=$DEV_VERSION" >> $GITHUB_OUTPUT | |
| echo "Dev version (shep.bot): $DEV_VERSION" | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm run build:release | |
| - name: Publish npm dev release to @shep.bot scope | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_SHEPBOT }} | |
| DEV_VERSION: ${{ steps.devver.outputs.version }} | |
| run: | | |
| if [ -z "${NODE_AUTH_TOKEN}" ]; then | |
| echo "::warning::NPM_TOKEN_SHEPBOT secret not set — skipping @shep.bot mirror publish" | |
| exit 0 | |
| fi | |
| npm version "$DEV_VERSION" --no-git-tag-version | |
| npm pkg set name='@shep.bot/cli' | |
| npm publish --tag dev --access public | |
| - name: Post PR comment with @shep.bot dev version | |
| uses: peter-evans/create-or-update-comment@v5 | |
| env: | |
| DEV_VERSION: ${{ steps.devver.outputs.version }} | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| comment-tag: dev-release-shepbot | |
| body: | | |
| ## Dev Release Published — @shep.bot mirror | |
| | Artifact | Version | Install | | |
| |----------|---------|---------| | |
| | **npm** | `${{ steps.devver.outputs.version }}` | `npm install -g @shep.bot/cli@${{ steps.devver.outputs.version }}` | | |
| --- | |
| <sub>Mirror publish from commit ${{ github.event.pull_request.head.sha }} | [View CI](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})</sub> |