This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 4dc6ebf167adac3ab21c94cb2d60fd39c8b7f6e9
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 13:30:13 2026 +0100

    [v3-4-test] Serialise the workflows that publish shared state (#75996) 
(#76022)
    
    An incremental registry build downloads the bucket's providers.json and
    modules.json, merges its own providers in and syncs them back. Docs
    publishes call it, release managers fire subset and fix-up publishes,
    and the backfill writes the same bucket, so two of them overlapping
    silently drop whichever finished first from the registry index.
    
    Docs publishes to one bucket and the two constraints publishers had the
    same exposure in smaller form. Provider and Airflow docs land in
    different paths, so those two kinds of docs sync keep running side by
    side. The packages-metadata.json they both rewrite from a scan of the
    whole bucket moves to its own job, serialised per bucket, using new
    --skip-packages-metadata and --packages-metadata-only flags of
    publish-docs-to-s3. The Docker Hub release grouped on the bare version
    string, which any other workflow using a version as its group would
    collide with.
    (cherry picked from commit c6a077b0efe7520d3e58dd7a5477a74ff14a4ba6)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .github/workflows/publish-docs-to-s3.yml           | 72 +++++++++++++++++++++-
 .github/workflows/registry-backfill.yml            |  6 ++
 .github/workflows/registry-build.yml               |  9 +++
 .github/workflows/release-constraints.yml          |  5 ++
 .github/workflows/release_dockerhub_image.yml      |  2 +-
 .../update-constraints-on-push-stable.yml          |  4 ++
 .github/workflows/update-constraints-on-push.yml   |  5 ++
 dev/breeze/doc/09_release_management_tasks.rst     | 11 ++++
 ...utput_release-management_publish-docs-to-s3.svg | 64 ++++++++++++-------
 ...utput_release-management_publish-docs-to-s3.txt |  2 +-
 .../commands/release_management_commands.py        | 30 ++++++++-
 .../commands/release_management_commands_config.py |  2 +
 .../src/airflow_breeze/utils/publish_docs_to_s3.py |  6 +-
 dev/breeze/tests/test_publish_docs_to_s3.py        | 25 ++++++++
 .../tests/test_release_management_commands.py      | 70 +++++++++++++++++++++
 15 files changed, 284 insertions(+), 29 deletions(-)

diff --git a/.github/workflows/publish-docs-to-s3.yml 
b/.github/workflows/publish-docs-to-s3.yml
index b039ff96747..4deb0b9132b 100644
--- a/.github/workflows/publish-docs-to-s3.yml
+++ b/.github/workflows/publish-docs-to-s3.yml
@@ -113,6 +113,7 @@ jobs:
       image-stash-ref: "publish-docs-${{ inputs.ref }}"
       registry-providers: ${{ 
steps.derive_registry_inputs.outputs.registry-providers }}
       registry-full-build: ${{ 
steps.derive_registry_inputs.outputs.registry-full-build }}
+      docs-kind: ${{ steps.docs_kind.outputs.docs-kind }}
     if: contains(fromJSON('[
       "ashb",
       "bugraoz93",
@@ -178,6 +179,26 @@ jobs:
             exit 0
           fi
           python3 dev/registry/derive_wave_providers.py
+      - name: "Classify the published docs"
+        id: docs_kind
+        env:
+          INCLUDE_DOCS: ${{ inputs.include-docs }}
+        shell: bash
+        # Provider and Airflow docs land in different paths of the bucket, so 
the publish job
+        # serialises each kind separately. A full build or a mixed list counts 
as `airflow`.
+        # Same non-provider list as the registry step above.
+        run: |
+          NON_PROVIDER_TOKENS=("all" "apache-airflow" "apache-airflow-ctl" 
"apache-airflow-mypy" "task-sdk" "java-sdk" "ts-sdk" "helm-chart" 
"docker-stack")
+          kind=providers
+          for token in $INCLUDE_DOCS; do
+            for np in "${NON_PROVIDER_TOKENS[@]}"; do
+              if [[ "$token" == "$np" ]]; then
+                kind=airflow
+              fi
+            done
+          done
+          echo "Publishing ${kind} docs ('${INCLUDE_DOCS}')."
+          echo "docs-kind=${kind}" >> "${GITHUB_OUTPUT}"
       - name: "Derive the default Python version"
         id: default_python
         shell: bash
@@ -585,6 +606,14 @@ jobs:
       needs.build-ts-sdk-docs.result == 'success' &&
       needs.update-registry.result != 'failure'
     name: "Publish documentation to S3"
+    outputs:
+      docs-synced: ${{ steps.sync_docs.outcome == 'success' }}
+    # Keyed on the resolved destination rather than the `destination` input, 
whose `auto` stands
+    # for either bucket, and on the kind of docs, so provider and Airflow 
publishes do not wait
+    # for each other. The bucket-wide packages-metadata.json is left to 
update-packages-metadata.
+    concurrency:
+      group: publish-docs-${{ needs.build-info.outputs.destination }}-${{ 
needs.build-info.outputs.docs-kind }}
+      cancel-in-progress: false
     permissions:
       id-token: write
       contents: read
@@ -697,6 +726,7 @@ jobs:
           aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }}
           aws-region: us-east-2
       - name: "Syncing docs to S3"
+        id: sync_docs
         env:
           DESTINATION_LOCATION: "${{ 
needs.build-info.outputs.destination-location }}"
           SOURCE_DIR_PATH: "/mnt/airflow-site/docs-archive/"
