diff --git a/.github/actions/setup-flutter-workspace/action.yml b/.github/actions/setup-flutter-workspace/action.yml new file mode 100644 index 0000000..1ca6cfc --- /dev/null +++ b/.github/actions/setup-flutter-workspace/action.yml @@ -0,0 +1,58 @@ +name: Setup Flutter workspace +description: Flutter toolchain, pub cache and melos bootstrap, optionally stamping the release version into app/pubspec.yaml. + +inputs: + channel: + description: Flutter channel passed to subosito/flutter-action. + required: false + default: stable + version: + description: When set, writes this version into app/pubspec.yaml after bootstrap. + required: false + default: "" + +runs: + using: composite + steps: + - uses: subosito/flutter-action@v2 + with: + channel: ${{ inputs.channel }} + cache: true + + - name: Cache pub dependencies + uses: actions/cache@v6 + with: + # Windows keeps the pub cache under LOCALAPPDATA, not ~/.pub-cache. + path: | + ~/.pub-cache + ~/AppData/Local/Pub/Cache + key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} + restore-keys: ${{ runner.os }}-pub- + + - name: Install Melos + shell: bash + run: dart pub global activate melos + + # Invoked through dart: on Windows the entry point is melos.bat, and + # git bash does not resolve .bat from PATH. + - name: Bootstrap workspace + shell: bash + run: dart pub global run melos:melos bootstrap + + # Must run after the cache step: the cache key hashes every pubspec.yaml. + - name: Stamp version into app/pubspec.yaml + if: inputs.version != '' + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + tmp="$(mktemp)" + sed "s/^version:.*/version: ${VERSION}/" app/pubspec.yaml > "$tmp" + mv "$tmp" app/pubspec.yaml + if [[ "$(grep -m1 '^version:' app/pubspec.yaml)" != "version: ${VERSION}" ]]; then + echo "::error::Failed to stamp version ${VERSION} into app/pubspec.yaml" + grep -m1 '^version:' app/pubspec.yaml + exit 1 + fi + echo "app/pubspec.yaml -> version: ${VERSION}" diff --git a/.github/actions/setup-linux-build-deps/action.yml b/.github/actions/setup-linux-build-deps/action.yml new file mode 100644 index 0000000..4af092b --- /dev/null +++ b/.github/actions/setup-linux-build-deps/action.yml @@ -0,0 +1,38 @@ +name: Setup Linux build dependencies +description: APT packages required to build and package CopyPaste on Linux, plus a toolchain preflight. + +runs: + using: composite + steps: + - name: Install Linux build dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + desktop-file-utils \ + libayatana-appindicator3-dev \ + libfuse2 \ + libgtk-3-dev \ + libkeybinder-3.0-dev \ + liblzma-dev \ + libx11-dev \ + libxtst-dev \ + lld-14 \ + ninja-build \ + patchelf \ + pkg-config \ + rpm + + - name: Verify Linux toolchain preflight + shell: bash + run: | + set -euo pipefail + test -x /usr/lib/llvm-14/bin/ld.lld || test -x /usr/bin/ld.lld-14 || test -x /usr/bin/ld.lld + pkg-config --modversion gtk+-3.0 + pkg-config --modversion keybinder-3.0 + pkg-config --modversion ayatana-appindicator3-0.1 + pkg-config --modversion x11 + pkg-config --modversion xtst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dec3142..0ba5dd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: name: Markdown Lint steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Lint Markdown files uses: DavidAnson/markdownlint-cli2-action@v19 @@ -35,25 +35,9 @@ jobs: name: Lint, Format & Test steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - 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: Install Melos - run: dart pub global activate melos - - - name: Bootstrap - run: melos bootstrap + - uses: ./.github/actions/setup-flutter-workspace - name: Check formatting run: dart format --output=none --set-exit-if-changed . @@ -85,7 +69,7 @@ jobs: run: melos run test:coverage - name: Upload coverage artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: coverage-reports @@ -109,7 +93,7 @@ jobs: name: Release Readiness steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Validate pubspec structure run: | @@ -130,7 +114,7 @@ jobs: echo "::error::Missing name field in $pkg/pubspec.yaml" errors=$((errors + 1)) fi - echo "✓ $pkg: name=$name version=$version" + echo "OK $pkg: name=$name version=$version" done [ $errors -eq 0 ] || exit 1 @@ -142,7 +126,15 @@ jobs: echo "::error::Missing .github/workflows/$wf (referenced by release.yml)" errors=$((errors + 1)) else - echo "✓ .github/workflows/$wf" + echo "OK .github/workflows/$wf" + fi + done + for act in setup-flutter-workspace setup-linux-build-deps; do + if [ ! -f ".github/actions/$act/action.yml" ]; then + echo "::error::Missing .github/actions/$act/action.yml (referenced by the build jobs)" + errors=$((errors + 1)) + else + echo "OK .github/actions/$act/action.yml" fi done [ $errors -eq 0 ] || exit 1 @@ -155,7 +147,7 @@ jobs: echo "::error::Missing required directory: $dir" errors=$((errors + 1)) else - echo "✓ $dir/" + echo "OK $dir/" fi done for file in app/l10n.yaml app/pubspec.yaml core/pubspec.yaml listener/pubspec.yaml; do @@ -163,7 +155,7 @@ jobs: echo "::error::Missing required file: $file" errors=$((errors + 1)) else - echo "✓ $file" + echo "OK $file" fi done [ $errors -eq 0 ] || exit 1 @@ -175,12 +167,11 @@ jobs: ref="refs/tags/$tag" if [[ "$ref" =~ refs/tags/v(.+) ]]; then VERSION="${BASH_REMATCH[1]}" - # Validate version format (semver) - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Tag $tag produces invalid version: $VERSION" exit 1 fi - echo "✓ $tag → $VERSION" + echo "OK $tag -> $VERSION" else echo "::error::Tag $tag would fail version extraction in release.yml" exit 1 @@ -193,25 +184,9 @@ jobs: name: Build Verification (Windows) steps: - - uses: actions/checkout@v6 - - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - 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: Install Melos - run: dart pub global activate melos + - uses: actions/checkout@v7 - - name: Get dependencies - run: melos bootstrap + - uses: ./.github/actions/setup-flutter-workspace - name: Build Windows release run: cd app; flutter build windows --release @@ -222,54 +197,11 @@ jobs: name: Build Verification (Linux) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - 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: Install Linux build dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - clang \ - cmake \ - desktop-file-utils \ - libayatana-appindicator3-dev \ - libfuse2 \ - libgtk-3-dev \ - libkeybinder-3.0-dev \ - liblzma-dev \ - libx11-dev \ - libxtst-dev \ - lld-14 \ - ninja-build \ - patchelf \ - pkg-config - - - name: Verify Linux toolchain preflight - run: | - set -euo pipefail - test -x /usr/lib/llvm-14/bin/ld.lld || test -x /usr/bin/ld.lld-14 || test -x /usr/bin/ld.lld - pkg-config --modversion gtk+-3.0 - pkg-config --modversion keybinder-3.0 - pkg-config --modversion ayatana-appindicator3-0.1 - pkg-config --modversion x11 - pkg-config --modversion xtst - - - name: Install Melos - run: dart pub global activate melos + - uses: ./.github/actions/setup-linux-build-deps - - name: Get dependencies - run: melos bootstrap + - uses: ./.github/actions/setup-flutter-workspace - name: Build Linux release run: cd app && flutter build linux --release @@ -280,25 +212,9 @@ jobs: name: Build Verification (macOS) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - 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: Install Melos - run: dart pub global activate melos - - - name: Get dependencies - run: melos bootstrap + - uses: ./.github/actions/setup-flutter-workspace - name: Build macOS release run: cd app && flutter build macos --release @@ -310,12 +226,12 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Download coverage reports - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: coverage-reports path: . @@ -335,4 +251,3 @@ jobs: -Dsonar.coverage.exclusions=**/main.dart,**/shell/**,**/services/auto_update_service.dart,**/windows_clipboard_listener.dart,**/l10n/**,**/*.g.dart,**/*.freezed.dart,**/core.dart,**/screens/settings_screen.dart -Dsonar.qualitygate.wait=true -Dsonar.qualitygate.timeout=300 - diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 18b30a7..6e48247 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -23,65 +23,25 @@ jobs: name: Build Linux (AppImage, deb, rpm) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} - name: Resolve version id: get_version + env: + VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - - IS_PRERELEASE="false" - if [[ "$VERSION" == *-* ]]; then - IS_PRERELEASE="true" - fi - + set -euo pipefail echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "IS_PRERELEASE=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION PreRelease: $IS_PRERELEASE" + echo "RPM_VERSION=${VERSION%%-*}" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (rpm: ${VERSION%%-*})" - - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true + - uses: ./.github/actions/setup-linux-build-deps - - name: Cache pub dependencies - uses: actions/cache@v5 + - uses: ./.github/actions/setup-flutter-workspace with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }} - restore-keys: ${{ runner.os }}-pub- - - - name: Install Linux build dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - clang \ - cmake \ - desktop-file-utils \ - libayatana-appindicator3-dev \ - libfuse2 \ - libgtk-3-dev \ - libkeybinder-3.0-dev \ - liblzma-dev \ - libx11-dev \ - libxtst-dev \ - lld-14 \ - ninja-build \ - patchelf \ - pkg-config \ - rpm - - - name: Verify Linux toolchain preflight - run: | - set -euo pipefail - test -x /usr/lib/llvm-14/bin/ld.lld || test -x /usr/bin/ld.lld-14 || test -x /usr/bin/ld.lld - pkg-config --modversion gtk+-3.0 - pkg-config --modversion keybinder-3.0 - pkg-config --modversion ayatana-appindicator3-0.1 - pkg-config --modversion x11 - pkg-config --modversion xtst + version: ${{ steps.get_version.outputs.VERSION }} - name: Install Fastforge run: dart pub global activate fastforge @@ -92,18 +52,6 @@ jobs: "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" chmod +x /usr/local/bin/appimagetool - - name: Install Melos - run: dart pub global activate melos - - - name: Get dependencies - run: melos bootstrap - - - name: Update pubspec version from tag - run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - sed -i "s/^version:.*/version: $VERSION/" app/pubspec.yaml - echo "Updated pubspec.yaml version to: $VERSION" - - name: Package AppImage env: APPIMAGE_EXTRACT_AND_RUN: "1" @@ -129,7 +77,6 @@ jobs: echo "Renamed to: $(basename "$DEST")" - name: Repack AppImage with AppImageUpdate metadata - if: env.STORE_BUILD != 'true' env: APPIMAGE_EXTRACT_AND_RUN: "1" run: | @@ -175,25 +122,18 @@ jobs: mv "$DEB" "$DEST" echo "Renamed to: $(basename "$DEST")" - - name: Normalize version for RPM - id: rpm_version - run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - RPM_VERSION="${VERSION%%-*}" - echo "RPM_VERSION=$RPM_VERSION" >> "$GITHUB_OUTPUT" - echo "RPM version (normalized): $RPM_VERSION" - - name: Package rpm run: | - RPM_VERSION="${{ steps.rpm_version.outputs.RPM_VERSION }}" + set -euo pipefail + RPM_VERSION="${{ steps.get_version.outputs.RPM_VERSION }}" + VERSION="${{ steps.get_version.outputs.VERSION }}" cd app sed -i "s/^version:.*/version: $RPM_VERSION/" pubspec.yaml fastforge package \ --platform linux \ --targets rpm \ - --build-dart-define "APP_VERSION=${{ steps.get_version.outputs.VERSION }}" \ + --build-dart-define "APP_VERSION=${VERSION}" \ --skip-clean - VERSION="${{ steps.get_version.outputs.VERSION }}" sed -i "s/^version:.*/version: $VERSION/" pubspec.yaml - name: Rename rpm @@ -222,7 +162,7 @@ jobs: exit 1 fi desktop-file-validate "$DESKTOP" - echo "✓ desktop-file-validate passed" + echo "desktop-file-validate passed" - name: Build portable tarball for OBS run: | @@ -254,25 +194,8 @@ jobs: sha256sum *.AppImage *.deb *.rpm *.tar.gz > SHA256SUMS cat SHA256SUMS - - name: Publish deb to Cloudsmith - if: github.event_name == 'push' - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - run: | - pip install cloudsmith-cli - cloudsmith push deb rgdevment/copypaste/any-distro/any-version \ - app/dist/CopyPaste_${{ steps.get_version.outputs.VERSION }}_amd64.deb --republish - - - name: Publish rpm to Cloudsmith - if: github.event_name == 'push' - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - run: | - cloudsmith push rpm rgdevment/copypaste/any-distro/any-version \ - app/dist/CopyPaste_${{ steps.get_version.outputs.VERSION }}_x86_64.rpm --republish - - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-linux path: | @@ -292,12 +215,12 @@ jobs: name: Publish to OpenSUSE Build Service steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} - name: Download Linux artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: release-linux path: linux-artifacts diff --git a/.github/workflows/release-mac.yml b/.github/workflows/release-mac.yml index f31a8b8..ffb0d55 100644 --- a/.github/workflows/release-mac.yml +++ b/.github/workflows/release-mac.yml @@ -30,54 +30,28 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} - name: Resolve version id: get_version + env: + VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - - IS_PRERELEASE="false" - if [[ "$VERSION" == *-* ]]; then - IS_PRERELEASE="true" - fi - + set -euo pipefail 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" + echo "Version: $VERSION Build: $BUILD_NAME ($BUILD_NUMBER)" - - uses: subosito/flutter-action@v2 + - uses: ./.github/actions/setup-flutter-workspace with: - channel: stable - cache: true - - - 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: Install Melos - run: dart pub global activate melos + version: ${{ steps.get_version.outputs.VERSION }} - - name: Get dependencies - run: melos bootstrap - - - name: Update pubspec version - run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - sed -i '' "s/^version:.*/version: $VERSION/" app/pubspec.yaml - echo "Updated pubspec.yaml version to: $VERSION" - - # ── Code Signing Setup ── - name: Import signing certificate if: env.MACOS_CERTIFICATE_P12 != '' run: | @@ -104,7 +78,6 @@ jobs: echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" echo "Certificate imported successfully" - # ── Build (universal: arm64 + x86_64) ── - name: Refresh CocoaPods lock run: | rm -f app/macos/Podfile.lock @@ -129,7 +102,6 @@ jobs: fi echo "Universal binary verified (x86_64 + arm64)" - # ── Code Sign the .app ── - name: Sign application if: env.MACOS_CERTIFICATE_P12 != '' run: | @@ -154,7 +126,6 @@ jobs: codesign --deep --force --sign - "$APP_PATH" echo "Ad-hoc signed (unsigned build)" - # ── Create DMG ── - name: Install create-dmg run: brew install create-dmg @@ -187,7 +158,6 @@ jobs: echo "DMG created: $DMG_NAME ($(du -h "app/dist/$DMG_NAME" | cut -f1))" - # ── Sign & Notarize DMG ── - name: Sign DMG if: env.MACOS_CERTIFICATE_P12 != '' run: | @@ -220,16 +190,13 @@ 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: app/dist/*.dmg retention-days: 5 - diff --git a/.github/workflows/release-win.yml b/.github/workflows/release-win.yml index 1457cd2..55824d2 100644 --- a/.github/workflows/release-win.yml +++ b/.github/workflows/release-win.yml @@ -27,33 +27,28 @@ 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 + shell: bash + env: + VERSION: ${{ inputs.version }} 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 + set -euo pipefail + IS_PRERELEASE="false" + if [[ "$VERSION" == *-* ]]; then + IS_PRERELEASE="true" + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "IS_PRERELEASE=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION PreRelease: $IS_PRERELEASE" + + - uses: ./.github/actions/setup-flutter-workspace with: - channel: stable + version: ${{ steps.get_version.outputs.VERSION }} - name: Install Fastforge run: dart pub global activate fastforge @@ -61,20 +56,6 @@ jobs: - name: Install Inno Setup run: choco install innosetup --no-progress - - name: Install Melos - run: dart pub global activate melos - - - name: Get dependencies - run: melos bootstrap - - - name: Update pubspec version from tag - shell: pwsh - run: | - $version = "${{ steps.get_version.outputs.VERSION }}" - $pubspec = "app/pubspec.yaml" - (Get-Content $pubspec) -replace '^version:\s+.*', "version: $version" | Set-Content $pubspec - Write-Host "Updated pubspec.yaml version to: $version" - - name: Build standalone (exe installer) shell: pwsh run: | @@ -117,12 +98,14 @@ jobs: $version = "${{ steps.get_version.outputs.VERSION }}" $distDir = "app/dist" $setup = Get-ChildItem -Path $distDir -Recurse -Filter "*-setup.exe" | Select-Object -First 1 - if ($setup) { - $newName = "CopyPaste_${version}_x64_Setup.exe" - $newPath = Join-Path $setup.DirectoryName $newName - Move-Item -Path $setup.FullName -Destination $newPath - Write-Host "Renamed to: $newName" + if (-not $setup) { + Write-Host "::error::No installer matching *-setup.exe under $distDir" + Get-ChildItem -Path $distDir -Recurse -File | ForEach-Object { Write-Host $_.FullName } + exit 1 } + $newName = "CopyPaste_${version}_x64_Setup.exe" + Move-Item -Path $setup.FullName -Destination (Join-Path $setup.DirectoryName $newName) + Write-Host "Renamed to: $newName" - name: Build store MSIX if: steps.get_version.outputs.IS_PRERELEASE != 'true' @@ -154,24 +137,22 @@ jobs: if ($found) { break } } - if ($found) { - $newName = "CopyPaste_${version}_x64_store$($found.Extension)" - $newPath = Join-Path $distDir $newName - New-Item -ItemType Directory -Path $distDir -Force | Out-Null - Move-Item -Path $found.FullName -Destination $newPath - Write-Host "Moved $($found.Name) to: $newPath" - } else { - Write-Warning "No MSIX package found" - Write-Host "--- All files under app/dist and app/build ---" + if (-not $found) { + Write-Host "::error::No MSIX package found under app/dist or app/build" @("app/dist", "app/build") | ForEach-Object { if (Test-Path $_) { Get-ChildItem -Path $_ -Recurse -File | ForEach-Object { Write-Host $_.FullName } } } + exit 1 } + $newName = "CopyPaste_${version}_x64_store$($found.Extension)" + New-Item -ItemType Directory -Path $distDir -Force | Out-Null + Move-Item -Path $found.FullName -Destination (Join-Path $distDir $newName) + Write-Host "Moved $($found.Name) to: $newName" + - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-windows path: app/dist/ retention-days: 5 - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38c30db..bd222a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,25 +14,102 @@ on: permissions: contents: write +# Releases publish to repositories shared with other projects; never run two at once. +concurrency: + group: release + cancel-in-progress: false + jobs: extract-version: runs-on: ubuntu-latest name: Extract Version + timeout-minutes: 5 outputs: version: ${{ steps.get_version.outputs.VERSION }} + is_prerelease: ${{ steps.get_version.outputs.IS_PRERELEASE }} + is_tag: ${{ steps.get_version.outputs.IS_TAG }} steps: - name: Resolve version id: get_version + env: + DISPATCH_VERSION: ${{ github.event.inputs.version }} run: | + set -euo pipefail + IS_TAG="false" if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then - VERSION="${{ github.event.inputs.version }}" + VERSION="$DISPATCH_VERSION" elif [[ "$GITHUB_REF" =~ refs/tags/v(.+) ]]; then VERSION="${BASH_REMATCH[1]}" + IS_TAG="true" else VERSION="2.0.0" fi + + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'$VERSION' is not a valid version (expected MAJOR.MINOR.PATCH[-prerelease])" + exit 1 + fi + + IS_PRERELEASE="false" + if [[ "$VERSION" == *-* ]]; then + IS_PRERELEASE="true" + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "Resolved version: $VERSION" + echo "IS_PRERELEASE=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" + echo "IS_TAG=$IS_TAG" >> "$GITHUB_OUTPUT" + echo "Resolved version: $VERSION (prerelease=$IS_PRERELEASE, tag=$IS_TAG)" + + # Fails now instead of after 45 minutes of builds, or worse, silently unsigned. + - name: Check publishing credentials + if: steps.get_version.outputs.IS_TAG == 'true' + env: + RELEASE_PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} + GIST_TOKEN: ${{ secrets.GIST_TOKEN }} + STORE_APP_ID: ${{ vars.STORE_APP_ID }} + PFX_BASE64: ${{ secrets.PFX_BASE64 }} + PFX_PASSWORD: ${{ secrets.PFX_PASSWORD }} + MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + IS_PRERELEASE: ${{ steps.get_version.outputs.IS_PRERELEASE }} + run: | + set -uo pipefail + status=0 + + require() { + if [[ -z "${!1:-}" ]]; then + echo "::error::$1 is not set; $2" + status=1 + else + echo "ok $1" + fi + } + prefer() { + if [[ -z "${!1:-}" ]]; then + echo "::warning::$1 is not set; $2" + else + echo "ok $1" + fi + } + + require RELEASE_PRIVATE_KEY "release-manifest.json cannot be signed" + require GIST_TOKEN "the Homebrew tap and Scoop bucket cannot be updated" + if [[ "$IS_PRERELEASE" != "true" ]]; then + require STORE_APP_ID "the Microsoft Store submission has no product id" + fi + + prefer PFX_BASE64 "the Windows installer will ship unsigned" + prefer PFX_PASSWORD "the Windows installer will ship unsigned" + prefer MACOS_CERTIFICATE_P12 "the macOS build will be ad-hoc signed and not notarized" + prefer MACOS_CERTIFICATE_PASSWORD "the macOS certificate cannot be imported" + prefer APPLE_ID "the DMG will not be notarized" + prefer APPLE_APP_PASSWORD "the DMG will not be notarized" + prefer APPLE_TEAM_ID "the DMG will not be notarized" + + exit $status build-windows: needs: extract-version @@ -58,7 +135,7 @@ jobs: github-release: runs-on: ubuntu-latest needs: [extract-version, build-windows, build-macos, build-linux] - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + if: needs.extract-version.outputs.is_tag == 'true' timeout-minutes: 10 name: Create GitHub Release @@ -68,7 +145,7 @@ jobs: attestations: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} fetch-depth: 0 @@ -88,19 +165,27 @@ jobs: # attest-build-provenance only fails when *every* pattern comes up # empty, so a single missing artifact would be signed away in silence. - - name: Verify every artifact to attest is present + - name: Verify every release artifact is present + env: + IS_PRERELEASE: ${{ needs.extract-version.outputs.is_prerelease }} run: | set -euo pipefail shopt -s globstar nullglob + patterns=( + 'artifacts/release-windows/**/*_Setup.exe' + 'artifacts/release-macos/*.dmg' + 'artifacts/release-linux/*.AppImage' + 'artifacts/release-linux/*.AppImage.zsync' + 'artifacts/release-linux/*.deb' + 'artifacts/release-linux/*.rpm' + 'artifacts/release-linux/*.tar.gz' + 'artifacts/release-linux/SHA256SUMS' + ) + if [[ "$IS_PRERELEASE" != "true" ]]; then + patterns+=( 'artifacts/release-windows/**/*_store.msix*' ) + fi status=0 - for pattern in \ - 'artifacts/release-windows/**/*_Setup.exe' \ - 'artifacts/release-windows/**/*_store.msix*' \ - 'artifacts/release-macos/*.dmg' \ - 'artifacts/release-linux/*.AppImage' \ - 'artifacts/release-linux/*.deb' \ - 'artifacts/release-linux/*.rpm' \ - 'artifacts/release-linux/*.tar.gz'; do + for pattern in "${patterns[@]}"; do matches=( $pattern ) if (( ${#matches[@]} == 0 )); then echo "::error::No artifact matched '${pattern}'" @@ -127,8 +212,8 @@ jobs: with: body: ${{ steps.tag_message.outputs.TAG_BODY }} generate_release_notes: true - prerelease: ${{ contains(github.ref_name, '-') }} - make_latest: true + prerelease: ${{ needs.extract-version.outputs.is_prerelease == 'true' }} + make_latest: ${{ needs.extract-version.outputs.is_prerelease != 'true' }} files: | artifacts/release-windows/**/*_Setup.exe artifacts/release-windows/**/*_store.msix* @@ -138,18 +223,19 @@ jobs: artifacts/release-linux/*.deb artifacts/release-linux/*.rpm artifacts/release-linux/*.tar.gz + artifacts/release-linux/SHA256SUMS env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-release-manifest: runs-on: ubuntu-latest - needs: github-release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [extract-version, github-release] + if: needs.extract-version.outputs.is_tag == 'true' timeout-minutes: 5 name: Sign and publish release-manifest.json steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref_name }} fetch-depth: 0 @@ -270,16 +356,11 @@ jobs: 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, '-') + if: needs.extract-version.outputs.is_tag == 'true' && needs.extract-version.outputs.is_prerelease != '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 @@ -321,8 +402,8 @@ jobs: update-homebrew-cask: runs-on: ubuntu-latest - needs: github-release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [extract-version, github-release] + if: needs.extract-version.outputs.is_tag == 'true' timeout-minutes: 5 name: Update Homebrew Tap @@ -454,8 +535,8 @@ jobs: update-scoop-bucket: runs-on: ubuntu-latest - needs: github-release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [extract-version, github-release] + if: needs.extract-version.outputs.is_tag == 'true' timeout-minutes: 5 name: Update Scoop Bucket diff --git a/RELEASING.md b/RELEASING.md index 2c10bda..31a5523 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -45,10 +45,6 @@ fans out to: | `publish-to-store` | Submits MSIX to the Microsoft Store (stable tags only, no `-rc`). | | `publish-obs` | Commits rendered `_service`, `.spec` and `debian.tar.xz` to OBS. | -Legacy (during the OBS transition): `release-linux.yml` still publishes to -Cloudsmith so existing users don't break. It will be removed once OBS has -produced two green releases. - ## Release manifest — what the pipeline overrides vs. what you own `release-manifest.json` is versioned in the repo but **most of it is @@ -237,7 +233,6 @@ If a release turns out bad **after** the tag is out: | ----------------------- | ------------------------ | ---------------------------------------- | | `RELEASE_PRIVATE_KEY` | Actions secret | Signs `release-manifest.json`. | | `STORE_APP_ID` | Actions variable | Microsoft Store product ID. | -| `CLOUDSMITH_API_KEY` | Actions secret (legacy) | Publishes deb/rpm to Cloudsmith. | | `OBS_USERNAME` | Actions secret | OBS account for `osc`. | | `OBS_PASSWORD` | Actions secret | OBS password / token for `osc`. | | `GITHUB_TOKEN` | Built-in | Releases, uploads, etc. |