Skip to content

ci(release): mirror npm publish to @shep.bot scope #2174

ci(release): mirror npm publish to @shep.bot scope

ci(release): mirror npm publish to @shep.bot scope #2174

Workflow file for this run

# =============================================================================
# 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>