Skip to content

feat(ci): name the docs sync branch after the exported commit - #490

Open
Tim Beyer (TimBeyer) wants to merge 1 commit into
mainfrom
feat/per-release-sync-branch
Open

Tim Beyer (TimBeyer) wants to merge 1 commit into
mainfrom
feat/per-release-sync-branch

Conversation

@TimBeyer

Copy link
Copy Markdown
Collaborator

Summary

The sync delivered to one fixed branch, sync/optimization-docs, force-pushed on every run. That converged the release fan-out onto a single pull request — worth keeping — but it made the branch permanent while each delivery was not.

Branch names now carry the exported commit: sync/optimization-docs-<short-sha>.

What the fixed branch actually cost

Two things, both verified rather than assumed:

  1. contentful-docs sets deleteBranchOnMerge=true. The branch was deleted on merge and recreated under the same name by the next sync. Not a forgotten branch — a resurrected one.
  2. A release landing while an earlier sync was open force-pushed over the commit a reviewer was reading. That repository allows squash merges only, so there was no history to fall back on. This is the sharper problem: a reviewer's in-progress comments could end up attached to content that no longer exists.

Why the commit is the right key, and the tag is not

Merging the grouped release pull request tags every component at the same merge commit. Confirmed against the repository's own tags:

2026-08-21  a735ef03  optimization-android-v1.1.0
2026-08-21  a735ef03  optimization-core-v1.3.0
2026-08-21  a735ef03  optimization-nextjs-v1.3.0
   … 9 tags, one commit

So keying on the commit gives exactly one identity per release moment, and all of that moment's release events converge on one branch and one pull request with no force-push race. Keying on the tag would have produced nine branches and nine pull requests for one release.

A later release exports a different commit and gets its own branch, which is never rewritten and never outlives its own merge. The remaining --force covers only re-running the same commit.

Superseding

Because each delivery now has a distinct branch, two sync pull requests could be open at once, and since a sync replaces the whole generated corpus they could only conflict with each other. Opening one closes any older sync pull request and deletes its branch, so an unreviewed sync is superseded rather than queued.

That also retires the old fixed branch on the first run, since sync/optimization-docs matches the prefix.

Validation

Exercised the supersede filter against a representative pull request list:

Pull request Head Action
#300 sync/optimization-docs-a735ef03 (current) left alone
#301 sync/optimization-docs-65efcf2e closed
#302 sync/optimization-docs (legacy fixed) closed
#303 feat/unrelated-docs-change left alone
#304 sync/openapi-something left alone
#305 renovate/fern-api-5.x left alone
  • workflow YAML parses; all run blocks pass bash -n; no ${{ }} inside any run:
  • pnpm fern:check, Prettier clean

The jq filter uses --arg rather than string interpolation so a branch name cannot alter the expression.

🤖 Generated with Claude Code

The sync delivered to one fixed branch, force-pushed. That converged the release
fan-out onto a single pull request, which is worth keeping, but it made the
branch permanent while each delivery was not.

contentful-docs sets deleteBranchOnMerge, so the branch was deleted on merge and
recreated by the next sync under the same name. Worse, a release landing while an
earlier sync was still open force-pushed over the commit a reviewer was reading,
and that repository allows squash merges only, so there was no history to fall
back on.

Naming the branch after the exported commit keeps the convergence and drops both
problems. Merging the grouped release pull request tags every component at one
merge commit, so all of that moment's release events resolve to the same branch;
a later release exports a different commit and gets its own branch, which is
never rewritten and never outlives its own merge.

Because each delivery now has a distinct branch, two sync pull requests could be
open at once, and since a sync replaces the whole generated corpus they could
only conflict with each other. Opening one now closes any older sync pull request
and deletes its branch, which also retires the previous fixed branch on the first
run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bito-code-review

Copy link
Copy Markdown

Impact Analysis by Bito

