Dev #63
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: CI | |
| on: | |
| pull_request: | |
| branches: [ main ] | |
| push: | |
| branches: [ main ] | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| name: Run Tests and Checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.12.4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run Biome check | |
| run: pnpm biome check | |
| - name: TypeScript type check | |
| run: pnpm tsc --noEmit | |
| - name: Run tests | |
| run: pnpm test | |
| build-and-push: | |
| name: Build and Push Image | |
| needs: test | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.actor != 'github-actions[bot]' | |
| runs-on: ubuntu-latest | |
| environment: production | |
| # Expose next version to the release job | |
| outputs: | |
| release_version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Compute release version (accepts .version=2.3 / 2.3.0 / v2.3.0) | |
| id: version | |
| run: | | |
| node <<'NODE' | |
| const cp = require('node:child_process'); | |
| const fs = require('node:fs'); | |
| function parseSemver(input) { | |
| const m = String(input || '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)(?:\.(\d+))?$/); | |
| if (!m) return null; | |
| return { major:+m[1], minor:+m[2], patch: m[3] !== undefined ? +m[3] : 0 }; | |
| } | |
| function cmp(a, b) { | |
| if (a.major !== b.major) return a.major - b.major; | |
| if (a.minor !== b.minor) return a.minor - b.minor; | |
| return a.patch - b.patch; | |
| } | |
| function latestTag() { | |
| try { | |
| const out = cp.execSync( | |
| "git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1", | |
| { encoding: 'utf8' } | |
| ).trim(); | |
| return out || null; | |
| } catch { return null; } | |
| } | |
| const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| const fromPkg = parseSemver(pkg.version) || { major:0, minor:1, patch:0 }; | |
| const lt = latestTag(); | |
| const current = lt ? parseSemver(lt) : fromPkg; | |
| let target; | |
| let reason = 'defaulting to patch bump'; | |
| if (fs.existsSync('.version')) { | |
| const raw = fs.readFileSync('.version', 'utf8').trim(); | |
| const desired = parseSemver(raw); | |
| if (desired) { | |
| if (cmp(desired, current) > 0) { | |
| target = `${desired.major}.${desired.minor}.${desired.patch}`; | |
| reason = `using override ${raw} -> ${target}`; | |
| } else { | |
| reason = `override "${raw}" is not greater than current ${current.major}.${current.minor}.${current.patch}`; | |
| } | |
| } else if (raw) { | |
| reason = `ignoring invalid override "${raw}"`; | |
| } | |
| } | |
| if (!target) { | |
| target = `${current.major}.${current.minor}.${current.patch + 1}`; | |
| } | |
| console.log(`Release version: ${target} (${reason})`); | |
| require('fs').appendFileSync(process.env.GITHUB_OUTPUT, `version=${target}\n`); | |
| NODE | |
| - name: Install doctl | |
| uses: digitalocean/action-doctl@v2 | |
| with: | |
| token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} | |
| - name: Log in to DigitalOcean Container Registry | |
| run: doctl registry login --expiry-seconds 1800 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build and push image | |
| id: build-image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| push: true | |
| build-args: | | |
| APP_VERSION=${{ steps.version.outputs.version }} | |
| tags: | | |
| ${{ secrets.DO_REGISTRY_REPOSITORY }}/agent8s:latest | |
| ${{ secrets.DO_REGISTRY_REPOSITORY }}/agent8s:${{ github.sha }} | |
| ${{ secrets.DO_REGISTRY_REPOSITORY }}/agent8s:v${{ steps.version.outputs.version }} | |
| release: | |
| name: Tag repo and create GitHub Release | |
| needs: build-and-push | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.actor != 'github-actions[bot]' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Configure git (for annotated tag) | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Create and push tag (idempotent) | |
| env: | |
| REL: ${{ needs.build-and-push.outputs.release_version }} | |
| run: | | |
| TAG="v${REL}" | |
| if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then | |
| echo "Tag $TAG already exists; skipping push." | |
| else | |
| git tag -a "$TAG" -m "release: $TAG" | |
| git push origin "$TAG" | |
| fi | |
| - name: Create GitHub release (idempotent) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REL: ${{ needs.build-and-push.outputs.release_version }} | |
| run: | | |
| TAG="v${REL}" | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists; nothing to do." | |
| else | |
| gh release create "$TAG" \ | |
| --repo="$GITHUB_REPOSITORY" \ | |
| --title="Agent 8s v${REL}" \ | |
| --generate-notes | |
| fi |