name: Pipeline on: workflow_run: workflows: ["CI"] types: [completed] branches: [main] concurrency: group: pipeline-main cancel-in-progress: false permissions: contents: write packages: write jobs: dev-publish: name: Dev Publish if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/singularity-forge/sf-ci-builder:latest credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} env: BLACKSMITH_CACHE_TOKEN: ${{ env.BLACKSMITH_CACHE_TOKEN }} BLACKSMITH_CACHE_URL: ${{ env.BLACKSMITH_CACHE_URL }} GITHUB_REPO_NAME: ${{ github.repository }} outputs: dev-version: ${{ steps.stamp.outputs.version }} steps: - uses: actions/checkout@v6 - name: Mark workspace safe for git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - uses: actions/setup-node@v6 with: node-version: '26.1' registry-url: https://registry.npmjs.org cache: 'npm' - name: Install dependencies run: npm ci - name: Install web host dependencies run: npm --prefix web ci - name: Cache Next.js build uses: useblacksmith/cache@v5 with: path: web/.next/cache key: nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-${{ hashFiles('web/app/**', 'web/components/**', 'web/lib/**', 'web/hooks/**') }} restore-keys: | nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}- nextjs-${{ runner.os }}- - name: Build core run: npm run build:core - name: Build web host run: npm run build:web-host - name: Stamp dev version and sync platform packages id: stamp run: | npm run pipeline:version-stamp npm run sync-platform-versions echo "version=$(node -e 'process.stdout.write(require("./package.json").version)')" >> "$GITHUB_OUTPUT" - name: Publish @dev run: | VERSION=$(node -e 'process.stdout.write(require("./package.json").version)') if npm view "sf-pi@${VERSION}" version 2>/dev/null; then echo "Version ${VERSION} already published — skipping" else npm publish --tag dev fi env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Smoke test (local) run: | chmod +x dist/loader.js export SF_SMOKE_BINARY="$(pwd)/dist/loader.js" npm run test:smoke test-verify: name: Test & Verify needs: dev-publish runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '26.1' registry-url: https://registry.npmjs.org cache: 'npm' - name: Install sf-pi@dev globally (with registry propagation retry) run: | for i in 1 2 3 4 5 6; do npm install -g sf-pi@dev && exit 0 echo "Attempt $i failed — waiting 10s for npm registry propagation..." sleep 10 done echo "Failed to install sf-pi@dev after 6 attempts" exit 1 - name: Run smoke tests (against installed binary) run: | export SF_SMOKE_BINARY=$(which sf) npm run test:smoke env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Install dependencies run: npm ci - name: Run fixture tests run: npm run test:fixtures - name: Run live regression tests (against installed binary) run: | export SF_SMOKE_BINARY=$(which sf) npm run test:live-regression - name: Promote to @next env: DEV_VERSION: ${{ needs.dev-publish.outputs.dev-version }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: npm dist-tag add "sf-pi@${DEV_VERSION}" next - name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push runtime Docker image env: DEV_VERSION: ${{ needs.dev-publish.outputs.dev-version }} run: | docker build --target runtime \ -t "ghcr.io/singularity-forge/sf-run:next" \ -t "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" \ . docker push ghcr.io/singularity-forge/sf-run:next docker push "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" prod-release: name: Production Release needs: [dev-publish, test-verify] runs-on: blacksmith-4vcpu-ubuntu-2404 environment: prod steps: - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PAT }} - uses: actions/setup-node@v6 with: node-version: '26.1' registry-url: https://registry.npmjs.org cache: 'npm' - name: Install dependencies run: npm ci - name: Cache Next.js build uses: useblacksmith/cache@v5 with: path: web/.next/cache key: nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-${{ hashFiles('web/app/**', 'web/components/**', 'web/lib/**', 'web/hooks/**') }} restore-keys: | nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}- nextjs-${{ runner.os }}- - name: Run live LLM tests (optional) continue-on-error: true run: npm run test:live || echo "::warning::Live LLM tests failed — non-blocking, but worth investigating" env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SF_LIVE_TESTS: "1" - name: Generate changelog and determine version id: release run: | OUTPUT=$(node scripts/generate-changelog.mjs) echo "$OUTPUT" | jq . echo "version=$(echo "$OUTPUT" | jq -r '.newVersion')" >> "$GITHUB_OUTPUT" echo "$OUTPUT" | jq -r '.changelogEntry' > /tmp/changelog-entry.md echo "$OUTPUT" | jq -r '.releaseNotes' > /tmp/release-notes.md - name: Bump version and sync packages env: RELEASE_VERSION: ${{ steps.release.outputs.version }} run: node scripts/bump-version.mjs "$RELEASE_VERSION" - name: Validate package files after version bump run: | node -e "require('./package.json')" && \ node -e "require('./packages/pi-coding-agent/package.json')" && \ node -e "require('./pkg/package.json')" && \ echo "All package.json files are valid" - name: Update CHANGELOG.md run: node scripts/update-changelog.mjs /tmp/changelog-entry.md - name: Commit and tag release env: RELEASE_VERSION: ${{ steps.release.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add package.json package-lock.json web/package-lock.json CHANGELOG.md native/npm/*/package.json pkg/package.json packages/pi-coding-agent/package.json git commit -m "release: v${RELEASE_VERSION}" git tag "v${RELEASE_VERSION}" git pull --rebase origin main - name: Build release run: npm run build - name: Publish release to npm @latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_VERSION: ${{ steps.release.outputs.version }} run: | OUTPUT=$(npm publish 2>&1) && echo "$OUTPUT" || { if echo "$OUTPUT" | grep -q "cannot publish over the previously published"; then echo "Version already published — promoting to latest" npm dist-tag add "sf-pi@${RELEASE_VERSION}" latest else echo "$OUTPUT" exit 1 fi } - name: Push release commit and tag env: RELEASE_VERSION: ${{ steps.release.outputs.version }} run: | git push origin main git push origin "v${RELEASE_VERSION}" - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.release.outputs.version }} run: | gh release create "v${RELEASE_VERSION}" \ --title "v${RELEASE_VERSION}" \ --notes-file /tmp/release-notes.md \ --latest - name: Post to Discord if: ${{ env.DISCORD_WEBHOOK != '' }} env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} RELEASE_VERSION: ${{ steps.release.outputs.version }} run: | NOTES=$(cat /tmp/release-notes.md) curl -s -X POST "$DISCORD_WEBHOOK" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg c "**SF v${RELEASE_VERSION} Released**\n\n${NOTES}\n\n\`npm i sf-pi@${RELEASE_VERSION}\`" '{content:$c}')" - name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Tag runtime Docker image as latest env: DEV_VERSION: ${{ needs.dev-publish.outputs.dev-version }} run: | docker pull "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" docker tag "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" ghcr.io/singularity-forge/sf-run:latest docker push ghcr.io/singularity-forge/sf-run:latest update-builder: name: Update CI Builder Image if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v6 with: fetch-depth: 2 - name: Check for Dockerfile changes id: check env: HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: | CHANGED=$(git diff --name-only "${HEAD_SHA}~1" "${HEAD_SHA}" -- Dockerfile || echo "") echo "changed=$([[ -n "$CHANGED" ]] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" - name: Log in to GHCR if: steps.check.outputs.changed == 'true' uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push CI builder image if: steps.check.outputs.changed == 'true' run: | docker build --target builder \ -t ghcr.io/singularity-forge/sf-ci-builder:latest \ . docker push ghcr.io/singularity-forge/sf-ci-builder:latest