# =============================================================================
# WORKFLOW: Main Branch Release Pipeline
# PURPOSE: Automate version management, releases, and security scanning on main
# TRIGGERS: Push to main branch (merges, direct commits)
# OUTPUTS: GitHub release with artifacts, NPM package, Docker image
# =============================================================================
name: Main
on:
push:
branches: [main]
# Prevent concurrent runs on the same ref to avoid race conditions during releases
# cancel-in-progress: false ensures releases complete even if new commits arrive
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
# SECURITY: Required permissions for release automation
# contents: write - Create releases and tags
# id-token: write - Generate SLSA attestations for supply chain security
# attestations: write - Attach attestations to artifacts
# security-events: write - Upload security scan results
# actions: read - Access workflow runs and artifacts
# packages: write - Push Docker images to GitHub Container Registry
permissions:
contents: write
id-token: write
attestations: write
security-events: write
actions: read
packages: write
jobs:
# =============================================================================
# VALIDATION PHASE
# Runs all quality checks in parallel to ensure code meets standards
# =============================================================================
validate:
# Reusable workflow handles: audit, typecheck, lint, format, tests
# FAILS IF: Any check fails, tests don't meet 80% coverage threshold
uses: ./.github/workflows/reusable-validate.yml
secrets:
DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }}
# =============================================================================
# SECURITY SCANNING PHASE
# Parallel security scans to identify vulnerabilities before release
# =============================================================================
# Scans TypeScript/JavaScript for common security issues (XSS, SQL injection, etc.)
security:
uses: ./.github/workflows/reusable-security.yml
# =============================================================================
# UNIFIED BUILD PHASE
# Single build job that creates artifacts to be reused throughout the workflow
# =============================================================================
build:
runs-on: ubuntu-latest
outputs:
artifact-name: dist-${{ github.sha }}
changed: ${{ steps.version.outputs.changed }}
version: ${{ steps.version.outputs.version }}
tag_sha: ${{ steps.tag.outputs.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.17.0
run_install: false
standalone: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Version packages
id: version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Custom script validates changesets and determines version
# FAILS IF: feat/fix commits exist without changesets
# Outputs: changed=true/false, version=X.Y.Z
node .github/scripts/version-and-release.js
- name: Commit version changes
if: steps.version.outputs.changed == 'true'
run: |
# Configure git with GitHub Actions bot identity
git config --local user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
git config --local user.name "${{ github.actor }}"
# Stage version-related changes
git add package.json CHANGELOG.md .changeset
# Commit with [skip actions] to prevent workflow recursion
git commit -m "chore(release): v${{ steps.version.outputs.version }} [skip actions]"
# Push changes to origin
git push origin main
echo "β
Version changes committed and pushed"
- name: Create and push tag
# Create tag BEFORE building artifacts so they're associated with the tag
id: tag
if: steps.version.outputs.changed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
# Configure git
git config --local user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
git config --local user.name "${{ github.actor }}"
# Create annotated tag
git tag -a "v${VERSION}" -m "Release v${VERSION}"
# Push tag to origin
git push origin "v${VERSION}"
# Get the tag SHA for artifact naming
TAG_SHA=$(git rev-list -n 1 "v${VERSION}")
echo "sha=${TAG_SHA}" >> $GITHUB_OUTPUT
echo "π Tag SHA for artifacts: ${TAG_SHA}"
- name: Build TypeScript
if: steps.version.outputs.changed == 'true'
run: |
pnpm build
echo "β
Built TypeScript once for entire workflow"
- name: Generate artifact manifest
if: steps.version.outputs.changed == 'true'
run: |
# Create a manifest of what's been built
cat > build-manifest.json <<EOF
{
"build_sha": "${{ github.sha }}",
"build_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"node_version": "$(node --version)",
"pnpm_version": "$(pnpm --version)",
"typescript_version": "$(pnpm list typescript --json | jq -r '.dependencies.typescript.version')",
"files": $(find dist -type f -name "*.js" | jq -R . | jq -s .)
}
EOF
echo "π Generated build manifest with $(find dist -type f -name "*.js" | wc -l) JavaScript files"
- name: Upload build artifact
if: steps.version.outputs.changed == 'true'
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: |
dist/
package.json
pnpm-lock.yaml
build-manifest.json
retention-days: 1 # Only needed for this workflow run
# =============================================================================
# PREPARE RELEASE ASSETS PHASE
# Centralized job for preparing all release artifacts (Docker, binaries, etc.)
# =============================================================================
docker:
name: Build Docker Image
needs: [validate, security, build]
if: vars.ENABLE_DOCKER_RELEASE == 'true' && needs.build.outputs.changed == 'true'
uses: ./.github/workflows/reusable-docker.yml
with:
platforms: 'linux/amd64,linux/arm64'
save-artifact: true
artifact-name: 'docker-image-${{ needs.build.outputs.version }}'
image-name: 'deepsource-mcp-server'
version: ${{ needs.build.outputs.version }}
tag_sha: ${{ github.sha }}
build_artifact: ${{ needs.build.outputs.artifact-name }}
npm:
name: Prepare NPM Package
needs: [validate, security, build]
if: vars.ENABLE_NPM_RELEASE == 'true' && needs.build.outputs.changed == 'true'
runs-on: ubuntu-latest
outputs:
built: ${{ steps.pack.outputs.built }}
artifact_name: ${{ steps.pack.outputs.artifact_name }}
tarball_name: ${{ steps.pack.outputs.tarball_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact-name }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.17.0
run_install: false
standalone: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
# Install all dependencies for packaging (dev and prod)
run: pnpm install --frozen-lockfile
- name: Create NPM package
id: pack
run: |
# Create the NPM package tarball
# Use tail -1 to get just the filename, as npm pack may output additional text
NPM_PACKAGE=$(npm pack 2>/dev/null | tail -1)
echo "π¦ Created NPM package: $NPM_PACKAGE"
# Generate metadata using github.sha for consistent naming with publish workflow
ARTIFACT_NAME="npm-package-${{ needs.build.outputs.version }}-${{ github.sha }}"
{
echo "artifact_name=$ARTIFACT_NAME"
echo "tarball_name=$NPM_PACKAGE"
echo "built=true"
} >> $GITHUB_OUTPUT
# Create manifest of included files for verification
npm pack --dry-run --json 2>/dev/null | jq -r '.[0].files[].path' > npm-package-manifest.txt
echo "π Package contains $(wc -l < npm-package-manifest.txt) files"
- name: Upload NPM package artifact
uses: actions/upload-artifact@v4
with:
name: npm-package-${{ needs.build.outputs.version }}-${{ github.sha }}
path: |
*.tgz
npm-package-manifest.txt
retention-days: 7
- name: Generate attestations for NPM package
uses: actions/attest-build-provenance@v2
with:
subject-path: '*.tgz'
# =============================================================================
# GITHUB RELEASE CREATION PHASE
# Creates GitHub release as the final step after version is committed
# =============================================================================
create-release:
name: Create GitHub Release
needs: [build, docker, npm]
# Run if build succeeded AND docker/npm either succeeded or were skipped
if: |
needs.build.outputs.changed == 'true' &&
!cancelled() &&
(needs.docker.result == 'success' || needs.docker.result == 'skipped') &&
(needs.npm.result == 'success' || needs.npm.result == 'skipped')
runs-on: ubuntu-latest
outputs:
released: ${{ steps.release.outputs.released }}
version: ${{ needs.build.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Checkout the newly created tag
ref: v${{ needs.build.outputs.version }}
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact-name }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.17.0
run_install: false
standalone: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
# Only production dependencies needed for SBOM generation
# Skip scripts to avoid running husky (dev dependency)
run: pnpm install --prod --frozen-lockfile --ignore-scripts
- name: Generate SBOM
run: pnpm sbom
- name: Create release artifacts
run: |
VERSION="${{ needs.build.outputs.version }}"
TAG_SHA="${{ needs.build.outputs.tag_sha }}"
tar -czf dist-${VERSION}-${TAG_SHA:0:7}.tar.gz dist/
zip -r dist-${VERSION}-${TAG_SHA:0:7}.zip dist/
- name: Extract release notes
run: |
VERSION="${{ needs.build.outputs.version }}"
awk -v version="## $VERSION" '
$0 ~ version { flag=1; next }
/^## [0-9]/ && flag { exit }
flag { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "Release v$VERSION" > release-notes.md
fi
# =============================================================================
# SUPPLY CHAIN SECURITY
# Generate attestations BEFORE creating release to avoid race condition
# This ensures the Main workflow is complete before triggering Publish workflow
# =============================================================================
- name: Generate attestations
# Generate SLSA provenance attestations for supply chain security
# Requires id-token: write permission
uses: actions/attest-build-provenance@v2
with:
subject-path: |
dist/**/*.js
sbom.cdx.json
dist-*-*.tar.gz
dist-*-*.zip
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.build.outputs.version }}
name: v${{ needs.build.outputs.version }}
body_path: release-notes.md
draft: false
prerelease: false
make_latest: true
files: |
sbom.cdx.json
dist-${{ needs.build.outputs.version }}-*.tar.gz
dist-${{ needs.build.outputs.version }}-*.zip
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
- name: Set release output
id: release
run: |
echo "released=true" >> $GITHUB_OUTPUT
echo "β
Released version ${{ needs.build.outputs.version }}"