diff --git a/.github/workflows/publish-rc.yaml b/.github/workflows/publish-rc.yaml new file mode 100644 index 0000000..aefd14d --- /dev/null +++ b/.github/workflows/publish-rc.yaml @@ -0,0 +1,140 @@ +name: Publish RC Package + +on: + workflow_dispatch: + +jobs: + publish-rc: + runs-on: ubuntu-latest + + permissions: + contents: write # Required for creating tags and releases + id-token: write # Required for trusted publishing to PyPI + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Connect to Tailscale + uses: tailscale/github-action@d1b6cd204f8dceda5b3eaad7f1f767be390056cd # v4.2.0 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} + tags: tag:ci + + # Servers are pinned to production so the staging host never ships in the package. + - name: Fetch and normalise staging OpenAPI spec + env: + STAGING_OPENAPI_URL: ${{ secrets.STAGING_OPENAPI_URL }} + run: | + curl -fsSL "$STAGING_OPENAPI_URL" -o openapi.json + python3 - <<'PY' + import json + + with open("openapi.json") as f: + spec = json.load(f) + + spec["servers"] = [{"description": "Production", "url": "https://api.reveng.ai"}] + + for schema in spec.get("components", {}).get("schemas", {}).values(): + if not isinstance(schema, dict) or schema.get("additionalProperties") is not False: + continue + if schema.get("properties"): + schema["additionalProperties"] = True + else: + # Empty-body schemas stay bare, otherwise they generate as + # Dict[str, object] instead of object. + del schema["additionalProperties"] + + with open("openapi.json", "w") as f: + json.dump(spec, f) + PY + + - name: Compute RC version + id: version + run: | + BASE=$(jq -r '.info.version' openapi.json) + TAGS=$(git ls-remote --tags --refs origin) + + # Staging may not be bumped yet; target the next unreleased patch + while echo "$TAGS" | grep -q "refs/tags/${BASE}$"; do + BASE=$(echo "$BASE" | awk -F. -v OFS=. '{$3++; print}') + done + + N=$(git ls-remote --tags --refs origin "refs/tags/${BASE}-rc.*" | wc -l) + VERSION="${BASE}-rc.$((N + 1))" + + echo "RC version: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Generate Python SDK + uses: openapi-generators/openapitools-generator-action@b729d184e6b3459572c37c0e37f88a832e69b552 # v1 + with: + generator: python + generator-tag: 'v7.17.0' + openapi-file: openapi.json + config-file: config.yml + template-dir: templates + command-args: | + --additional-properties=packageVersion=${{ steps.version.outputs.version }} + + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.REVENG_APP_ID }} + private-key: ${{ secrets.REVENG_APP_PRIVATE_KEY }} + + # The tag points at a detached commit holding the generated SDK; no branch is updated. + - name: Commit, tag and push + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + SDK_VERSION: ${{ steps.version.outputs.version }} + STAGING_OPENAPI_URL: ${{ secrets.STAGING_OPENAPI_URL }} + run: | + rm -Rf docs && mv python-client/docs . + rm -Rf revengai && mv python-client/revengai . + rm -Rf README.md && mv python-client/README.md . + rm -Rf python-client openapi.json + echo "$SDK_VERSION" > .sdk-version + + STAGING_HOST=$(echo "$STAGING_OPENAPI_URL" | awk -F/ '{print $3}') + if grep -rqF "$STAGING_HOST" .; then + echo "Error: staging host leaked into generated SDK" + exit 1 + fi + + git config --global user.name "reveng-github[bot]" + git config --global user.email "reveng-github[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + git add . + git commit -m "RC SDK ${SDK_VERSION}" + git tag -a "$SDK_VERSION" -m "$SDK_VERSION" + git push origin "$SDK_VERSION" + + - name: Create GitHub pre-release + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + SDK_VERSION: ${{ steps.version.outputs.version }} + run: | + gh release create "$SDK_VERSION" \ + --title "$SDK_VERSION" \ + --notes "$SDK_VERSION" \ + --prerelease \ + --latest=false \ + --verify-tag + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version-file: '.python-version' + + - name: Install uv + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + + # uv-dynamic-versioning turns tag vX.Y.Z-rc.N into PEP 440 X.Y.ZrcN + - name: Build and publish package to PyPI + run: | + uv build + uv publish