@@ -705,7 +735,8 @@ jobs:
         run: |
           breeze release-management publish-docs-to-s3 --source-dir-path 
${SOURCE_DIR_PATH} \
           --destination-location ${DESTINATION_LOCATION} --stable-versions \
-          --exclude-docs "${EXCLUDE_DOCS}" --overwrite 
${SKIP_WRITE_TO_STABLE_FOLDER}
+          --exclude-docs "${EXCLUDE_DOCS}" --overwrite 
${SKIP_WRITE_TO_STABLE_FOLDER} \
+          --skip-packages-metadata
       - name: "Download schema artifacts"
         if: needs.build-info.outputs.publish-execution-api-schema == 'true' || 
needs.build-info.outputs.publish-supervisor-schema == 'true' || 
needs.build-info.outputs.publish-dag-schema == 'true'
         uses: 
actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333  # v8.0.2
@@ -735,6 +766,45 @@ jobs:
           breeze release-management publish-schemas-to-s3 \
             --destination-location "${SCHEMAS_DESTINATION}" "${args[@]}"
 
+  update-packages-metadata:
+    needs: [build-info, publish-docs-to-s3]
+    # Keyed on the sync step rather than the job, so a schema publish failing 
after a complete sync
+    # still gets its docs into the metadata, while a partial sync is not 
advertised.
+    if: ${{ !cancelled() && needs.publish-docs-to-s3.outputs.docs-synced == 
'true' }}
+    name: "Update packages metadata in S3"
+    # packages-metadata.json is rebuilt from a scan of every package in the 
bucket, so provider and
+    # Airflow publishes share it. One group per bucket keeps a scan from 
missing a version another
+    # run is still writing; a pending run cancelled by a later one loses 
nothing, as that later run
+    # rescans the whole bucket.
+    concurrency:
+      group: publish-docs-metadata-${{ needs.build-info.outputs.destination }}
+      cancel-in-progress: false
+    permissions:
+      contents: read
+    runs-on: ubuntu-latest
+    steps:
+      - name: "Cleanup repo"
+        shell: bash
+        run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - name: "Install Breeze"
+        uses: ./.github/actions/breeze
+      - name: Configure AWS credentials
+        uses: 
aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd  
# v6.3.0
+        with:
+          aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-2
+      - name: "Regenerate packages-metadata.json"
+        env:
+          DESTINATION_LOCATION: "${{ 
needs.build-info.outputs.destination-location }}"
+        run: >
+          breeze release-management publish-docs-to-s3
+          --destination-location ${DESTINATION_LOCATION} 
--packages-metadata-only
+
   update-registry:
     # Runs alongside the docs build rather than after it: the registry reads 
nothing the docs
     # build produces, so ordering the two only added its whole duration to the 
