Regenerate models #114
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
| # This workflow regenerates Pydantic models, TypedDicts, and Literal aliases (src/apify_client/_{models,typeddicts,literals}.py) from the OpenAPI spec. | |
| # | |
| # It can be triggered in two ways: | |
| # 1. Automatically via workflow_dispatch from the apify-docs CI pipeline. | |
| # 2. Manually from the GitHub UI (without any inputs) to regenerate from the live published spec. | |
| name: Regenerate models | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| docs_pr_number: | |
| description: PR number in apify/apify-docs that triggered this workflow (optional for manual runs) | |
| required: false | |
| type: string | |
| docs_workflow_run_id: | |
| description: Workflow run ID in apify/apify-docs that built the OpenAPI spec artifact (optional for manual runs) | |
| required: false | |
| type: string | |
| docs_pr_author: | |
| description: GitHub login of the apify-docs PR author (optional for manual runs) | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: regenerate-models-${{ inputs.docs_pr_number || 'manual' }} | |
| cancel-in-progress: true | |
| jobs: | |
| regenerate-models: | |
| name: Regenerate models | |
| runs-on: ubuntu-latest | |
| env: | |
| DOCS_PR_NUMBER: ${{ inputs.docs_pr_number }} | |
| BRANCH: ${{ inputs.docs_pr_number && format('update-models-docs-pr-{0}', inputs.docs_pr_number) || 'update-models-manual' }} | |
| TITLE: "${{ inputs.docs_pr_number && format('[TODO]: update generated models from apify-docs PR #{0}', inputs.docs_pr_number) || '[TODO]: update generated models from published OpenAPI spec' }}" | |
| ASSIGNEE: ${{ inputs.docs_pr_author || github.actor }} | |
| REVIEWER: vdusek | |
| LABEL: t-tooling | |
| steps: | |
| - name: Validate inputs | |
| if: inputs.docs_pr_number || inputs.docs_workflow_run_id | |
| env: | |
| DOCS_WORKFLOW_RUN_ID: ${{ inputs.docs_workflow_run_id }} | |
| run: | | |
| if [[ -n "$DOCS_PR_NUMBER" ]] && ! [[ "$DOCS_PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::docs_pr_number must be a positive integer, got: $DOCS_PR_NUMBER" | |
| exit 1 | |
| fi | |
| if [[ -n "$DOCS_WORKFLOW_RUN_ID" ]] && ! [[ "$DOCS_WORKFLOW_RUN_ID" =~ ^[0-9]+$ ]]; then | |
| echo "::error::docs_workflow_run_id must be a numeric run ID, got: $DOCS_WORKFLOW_RUN_ID" | |
| exit 1 | |
| fi | |
| - name: Checkout apify-client-python | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| # Download the pre-built OpenAPI spec artifact from the apify-docs workflow run. | |
| # Skipped for manual runs — datamodel-codegen will fetch from the published spec URL instead. | |
| - name: Download OpenAPI spec artifact | |
| if: inputs.docs_workflow_run_id | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: openapi-bundles | |
| path: openapi-spec | |
| repository: apify/apify-docs | |
| run-id: ${{ inputs.docs_workflow_run_id }} | |
| github-token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v8.2.0 | |
| with: | |
| python-version: "3.14" | |
| - name: Install dependencies | |
| run: uv run poe install-dev | |
| # When a docs workflow run ID is provided, use the downloaded artifact. | |
| # Otherwise, datamodel-codegen fetches from the default URL configured in pyproject.toml. | |
| - name: Generate models from OpenAPI spec | |
| run: | | |
| if [[ -f openapi-spec/openapi.json ]]; then | |
| uv run poe generate-models-from-file openapi-spec/openapi.json | |
| else | |
| uv run poe generate-models | |
| fi | |
| # Proceed only when regeneration actually changes the models relative to the current master. | |
| # The job runs on a fresh master checkout, so anything already merged into master (e.g. a | |
| # manually merged client PR carrying the same spec change, or a docs PR that merged master in | |
| # and re-emits an already-applied change) produces no diff here and we skip — instead of | |
| # opening a PR that just duplicates what master already has. | |
| - name: Check for model changes | |
| id: changes | |
| run: | | |
| if git diff --quiet -- src/apify_client/_models.py src/apify_client/_typeddicts.py src/apify_client/_literals.py; then | |
| echo "No model changes relative to master — nothing to regenerate." | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Point the auto-update branch at the current master so the signed-commit step below records | |
| # the regenerated models as a single commit on top of it. Resetting to master (instead of | |
| # building on a possibly stale existing branch) is what keeps the committed diff relative to | |
| # the current master: an already-merged change can never reappear. Any previous content on the | |
| # branch is intentionally replaced, so the PR always reflects "current master + freshly | |
| # regenerated models" — and it still updates whenever the source docs PR gets new commits. | |
| - name: Point branch at current master | |
| if: steps.changes.outputs.has_changes == 'true' | |
| run: | | |
| git checkout -B "$BRANCH" | |
| git push --force origin "HEAD:refs/heads/$BRANCH" | |
| - name: Commit model changes | |
| id: commit | |
| if: steps.changes.outputs.has_changes == 'true' | |
| uses: apify/actions/signed-commit@v1.2.0 | |
| with: | |
| message: ${{ env.TITLE }} | |
| add: 'src/apify_client/_models.py src/apify_client/_typeddicts.py src/apify_client/_literals.py' | |
| github-token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| branch: ${{ env.BRANCH }} | |
| create-branch: 'false' | |
| - name: Create or update PR | |
| if: steps.commit.outputs.committed == 'true' | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| run: | | |
| EXISTING_PR=$(gh pr list --head "$BRANCH" --json url --jq '.[0].url' 2>/dev/null || true) | |
| if [[ -n "$EXISTING_PR" ]]; then | |
| echo "PR already exists: $EXISTING_PR" | |
| echo "pr_url=$EXISTING_PR" >> "$GITHUB_OUTPUT" | |
| echo "created=false" >> "$GITHUB_OUTPUT" | |
| else | |
| if [[ -n "$DOCS_PR_NUMBER" ]]; then | |
| DOCS_PR_URL="https://github.com/apify/apify-docs/pull/${DOCS_PR_NUMBER}" | |
| BODY="This PR updates the auto-generated Pydantic models and TypedDicts based on OpenAPI specification changes in [apify-docs PR #${DOCS_PR_NUMBER}](${DOCS_PR_URL})." | |
| else | |
| BODY="This PR updates the auto-generated Pydantic models and TypedDicts from the [published OpenAPI specification](https://docs.apify.com/api/openapi.json)." | |
| fi | |
| PR_URL=$(gh pr create \ | |
| --title "$TITLE" \ | |
| --body "$BODY" \ | |
| --head "$BRANCH" \ | |
| --base master \ | |
| --reviewer "$REVIEWER" \ | |
| --assignee "$ASSIGNEE" \ | |
| --label "$LABEL") | |
| echo "Created PR: $PR_URL" | |
| echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" | |
| echo "created=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Post a cross-repo comment on the original docs PR so reviewers know about the corresponding client-python PR. | |
| - name: Comment on apify-docs PR | |
| if: steps.commit.outputs.committed == 'true' && inputs.docs_pr_number | |
| env: | |
| GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| PR_CREATED: ${{ steps.pr.outputs.created }} | |
| PR_URL: ${{ steps.pr.outputs.pr_url }} | |
| DOCS_PR_AUTHOR: ${{ inputs.docs_pr_author }} | |
| run: | | |
| MENTION="" | |
| if [[ -n "$DOCS_PR_AUTHOR" ]]; then | |
| MENTION="@${DOCS_PR_AUTHOR} " | |
| fi | |
| if [[ "$PR_CREATED" = "true" ]]; then | |
| HEADLINE="A companion PR has been opened in \`apify-client-python\` with the regenerated models: ${PR_URL}" | |
| else | |
| HEADLINE="The companion \`apify-client-python\` PR has been updated with the latest spec changes: ${PR_URL}" | |
| fi | |
| COMMENT=$(printf '%s\n' \ | |
| "> [!IMPORTANT]" \ | |
| "> **Action required** — ${MENTION}please coordinate this docs PR with the Python API client PR linked below." \ | |
| ">" \ | |
| "> Because this PR modifies the OpenAPI specification, the generated models in \`apify-client-python\` must be regenerated to stay in sync. This has already been done automatically:" \ | |
| ">" \ | |
| "> ${HEADLINE}" \ | |
| ">" \ | |
| "> - Please make sure to review and merge both PRs together to keep the OpenAPI spec and API clients in sync." \ | |
| "> - You can ask for review and help from the Tooling team if needed.") | |
| gh pr comment "$DOCS_PR_NUMBER" \ | |
| --repo apify/apify-docs \ | |
| --body "$COMMENT" | |
| - name: Comment on failure | |
| if: failure() && inputs.docs_pr_number | |
| env: | |
| GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| run: | | |
| gh pr comment "$DOCS_PR_NUMBER" \ | |
| --repo apify/apify-docs \ | |
| --body "Python client model regeneration failed. [See workflow run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})." \ | |
| || echo "Warning: Failed to post failure comment to apify/apify-docs PR #$DOCS_PR_NUMBER." |