diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml new file mode 100644 index 000000000..f47f025cf --- /dev/null +++ b/.github/workflows/package-and-publish.yml @@ -0,0 +1,295 @@ +# Terms: +# "build" - Compile web project using webpack. +# "package" - Produce a distributive package for a specific platform as a workflow artifact. +# "publish" - Send a package to corresponding store and GitHub release page. +# "release" - build + package + publish +# +# Jobs in this workflow will skip the "publish" step when `SHOULD_PUBLISH` is not set. + +name: Package and publish + +on: + push: + branches: ["master"] + workflow_dispatch: + inputs: + forceRelease: + description: "Force release build even without publishing" + required: false + type: boolean + default: false + +env: + IS_ON_MASTER: ${{ github.ref == 'refs/heads/master' }} + SHOULD_PUBLISH: ${{ github.ref == 'refs/heads/master' && vars.PUBLISH_REPO || '' }} + PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + UPDATER_GIST_URL: ${{ secrets.UPDATER_GIST_URL }} + UPDATER_GIST_ID: ${{ secrets.UPDATER_GIST_ID }} + +jobs: + get-version: + runs-on: ubuntu-latest + outputs: + package-version: ${{ steps.extract-version.outputs.package-version }} + tag-name: ${{ steps.extract-version.outputs.tag-name }} + should-publish: ${{ steps.extract-version.outputs.should-publish }} + release-name: ${{ steps.extract-version.outputs.release-name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Extract version and tag + id: extract-version + run: | + PACKAGE_VERSION=$(grep -m1 '^version' tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + TAG_NAME="air_v${PACKAGE_VERSION}" + RELEASE_NAME="Telegram Air v${PACKAGE_VERSION}" + echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "release-name=$RELEASE_NAME" >> $GITHUB_OUTPUT + echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT + echo "Extracted version: $PACKAGE_VERSION" + echo "Generated tag: $TAG_NAME" + echo "Generated release name: $RELEASE_NAME" + + check-version: + runs-on: ubuntu-latest + needs: get-version + outputs: + should-skip: ${{ steps.check-release.outputs.should-skip }} + steps: + - name: Check if release already exists + id: check-release + env: + PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }} + TAG_NAME: ${{ needs.get-version.outputs.tag-name }} + run: | + # For non-master branches or when publishing is disabled, always continue + if [ -z "$SHOULD_PUBLISH" ]; then + echo "🚧 Publishing disabled (non-master branch or PUBLISH_REPO not set)" + echo "should-skip=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Checking if release already exists for tag: $TAG_NAME" + + RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ + "https://api.github.com/repos/$PUBLISH_REPO/releases/tags/$TAG_NAME") + + if echo "$RESPONSE" | jq -e '.tag_name' > /dev/null; then + IS_DRAFT=$(echo "$RESPONSE" | jq -r '.draft') + if [ "$IS_DRAFT" = "false" ]; then + echo "✅ Published release already exists for version $PACKAGE_VERSION" + echo "should-skip=true" >> $GITHUB_OUTPUT + else + echo "📝 Draft release exists for version $PACKAGE_VERSION, will continue" + echo "should-skip=false" >> $GITHUB_OUTPUT + fi + else + echo "🆕 No release found for version $PACKAGE_VERSION, will create new release" + echo "should-skip=false" >> $GITHUB_OUTPUT + fi + + create-release: + runs-on: ubuntu-latest + needs: [get-version, check-version] + if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true' + outputs: + releaseId: ${{ steps.create-release.outputs.releaseId }} + steps: + - name: Create draft release + id: create-release + env: + PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }} + TAG_NAME: ${{ needs.get-version.outputs.tag-name }} + RELEASE_NAME: ${{ needs.get-version.outputs.release-name }} + run: | + echo "Creating draft release for tag: $TAG_NAME" + echo "Repository: $PUBLISH_REPO" + RESPONSE=$(curl -X POST \ + -H "Authorization: token $GH_TOKEN" \ + -d '{"tag_name": "'"$TAG_NAME"'", "name": "'"$RELEASE_NAME"'", "draft": true}' \ + "https://api.github.com/repos/$PUBLISH_REPO/releases") + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + echo "Extracted Release ID: $RELEASE_ID" + if [ "$RELEASE_ID" = "null" ]; then + echo "Error: Failed to create release. Response was: $RESPONSE" + exit 1 + fi + echo "releaseId=$RELEASE_ID" >> $GITHUB_OUTPUT + + package-tauri: + name: Build, package and publish Tauri + needs: [get-version, check-version, create-release] + if: ${{ !failure() && !cancelled() && needs.check-version.outputs.should-skip != 'true' }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + settings: + - platform: "macos-latest" + args: "--target aarch64-apple-darwin" + - platform: "macos-latest" + args: "--target x86_64-apple-darwin" + - platform: "ubuntu-22.04" + args: "" + runs-on: ${{ matrix.settings.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set Xcode version + if: matrix.settings.platform == 'macos-latest' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Setup Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: "yarn" + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Install Tauri dependencies (ubuntu only) + if: matrix.settings.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Cache node modules + id: yarn-cache + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Extract repository owner and name + id: repository-info + if: needs.get-version.outputs.should-publish != '' + shell: bash + run: | + echo "owner=${PUBLISH_REPO%%/*}" >> $GITHUB_OUTPUT + echo "repo=${PUBLISH_REPO#*/}" >> $GITHUB_OUTPUT + + - name: Define Tauri configuration overrides + id: config-overrides + uses: actions/github-script@v7 + env: + BASE_URL: ${{ vars.BASE_URL }} + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }} + WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }} + with: + script: | + const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/'); + const moduleUrl = `file:///${workspacePath}/deploy/prepareTauriConfig.js`; + const { default: prepareTauriConfig } = await import(moduleUrl) + const config = prepareTauriConfig(); + + const configJson = JSON.stringify(config); + console.log(configJson); + + core.setOutput("json", configJson); + + - name: Build, package and publish + uses: tauri-apps/tauri-action@v0 + id: build-tauri + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + BASE_URL: ${{ vars.BASE_URL }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.UPDATER_PRIVATE_KEY_PASSWORD }} + WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }} + with: + args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}" + includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }} + includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }} + releaseId: ${{ needs.create-release.outputs.releaseId }} + owner: ${{ steps.repository-info.outputs.owner }} + repo: ${{ steps.repository-info.outputs.repo }} + + - name: Get file info + id: file-info + shell: bash + run: | + FULL_PATH=$(echo "${{ fromJSON(steps.build-tauri.outputs.artifactPaths)[0] }}") + FILENAME=$(basename "$FULL_PATH") + NAME="${FILENAME%.*}" + FILE_PATH=$(cd "$(dirname "$FULL_PATH")" && pwd) + ARCHITECTURE=$(echo "${{ matrix.settings.args }}" | grep -oE 'x86_64|aarch64' || echo "") + echo "name=$NAME" >> $GITHUB_OUTPUT + echo "filename=$FILENAME" >> $GITHUB_OUTPUT + echo "architecture=$ARCHITECTURE" >> $GITHUB_OUTPUT + echo "path=$FILE_PATH" >> $GITHUB_OUTPUT + + # MacOS release — use the DMG produced by tauri-action directly + - name: Upload release asset (MacOS) + if: matrix.settings.platform == 'macos-latest' && needs.get-version.outputs.should-publish != '' + shell: bash + run: | + SANITIZED_FILENAME=$(echo "${{ steps.file-info.outputs.name }}" | sed 's/ /./g') + PUBLISH_FILE_NAME="$SANITIZED_FILENAME-${{ steps.file-info.outputs.architecture }}.dmg" + FILE_PATH="${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}" + RELEASE_ID="${{ needs.create-release.outputs.releaseId }}" + curl -X POST -H "Authorization: Bearer $GH_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$FILE_PATH" \ + "https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME" + + - name: Upload artifact (MacOS) + if: matrix.settings.platform == 'macos-latest' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.file-info.outputs.name }}-${{ steps.file-info.outputs.architecture }}.dmg + path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }} + + # Linux release + - name: Upload Linux artifact (Linux) + if: matrix.settings.platform == 'ubuntu-22.04' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.file-info.outputs.filename }} + path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }} + + publish-release: + runs-on: ubuntu-latest + needs: [get-version, check-version, create-release, package-tauri] + if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true' + env: + RELEASE_ID: ${{ needs.create-release.outputs.releaseId }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Publish release + run: | + curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" -d '{"draft": false}' "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID" + + - name: Update Gist with JSON + run: | + ASSET_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets" | jq -r '.[] | select(.name == "latest.json") | .id') + JSON_CONTENT=$(curl -sSL -H "Accept: application/octet-stream" -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID") + GIST_CONTENT=$(jq -n --arg json "$JSON_CONTENT" '{"files":{"updater.json":{"content":$json}}}') + curl -X PATCH -H "Authorization: token $GH_TOKEN" -d "$GIST_CONTENT" "https://api.github.com/gists/$UPDATER_GIST_ID"