diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 442b29d03..0ded6ba04 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -799,13 +799,40 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" - gh release upload "${TAG}" release/checksums.txt --clobber - gh release upload "${TAG}" release/*.sha256 --clobber + release_upload_with_retry() { + local attempt=1 + local max_attempts=5 + local wait_seconds=15 + + while true; do + if gh release upload "$@"; then + return 0 + fi + + if [ "$attempt" -ge "$max_attempts" ]; then + echo "::error::gh release upload failed after ${max_attempts} attempts: $*" + return 1 + fi + + echo "gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s: $*" + sleep "$wait_seconds" + attempt=$((attempt + 1)) + if [ "$wait_seconds" -lt 120 ]; then + wait_seconds=$((wait_seconds * 2)) + if [ "$wait_seconds" -gt 120 ]; then + wait_seconds=120 + fi + fi + done + } + + release_upload_with_retry "${TAG}" release/checksums.txt --clobber + release_upload_with_retry "${TAG}" release/*.sha256 --clobber if ls release/*.sig 1> /dev/null 2>&1; then - gh release upload "${TAG}" release/*.sig --clobber + release_upload_with_retry "${TAG}" release/*.sig --clobber fi if ls release/*.sshsig 1> /dev/null 2>&1; then - gh release upload "${TAG}" release/*.sshsig --clobber + release_upload_with_retry "${TAG}" release/*.sshsig --clobber fi - name: Upload release assets @@ -813,13 +840,40 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" + release_upload_with_retry() { + local attempt=1 + local max_attempts=5 + local wait_seconds=15 + + while true; do + if gh release upload "$@"; then + return 0 + fi + + if [ "$attempt" -ge "$max_attempts" ]; then + echo "::error::gh release upload failed after ${max_attempts} attempts: $*" + return 1 + fi + + echo "gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s: $*" + sleep "$wait_seconds" + attempt=$((attempt + 1)) + if [ "$wait_seconds" -lt 120 ]; then + wait_seconds=$((wait_seconds * 2)) + if [ "$wait_seconds" -gt 120 ]; then + wait_seconds=120 + fi + fi + done + } + if ls release/*.sbom.spdx.json 1> /dev/null 2>&1; then - gh release upload "${TAG}" release/*.sbom.spdx.json --clobber + release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber fi - gh release upload "${TAG}" release/*.tar.gz --clobber - gh release upload "${TAG}" release/*.zip --clobber + release_upload_with_retry "${TAG}" release/*.tar.gz --clobber + release_upload_with_retry "${TAG}" release/*.zip --clobber if ls release/*.tgz 1> /dev/null 2>&1; then - gh release upload "${TAG}" release/*.tgz --clobber + release_upload_with_retry "${TAG}" release/*.tgz --clobber fi for bare_agent in \ release/pulse-agent-linux-amd64 \ @@ -833,15 +887,15 @@ jobs: release/pulse-agent-windows-arm64.exe \ release/pulse-agent-windows-386.exe; do if [ -f "${bare_agent}" ]; then - gh release upload "${TAG}" "${bare_agent}" --clobber + release_upload_with_retry "${TAG}" "${bare_agent}" --clobber fi done - gh release upload "${TAG}" release/install.sh --clobber + release_upload_with_retry "${TAG}" release/install.sh --clobber if [ -f release/install.ps1 ]; then - gh release upload "${TAG}" release/install.ps1 --clobber + release_upload_with_retry "${TAG}" release/install.ps1 --clobber fi - gh release upload "${TAG}" release/install-docker.sh --clobber - gh release upload "${TAG}" release/pulse-auto-update.sh --clobber + release_upload_with_retry "${TAG}" release/install-docker.sh --clobber + release_upload_with_retry "${TAG}" release/pulse-auto-update.sh --clobber - name: Publish release if: ${{ github.event.inputs.draft_only != 'true' }} diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 994ffc538..a0b17f627 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -462,6 +462,11 @@ unpublished tag must locate the existing draft release, retarget its git tag and release `target_commitish` to the current governed release-line head, and continue publication without requiring an operator to delete the tag manually; published tags remain immutable and must still fail closed. +That same upload boundary must tolerate transient GitHub release-asset API +failures. `.github/workflows/create-release.yml` must retry every +`gh release upload` operation with bounded backoff before failing the release +job, because a single 5xx response during upload can otherwise strand a draft +release with a partial asset set and no validation run. That same public release-body boundary also owns publish-safe packet rendering. When operators pass draft packet markdown to `.github/workflows/create-release.yml`, the workflow must sanitize draft-only framing and append the standardized diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 431ac7bc2..2487380de 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -114,7 +114,7 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) { `SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`, `SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`, `install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft`, - `gh release upload "${TAG}" release/*.sbom.spdx.json --clobber`, + `release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber`, `release/pulse-agent-linux-amd64`, `release/pulse-agent-linux-arm64`, `release/pulse-agent-linux-armv7`, @@ -125,11 +125,14 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) { `release/pulse-agent-windows-amd64.exe`, `release/pulse-agent-windows-arm64.exe`, `release/pulse-agent-windows-386.exe`, - `gh release upload "${TAG}" release/install.sh --clobber`, + `release_upload_with_retry "${TAG}" release/install.sh --clobber`, `if [ -f release/install.ps1 ]; then`, - `gh release upload "${TAG}" release/install.ps1 --clobber`, - `gh release upload "${TAG}" release/*.sig --clobber`, - `gh release upload "${TAG}" release/*.sshsig --clobber`, + `release_upload_with_retry "${TAG}" release/install.ps1 --clobber`, + `release_upload_with_retry "${TAG}" release/*.sig --clobber`, + `release_upload_with_retry "${TAG}" release/*.sshsig --clobber`, + `gh release upload "$@"`, + `gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s`, + `gh release upload failed after ${max_attempts} attempts`, `uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`, `subject-path: release/*`, `gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate`, diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index a6ea7227e..71e3cd818 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -404,9 +404,12 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content) self.assertIn('SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"', content) self.assertIn('install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft', content) - self.assertIn('gh release upload "${TAG}" release/*.sig --clobber', content) - self.assertIn('gh release upload "${TAG}" release/*.sshsig --clobber', content) - self.assertIn('gh release upload "${TAG}" release/*.sbom.spdx.json --clobber', content) + self.assertIn('release_upload_with_retry "${TAG}" release/*.sig --clobber', content) + self.assertIn('release_upload_with_retry "${TAG}" release/*.sshsig --clobber', content) + self.assertIn('release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber', content) + self.assertIn('gh release upload "$@"', content) + self.assertIn('gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s', content) + self.assertIn('gh release upload failed after ${max_attempts} attempts', content) self.assertIn("provenance: mode=max", content) self.assertIn("sbom: true", content) self.assertIn("id-token: write", content)