wait. The publish
diff --git a/.github/workflows/registry-backfill.yml 
b/.github/workflows/registry-backfill.yml
index e47631e2d19..6dd2967594a 100644
--- a/.github/workflows/registry-backfill.yml
+++ b/.github/workflows/registry-backfill.yml
@@ -283,6 +283,12 @@ jobs:
     needs: [prepare, backfill]
     runs-on: ubuntu-latest
     name: "Publish versions.json"
+    # Shares registry-build.yml's group, so it never runs alongside a registry 
publish to the same
+    # bucket. Only this job: the backfill matrix writes per-version paths of 
its own, and putting
+    # a matrix in one group would cancel all but two of its entries.
+    concurrency:
+      group: registry-publish-${{ inputs.destination }}
+      cancel-in-progress: false
     steps:
       - name: "Checkout repository"
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
diff --git a/.github/workflows/registry-build.yml 
b/.github/workflows/registry-build.yml
index afc3fa59ae3..7afc284682f 100644
--- a/.github/workflows/registry-build.yml
+++ b/.github/workflows/registry-build.yml
@@ -93,6 +93,15 @@ jobs:
   build-and-publish-registry:
     timeout-minutes: 45
     name: "Build & Publish Registry"
+    # An incremental build downloads the bucket's providers.json and 
modules.json, merges its own
+    # providers in and syncs them back, so two publishes to one bucket at once 
lose whichever
+    # finishes first. Shared with registry-backfill.yml's publish job; set on 
the job because this
+    # workflow is also called from publish-docs-to-s3.yml. GitHub keeps one 
pending run per group,
+    # so a third publish cancels the waiting one: that run shows as cancelled 
and is re-run, which
+    # beats the silent loss this replaces.
+    concurrency:
+      group: registry-publish-${{ inputs.destination }}
+      cancel-in-progress: false
     # On a dispatch this enforces the committer allowlist below; on the 
`workflow_call` path the
     # caller has already gated on its own allowlist, so this must not re-check 
it against a
     # second, separately-maintained copy.
diff --git a/.github/workflows/release-constraints.yml 
b/.github/workflows/release-constraints.yml
index faa75605bd5..f1f1437c58f 100644
--- a/.github/workflows/release-constraints.yml
+++ b/.github/workflows/release-constraints.yml
@@ -202,6 +202,11 @@ jobs:
     timeout-minutes: 20
     name: "Publish and tag constraints"
     needs: [build-info, generate-constraints]
+    # Shared with the publish jobs of update-constraints-on-push.yml and
+    # update-constraints-on-push-stable.yml, so they never push to the same 
constraints branch at once.
+    concurrency:
+      group: constraints-publish-${{ needs.build-info.outputs.target-branch }}
+      cancel-in-progress: false
     permissions:
       contents: write
       packages: read
diff --git a/.github/workflows/release_dockerhub_image.yml 
b/.github/workflows/release_dockerhub_image.yml
index 4c65a553815..fb81f83d8ac 100644
--- a/.github/workflows/release_dockerhub_image.yml
+++ b/.github/workflows/release_dockerhub_image.yml
@@ -35,7 +35,7 @@ permissions:
   contents: read
   packages: read
 concurrency:
-  group: ${{ github.event.inputs.airflowVersion }}
+  group: release-dockerhub-image-${{ github.event.inputs.airflowVersion }}
   cancel-in-progress: true
 env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/update-constraints-on-push-stable.yml 
b/.github/workflows/update-constraints-on-push-stable.yml
index 744ee4bc93f..47379a8933d 100644
--- a/.github/workflows/update-constraints-on-push-stable.yml
+++ b/.github/workflows/update-constraints-on-push-stable.yml
@@ -119,6 +119,10 @@ jobs:
     timeout-minutes: 20
     name: "Commit and push constraints"
     needs: [build-info, generate-constraints]
+    # Shared with the other constraints publishers, so they never push to one 
branch at once.
+    concurrency:
+      group: constraints-publish-${{ 
needs.build-info.outputs.default-constraints-branch }}
+      cancel-in-progress: false
     permissions:
       contents: write
       packages: read
