diff --git a/.github/workflows/manual_regenerate_models.yaml b/.github/workflows/manual_regenerate_models.yaml index ab90f72a..0cf4d9d9 100644 --- a/.github/workflows/manual_regenerate_models.yaml +++ b/.github/workflows/manual_regenerate_models.yaml @@ -63,6 +63,45 @@ jobs: with: token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} + # Record master's SHA up front: a later step checks out the auto-update branch, so the change check below + # can't compare against HEAD. Resolve master from the remote rather than the checked-out ref: the run may be + # dispatched from a non-master ref, but that check and the PR both target master, so master is the baseline. + - name: Record master ref + id: base + run: | + git fetch --depth=1 origin master + echo "sha=$(git rev-parse FETCH_HEAD)" >> "$GITHUB_OUTPUT" + + # Does the auto-update branch already exist on the remote? If so, a previous dispatch opened the PR + # and we append to it. If not, we start it from master (below) so signed-commit creates it there. + - name: Determine auto-update branch state + id: branch + run: | + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "create=false" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "create=true" >> "$GITHUB_OUTPUT" + fi + + # Check out the existing branch before regenerating so the new models land as a NEW commit on top, + # mirroring the commit just pushed to the docs PR. We must switch now, while the tree is clean: + # signed-commit's own checkout is a plain `git checkout` that would refuse to overwrite the + # regenerated files (which differ between master and the branch) once they're in the working tree. + - name: Check out existing auto-update branch + if: steps.branch.outputs.exists == 'true' + run: | + git fetch --depth=1 origin "$BRANCH" + git checkout -B "$BRANCH" FETCH_HEAD + + # The branch doesn't exist yet, so regenerate from the recorded master SHA rather than the dispatched + # ref (a manual run may be dispatched from a non-master ref). Regenerating on master keeps the codegen + # tooling current, and signed-commit then creates the branch (the PR head) on top of master, the PR base. + - name: Start the auto-update branch from master + if: steps.branch.outputs.exists == 'false' + run: git checkout "${{ steps.base.outputs.sha }}" + # 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 @@ -93,46 +132,44 @@ jobs: 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. + # Proceed only when the regenerated models differ from master (compared against the recorded SHA, + # since the tree may now sit on the branch), which skips empty-PR runs: a spec change that doesn't + # affect the client models, or one already merged into master. Also avoids creating an empty branch. - 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 + if git diff --quiet "${{ steps.base.outputs.sha }}" -- 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 + # Append the regenerated models to the auto-update branch as a single signed ("Verified") commit + # via apify/actions/signed-commit (GitHub's createCommitOnBranch GraphQL mutation). It's added on + # top of the branch tip, never resetting to master: a fresh docs PR creates the branch + # (create-branch) and its first commit, and each later docs-PR commit triggers a dispatch that + # appends another, so the client PR mirrors the docs PR and stays open. + # + # Appending (rather than force-pushing the branch to master, as before) is what keeps the PR open: + # a "branch == master" tip makes the PR head equal its base, which GitHub auto-closes, and each + # dispatch then opens a duplicate PR. signed-commit also sets committed=false when the staged + # files match the tip, so a repeated dispatch regenerating identical models adds no commit. + - name: Commit regenerated models 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' + 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' + create-branch: "${{ steps.branch.outputs.create }}" + # Ensure exactly one PR exists for this branch. It's no longer auto-closed, so an existing PR is + # reused. Only the first run (or one whose PR step a concurrent dispatch cancelled) creates it. - name: Create or update PR - if: steps.commit.outputs.committed == 'true' + if: steps.changes.outputs.has_changes == 'true' id: pr env: GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} @@ -164,9 +201,11 @@ jobs: 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. + # Post a cross-repo comment on the docs PR pointing reviewers to the companion client-python PR. + # Only when something happened: the PR was just created, or a commit was appended. A repeated + # dispatch that changes nothing (committed=false) posts no comment, avoiding noise on the docs PR. - name: Comment on apify-docs PR - if: steps.commit.outputs.committed == 'true' && inputs.docs_pr_number + if: inputs.docs_pr_number && (steps.pr.outputs.created == 'true' || steps.commit.outputs.committed == 'true') env: GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} PR_CREATED: ${{ steps.pr.outputs.created }}