Cross-Repository Impact Analysis
What Changed Impact of Change Suggested Review Actions
The documentation sync workflow changed from one fixed branch to a branch derived from the exported commit SHA, and it now closes older open synchronization pull requests. - verify-needed: contentful/contentful-docs branch and pull-request automation consuming the former sync/optimization-docs convention: The workflow no longer pushes the fixed branch and instead creates sync/optimization-docs-<short-sha>. Any target-repository automation, branch protection, allowlist, or cleanup logic keyed to the former branch name may need updating. - Verify in contentful/contentful-docs that branch protection, GitHub Actions, PR labeling, and cleanup automation do not require the exact branch name sync/optimization-docs.
- Run the workflow twice for the same commit and confirm that it updates one open PR rather than creating duplicates.
- Run a later commit while the previous sync PR remains open and confirm that the older PR is closed and its branch is deleted.
Code Paths Analyzed

Impact:
The change makes generated-documentation delivery commit-specific rather than fixed-branch based. Release fan-out events for component tags pointing to the same commit converge on one branch and pull request, while later commits receive independent branches. The workflow also actively supersedes older open documentation-sync pull requests.

Flow:
Release publication or manual dispatch → checkout the requested source ref → resolve ref and short commit SHA → generate and validate the Fern bundle → obtain a scoped Vault GitHub App token → checkout contentful-docs → apply generated documentation → create or update sync/optimization-docs-&lt;sha&gt; → close older open sync pull requests and delete their branches.

Direct Changes (Diff Files):
• .github/workflows/sync-fern-docs.yaml [1-241] — Renames the fixed branch environment variable to SYNC_BRANCH_PREFIX, derives a branch from the exported commit SHA, uses the derived branch for push and PR operations, captures the PR URL, and closes older open synchronization PRs.
• docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.md [42-142] — Documents the commit-derived branch strategy, the rationale for replacing the fixed force-pushed branch, and the consequence that older open sync PRs are superseded.

Repository Impact:
Documentation generation and publication workflow: The workflow's branch, pull-request reuse, and supersession behavior changes while Fern validation and bundle generation remain unchanged.
Release automation: Multiple release events for components from one grouped release are expected to converge by commit SHA, while releases from different commits no longer overwrite the same review branch.
Pull-request lifecycle management: The workflow now enumerates open PRs whose head branches start with the configured prefix, closes them, and deletes their branches after opening or updating the current PR.

Cross-Repository Dependencies:
contentful/contentful-docs: The workflow checks out this repository, applies the generated corpus, pushes a branch, and opens or edits a pull request against its main branch.
Vault and GitHub App cross-repository authorization: The workflow continues to derive a target-specific role and secret path and uses the resulting short-lived token for contentful-docs operations.

Database/Caching Impact:
• None

API Contract Violations:
• The former fixed branch contract, sync/optimization-docs, is removed in favor of sync/optimization-docs-&lt;short-sha&gt;. Target-repository automation that assumes the exact old branch name could break.
• The workflow assumes all synchronization branches with the configured prefix are safe to close and delete. Any unrelated branch using the same prefix would be treated as a superseded documentation sync.

Infrastructure Dependencies:
• GitHub Actions release and workflow_dispatch triggers remain the entry points.
• The workflow still requires the target-owned Vault cross-repository grant, GitHub App token, and permissions for contents: write and pull_requests: write in contentful-docs.
• The target repository must permit branch deletion through gh pr close --delete-branch.
• The workflow relies on git, gh, jq, pnpm, Node.js from .nvmrc, and the existing pnpm lockfile.
• The concurrency group remains global as fern-docs-sync with cancel-in-progress: false; queued runs are expected to complete rather than be cancelled.

Additional Insights:
Branch-name collision risk: The branch uses git rev-parse --short HEAD. Short SHA collisions are unlikely but possible at repository scale; using a full SHA would provide a stronger uniqueness guarantee if branch identity must be collision-proof.
Supersession behavior: Because each sync replaces the whole generated corpus, closing older open sync PRs avoids competing full-corpus changes but intentionally discards review continuity for an unmerged older export.

Testing Recommendations

Frontend Impact:
None.

