From d1e38117dd60d16c2dff845286d2ab434434f31e Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 7 Aug 2026 11:58:11 -0400 Subject: [PATCH 1/3] feat: add GitHub actions for version resolution, app version setting, and Flutter setup - Implemented `resolve-version` action to derive version representations from a semver string. - Created `set-app-version` action to update the app's pubspec with the release version. - Added `setup-flutter` action to install the Flutter SDK and bootstrap the melos workspace. - Updated CI workflow to utilize the new actions for version resolution and Flutter setup. - Refactored release workflows for macOS and Windows to integrate version handling and app version setting. - Enhanced release workflow to include checks for distribution credentials and publish settings. - Updated badge workflow to use the latest actions versions. --- .../actions/fetch-release-asset/action.yml | 70 +++ .github/actions/publish-manifest/action.yml | 98 ++++ .github/actions/resolve-version/action.yml | 67 +++ .github/actions/set-app-version/action.yml | 27 ++ .github/actions/setup-flutter/action.yml | 31 ++ .github/workflows/ci.yml | 75 +-- .github/workflows/release-mac.yml | 108 ++--- .github/workflows/release-win.yml | 63 +-- .github/workflows/release.yml | 442 +++++++++--------- .github/workflows/update-badge.yml | 4 +- 10 files changed, 599 insertions(+), 386 deletions(-) create mode 100644 .github/actions/fetch-release-asset/action.yml create mode 100644 .github/actions/publish-manifest/action.yml create mode 100644 .github/actions/resolve-version/action.yml create mode 100644 .github/actions/set-app-version/action.yml create mode 100644 .github/actions/setup-flutter/action.yml diff --git a/.github/actions/fetch-release-asset/action.yml b/.github/actions/fetch-release-asset/action.yml new file mode 100644 index 0000000..ae44cd6 --- /dev/null +++ b/.github/actions/fetch-release-asset/action.yml @@ -0,0 +1,70 @@ +name: Fetch release asset +description: Downloads a published release asset, retrying while GitHub finishes making it available, and returns its checksum + +inputs: + repository: + description: Repository the release belongs to, as owner/name + required: true + tag: + description: Release tag the asset was published under + required: true + asset: + description: Asset file name + required: true + attempts: + description: How many times to retry the download + required: false + default: "5" + +outputs: + url: + description: Public download URL of the asset + value: ${{ steps.fetch.outputs.url }} + path: + description: Local path of the downloaded asset + value: ${{ steps.fetch.outputs.path }} + sha256: + description: SHA-256 checksum of the downloaded asset + value: ${{ steps.fetch.outputs.sha256 }} + +runs: + using: composite + steps: + - name: Download asset and compute checksum + id: fetch + shell: bash + env: + ASSET_REPOSITORY: ${{ inputs.repository }} + ASSET_TAG: ${{ inputs.tag }} + ASSET_NAME: ${{ inputs.asset }} + ASSET_ATTEMPTS: ${{ inputs.attempts }} + run: | + set -euo pipefail + + url="https://github.com/${ASSET_REPOSITORY}/releases/download/${ASSET_TAG}/${ASSET_NAME}" + dest="${RUNNER_TEMP}/${ASSET_NAME}" + + downloaded="" + for attempt in $(seq 1 "$ASSET_ATTEMPTS"); do + if curl -fSL -o "$dest" "$url"; then + downloaded=1 + break + fi + echo "Asset not downloadable yet (attempt ${attempt}/${ASSET_ATTEMPTS}); retrying..." + sleep $((attempt * 10)) + done + + if [[ -z "$downloaded" || ! -s "$dest" ]]; then + echo "::error::Could not download ${url}" + exit 1 + fi + + sha256="$(sha256sum "$dest" | awk '{print $1}')" + + { + echo "url=$url" + echo "path=$dest" + echo "sha256=$sha256" + } >> "$GITHUB_OUTPUT" + + echo "Fetched ${ASSET_NAME} (sha256 ${sha256})" diff --git a/.github/actions/publish-manifest/action.yml b/.github/actions/publish-manifest/action.yml new file mode 100644 index 0000000..6376aef --- /dev/null +++ b/.github/actions/publish-manifest/action.yml @@ -0,0 +1,98 @@ +name: Publish package manifest +description: Commits a generated package manifest to an external repository, rebasing onto releases published concurrently + +inputs: + repository: + description: Target repository, as owner/name + required: true + token: + description: Token with contents write access on the target repository + required: true + path: + description: Manifest path inside the target repository + required: true + content: + description: Full manifest contents + required: true + message: + description: Commit message + required: true + branch: + description: Branch to push to + required: false + default: main + attempts: + description: How many times to retry a rejected push + required: false + default: "5" + validate-json: + description: Parse the manifest as JSON before committing + required: false + default: "false" + +runs: + using: composite + steps: + - name: Commit manifest + shell: bash + env: + TARGET_REPOSITORY: ${{ inputs.repository }} + TARGET_TOKEN: ${{ inputs.token }} + TARGET_PATH: ${{ inputs.path }} + TARGET_BRANCH: ${{ inputs.branch }} + MANIFEST_CONTENT: ${{ inputs.content }} + COMMIT_MESSAGE: ${{ inputs.message }} + PUSH_ATTEMPTS: ${{ inputs.attempts }} + VALIDATE_JSON: ${{ inputs.validate-json }} + run: | + set -uo pipefail + + if [[ -z "${MANIFEST_CONTENT//[[:space:]]/}" ]]; then + echo "::error::Manifest content is empty" + exit 1 + fi + + clone="${RUNNER_TEMP}/publish-manifest" + rm -rf "$clone" + + if ! git clone "https://x-access-token:${TARGET_TOKEN}@github.com/${TARGET_REPOSITORY}.git" "$clone"; then + echo "::error::Could not clone ${TARGET_REPOSITORY}" + exit 1 + fi + + cd "$clone" || exit 1 + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + mkdir -p "$(dirname "$TARGET_PATH")" + printf '%s\n' "${MANIFEST_CONTENT%$'\n'}" > "$TARGET_PATH" + + if [[ "$VALIDATE_JSON" == "true" ]] && ! jq empty "$TARGET_PATH"; then + echo "::error::Generated manifest is not valid JSON" + cat "$TARGET_PATH" + exit 1 + fi + + git add "$TARGET_PATH" + if git diff --cached --quiet; then + echo "Manifest already up to date; nothing to publish" + exit 0 + fi + + if ! git commit -m "$COMMIT_MESSAGE"; then + echo "::error::Could not commit ${TARGET_PATH}" + exit 1 + fi + + for attempt in $(seq 1 "$PUSH_ATTEMPTS"); do + if git push origin "HEAD:${TARGET_BRANCH}"; then + echo "Published ${TARGET_PATH} to ${TARGET_REPOSITORY}" + exit 0 + fi + echo "Push rejected (attempt ${attempt}/${PUSH_ATTEMPTS}); rebasing onto concurrent release..." + git fetch origin "$TARGET_BRANCH" && git rebase "origin/${TARGET_BRANCH}" + sleep $((RANDOM % 5 + 3)) + done + + echo "::error::Could not push ${TARGET_PATH} to ${TARGET_REPOSITORY} after ${PUSH_ATTEMPTS} attempts" + exit 1 diff --git a/.github/actions/resolve-version/action.yml b/.github/actions/resolve-version/action.yml new file mode 100644 index 0000000..719a8d5 --- /dev/null +++ b/.github/actions/resolve-version/action.yml @@ -0,0 +1,67 @@ +name: Resolve version +description: Derives every version representation the release pipeline needs from a single semver string + +inputs: + version: + description: Semver string, optionally with a prerelease suffix (e.g. 1.2.3 or 1.2.3-beta.1) + required: true + +outputs: + version: + description: The version exactly as given + value: ${{ steps.derive.outputs.version }} + build-name: + description: Version stripped of its prerelease suffix + value: ${{ steps.derive.outputs.build-name }} + build-number: + description: Monotonic integer derived from build-name, used as CFBundleVersion + value: ${{ steps.derive.outputs.build-number }} + version4: + description: Four-component version required by MSIX manifests + value: ${{ steps.derive.outputs.version4 }} + is-prerelease: + description: "true when the version carries a prerelease suffix" + value: ${{ steps.derive.outputs.is-prerelease }} + +runs: + using: composite + steps: + - name: Derive version representations + id: derive + shell: bash + env: + RAW_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + version="$RAW_VERSION" + if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){0,2}(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'$version' is not a supported version string (expected MAJOR[.MINOR[.PATCH]][-PRERELEASE])" + exit 1 + fi + + if [[ "$version" == *-* ]]; then + is_prerelease=true + else + is_prerelease=false + fi + + build_name="${version%%-*}" + IFS=. read -r major minor patch <<< "$build_name" + + major=$((10#${major:-0})) + minor=$((10#${minor:-0})) + patch=$((10#${patch:-0})) + + build_number=$(printf '%d%03d%03d' "$major" "$minor" "$patch") + version4="${major}.${minor}.${patch}.0" + + { + echo "version=$version" + echo "build-name=$build_name" + echo "build-number=$build_number" + echo "version4=$version4" + echo "is-prerelease=$is_prerelease" + } >> "$GITHUB_OUTPUT" + + echo "version=$version build-name=$build_name build-number=$build_number version4=$version4 prerelease=$is_prerelease" diff --git a/.github/actions/set-app-version/action.yml b/.github/actions/set-app-version/action.yml new file mode 100644 index 0000000..4540131 --- /dev/null +++ b/.github/actions/set-app-version/action.yml @@ -0,0 +1,27 @@ +name: Set app version +description: Writes the release version into the app pubspec so the produced binaries carry it + +inputs: + version: + description: Version to write into the pubspec + required: true + pubspec: + description: Path to the pubspec to rewrite + required: false + default: apps/linkunbound/pubspec.yaml + +runs: + using: composite + steps: + - name: Write version into pubspec + shell: bash + env: + APP_VERSION: ${{ inputs.version }} + PUBSPEC: ${{ inputs.pubspec }} + run: | + set -euo pipefail + + sed "s|^version:.*|version: ${APP_VERSION}|" "$PUBSPEC" > "${PUBSPEC}.tmp" + mv "${PUBSPEC}.tmp" "$PUBSPEC" + + grep '^version:' "$PUBSPEC" diff --git a/.github/actions/setup-flutter/action.yml b/.github/actions/setup-flutter/action.yml new file mode 100644 index 0000000..f5fb8d2 --- /dev/null +++ b/.github/actions/setup-flutter/action.yml @@ -0,0 +1,31 @@ +name: Set up Flutter workspace +description: Installs the pinned Flutter SDK with SDK and pub caches, then bootstraps the melos workspace + +inputs: + flutter-version: + description: Flutter SDK version to install + required: false + default: "3.44.1" + bootstrap: + description: Run melos bootstrap after installing the SDK + required: false + default: "true" + +runs: + using: composite + steps: + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ inputs.flutter-version }} + cache: true + + - name: Install melos + if: inputs.bootstrap == 'true' + shell: bash + run: dart pub global activate melos + + - name: Bootstrap workspace + if: inputs.bootstrap == 'true' + shell: bash + run: melos bootstrap diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f27a096..88b4ce1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: name: Markdown Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Lint Markdown files uses: DavidAnson/markdownlint-cli2-action@v19 @@ -34,26 +34,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: subosito/flutter-action@v2 - with: - channel: stable - flutter-version: 3.44.1 - cache: true - - - name: Cache pub dependencies - uses: actions/cache@v4 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} - restore-keys: ${{ runner.os }}-pub- - - - name: Install melos - run: dart pub global activate melos - - - name: Bootstrap workspace - run: melos bootstrap + - name: Set up Flutter workspace + uses: ./.github/actions/setup-flutter - name: Check formatting run: dart format --set-exit-if-changed . @@ -89,7 +73,7 @@ jobs: cat /tmp/core_lcov.info /tmp/app_lcov.info > coverage-merged.info - name: Upload coverage artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-reports path: coverage-merged.info @@ -101,10 +85,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Download coverage reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: coverage-reports path: coverage @@ -123,30 +107,15 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: subosito/flutter-action@v2 - with: - channel: stable - flutter-version: 3.44.1 - cache: true - - - name: Cache pub dependencies - uses: actions/cache@v4 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} - restore-keys: ${{ runner.os }}-pub- - - - name: Resolve dependencies - run: | - cd packages/core && dart pub get - cd ../../apps/linkunbound && flutter pub get + - name: Set up Flutter workspace + uses: ./.github/actions/setup-flutter - name: Download coverage reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: coverage-reports path: coverage @@ -181,26 +150,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - - uses: subosito/flutter-action@v2 - with: - channel: stable - flutter-version: 3.44.1 - cache: true - - - name: Cache pub dependencies - uses: actions/cache@v4 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} - restore-keys: ${{ runner.os }}-pub- - - - name: Install melos - run: dart pub global activate melos + - uses: actions/checkout@v7 - - name: Bootstrap workspace - run: melos bootstrap + - name: Set up Flutter workspace + uses: ./.github/actions/setup-flutter - name: Build ${{ matrix.name }} release working-directory: apps/linkunbound diff --git a/.github/workflows/release-mac.yml b/.github/workflows/release-mac.yml index e7cd423..c736784 100644 --- a/.github/workflows/release-mac.yml +++ b/.github/workflows/release-mac.yml @@ -28,60 +28,32 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + RELEASE_APP_PATH: apps/linkunbound/build/macos/Build/Products/Release/LinkUnbound.app steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} - name: Resolve version - id: get_version - run: | - VERSION="${{ inputs.version }}" - - IS_PRERELEASE="false" - if [[ "$VERSION" == *-* ]]; then - IS_PRERELEASE="true" - fi - - BUILD_NAME="${VERSION%-*}" - BUILD_NUMBER=$(echo "$BUILD_NAME" | awk -F. '{printf "%d%03d%03d", $1, $2, $3}') - - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "BUILD_NAME=$BUILD_NAME" >> "$GITHUB_OUTPUT" - echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_OUTPUT" - echo "IS_PRERELEASE=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION Build: $BUILD_NAME ($BUILD_NUMBER) PreRelease: $IS_PRERELEASE" - - - uses: subosito/flutter-action@v2 + id: version + uses: ./.github/actions/resolve-version with: - channel: stable - flutter-version: 3.44.1 - cache: true + version: ${{ inputs.version }} - - name: Cache pub dependencies - uses: actions/cache@v5 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} - restore-keys: ${{ runner.os }}-pub- + - name: Set up Flutter workspace + uses: ./.github/actions/setup-flutter - - name: Install Melos - run: dart pub global activate melos - - - name: Get dependencies - run: melos bootstrap - - - name: Update pubspec version - run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - sed -i '' "s/^version:.*/version: $VERSION/" apps/linkunbound/pubspec.yaml - echo "Updated pubspec.yaml version to: $VERSION" + - name: Set app version + uses: ./.github/actions/set-app-version + with: + version: ${{ steps.version.outputs.version }} - # ── Code Signing Setup ── - name: Import signing certificate if: env.MACOS_CERTIFICATE_P12 != '' run: | + set -euo pipefail + KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" @@ -105,24 +77,25 @@ jobs: echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" echo "Certificate imported successfully" - # ── Build (universal: arm64 + x86_64) ── - name: Refresh CocoaPods lock run: | + set -euo pipefail rm -f apps/linkunbound/macos/Podfile.lock cd apps/linkunbound/macos && pod install --repo-update - name: Build macOS release (universal) + working-directory: apps/linkunbound run: | - cd apps/linkunbound flutter build macos --release \ - --build-name="${{ steps.get_version.outputs.BUILD_NAME }}" \ - --build-number="${{ steps.get_version.outputs.BUILD_NUMBER }}" \ - --dart-define="APP_VERSION=${{ steps.get_version.outputs.VERSION }}" + --build-name="${{ steps.version.outputs.build-name }}" \ + --build-number="${{ steps.version.outputs.build-number }}" \ + --dart-define="APP_VERSION=${{ steps.version.outputs.version }}" - name: Verify universal binary run: | - APP="apps/linkunbound/build/macos/Build/Products/Release/LinkUnbound.app" - ARCHS=$(lipo -archs "$APP/Contents/MacOS/LinkUnbound") + set -euo pipefail + + ARCHS=$(lipo -archs "$RELEASE_APP_PATH/Contents/MacOS/LinkUnbound") echo "Architectures: $ARCHS" if [[ "$ARCHS" != *"x86_64"* ]] || [[ "$ARCHS" != *"arm64"* ]]; then echo "::error::Expected universal binary (x86_64 + arm64), got: $ARCHS" @@ -130,11 +103,10 @@ jobs: fi echo "Universal binary verified (x86_64 + arm64)" - # ── Code Sign the .app ── - name: Sign application if: env.MACOS_CERTIFICATE_P12 != '' run: | - APP_PATH="apps/linkunbound/build/macos/Build/Products/Release/LinkUnbound.app" + set -eu SIGN_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') echo "Signing with identity: $SIGN_IDENTITY" @@ -142,28 +114,27 @@ jobs: codesign --deep --force --options runtime \ --entitlements "apps/linkunbound/macos/Runner/Release.entitlements" \ --sign "$SIGN_IDENTITY" \ - "$APP_PATH" + "$RELEASE_APP_PATH" echo "Verifying signature..." - codesign --verify --deep --strict "$APP_PATH" + codesign --verify --deep --strict "$RELEASE_APP_PATH" echo "Signature verified" - name: Ad-hoc sign (unsigned build) if: env.MACOS_CERTIFICATE_P12 == '' run: | - APP_PATH="apps/linkunbound/build/macos/Build/Products/Release/LinkUnbound.app" - codesign --deep --force --sign - "$APP_PATH" + set -euo pipefail + codesign --deep --force --sign - "$RELEASE_APP_PATH" echo "Ad-hoc signed (unsigned build)" - # ── Create DMG ── - name: Install create-dmg run: brew install create-dmg - name: Create DMG + env: + DMG_PATH: apps/linkunbound/dist/LinkUnbound_${{ steps.version.outputs.version }}_universal.dmg run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - APP_PATH="apps/linkunbound/build/macos/Build/Products/Release/LinkUnbound.app" - DMG_NAME="LinkUnbound_${VERSION}_universal.dmg" + set -euo pipefail mkdir -p apps/linkunbound/dist @@ -176,22 +147,23 @@ jobs: --app-drop-link 480 190 \ --hide-extension "LinkUnbound.app" \ --no-internet-enable \ - "apps/linkunbound/dist/$DMG_NAME" \ - "$APP_PATH" \ + "$DMG_PATH" \ + "$RELEASE_APP_PATH" \ || true - if [[ ! -f "apps/linkunbound/dist/$DMG_NAME" ]]; then + if [[ ! -f "$DMG_PATH" ]]; then echo "::error::DMG was not created" exit 1 fi - echo "DMG created: $DMG_NAME ($(du -h "apps/linkunbound/dist/$DMG_NAME" | cut -f1))" + echo "DMG created: $DMG_PATH ($(du -h "$DMG_PATH" | cut -f1))" - # ── Sign & Notarize DMG ── - name: Sign DMG if: env.MACOS_CERTIFICATE_P12 != '' + env: + DMG_PATH: apps/linkunbound/dist/LinkUnbound_${{ steps.version.outputs.version }}_universal.dmg run: | - DMG_PATH="apps/linkunbound/dist/LinkUnbound_${{ steps.get_version.outputs.VERSION }}_universal.dmg" + set -eu SIGN_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') echo "Signing DMG with identity: $SIGN_IDENTITY" @@ -202,8 +174,10 @@ jobs: - name: Notarize DMG if: env.APPLE_ID != '' && env.MACOS_CERTIFICATE_P12 != '' + env: + DMG_PATH: apps/linkunbound/dist/LinkUnbound_${{ steps.version.outputs.version }}_universal.dmg run: | - DMG_PATH="apps/linkunbound/dist/LinkUnbound_${{ steps.get_version.outputs.VERSION }}_universal.dmg" + set -euo pipefail echo "Submitting for notarization..." xcrun notarytool submit "$DMG_PATH" \ @@ -220,14 +194,12 @@ jobs: spctl --assess --type open --context context:primary-signature "$DMG_PATH" echo "Notarization complete" - # ── Cleanup ── - name: Cleanup keychain if: always() && env.KEYCHAIN_PATH != '' run: security delete-keychain "$KEYCHAIN_PATH" || true - # ── Upload ── - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-macos path: apps/linkunbound/dist/*.dmg diff --git a/.github/workflows/release-win.yml b/.github/workflows/release-win.yml index 4b7c52e..e072d85 100644 --- a/.github/workflows/release-win.yml +++ b/.github/workflows/release-win.yml @@ -17,7 +17,7 @@ permissions: contents: read jobs: - release-windows: + build-windows: runs-on: windows-latest timeout-minutes: 30 name: Build Windows @@ -27,34 +27,18 @@ jobs: PFX_PASSWORD: ${{ secrets.PFX_PASSWORD }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} - name: Resolve version - id: get_version - shell: pwsh - run: | - $version = "${{ inputs.version }}" - - $isPrerelease = if ($version -match '-') { 'true' } else { 'false' } - $baseVersion = $version -replace '-.*', '' - $parts = $baseVersion.Split('.') - $version4 = switch ($parts.Count) { - 1 { "$baseVersion.0.0.0" } - 2 { "$baseVersion.0.0" } - 3 { "$baseVersion.0" } - default { $baseVersion } - } - echo "VERSION=$version" >> $env:GITHUB_OUTPUT - echo "VERSION4=$version4" >> $env:GITHUB_OUTPUT - echo "IS_PRERELEASE=$isPrerelease" >> $env:GITHUB_OUTPUT - Write-Host "Version: $version (MSIX: $version4) (PreRelease: $isPrerelease)" - - - uses: subosito/flutter-action@v2 + id: version + uses: ./.github/actions/resolve-version with: - channel: stable - flutter-version: 3.44.1 + version: ${{ inputs.version }} + + - name: Set up Flutter workspace + uses: ./.github/actions/setup-flutter - name: Install Fastforge run: dart pub global activate fastforge @@ -62,19 +46,12 @@ jobs: - name: Install Inno Setup run: choco install innosetup --no-progress - - name: Get dependencies - working-directory: apps/linkunbound - run: flutter pub get - - - name: Update pubspec version from tag - shell: pwsh - run: | - $version = "${{ steps.get_version.outputs.VERSION }}" - $pubspec = "apps/linkunbound/pubspec.yaml" - (Get-Content $pubspec) -replace '^version:\s+.*', "version: $version" | Set-Content $pubspec - Write-Host "Updated pubspec.yaml version to: $version" + - name: Set app version + uses: ./.github/actions/set-app-version + with: + version: ${{ steps.version.outputs.version }} - - name: Build standalone (exe installer) + - name: Build standalone installer shell: pwsh run: | Push-Location apps/linkunbound @@ -82,7 +59,7 @@ jobs: --platform windows ` --targets exe ` --build-dart-define "STORE_BUILD=false" ` - --build-dart-define "APP_VERSION=${{ steps.get_version.outputs.VERSION }}" + --build-dart-define "APP_VERSION=${{ steps.version.outputs.version }}" Pop-Location - name: Decode signing certificate @@ -113,7 +90,7 @@ jobs: - name: Rename standalone installer shell: pwsh run: | - $version = "${{ steps.get_version.outputs.VERSION }}" + $version = "${{ steps.version.outputs.version }}" $distDir = "apps/linkunbound/dist" $setup = Get-ChildItem -Path $distDir -Recurse -Filter "*-setup.exe" | Select-Object -First 1 if ($setup) { @@ -124,7 +101,7 @@ jobs: } - name: Build store MSIX - if: steps.get_version.outputs.IS_PRERELEASE != 'true' + if: steps.version.outputs.is-prerelease != 'true' shell: pwsh run: | Push-Location apps/linkunbound @@ -132,14 +109,14 @@ jobs: --platform windows ` --targets msix ` --build-dart-define "STORE_BUILD=true" ` - --build-dart-define "APP_VERSION=${{ steps.get_version.outputs.VERSION }}" + --build-dart-define "APP_VERSION=${{ steps.version.outputs.version }}" Pop-Location - name: Move and rename store MSIX - if: steps.get_version.outputs.IS_PRERELEASE != 'true' + if: steps.version.outputs.is-prerelease != 'true' shell: pwsh run: | - $version = "${{ steps.get_version.outputs.VERSION }}" + $version = "${{ steps.version.outputs.version }}" $distDir = "apps/linkunbound/dist" $extensions = @("*.msixupload", "*.msixbundle", "*.msix") @@ -167,7 +144,7 @@ jobs: } - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-windows path: apps/linkunbound/dist/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0178e15..470c055 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,45 +12,127 @@ on: default: "1.0.0-dev" permissions: - contents: write + contents: read jobs: - extract-version: + prepare: runs-on: ubuntu-latest - name: Extract Version + name: Prepare release + timeout-minutes: 5 outputs: - version: ${{ steps.get_version.outputs.VERSION }} + version: ${{ steps.version.outputs.version }} + is-prerelease: ${{ steps.version.outputs.is-prerelease }} + package-name: ${{ steps.naming.outputs.package-name }} + package-desc: ${{ steps.naming.outputs.package-desc }} + publish-store: ${{ steps.credentials.outputs.publish-store }} + publish-taps: ${{ steps.credentials.outputs.publish-taps }} + steps: - - name: Resolve version - id: get_version + - uses: actions/checkout@v7 + + - name: Read version from event + id: event + env: + DISPATCH_VERSION: ${{ github.event.inputs.version }} run: | + set -euo pipefail + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then - VERSION="${{ github.event.inputs.version }}" - elif [[ "$GITHUB_REF" =~ refs/tags/v(.+) ]]; then - VERSION="${BASH_REMATCH[1]}" + version="$DISPATCH_VERSION" + elif [[ "$GITHUB_REF" =~ ^refs/tags/v(.+)$ ]]; then + version="${BASH_REMATCH[1]}" + else + version="1.0.0" + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Resolve version + id: version + uses: ./.github/actions/resolve-version + with: + version: ${{ steps.event.outputs.version }} + + - name: Select package identity + id: naming + env: + IS_PRERELEASE: ${{ steps.version.outputs.is-prerelease }} + run: | + set -euo pipefail + + if [[ "$IS_PRERELEASE" == "true" ]]; then + name="linkunbound-beta" + desc="Smart browser router for HTTP(S) links (beta)" + else + name="linkunbound" + desc="Smart browser router for HTTP(S) links" + fi + + { + echo "package-name=$name" + echo "package-desc=$desc" + } >> "$GITHUB_OUTPUT" + + - name: Check distribution credentials + id: credentials + env: + STORE_TENANT_ID: ${{ secrets.STORE_TENANT_ID }} + STORE_SELLER_ID: ${{ secrets.STORE_SELLER_ID }} + STORE_CLIENT_ID: ${{ secrets.STORE_CLIENT_ID }} + STORE_CLIENT_SECRET: ${{ secrets.STORE_CLIENT_SECRET }} + STORE_APP_ID: ${{ vars.STORE_APP_ID }} + GIST_TOKEN: ${{ secrets.GIST_TOKEN }} + MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + PFX_BASE64: ${{ secrets.PFX_BASE64 }} + run: | + set -euo pipefail + + missing="" + for name in STORE_TENANT_ID STORE_SELLER_ID STORE_CLIENT_ID STORE_CLIENT_SECRET STORE_APP_ID; do + if [[ -z "${!name}" ]]; then + missing="$missing $name" + fi + done + + if [[ -n "$missing" ]]; then + echo "::warning::Microsoft Store publishing will be skipped; missing:${missing}" + echo "publish-store=false" >> "$GITHUB_OUTPUT" else - VERSION="1.0.0" + echo "publish-store=true" >> "$GITHUB_OUTPUT" + fi + + if [[ -z "$GIST_TOKEN" ]]; then + echo "::warning::Homebrew and Scoop updates will be skipped; missing GIST_TOKEN" + echo "publish-taps=false" >> "$GITHUB_OUTPUT" + else + echo "publish-taps=true" >> "$GITHUB_OUTPUT" + fi + + if [[ -z "$MACOS_CERTIFICATE_P12" ]]; then + echo "::warning::MACOS_CERTIFICATE_P12 is not set; the macOS build will be ad-hoc signed and not notarized" + fi + + if [[ -z "$PFX_BASE64" ]]; then + echo "::warning::PFX_BASE64 is not set; the Windows installer will not be signed" fi - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "Resolved version: $VERSION" build-windows: - needs: extract-version + needs: prepare uses: ./.github/workflows/release-win.yml with: - version: ${{ needs.extract-version.outputs.version }} + version: ${{ needs.prepare.outputs.version }} secrets: inherit build-macos: - needs: extract-version + needs: prepare uses: ./.github/workflows/release-mac.yml with: - version: ${{ needs.extract-version.outputs.version }} + version: ${{ needs.prepare.outputs.version }} secrets: inherit github-release: runs-on: ubuntu-latest - needs: [extract-version, build-windows, build-macos] + needs: [prepare, build-windows, build-macos] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') timeout-minutes: 10 name: Create GitHub Release @@ -61,27 +143,34 @@ jobs: attestations: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} fetch-depth: 0 - name: Extract tag message id: tag_message + env: + TAG: ${{ github.ref_name }} run: | - MSG=$(git tag -l --format='%(contents:subject)%0a%0a%(contents:body)' "${{ github.ref_name }}" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}') - echo "TAG_BODY<> $GITHUB_OUTPUT - echo "$MSG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + set -euo pipefail + + MSG=$(git tag -l --format='%(contents:subject)%0a%0a%(contents:body)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}') + + { + echo "TAG_BODY<> "$GITHUB_OUTPUT" - name: Download Windows artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: release-windows path: artifacts/windows - name: Download macOS artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: release-macos path: artifacts/macos @@ -97,11 +186,11 @@ jobs: artifacts/macos/**/*.dmg - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: body: ${{ steps.tag_message.outputs.TAG_BODY }} generate_release_notes: true - prerelease: ${{ contains(github.ref_name, '-') }} + prerelease: ${{ needs.prepare.outputs.is-prerelease == 'true' }} make_latest: true files: | artifacts/windows/**/*_Setup.exe @@ -112,119 +201,113 @@ jobs: update-homebrew-cask: runs-on: ubuntu-latest - needs: github-release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [prepare, github-release] + if: needs.prepare.outputs.publish-taps == 'true' timeout-minutes: 5 name: Update Homebrew Tap steps: - - name: Update Homebrew Tap - env: - GH_TOKEN: ${{ secrets.GIST_TOKEN }} - run: | - TAG="${GITHUB_REF_NAME}" - VERSION="${TAG#v}" - - DMG_NAME="LinkUnbound_${VERSION}_universal.dmg" - DMG_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${DMG_NAME}" - - echo "Downloading DMG to compute SHA256..." - DOWNLOADED="" - for attempt in 1 2 3 4 5; do - if curl -fSL -o "/tmp/${DMG_NAME}" "${DMG_URL}"; then - DOWNLOADED=1 - break - fi - echo "Asset not downloadable yet (attempt ${attempt}/5); retrying..." - sleep $((attempt * 10)) - done - if [[ -z "$DOWNLOADED" || ! -s "/tmp/${DMG_NAME}" ]]; then - echo "Could not download ${DMG_NAME}" - exit 1 - fi - DMG_SHA256=$(sha256sum "/tmp/${DMG_NAME}" | awk '{print $1}') - rm -f "/tmp/${DMG_NAME}" - - echo "Version: ${VERSION}" - echo "DMG SHA256: ${DMG_SHA256}" - - if [[ "$VERSION" == *-* ]]; then - CASK_FILE="Casks/linkunbound-beta.rb" - CASK_NAME="linkunbound-beta" - CASK_DESC="Smart browser router for HTTP(S) links (beta)" - else - CASK_FILE="Casks/linkunbound.rb" - CASK_NAME="linkunbound" - CASK_DESC="Smart browser router for HTTP(S) links" - fi - - git clone "https://x-access-token:${GH_TOKEN}@github.com/rgdevment/homebrew-tap.git" /tmp/homebrew-tap - cd /tmp/homebrew-tap + - uses: actions/checkout@v7 - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - mkdir -p Casks - - cat > "${CASK_FILE}" <<- CASK_EOF - cask "${CASK_NAME}" do - version "${VERSION}" - sha256 "${DMG_SHA256}" - - url "${DMG_URL}" - name "LinkUnbound" - desc "${CASK_DESC}" - homepage "https://github.com/${{ github.repository }}" - - depends_on macos: :ventura + - name: Fetch published DMG + id: asset + uses: ./.github/actions/fetch-release-asset + with: + repository: ${{ github.repository }} + tag: ${{ github.ref_name }} + asset: LinkUnbound_${{ needs.prepare.outputs.version }}_universal.dmg - app "LinkUnbound.app" + - name: Publish cask + uses: ./.github/actions/publish-manifest + with: + repository: rgdevment/homebrew-tap + token: ${{ secrets.GIST_TOKEN }} + path: Casks/${{ needs.prepare.outputs.package-name }}.rb + message: Update ${{ needs.prepare.outputs.package-name }} to ${{ needs.prepare.outputs.version }} + content: | + cask "${{ needs.prepare.outputs.package-name }}" do + version "${{ needs.prepare.outputs.version }}" + sha256 "${{ steps.asset.outputs.sha256 }}" + + url "${{ steps.asset.outputs.url }}" + name "LinkUnbound" + desc "${{ needs.prepare.outputs.package-desc }}" + homepage "https://github.com/${{ github.repository }}" + + depends_on macos: :ventura + + app "LinkUnbound.app" + + zap trash: [ + "~/Library/Application Support/com.rgdevment.linkunbound", + "~/Library/Preferences/com.rgdevment.linkunbound.plist", + ] + end - zap trash: [ - "~/Library/Application Support/com.rgdevment.linkunbound", - "~/Library/Preferences/com.rgdevment.linkunbound.plist", - ] - end - CASK_EOF + update-scoop-bucket: + runs-on: ubuntu-latest + needs: [prepare, github-release] + if: needs.prepare.outputs.publish-taps == 'true' + timeout-minutes: 5 + name: Update Scoop Bucket - git add "${CASK_FILE}" - if git diff --cached --quiet; then - echo "Cask already up to date; nothing to publish" - exit 0 - fi - git commit -m "Update ${CASK_NAME} to ${VERSION}" + steps: + - uses: actions/checkout@v7 - for attempt in 1 2 3 4 5; do - if git push origin HEAD:main; then - echo "Homebrew Tap updated: cask ${CASK_NAME} → ${VERSION}" - exit 0 - fi - echo "Push rejected (attempt ${attempt}/5); rebasing onto concurrent release..." - git fetch origin main && git rebase origin/main - sleep $((RANDOM % 5 + 3)) - done + - name: Fetch published installer + id: asset + uses: ./.github/actions/fetch-release-asset + with: + repository: ${{ github.repository }} + tag: ${{ github.ref_name }} + asset: LinkUnbound_${{ needs.prepare.outputs.version }}_x64_Setup.exe - echo "Could not push ${CASK_NAME} ${VERSION} after 5 attempts" - exit 1 + - name: Publish manifest + uses: ./.github/actions/publish-manifest + with: + repository: rgdevment/scoop-bucket + token: ${{ secrets.GIST_TOKEN }} + path: bucket/${{ needs.prepare.outputs.package-name }}.json + message: ${{ needs.prepare.outputs.package-name }} ${{ needs.prepare.outputs.version }} + validate-json: "true" + content: | + { + "version": "${{ needs.prepare.outputs.version }}", + "description": "${{ needs.prepare.outputs.package-desc }}", + "homepage": "https://github.com/${{ github.repository }}", + "license": "GPL-3.0-only", + "architecture": { + "64bit": { + "url": "${{ steps.asset.outputs.url }}", + "hash": "${{ steps.asset.outputs.sha256 }}" + } + }, + "innosetup": true, + "extract_dir": "{app}", + "shortcuts": [ + [ + "linkunbound.exe", + "LinkUnbound" + ] + ], + "post_install": [ + "Start-Process -FilePath \"$dir\\linkunbound.exe\" -ArgumentList \"--register\" -Wait" + ] + } publish-to-store: runs-on: windows-latest - needs: [extract-version, github-release] - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(needs.extract-version.outputs.version, '-') + needs: [prepare, github-release] + if: needs.prepare.outputs.is-prerelease != 'true' && needs.prepare.outputs.publish-store == 'true' timeout-minutes: 15 name: Publish to Microsoft Store steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.ref_name }} - fetch-depth: 0 - - name: Install Microsoft Store Developer CLI - uses: microsoft/microsoft-store-apppublisher@v1.3 + uses: microsoft/microsoft-store-apppublisher@v1.4 - name: Download Windows artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: release-windows path: artifacts/windows @@ -233,112 +316,47 @@ jobs: id: find_msix shell: bash run: | - MSIX=$(find artifacts/windows -name "*.msixupload" | head -1) - [ -z "$MSIX" ] && MSIX=$(find artifacts/windows -name "*.msixbundle" | head -1) - [ -z "$MSIX" ] && MSIX=$(find artifacts/windows -name "*.msix" | head -1) + set -eu + + MSIX="" + for pattern in "*.msixupload" "*.msixbundle" "*.msix"; do + MSIX=$(find artifacts/windows -name "$pattern" | head -1) + if [ -n "$MSIX" ]; then + break + fi + done + if [ -z "$MSIX" ]; then - echo "Error: No MSIX package found in release-windows artifact" + echo "::error::No MSIX package found in release-windows artifact" find artifacts/windows -type f exit 1 fi - echo "MSIX_PATH=$MSIX" >> $GITHUB_OUTPUT + + echo "MSIX_PATH=$MSIX" >> "$GITHUB_OUTPUT" echo "Found: $MSIX" - name: Configure Microsoft Store CLI shell: bash + env: + STORE_TENANT_ID: ${{ secrets.STORE_TENANT_ID }} + STORE_SELLER_ID: ${{ secrets.STORE_SELLER_ID }} + STORE_CLIENT_ID: ${{ secrets.STORE_CLIENT_ID }} + STORE_CLIENT_SECRET: ${{ secrets.STORE_CLIENT_SECRET }} run: | + set -euo pipefail + msstore reconfigure \ - --tenantId "${{ secrets.STORE_TENANT_ID }}" \ - --sellerId "${{ secrets.STORE_SELLER_ID }}" \ - --clientId "${{ secrets.STORE_CLIENT_ID }}" \ - --clientSecret "${{ secrets.STORE_CLIENT_SECRET }}" + --tenantId "$STORE_TENANT_ID" \ + --sellerId "$STORE_SELLER_ID" \ + --clientId "$STORE_CLIENT_ID" \ + --clientSecret "$STORE_CLIENT_SECRET" - name: Publish to Microsoft Store shell: bash - run: | - msstore publish "${{ steps.find_msix.outputs.MSIX_PATH }}" \ - --appId "${{ vars.STORE_APP_ID }}" - - update-scoop-bucket: - runs-on: ubuntu-latest - needs: github-release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - timeout-minutes: 5 - name: Update Scoop Bucket - - steps: - - name: Update Scoop Bucket env: - GH_TOKEN: ${{ secrets.GIST_TOKEN }} + MSIX_PATH: ${{ steps.find_msix.outputs.MSIX_PATH }} + STORE_APP_ID: ${{ vars.STORE_APP_ID }} run: | - TAG="${GITHUB_REF_NAME}" - VERSION="${TAG#v}" - BASE="https://github.com/${{ github.repository }}/releases/download/${TAG}" - SETUP="LinkUnbound_${VERSION}_x64_Setup.exe" - - if [[ "$VERSION" == *-* ]]; then - NAME="linkunbound-beta"; SUFFIX=" (beta)" - else - NAME="linkunbound"; SUFFIX="" - fi - - DOWNLOADED="" - for attempt in 1 2 3 4 5; do - if curl -fSL -o "/tmp/${SETUP}" "${BASE}/${SETUP}"; then - DOWNLOADED=1 - break - fi - echo "Asset not downloadable yet (attempt ${attempt}/5); retrying..." - sleep $((attempt * 10)) - done - if [[ -z "$DOWNLOADED" || ! -s "/tmp/${SETUP}" ]]; then - echo "Could not download ${SETUP} from ${BASE}" - exit 1 - fi - SHA=$(sha256sum "/tmp/${SETUP}" | awk '{print $1}') - - git clone "https://x-access-token:${GH_TOKEN}@github.com/rgdevment/scoop-bucket.git" /tmp/bucket - cd /tmp/bucket - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - mkdir -p bucket - - jq -n \ - --arg version "$VERSION" \ - --arg desc "Smart browser router for HTTP(S) links${SUFFIX}" \ - --arg home "https://github.com/${{ github.repository }}" \ - --arg url "${BASE}/${SETUP}" \ - --arg hash "$SHA" \ - '{ - version: $version, - description: $desc, - homepage: $home, - license: "GPL-3.0-only", - architecture: {"64bit": {url: $url, hash: $hash}}, - innosetup: true, - extract_dir: "{app}", - shortcuts: [["linkunbound.exe", "LinkUnbound"]], - post_install: [ - "Start-Process -FilePath \"$dir\\linkunbound.exe\" -ArgumentList \"--register\" -Wait" - ] - }' > "bucket/${NAME}.json" - - git add "bucket/${NAME}.json" - if git diff --cached --quiet; then - echo "Manifest already up to date; nothing to publish" - exit 0 - fi - git commit -m "${NAME} ${VERSION}" - - for attempt in 1 2 3 4 5; do - if git push origin HEAD:main; then - echo "Scoop bucket updated: ${NAME} → ${VERSION}" - exit 0 - fi - echo "Push rejected (attempt ${attempt}/5); rebasing onto concurrent release..." - git fetch origin main && git rebase origin/main - sleep $((RANDOM % 5 + 3)) - done + set -euo pipefail - echo "Could not push ${NAME} ${VERSION} after 5 attempts" - exit 1 + msstore publish "$MSIX_PATH" --appId "$STORE_APP_ID" diff --git a/.github/workflows/update-badge.yml b/.github/workflows/update-badge.yml index 9557d29..1133dfa 100644 --- a/.github/workflows/update-badge.yml +++ b/.github/workflows/update-badge.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v9 with: enable-cache: false From 4b81bdbe1365f0f49867eb6107c6344d5c1bf9e3 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 7 Aug 2026 12:10:05 -0400 Subject: [PATCH 2/3] fix: update setup-uv action to version 9.0.0 in update-badge workflow --- .github/workflows/release.yml | 6 +++--- .github/workflows/update-badge.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 470c055..f870cfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -202,7 +202,7 @@ jobs: update-homebrew-cask: runs-on: ubuntu-latest needs: [prepare, github-release] - if: needs.prepare.outputs.publish-taps == 'true' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.prepare.outputs.publish-taps == 'true' timeout-minutes: 5 name: Update Homebrew Tap @@ -247,7 +247,7 @@ jobs: update-scoop-bucket: runs-on: ubuntu-latest needs: [prepare, github-release] - if: needs.prepare.outputs.publish-taps == 'true' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.prepare.outputs.publish-taps == 'true' timeout-minutes: 5 name: Update Scoop Bucket @@ -298,7 +298,7 @@ jobs: publish-to-store: runs-on: windows-latest needs: [prepare, github-release] - if: needs.prepare.outputs.is-prerelease != 'true' && needs.prepare.outputs.publish-store == 'true' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.prepare.outputs.is-prerelease != 'true' && needs.prepare.outputs.publish-store == 'true' timeout-minutes: 15 name: Publish to Microsoft Store diff --git a/.github/workflows/update-badge.yml b/.github/workflows/update-badge.yml index 1133dfa..3b4394b 100644 --- a/.github/workflows/update-badge.yml +++ b/.github/workflows/update-badge.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v7 - name: Setup uv - uses: astral-sh/setup-uv@v9 + uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: false From 1a7ec18283b3162b7def640d9b61a99867738457 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 7 Aug 2026 12:21:37 -0400 Subject: [PATCH 3/3] fix: update melos bootstrap command to use dart pub for Windows compatibility --- .github/actions/setup-flutter/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-flutter/action.yml b/.github/actions/setup-flutter/action.yml index f5fb8d2..6150965 100644 --- a/.github/actions/setup-flutter/action.yml +++ b/.github/actions/setup-flutter/action.yml @@ -28,4 +28,5 @@ runs: - name: Bootstrap workspace if: inputs.bootstrap == 'true' shell: bash - run: melos bootstrap + # Invoked through pub because the Windows shim is melos.bat, which bash does not resolve. + run: dart pub global run melos bootstrap