diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9efe8950..6aee7555 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -722,7 +722,7 @@ jobs: create-release: needs: [build-android, build-ios, build-macos, build-windows, package-windows, build-linux] - if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && (inputs.build_android || inputs.build_ios || inputs.build_macos || inputs.build_windows || inputs.build_linux) }} + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && inputs.build_android && inputs.build_ios && inputs.build_macos && inputs.build_windows && inputs.build_linux }} runs-on: ubuntu-latest permissions: contents: write @@ -732,6 +732,29 @@ jobs: sparse-checkout: pubspec.yaml sparse-checkout-cone-mode: false + - name: Read version from pubspec.yaml + id: version + run: | + VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//') + BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT + + - name: Refuse to overwrite a published release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + run: | + if RELEASE_DRAFT=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$VERSION" --jq '.draft' 2>&1); then + if [ "$RELEASE_DRAFT" != "true" ]; then + echo "::error::Release $VERSION is already published. Publish a new version instead of replacing immutable assets." + exit 1 + fi + elif [[ "$RELEASE_DRAFT" != *"HTTP 404"* ]]; then + echo "::error::Failed to check whether release $VERSION is already published: $RELEASE_DRAFT" + exit 1 + fi + - name: Download Android artifacts if: ${{ inputs.build_android }} uses: actions/download-artifact@v4 @@ -791,14 +814,6 @@ jobs: - name: Display structure of downloaded files run: ls -R artifacts - - name: Read version from pubspec.yaml - id: version - run: | - VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//') - BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//') - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT - - name: Generate appcast.xml run: | VERSION="${{ steps.version.outputs.version }}" diff --git a/scripts/check_build_workflow.py b/scripts/check_build_workflow.py index e89532c0..cf59837b 100644 --- a/scripts/check_build_workflow.py +++ b/scripts/check_build_workflow.py @@ -176,6 +176,42 @@ for artifact in ( "linux-arm64", ): require(f"name: {artifact}" in release, f"release download lost {artifact}") + +release_if = re.search(r"(?m)^ if: (.+)$", release) +require(release_if is not None, "release job must have an explicit condition") +release_condition = release_if.group(1) if release_if else "" +for build_input in ( + "build_android", + "build_ios", + "build_macos", + "build_windows", + "build_linux", +): + require( + f"&& inputs.{build_input}" in release_condition, + f"release publication must require {build_input}", + ) + +guard_name = "Refuse to overwrite a published release" +require(guard_name in release, "release job must reject published tag reuse") +require( + 'gh api "repos/$GITHUB_REPOSITORY/releases/tags/$VERSION"' in release, + "published release guard must query the exact version tag", +) +require( + '"$RELEASE_DRAFT" != "true"' in release, + "published release guard must allow only draft releases", +) +require( + "HTTP 404" in release and "Failed to check whether release" in release, + "published release guard must distinguish missing releases and fail closed", +) +guard_position = release.find(guard_name) +download_position = release.find("Download Android artifacts") +require( + guard_position >= 0 and download_position >= 0 and guard_position < download_position, + "published release guard must run before artifact downloads", +) require( "tag_name: ${{ steps.version.outputs.version }}" in release, "release must explicitly use the pubspec version as its tag",