diff --git a/.github/workflows/update-constraints-on-push.yml 
b/.github/workflows/update-constraints-on-push.yml
index 4c84567e23d..4ffe806242f 100644
--- a/.github/workflows/update-constraints-on-push.yml
+++ b/.github/workflows/update-constraints-on-push.yml
@@ -210,6 +210,11 @@ jobs:
     timeout-minutes: 20
     name: "Commit and push constraints"
     needs: [build-info, generate-constraints]
+    # Shared with the other constraints publishers. The baseline check below 
still decides
+    # whether this run may publish at all; this only stops the two pushes 
overlapping.
+    concurrency:
+      group: constraints-publish-${{ 
needs.build-info.outputs.default-constraints-branch }}
+      cancel-in-progress: false
     permissions:
       contents: write
       packages: read
diff --git a/dev/breeze/doc/09_release_management_tasks.rst 
b/dev/breeze/doc/09_release_management_tasks.rst
index 431784aeb16..2b69b30f11f 100644
--- a/dev/breeze/doc/09_release_management_tasks.rst
+++ b/dev/breeze/doc/09_release_management_tasks.rst
@@ -1055,6 +1055,17 @@ To check what documents will be published to S3, you can 
use the ``--dry-run`` f
      breeze release-management publish-docs --source-dir-path 
/User/pavan/airflow-site/docs-archive
      --destination-location s3://airflow-docs/docs --dry-run
 
+After the sync the command rebuilds ``manifest/packages-metadata.json`` from a 
scan of every package in
+the bucket. When several publishes to one bucket can run at once, sync with 
``--skip-packages-metadata``
+and rebuild the file in a separate, serialised step with 
``--packages-metadata-only``, which needs no
+``--source-dir-path``. The ``Publish Docs to S3`` workflow does this so that 
provider and Airflow docs
+publishes can sync side by side.
+
+.. code-block:: bash
+
+     breeze release-management publish-docs-to-s3 --destination-location 
s3://airflow-docs/docs \
+     --packages-metadata-only
+
 These are all available flags of ``release-management publish-docs-to-s3`` 
command:
 
 .. image:: ./images/output_release-management_publish-docs.svg
diff --git 
a/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.svg 
b/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.svg
index 915c4a99ff6..f3d633c015f 100644
--- a/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.svg
+++ b/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 586.8" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 708.8" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -37,15 +37,15 @@
 .breeze-release-management-publish-docs-to-s3-r3 { fill: #c5c8c6;font-weight: 
bold }
 .breeze-release-management-publish-docs-to-s3-r4 { fill: #68a0b3;font-weight: 
bold }
 .breeze-release-management-publish-docs-to-s3-r5 { fill: #868887 }
-.breeze-release-management-publish-docs-to-s3-r6 { fill: #cc555a }
-.breeze-release-management-publish-docs-to-s3-r7 { fill: #8a4346 }
-.breeze-release-management-publish-docs-to-s3-r8 { fill: #8d7b39 }
+.breeze-release-management-publish-docs-to-s3-r6 { fill: #8d7b39 }
+.breeze-release-management-publish-docs-to-s3-r7 { fill: #cc555a }
+.breeze-release-management-publish-docs-to-s3-r8 { fill: #8a4346 }
 .breeze-release-management-publish-docs-to-s3-r9 { fill: #98a84b;font-weight: 
bold }
     </style>
 
     <defs>
     <clipPath id="breeze-release-management-publish-docs-to-s3-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="535.8" />
+      <rect x="0" y="0" width="1463.0" height="657.8" />
     </clipPath>
     <clipPath id="breeze-release-management-publish-docs-to-s3-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -110,9 +110,24 @@
 <clipPath id="breeze-release-management-publish-docs-to-s3-line-20">
     <rect x="0" y="489.5" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-release-management-publish-docs-to-s3-line-21">
+    <rect x="0" y="513.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-publish-docs-to-s3-line-22">
+    <rect x="0" y="538.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-publish-docs-to-s3-line-23">
+    <rect x="0" y="562.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-publish-docs-to-s3-line-24">
+    <rect x="0" y="587.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-publish-docs-to-s3-line-25">
+    <rect x="0" y="611.5" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="584.8" rx="8"/><text 
class="breeze-release-management-publish-docs-to-s3-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;release-management&#160;publish-docs-to-s3</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="706.8" rx="8"/><text 
class="breeze-release-management-publish-docs-to-s3-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;release-management&#160;publish-docs-to-s3</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -128,22 +143,27 @@
 </text><text class="breeze-release-management-publish-docs-to-s3-r1" x="12.2" 
y="93.2" textLength="256.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-3)">Publishes&#160;docs&#160;to&#160;S3.</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="1464" y="93.2" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-3)">
 </text><text class="breeze-release-management-publish-docs-to-s3-r1" x="1464" 
y="117.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-4)">
 </text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="142" textLength="24.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-5)">╭─</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="24.4" y="142" 
textLength="244" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-5)">&#160;Publish&#160;docs&#160;to&#160;S3&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="268.4" y="142" 
textLength="11 [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="166.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-6)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r6" x="24.4" y="166.4" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-6)">*</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="166.4" 
textLength="353.8" clip-path="url(#breeze-release-manag [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="190.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-7)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r6" x="24.4" y="190.8" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-7)">*</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="190.8" 
textLength="353.8" clip-path="url(#breeze-release-manag [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="215.2" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-8)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r8" x="439.2" y="215.2" 
textLength="524.6" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-8)">(s3://live-docs-airflow-apache-org/docs/&#160;|&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="1451.8" y="215. 
[...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="239.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-9)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r8" x="439.2" y="239.6" 
textLength="524.6" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-9)">s3://staging-docs-airflow-apache-org/docs/)</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="1451.8" y="239.6" 
textLen [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="264" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-10)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="264" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-10)">--exclude-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-manage [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="288.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-11)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="288.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-11)">--dry-run&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="312.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-12)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="312.8" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-12)">--overwrite&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="br [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="337.2" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-13)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="337.2" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-13)">--parallelism&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-relea [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="361.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-14)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="439.2" y="361.6" 
textLength="146.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-14)">[default:&#160;4]</text><text
 class="breeze-release-management-publish-docs-to-s3-r8" x="597.8" y="361.6" 
textLength="280.6" clip-path="ur [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="386" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-15)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="386" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-15)">--stable-versions&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish-do [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="410.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-16)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="410.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-16)">--publish-all-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish-doc [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="434.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-17)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="434.8" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-17)">--skip-write-to-stable-folder</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="434.8" 
textLength="439.2" clip [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="459.2" textLength="1464" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-18)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="1464" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-18)">
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="483.6" textLength="24.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-19)">╭─</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="24.4" y="483.6" 
textLength="195.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-19)">&#160;Common&#160;options&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="219.6" y="483.6" 
textLength="1220"  [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="508" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-20)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="24.4" y="508" 
textLength="73.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-20)">--help</text><text
 class="breeze-release-management-publish-docs-to-s3-r9" x="122" y="508" 
textLength="24.4" clip-path="url(#breeze-release-mana [...]
-</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="532.4" textLength="1464" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-21)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="1464" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-21)">
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="166.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-6)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="166.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-6)">--source-dir-path&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish- [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="190.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-7)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="439.2" y="190.8" 
textLength="292.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-7)">--packages-metadata-only</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="732" y="190.8" 
textLength="122" clip-path="u [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="215.2" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-8)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r7" x="24.4" y="215.2" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-8)">*</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="215.2" 
textLength="353.8" clip-path="url(#breeze-release-manag [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="239.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-9)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r6" x="439.2" y="239.6" 
textLength="524.6" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-9)">(s3://live-docs-airflow-apache-org/docs/&#160;|&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="1451.8" y="239. 
[...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="264" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-10)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r6" x="439.2" y="264" 
textLength="524.6" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-10)">s3://staging-docs-airflow-apache-org/docs/)</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="1451.8" y="264" 
textLength= [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="288.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-11)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="288.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-11)">--exclude-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-ma [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="312.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-12)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="312.8" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-12)">--dry-run&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="337.2" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-13)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="337.2" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-13)">--overwrite&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="br [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="361.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-14)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="361.6" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-14)">--parallelism&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-relea [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="386" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-15)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="439.2" y="386" 
textLength="146.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-15)">[default:&#160;4]</text><text
 class="breeze-release-management-publish-docs-to-s3-r6" x="597.8" y="386" 
textLength="280.6" clip-path="url(#bre [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="410.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-16)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="410.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-16)">--stable-versions&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publis [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="434.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-17)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="434.8" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-17)">--publish-all-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish-doc [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="459.2" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-18)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="459.2" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-18)">--skip-write-to-stable-folder</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="459.2" 
textLength="439.2" clip [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="483.6" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-19)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="483.6" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-19)">--skip-packages-metadata&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="483.6 
[...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="508" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-20)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="508" 
textLength="134.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-20)">again&#160;with&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="573.4" y="508" 
textLength="292.8" clip-path="url( [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="532.4" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-21)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="61" y="532.4" 
textLength="353.8" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-21)">--packages-metadata-only&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="532.4 
[...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="556.8" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-22)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="439.2" y="556.8" 
textLength="1000.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-22)">bucket.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#16
 [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="581.2" textLength="1464" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-23)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="1464" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-23)">
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="605.6" textLength="24.4" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-24)">╭─</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="24.4" y="605.6" 
textLength="195.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-24)">&#160;Common&#160;options&#160;</text><text
 class="breeze-release-management-publish-docs-to-s3-r5" x="219.6" y="605.6" 
textLength="1220"  [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="630" textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-25)">│</text><text
 class="breeze-release-management-publish-docs-to-s3-r4" x="24.4" y="630" 
textLength="73.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-25)">--help</text><text
 class="breeze-release-management-publish-docs-to-s3-r9" x="122" y="630" 
textLength="24.4" clip-path="url(#breeze-release-mana [...]
+</text><text class="breeze-release-management-publish-docs-to-s3-r5" x="0" 
y="654.4" textLength="1464" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-26)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-release-management-publish-docs-to-s3-r1" x="1464" y="654.4" 
textLength="12.2" 
clip-path="url(#breeze-release-management-publish-docs-to-s3-line-26)">
 </text>
     </g>
     </g>
diff --git 
a/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.txt 
b/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.txt
index bfad2d6f5eb..7e1d18938f5 100644
--- a/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.txt
+++ b/dev/breeze/doc/images/output_release-management_publish-docs-to-s3.txt
@@ -1 +1 @@
-b11847cc9e7091d948597ac5823c98d3
+042c33ea29854b37b9482ea09accedc6
diff --git 
a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py 
b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index 800e1e36dc7..0c8e399da76 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -4828,8 +4828,9 @@ def generate_issue_content(
 @release_management_group.command(name="publish-docs-to-s3", help="Publishes 
docs to S3.")
 @click.option(
     "--source-dir-path",
-    help="Path to the directory with the generated documentation.",
-    required=True,
+    help="Path to the directory with the generated documentation. Required 
unless "
+    "--packages-metadata-only is used.",
+    default="",
 )
 @click.option(
     "--exclude-docs",
@@ -4865,6 +4866,17 @@ def generate_issue_content(
     is_flag=True,
     help="Skip writing stable versions folder.",
 )
[email protected](
+    "--skip-packages-metadata",
+    is_flag=True,
+    help="Skip regenerating manifest/packages-metadata.json after the sync. "
+    "Run the command again with --packages-metadata-only to regenerate it.",
+)
[email protected](
+    "--packages-metadata-only",
+    is_flag=True,
+    help="Only regenerate manifest/packages-metadata.json from the docs 
already in the bucket.",
+)
 @option_parallelism
 def publish_docs_to_s3(
     source_dir_path: str,
@@ -4876,12 +4888,23 @@ def publish_docs_to_s3(
     publish_all_docs: bool,
     stable_versions: bool,
     skip_write_to_stable_folder: bool,
+    skip_packages_metadata: bool,
+    packages_metadata_only: bool,
 ):
     from airflow_breeze.utils.publish_docs_to_s3 import S3DocsPublish
 
     if publish_all_docs and stable_versions:
         console_print("[error]You cannot use --publish-all and 
--stable-versions together[/]")
         sys.exit(1)
+    if packages_metadata_only and (publish_all_docs or stable_versions or 
skip_packages_metadata):
+        console_print(
+            "[error]You cannot use --packages-metadata-only with 
--publish-all-docs, "
+            "--stable-versions or --skip-packages-metadata[/]"
+        )
+        sys.exit(1)
+    if not packages_metadata_only and not source_dir_path:
+        console_print("[error]--source-dir-path is required unless 
--packages-metadata-only is used[/]")
+        sys.exit(1)
 
     destination_location = destination_location.rstrip("/")
     source_dir_path = source_dir_path.rstrip("/")
@@ -4899,7 +4922,10 @@ def publish_docs_to_s3(
         destination_location=destination_location,
         parallelism=parallelism,
         skip_write_to_stable_folder=skip_write_to_stable_folder,
+        skip_packages_metadata=skip_packages_metadata,
     )
+    if packages_metadata_only:
+        docs_to_s3.generate_packages_metadata()
     if publish_all_docs:
         docs_to_s3.publish_all_docs()
     if stable_versions:
diff --git 
a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
index 305e1f01d93..94de79045ac 100644
--- 
a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
+++ 
b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
@@ -585,6 +585,8 @@ RELEASE_MANAGEMENT_PARAMETERS: dict[str, list[dict[str, str 
| list[str]]]] = {
                 "--stable-versions",
                 "--publish-all-docs",
                 "--skip-write-to-stable-folder",
+                "--skip-packages-metadata",
+                "--packages-metadata-only",
             ],
         }
     ],
diff --git a/dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py 
b/dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py
index 3158c2d42b0..0b914fa7639 100644
--- a/dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py
+++ b/dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py
@@ -77,6 +77,7 @@ class S3DocsPublish:
         overwrite: bool = False,
         parallelism: int = 1,
         skip_write_to_stable_folder: bool = False,
+        skip_packages_metadata: bool = False,
     ):
         self.source_dir_path = source_dir_path
         self.destination_location = destination_location
@@ -86,6 +87,7 @@ class S3DocsPublish:
         self.parallelism = parallelism
         self.source_dest_mapping: list[tuple[str, str]] = []
         self.skip_write_to_stable_folder = skip_write_to_stable_folder
+        self.skip_packages_metadata = skip_packages_metadata
 
     @cached_property
     def get_all_docs(self):
@@ -244,8 +246,8 @@ class S3DocsPublish:
             include_success_outputs=False,
         )
 
-        # Now generate the packages-metadata.json
-        self.generate_packages_metadata()
+        if not self.skip_packages_metadata:
+            self.generate_packages_metadata()
 
         # Add redirects to package folders
         [
diff --git a/dev/breeze/tests/test_publish_docs_to_s3.py 
b/dev/breeze/tests/test_publish_docs_to_s3.py
index b1b14126920..814fd37f2f1 100644
--- a/dev/breeze/tests/test_publish_docs_to_s3.py
+++ b/dev/breeze/tests/test_publish_docs_to_s3.py
@@ -273,3 +273,28 @@ class TestPublishDocsToS3:
         self.publish_docs_to_s3.publish_all_docs()
 
         assert self.publish_docs_to_s3.source_dest_mapping == 
expected_source_dest_mapping
+
+    @pytest.mark.parametrize(
+        ("skip_packages_metadata", "expected_calls"),
+        [
+            pytest.param(False, 1, id="generates_metadata"),
+            pytest.param(True, 0, id="skips_metadata"),
+        ],
+    )
+    @patch.object(S3DocsPublish, "generate_packages_metadata")
+    @patch("airflow_breeze.utils.publish_docs_to_s3.check_async_run_results")
+    @patch("airflow_breeze.utils.publish_docs_to_s3.run_with_pool")
+    def test_run_publish_packages_metadata(
+        self,
+        mock_run_with_pool,
+        mock_check_async_run_results,
+        mock_generate_packages_metadata,
+        skip_packages_metadata,
+        expected_calls,
+    ):
+        mock_run_with_pool.return_value.__enter__.return_value = (MagicMock(), 
[])
+        self.publish_docs_to_s3.skip_packages_metadata = skip_packages_metadata
+
+        self.publish_docs_to_s3.run_publish()
+
+        assert mock_generate_packages_metadata.call_count == expected_calls
diff --git a/dev/breeze/tests/test_release_management_commands.py 
b/dev/breeze/tests/test_release_management_commands.py
index 68b42608445..7eb67b1dde2 100644
--- a/dev/breeze/tests/test_release_management_commands.py
+++ b/dev/breeze/tests/test_release_management_commands.py
@@ -19,8 +19,10 @@ from __future__ import annotations
 import subprocess
 from pathlib import Path
 from types import SimpleNamespace
+from unittest import mock
 
 import pytest
+from click.testing import CliRunner
 
 from airflow_breeze import global_constants
 from airflow_breeze.commands import release_management_commands
@@ -35,9 +37,11 @@ from airflow_breeze.commands.release_management_commands 
import (
     get_prs_from_git_log_for_new_provider,
     get_suffix_from_package_in_dist,
     is_package_in_dist,
+    publish_docs_to_s3,
 )
 from airflow_breeze.global_constants import DEFAULT_PYTHON_MAJOR_MINOR_VERSION
 from airflow_breeze.utils import shared_options
+from airflow_breeze.utils.publish_docs_to_s3 import S3DocsPublish
 
 
 @pytest.mark.parametrize(
@@ -414,3 +418,69 @@ def 
test_tag_providers_detects_push_remote_in_dry_run(monkeypatch, tmp_path):
         shared_options.set_dry_run(False)
 
     assert ["git", "push", "upstream", "providers-amazon/9.29.0"] in commands
+
+
+DOCS_DESTINATION = "s3://staging-docs-airflow-apache-org/docs/"
+
+
[email protected](S3DocsPublish, "publish_stable_version_docs", autospec=True)
[email protected](S3DocsPublish, "generate_packages_metadata", autospec=True)
+def 
test_publish_docs_to_s3_packages_metadata_only(mock_generate_packages_metadata, 
mock_publish_stable):
+    result = CliRunner().invoke(
+        publish_docs_to_s3,
+        ["--destination-location", DOCS_DESTINATION, 
"--packages-metadata-only"],
+        catch_exceptions=False,
+    )
+
+    assert result.exit_code == 0
+    mock_generate_packages_metadata.assert_called_once()
+    mock_publish_stable.assert_not_called()
+
+
[email protected](S3DocsPublish, "publish_stable_version_docs", autospec=True)
+def 
test_publish_docs_to_s3_forwards_skip_packages_metadata(mock_publish_stable):
+    result = CliRunner().invoke(
+        publish_docs_to_s3,
+        [
+            "--destination-location",
+            DOCS_DESTINATION,
+            "--source-dir-path",
+            "/tmp/docs-archive",
+            "--stable-versions",
+            "--skip-packages-metadata",
+        ],
+        catch_exceptions=False,
+    )
+
+    assert result.exit_code == 0
+    (publisher,) = mock_publish_stable.call_args.args
+    assert publisher.skip_packages_metadata is True
+
+
[email protected](
+    "args",
+    [
+        pytest.param(["--stable-versions"], id="source_dir_path_missing"),
+        pytest.param(
+            ["--packages-metadata-only", "--stable-versions"], 
id="metadata_only_with_stable_versions"
+        ),
+        pytest.param(
+            ["--packages-metadata-only", "--publish-all-docs"], 
id="metadata_only_with_publish_all_docs"
+        ),
+        pytest.param(
+            ["--packages-metadata-only", "--skip-packages-metadata"], 
id="metadata_only_with_skip_metadata"
+        ),
+    ],
+)
[email protected](S3DocsPublish, "publish_stable_version_docs", autospec=True)
[email protected](S3DocsPublish, "publish_all_docs", autospec=True)
[email protected](S3DocsPublish, "generate_packages_metadata", autospec=True)
+def test_publish_docs_to_s3_rejects_invalid_options(
+    mock_generate_packages_metadata, mock_publish_all, mock_publish_stable, 
args
+):
+    result = CliRunner().invoke(publish_docs_to_s3, ["--destination-location", 
DOCS_DESTINATION, *args])
+
+    assert result.exit_code == 1
+    mock_generate_packages_metadata.assert_not_called()
+    mock_publish_all.assert_not_called()
+    mock_publish_stable.assert_not_called()

Reply via email to