|
| 1 | +name: CI |
| 2 | +run-name: "CI ${{ github.ref_name }} by @${{ github.actor }}" |
| 3 | + |
| 4 | +# CI on GitHub-hosted runners: validate, build the multi-arch image, tag releases. |
| 5 | +# |
| 6 | +# Version = <version.txt MAJOR.MINOR>.<patch>, patch = (max existing x.y tag) + 1, |
| 7 | +# so it starts at 0 and resets per minor: |
| 8 | +# push to main -> ghcr.io/comet-ml/opik-mcp:0.2.0 (+ :main, + git tag 0.2.0) |
| 9 | +# PR -> build only (no push, no secrets — fork-safe); version |
| 10 | +# carries a -<branch> suffix for the image tag if built |
| 11 | +# |
| 12 | +# Bump version.txt (e.g. 0.2 -> 0.3) to start a new minor line; patch resets to 0. |
| 13 | +# Release is manual (release.yaml): pick a git tag, promote its image, publish. |
| 14 | + |
| 15 | +on: |
| 16 | + workflow_dispatch: |
| 17 | + pull_request: |
| 18 | + branches: |
| 19 | + - main |
| 20 | + push: |
| 21 | + branches: |
| 22 | + - main |
| 23 | + |
| 24 | +permissions: |
| 25 | + contents: read |
| 26 | + |
| 27 | +env: |
| 28 | + IMAGE: ghcr.io/comet-ml/opik-mcp |
| 29 | + |
| 30 | +jobs: |
| 31 | + |
| 32 | + version: |
| 33 | + runs-on: ubuntu-latest |
| 34 | + permissions: |
| 35 | + contents: write # push the git tag on main builds |
| 36 | + outputs: |
| 37 | + version: ${{ steps.v.outputs.version }} # image tag (may carry -branch) |
| 38 | + pyver: ${{ steps.v.outputs.pyver }} # PEP 440 form for the wheel |
| 39 | + steps: |
| 40 | + - uses: actions/checkout@v6 |
| 41 | + with: |
| 42 | + fetch-depth: 0 |
| 43 | + fetch-tags: true |
| 44 | + - name: Compute version (patch = max x.y tag + 1; tag on main) |
| 45 | + id: v |
| 46 | + env: |
| 47 | + REF_NAME: ${{ github.head_ref || github.ref_name }} |
| 48 | + EVENT: ${{ github.event_name }} |
| 49 | + run: | |
| 50 | + set -euo pipefail |
| 51 | + BASE=$(tr -d ' \t\r\n' < version.txt) |
| 52 | + if ! echo "$BASE" | grep -qE '^[0-9]+\.[0-9]+$'; then |
| 53 | + echo "::error::version.txt must be MAJOR.MINOR (x.y), got '$BASE'"; exit 1 |
| 54 | + fi |
| 55 | +
|
| 56 | + # Highest existing patch among tags exactly "BASE.<digits>" (-1 if none → next is 0). |
| 57 | + find_max_patch() { |
| 58 | + local max=-1 rem |
| 59 | + while IFS= read -r t; do |
| 60 | + [ -z "$t" ] && continue |
| 61 | + rem="${t#"$BASE."}" |
| 62 | + if [[ "$rem" =~ ^[0-9]+$ ]] && (( rem > max )); then max=$rem; fi |
| 63 | + done < <(git tag --list "$BASE.*") |
| 64 | + echo "$max" |
| 65 | + } |
| 66 | +
|
| 67 | + if [ "$REF_NAME" = "main" ] && [ "$EVENT" = "push" ]; then |
| 68 | + git config user.name "github-actions" |
| 69 | + git config user.email "github-actions@comet.com" |
| 70 | + # Retry to stay race-safe if two main builds tag at once. |
| 71 | + for attempt in 1 2 3 4 5; do |
| 72 | + if [ "$attempt" -gt 1 ]; then |
| 73 | + git fetch --tags --force >/dev/null 2>&1 || true |
| 74 | + sleep $(( RANDOM % 3 + 1 )) |
| 75 | + fi |
| 76 | + PATCH=$(( $(find_max_patch) + 1 )) |
| 77 | + VERSION="$BASE.$PATCH" |
| 78 | + if git tag "$VERSION" 2>/dev/null && git push origin "refs/tags/$VERSION" 2>/dev/null; then |
| 79 | + echo "Tagged $VERSION" |
| 80 | + break |
| 81 | + fi |
| 82 | + git tag -d "$VERSION" 2>/dev/null || true |
| 83 | + if [ "$attempt" -eq 5 ]; then |
| 84 | + echo "::error::failed to create tag after 5 attempts"; exit 1 |
| 85 | + fi |
| 86 | + done |
| 87 | + else |
| 88 | + PATCH=$(( $(find_max_patch) + 1 )) |
| 89 | + # Normalize: lowercase, non-alnum -> '-', cap at 20 chars, |
| 90 | + # strip trailing dashes. |
| 91 | + SLUG=$(echo "$REF_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9]+#-#g; s#^-+##; s#-+$##') |
| 92 | + SLUG=${SLUG:0:20}; SLUG=${SLUG%-} |
| 93 | + VERSION="$BASE.$PATCH-$SLUG" |
| 94 | + fi |
| 95 | + # The image tag may carry a -branch suffix (fine for Docker tags), but |
| 96 | + # the wheel/_version.py must be PEP 440 — so branch builds use .devN. |
| 97 | + if [ "$REF_NAME" = "main" ] && [ "$EVENT" = "push" ]; then |
| 98 | + PYVER="$BASE.$PATCH" |
| 99 | + else |
| 100 | + PYVER="$BASE.$PATCH.dev0" |
| 101 | + fi |
| 102 | + echo "version=$VERSION" | tee -a "$GITHUB_OUTPUT" |
| 103 | + echo "pyver=$PYVER" | tee -a "$GITHUB_OUTPUT" |
| 104 | + echo "### Version: $VERSION" >> "$GITHUB_STEP_SUMMARY" |
| 105 | +
|
| 106 | + python-checks: |
| 107 | + runs-on: ubuntu-latest |
| 108 | + steps: |
| 109 | + - uses: actions/checkout@v6 |
| 110 | + - name: Generate _version.py (required before any uv build) |
| 111 | + run: make version |
| 112 | + - uses: astral-sh/setup-uv@v6 |
| 113 | + with: |
| 114 | + version: 'latest' |
| 115 | + - name: Install Python |
| 116 | + run: uv python install 3.13 |
| 117 | + - name: Install dependencies |
| 118 | + run: uv sync --extra dev |
| 119 | + - name: Run lint + typecheck + tests |
| 120 | + run: make check |
| 121 | + |
| 122 | + helm-lint: |
| 123 | + runs-on: ubuntu-latest |
| 124 | + steps: |
| 125 | + - uses: actions/checkout@v6 |
| 126 | + - uses: azure/setup-helm@v4 |
| 127 | + with: |
| 128 | + version: v3.16.2 |
| 129 | + - name: Helm lint |
| 130 | + run: helm lint helm/opik-mcp -f helm/opik-mcp/values.yaml |
| 131 | + - name: Helm template |
| 132 | + run: helm template opik-mcp helm/opik-mcp -f helm/opik-mcp/values.yaml > /dev/null |
| 133 | + |
| 134 | + build-image: |
| 135 | + needs: version |
| 136 | + runs-on: ubuntu-latest |
| 137 | + permissions: |
| 138 | + contents: read |
| 139 | + packages: write |
| 140 | + steps: |
| 141 | + - uses: actions/checkout@v6 |
| 142 | + - name: Generate _version.py (baked into the image via COPY src) |
| 143 | + run: VERSION="${{ needs.version.outputs.pyver }}" make version |
| 144 | + - uses: docker/setup-qemu-action@v3 |
| 145 | + - uses: docker/setup-buildx-action@v3 |
| 146 | + - name: Login to GHCR |
| 147 | + if: github.event_name != 'pull_request' |
| 148 | + uses: docker/login-action@v3 |
| 149 | + with: |
| 150 | + registry: ghcr.io |
| 151 | + username: ${{ github.actor }} |
| 152 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 153 | + - name: Build and push (push only off PRs) |
| 154 | + uses: docker/build-push-action@v6 |
| 155 | + with: |
| 156 | + context: . |
| 157 | + file: Dockerfile |
| 158 | + platforms: linux/amd64,linux/arm64 |
| 159 | + push: ${{ github.event_name != 'pull_request' }} |
| 160 | + tags: | |
| 161 | + ${{ env.IMAGE }}:${{ needs.version.outputs.version }} |
| 162 | + ${{ github.ref_name == 'main' && format('{0}:main', env.IMAGE) || '' }} |
| 163 | + cache-from: type=gha |
| 164 | + cache-to: type=gha,mode=max |
0 commit comments