Service Integration:
• Run a release-triggered workflow with multiple component releases pointing to the same commit and verify that all runs converge on sync/optimization-docs-&lt;sha&gt; and one open PR.
• Run workflow_dispatch from the default branch with an explicit ref and verify that pnpm docs:fern -- --ref uses that ref while Vault role selection still authenticates against the default-branch launch context.
• Create an older open PR with a sync/optimization-docs-* head branch, run a newer sync, and verify that the older PR is closed with the supersession comment and its branch is deleted.
• Verify that an unchanged generated corpus exits at the git status check without creating or closing pull requests.

Data Serialization:
• Validate that the resolve step writes ref, sha, and branch outputs correctly, including an empty inputs.ref on release events and an explicit inputs.ref on manual dispatch.
• Check the generated PR body and commit message for correct SOURCE_REPO, EXPORT_REF, and EXPORT_SHA values.

Privacy Compliance:
• Confirm workflow logs do not print the Vault-issued GITHUB_TOKEN or the resolved secret value.
• Confirm the GitHub App token remains scoped to contentful-docs and that branch cleanup cannot target repositories outside DOCS_REPO.

Backward Compatibility:
• Search contentful-docs automation and branch-protection configuration for the removed exact branch name sync/optimization-docs and update any consumers before merge.
• Verify that existing open fixed-branch sync PRs are handled safely: the new prefix-based cleanup should either close the old fixed branch explicitly or document that manual cleanup is required.

OAuth Functionality:
• None

Cross-Service Communication:
• Perform a dry-run or controlled run against contentful-docs to verify checkout, push, PR creation, PR editing, and branch deletion with the Vault-issued token.
• Confirm that contentful-docs accepts generated navigation and page changes from a commit-derived head branch and that its Fern preview/check workflow still runs.

Reliability Testing:
• None

Additional Insights:
• Run actionlint or an equivalent GitHub Actions workflow parser and shellcheck on the modified YAML and embedded shell.
• Consider testing with a full commit SHA or documenting why the short SHA is sufficient for branch uniqueness.
• Check the behavior when gh pr list returns more than 100 open prefixed PRs; the current --limit 100 could leave older synchronization PRs open.

Analysis based on known dependency patterns and edges. Actual impact may vary.

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #b98f73

Actionable Suggestions - 1
  • .github/workflows/sync-fern-docs.yaml - 1
    • Undefined `SYNC_BRANCH_PREFIX` may close unrelated PRs · Line 234-236
Additional Suggestions - 1
  • .github/workflows/sync-fern-docs.yaml - 1
    • Supersede loop pagination cap · Line 234-234
      The supersede loop uses `gh pr list --state open --limit 100` to find older sync PRs to close. If contentful-docs ever has more than 100 open PRs, older `sync/optimization-docs-*` PRs beyond the first 100 are missed, leaving stale branches that outlive their commit — the exact outcome this change is meant to prevent. Please paginate through all open PRs (or raise the limit to a value that reliably covers the repo's open-PR count) so the loop always finds every open sync PR.
Review Details
  • Files reviewed - 2 · Commit Range: 8eaf13c..8eaf13c
    • .github/workflows/sync-fern-docs.yaml
    • docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.md
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Default Agent You can customize the agent settings here or contact your Bito workspace admin at jared.jolton@contentful.com.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread .github/workflows/sync-fern-docs.yaml
@bito-code-review

Copy link
Copy Markdown

✅ Review Settings Overridden

Status: Overridden Successfully

Guidelines:

  • Accepted:

    • General : Review Posture, Repo Truth And Boundaries, Domain Invariants

    • Language : typescript= TypeScript Review Evidence

Note: Extra guidelines beyond 3 general purpose guidelines and 1 language specific guideline per language are not processed. Guidelines are fetched from the source branch.

@phobetron

Copy link
Copy Markdown
Collaborator

Regarding Bito’s additional “Supersede loop pagination cap” suggestion: the edge case is technically possible because --limit 100 is applied before the local jq prefix filter. It would require more than 100 open pull requests with an older sync PR outside the fetched set. contentful/contentful-docs currently has 12 open pull requests and no open sync PRs, so this is not a current defect or a material blocker for this change. Pagination or server-side narrowing would be optional robustness hardening rather than required PR scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants