fix: config path mismatch + CalVer race condition (Devin review round 2)

- _discover_and_register_tables reads from data_source.keboola.url
  (matches what /api/admin/configure writes) instead of top-level
  keboola.url which doesn't exist
- CalVer: claim git tag BEFORE Docker build with retry loop (up to 5
  attempts). Prevents race where two concurrent CI runs get same N.
  Git tag acts as a distributed lock for version uniqueness.

663 tests pass.
This commit is contained in:
ZdenekSrotyr 2026-04-10 13:30:05 +02:00
parent 49f109bf73
commit c79d85f87c
2 changed files with 30 additions and 20 deletions

View file

@ -42,32 +42,48 @@ jobs:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true fetch-tags: true
- name: Determine channel and version - name: Claim version tag (with retry to avoid race conditions)
id: meta id: meta
run: | run: |
YEAR_MONTH=$(date +%Y.%m) git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
YEAR_MONTH=$(date +%Y.%m)
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
CHANNEL="stable" CHANNEL="stable"
else else
CHANNEL="dev" CHANNEL="dev"
fi fi
# Count existing tags GLOBALLY across all channels for this month
# (spec requires unique N per month: dev-2026.04.1 and stable-2026.04.2, never both .1)
EXISTING=$(git tag -l "*-${YEAR_MONTH}.*" | wc -l | tr -d ' ')
N=$((EXISTING + 1))
VERSION="${YEAR_MONTH}.${N}"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
# Claim a unique version by pushing a git tag BEFORE building.
# Retry up to 5 times if another CI run took our N.
for ATTEMPT in 1 2 3 4 5; do
git fetch --tags --force
EXISTING=$(git tag -l "*-${YEAR_MONTH}.*" | wc -l | tr -d ' ')
N=$((EXISTING + 1))
VERSION="${YEAR_MONTH}.${N}"
TAG="${CHANNEL}-${VERSION}"
git tag -a "$TAG" -m "Release $TAG"
if git push origin "$TAG" 2>/dev/null; then
echo "Claimed tag $TAG (attempt $ATTEMPT)"
break
else
echo "Tag $TAG already exists, retrying... (attempt $ATTEMPT)"
git tag -d "$TAG"
sleep 1
fi
done
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "versioned_tag=${CHANNEL}-${VERSION}" >> "$GITHUB_OUTPUT" echo "versioned_tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "Channel: ${CHANNEL}" echo "Channel: ${CHANNEL}"
echo "Version: ${VERSION}" echo "Version: ${VERSION}"
echo "Versioned tag: ${CHANNEL}-${VERSION}" echo "Versioned tag: ${TAG}"
- name: Log in to GHCR - name: Log in to GHCR
uses: docker/login-action@v4 uses: docker/login-action@v4
@ -88,14 +104,6 @@ jobs:
ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.versioned_tag }} ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.versioned_tag }}
ghcr.io/${{ github.repository }}:sha-${{ steps.meta.outputs.short_sha }} ghcr.io/${{ github.repository }}:sha-${{ steps.meta.outputs.short_sha }}
- name: Create git tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
TAG="${{ steps.meta.outputs.versioned_tag }}"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG" || echo "Tag $TAG already exists, skipping"
smoke-test: smoke-test:
needs: build-and-push needs: build-and-push
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'

View file

@ -289,8 +289,10 @@ def _discover_and_register_tables(conn: duckdb.DuckDBPyConnection, user_email: s
return {"registered": 0, "skipped": 0, "errors": 0, "tables": [], "source": source_type} return {"registered": 0, "skipped": 0, "errors": 0, "tables": [], "source": source_type}
from connectors.keboola.client import KeboolaClient from connectors.keboola.client import KeboolaClient
url = get_value("keboola", "url", default="") # Read from data_source.keboola (matches what /api/admin/configure writes)
token = os.environ.get(get_value("keboola", "token_env", default="KEBOOLA_STORAGE_TOKEN"), "") url = get_value("data_source", "keboola", "url", default="")
token_env = get_value("data_source", "keboola", "token_env", default="KEBOOLA_STORAGE_TOKEN")
token = os.environ.get(token_env, "") if token_env else ""
if not token: if not token:
token = os.environ.get("KEBOOLA_STORAGE_TOKEN", "") token = os.environ.get("KEBOOLA_STORAGE_TOKEN", "")