<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Thomas Cosialls</title>
    <description>The latest articles on Forem by Thomas Cosialls (@tomtomdu73).</description>
    <link>https://forem.com/tomtomdu73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F465494%2Fb64c7d8e-8469-4d9d-a7a9-b5ca316390b6.jpeg</url>
      <title>Forem: Thomas Cosialls</title>
      <link>https://forem.com/tomtomdu73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tomtomdu73"/>
    <language>en</language>
    <item>
      <title>Expo app : RedirectTo field with Supabase Auth always set to localhost</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:44:48 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/expo-app-redirectto-field-with-supabase-auth-always-set-to-localhost-2mpe</link>
      <guid>https://forem.com/tomtomdu73/expo-app-redirectto-field-with-supabase-auth-always-set-to-localhost-2mpe</guid>
      <description>&lt;p&gt;You are building an Expo Mobile app with Supabase as a backend?&lt;/p&gt;

&lt;p&gt;You have correctly set the redirect url to &lt;strong&gt;exp://&lt;/strong&gt;/auth/callback** in the URL Configuration of Supabase Auth and want to try your flow on Expo Go?&lt;/p&gt;

&lt;p&gt;If you keep seeing localhost as the redirectTo address in the reset password link set by email, here is the fix: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx expo start --tunnel&lt;/code&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Ship Your Tauri v2 App Like a Pro: GitHub Actions and Release Automation (Part 2/2)</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Fri, 20 Feb 2026 05:22:38 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-github-actions-and-release-automation-part-22-2ef7</link>
      <guid>https://forem.com/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-github-actions-and-release-automation-part-22-2ef7</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-code-signing-for-macos-and-windows-part-12-3o9n"&gt;Part 1&lt;/a&gt;, we set up code signing for macOS (Developer ID + notarization) and Windows (Azure Key Vault). We configured &lt;code&gt;tauri.conf.json&lt;/code&gt;, created entitlements, set up relic, and generated updater signing keys.&lt;/p&gt;

&lt;p&gt;Now we wire it all together. In this part, we'll build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;GitHub Actions workflow&lt;/strong&gt; that builds, signs, and publishes your app for macOS and Windows&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;release automation script&lt;/strong&gt; that bumps versions, generates changelogs, and triggers the pipeline with a single command&lt;/li&gt;
&lt;li&gt;The full &lt;strong&gt;end-to-end flow&lt;/strong&gt; from "I want to release v1.0.0" to ".dmg and .exe on GitHub Releases"&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
GitHub Actions Workflow

&lt;ul&gt;
&lt;li&gt;Trigger: Tag-Based Releases&lt;/li&gt;
&lt;li&gt;Build Matrix&lt;/li&gt;
&lt;li&gt;Full Workflow File&lt;/li&gt;
&lt;li&gt;Step-by-Step Breakdown&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Repository Settings

&lt;ul&gt;
&lt;li&gt;Workflow Permissions&lt;/li&gt;
&lt;li&gt;Adding Secrets&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Release Automation Script

&lt;ul&gt;
&lt;li&gt;What It Does&lt;/li&gt;
&lt;li&gt;The Script&lt;/li&gt;
&lt;li&gt;npm Script Entry Point&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The Full Release Flow&lt;/li&gt;

&lt;li&gt;

Verifying Your Release

&lt;ul&gt;
&lt;li&gt;macOS Verification&lt;/li&gt;
&lt;li&gt;Windows Verification&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Troubleshooting&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  GitHub Actions Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Trigger: Tag-Based Releases
&lt;/h3&gt;

&lt;p&gt;The cleanest approach is triggering builds when you push a version tag. No manual workflow dispatching, no branch-based heuristics -- push a &lt;code&gt;v*&lt;/code&gt; tag and the pipeline runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means &lt;code&gt;v0.1.0&lt;/code&gt;, &lt;code&gt;v1.0.0-beta.1&lt;/code&gt;, or any tag starting with &lt;code&gt;v&lt;/code&gt; will trigger the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Matrix
&lt;/h3&gt;

&lt;p&gt;We need three build targets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macos-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aarch64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.dmg&lt;/code&gt; for Apple Silicon Macs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macos-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x86_64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.dmg&lt;/code&gt; for Intel Macs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;windows-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x86_64-pc-windows-msvc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.exe&lt;/code&gt; NSIS installer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not a universal macOS binary?&lt;/strong&gt; Universal binaries (&lt;code&gt;universal-apple-darwin&lt;/code&gt;) combine both architectures into one &lt;code&gt;.dmg&lt;/code&gt;, but they double the file size. Separate builds keep downloads smaller, and GitHub Releases can host both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use &lt;code&gt;fail-fast: false&lt;/code&gt; so one platform failing doesn't cancel the others. If the macOS build fails, you still get the Windows artifact (and vice versa).&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Workflow File
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/release.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v*"&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;aarch64-apple-darwin"&lt;/span&gt;
            &lt;span class="na"&gt;rust_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aarch64-apple-darwin&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;x86_64-apple-darwin"&lt;/span&gt;
            &lt;span class="na"&gt;rust_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x86_64-apple-darwin&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;x86_64-pc-windows-msvc"&lt;/span&gt;
            &lt;span class="na"&gt;rust_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x86_64-pc-windows-msvc&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.platform }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lts/*"&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Rust toolchain&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtolnay/rust-toolchain@stable&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.rust_target }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rust cache&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;swatinem/rust-cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workspaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src-tauri&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install frontend dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Apple signing certificate&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'macOS'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_CERTIFICATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_CERTIFICATE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE_PASSWORD }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;CERT_PATH=$RUNNER_TEMP/certificate.p12&lt;/span&gt;
          &lt;span class="s"&gt;KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain-db&lt;/span&gt;
          &lt;span class="s"&gt;KEYCHAIN_PASSWORD=$(openssl rand -base64 24)&lt;/span&gt;

          &lt;span class="s"&gt;echo "$APPLE_CERTIFICATE" | base64 --decode &amp;gt; "$CERT_PATH"&lt;/span&gt;

          &lt;span class="s"&gt;security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security default-keychain -s "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;security list-keychains -d user -s "$KEYCHAIN_PATH"&lt;/span&gt;

          &lt;span class="s"&gt;echo "Available signing identities:"&lt;/span&gt;
          &lt;span class="s"&gt;security find-identity -v -p codesigning "$KEYCHAIN_PATH"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and release&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tauri-apps/tauri-action@v0&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="c1"&gt;# macOS signing + notarization&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_CERTIFICATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_CERTIFICATE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_SIGNING_IDENTITY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_SIGNING_IDENTITY }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_TEAM_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_TEAM_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_PASSWORD }}&lt;/span&gt;
          &lt;span class="c1"&gt;# Tauri updater signing&lt;/span&gt;
          &lt;span class="na"&gt;TAURI_SIGNING_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}&lt;/span&gt;
          &lt;span class="c1"&gt;# Windows signing (Azure Key Vault via relic)&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_SECRET }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;tagName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v__VERSION__&lt;/span&gt;
          &lt;span class="na"&gt;releaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YourApp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v__VERSION__"&lt;/span&gt;
          &lt;span class="na"&gt;releaseBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;See&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[CHANGELOG](https://github.com/YOUR_USERNAME/YOUR_REPO/blob/main/CHANGELOG.md)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;details."&lt;/span&gt;
          &lt;span class="na"&gt;releaseDraft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;prerelease&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.args }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step-by-Step Breakdown
&lt;/h3&gt;

&lt;p&gt;Let's walk through what each step does and why it's there.&lt;/p&gt;

&lt;h4&gt;
  
  
  Concurrency
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you push a tag, realize there's a typo, delete it, and push a corrected one -- the in-progress build is cancelled. This prevents wasting runner minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Permissions
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Required for &lt;code&gt;tauri-action&lt;/code&gt; to create a GitHub Release and upload artifacts. Without this, you'll get a &lt;code&gt;"Resource not accessible by integration"&lt;/code&gt; error.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checkout
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard checkout. Nothing special here.&lt;/p&gt;

&lt;h4&gt;
  
  
  Node.js + npm
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node.js&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lts/*"&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uses the latest LTS version and caches &lt;code&gt;node_modules&lt;/code&gt; based on &lt;code&gt;package-lock.json&lt;/code&gt;. The frontend build runs via &lt;code&gt;beforeBuildCommand&lt;/code&gt; in your Tauri config.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rust Toolchain
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Rust toolchain&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtolnay/rust-toolchain@stable&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.rust_target }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installs the stable Rust toolchain with the specific cross-compilation target. For macOS runners, this might be &lt;code&gt;aarch64-apple-darwin&lt;/code&gt; (ARM) or &lt;code&gt;x86_64-apple-darwin&lt;/code&gt; (Intel).&lt;/p&gt;

&lt;h4&gt;
  
  
  Rust Cache
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rust cache&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;swatinem/rust-cache@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workspaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src-tauri&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caches the &lt;code&gt;target/&lt;/code&gt; directory between runs. Without this, Rust compilation takes 5-15 minutes per build. With caching, subsequent builds are much faster.&lt;/p&gt;

&lt;h4&gt;
  
  
  Frontend Dependencies
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install frontend dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm ci&lt;/code&gt; (not &lt;code&gt;npm install&lt;/code&gt;) does a clean install from the lockfile. This ensures reproducible builds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Apple Certificate Import (macOS only)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Apple signing certificate&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'macOS'&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_CERTIFICATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_CERTIFICATE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE_PASSWORD }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;CERT_PATH=$RUNNER_TEMP/certificate.p12&lt;/span&gt;
    &lt;span class="s"&gt;KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain-db&lt;/span&gt;
    &lt;span class="s"&gt;KEYCHAIN_PASSWORD=$(openssl rand -base64 24)&lt;/span&gt;

    &lt;span class="s"&gt;echo "$APPLE_CERTIFICATE" | base64 --decode &amp;gt; "$CERT_PATH"&lt;/span&gt;

    &lt;span class="s"&gt;security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security default-keychain -s "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;security list-keychains -d user -s "$KEYCHAIN_PATH"&lt;/span&gt;

    &lt;span class="s"&gt;echo "Available signing identities:"&lt;/span&gt;
    &lt;span class="s"&gt;security find-identity -v -p codesigning "$KEYCHAIN_PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most involved step. Here's what each line does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decode the certificate&lt;/strong&gt; -- the base64-encoded &lt;code&gt;.p12&lt;/code&gt; from your secret is written to a temp file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a temporary keychain&lt;/strong&gt; -- GitHub Actions runners have a default keychain, but importing into a fresh one avoids conflicts. The password is randomly generated for this build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set it as the default&lt;/strong&gt; -- so &lt;code&gt;codesign&lt;/code&gt; uses it automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure keychain settings&lt;/strong&gt; -- &lt;code&gt;-lut 21600&lt;/code&gt; sets a 6-hour lock timeout (enough for the longest build)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlock the keychain&lt;/strong&gt; -- so signing doesn't prompt for a password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import the certificate&lt;/strong&gt; -- &lt;code&gt;-A&lt;/code&gt; allows all apps to access it, &lt;code&gt;-t cert -f pkcs12&lt;/code&gt; specifies the format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set partition list&lt;/strong&gt; -- this is the critical step that allows &lt;code&gt;codesign&lt;/code&gt; to access the imported key without a GUI prompt. Without this, signing fails silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register the keychain&lt;/strong&gt; -- adds it to the user's keychain search list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List identities&lt;/strong&gt; -- a debugging aid that prints available signing identities in the build log&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not let &lt;code&gt;tauri-action&lt;/code&gt; handle the certificate import?&lt;/strong&gt; You can -- &lt;code&gt;tauri-action&lt;/code&gt; does support certificate import via its own inputs. But doing it explicitly gives you more control and better error messages when something goes wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Build and Release
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and release&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tauri-apps/tauri-action@v0&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_CERTIFICATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_CERTIFICATE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_CERTIFICATE_PASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_SIGNING_IDENTITY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_SIGNING_IDENTITY }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_TEAM_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_TEAM_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;APPLE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_PASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;TAURI_SIGNING_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_SECRET }}&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tagName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v__VERSION__&lt;/span&gt;
    &lt;span class="na"&gt;releaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YourApp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v__VERSION__"&lt;/span&gt;
    &lt;span class="na"&gt;releaseBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;See&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[CHANGELOG](https://github.com/YOUR_USERNAME/YOUR_REPO/blob/main/CHANGELOG.md)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;details."&lt;/span&gt;
    &lt;span class="na"&gt;releaseDraft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;prerelease&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.args }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tauri-action&lt;/code&gt; does the heavy lifting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;npm run tauri build&lt;/code&gt; with the specified &lt;code&gt;--target&lt;/code&gt; argument&lt;/li&gt;
&lt;li&gt;Tauri compiles the Rust backend, bundles the frontend, and creates the installer&lt;/li&gt;
&lt;li&gt;During bundling, Tauri calls &lt;code&gt;codesign&lt;/code&gt; (macOS) or your &lt;code&gt;signCommand&lt;/code&gt; (Windows) to sign the binaries&lt;/li&gt;
&lt;li&gt;For macOS, Tauri submits the signed app to Apple for notarization and waits for the result&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;createUpdaterArtifacts&lt;/code&gt; is enabled, it generates &lt;code&gt;.sig&lt;/code&gt; files and &lt;code&gt;latest.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates (or updates) a &lt;strong&gt;draft&lt;/strong&gt; GitHub Release for the tag&lt;/li&gt;
&lt;li&gt;Uploads all artifacts to the release&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key &lt;code&gt;with&lt;/code&gt; parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tagName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v__VERSION__&lt;/code&gt; -- the &lt;code&gt;__VERSION__&lt;/code&gt; placeholder is replaced with the version from &lt;code&gt;tauri.conf.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;releaseName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display name for the GitHub Release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;releaseBody&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Release description (we link to the changelog)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;releaseDraft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; creates a draft release -- you review it before publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prerelease&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether to mark it as a prerelease&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;args&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passed directly to &lt;code&gt;tauri build&lt;/code&gt; (e.g., &lt;code&gt;--target aarch64-apple-darwin&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since all three matrix jobs reference the same tag, they all upload to the &lt;strong&gt;same&lt;/strong&gt; draft release. The first job to run creates the release, and subsequent jobs add their artifacts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository Settings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Workflow Permissions
&lt;/h3&gt;

&lt;p&gt;By default, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; has read-only permissions. You need to enable write access:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your repository on GitHub&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settings &amp;gt; Actions &amp;gt; General&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;Workflow permissions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Read and write permissions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Adding Secrets
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions&lt;/strong&gt; and add each secret:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLE_CERTIFICATE_PASSWORD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLE_SIGNING_IDENTITY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLE_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLE_PASSWORD&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Updater:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is automatically provided by GitHub Actions -- you don't need to create it manually.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Release Automation Script
&lt;/h2&gt;

&lt;p&gt;Now that CI is configured, you need a convenient way to trigger it. We'll build a shell script that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bumps the version in all three files (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;tauri.conf.json&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Regenerates &lt;code&gt;Cargo.lock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto-generates a changelog from conventional commits&lt;/li&gt;
&lt;li&gt;Creates a git commit and tag&lt;/li&gt;
&lt;li&gt;Tells you to push&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What It Does
&lt;/h3&gt;

&lt;p&gt;The release flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./scripts/release.sh patch
    |
    v
Bumps 0.1.0 -&amp;gt; 0.1.1 in package.json, tauri.conf.json, Cargo.toml
    |
    v
Regenerates Cargo.lock
    |
    v
Parses git log for conventional commits since last tag
    |
    v
Generates changelog entry and inserts into CHANGELOG.md
    |
    v
Creates commit: "chore: release v0.1.1"
    |
    v
Creates tag: v0.1.1
    |
    v
You run: git push origin main --tags
    |
    v
GitHub Actions triggers on the v* tag
    |
    v
Draft release appears with .dmg and .exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Script
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;scripts/release.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Release Script&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./scripts/release.sh [major|minor|patch|x.y.z]&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Automates the full release process:&lt;/span&gt;
&lt;span class="c"&gt;#   1. Bumps version in package.json, tauri.conf.json, Cargo.toml&lt;/span&gt;
&lt;span class="c"&gt;#   2. Regenerates Cargo.lock&lt;/span&gt;
&lt;span class="c"&gt;#   3. Generates changelog from conventional commits since last tag&lt;/span&gt;
&lt;span class="c"&gt;#   4. Creates a release commit and git tag&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# The CI release workflow (.github/workflows/release.yml) triggers on tag push&lt;/span&gt;
&lt;span class="c"&gt;# and builds cross-platform binaries automatically.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;31m'&lt;/span&gt;
&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;32m'&lt;/span&gt;
&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[1;33m'&lt;/span&gt;
&lt;span class="nv"&gt;BLUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;34m'&lt;/span&gt;
&lt;span class="nv"&gt;DIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[2m'&lt;/span&gt;
&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0m'&lt;/span&gt;

step&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\n%b==&amp;gt;%b %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BLUE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
ok&lt;span class="o"&gt;()&lt;/span&gt;   &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'  %bok%b %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
fail&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'  %berror%b %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ROOT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Check required tools&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;cmd &lt;span class="k"&gt;in &lt;/span&gt;node cargo git&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"Required tool not found: &lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Parse arguments&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage: ./scripts/release.sh [major|minor|patch|x.y.z]"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Examples:"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ./scripts/release.sh patch   # 0.1.8 -&amp;gt; 0.1.9"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ./scripts/release.sh minor   # 0.1.8 -&amp;gt; 0.2.0"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ./scripts/release.sh major   # 0.1.8 -&amp;gt; 1.0.0"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ./scripts/release.sh 0.2.0   # explicit version"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Read current version from package.json&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="nv"&gt;CURRENT_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"require('./package.json').version"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; MAJOR MINOR PATCH &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;major&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;MAJOR &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;.0.0"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    minor&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;MINOR &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;.0"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    patch&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MINOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;PATCH &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[0-9]+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;0-9]+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;0-9]+&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            &lt;/span&gt;fail &lt;span class="s2"&gt;"Invalid version format. Use x.y.z (e.g., 1.2.3)"&lt;/span&gt;
        &lt;span class="k"&gt;fi
        &lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%bRelease%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BLUE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'  current : %b%s%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'  next    : %b%s%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Pre-flight checks&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

step &lt;span class="s2"&gt;"Running pre-flight checks"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;fail &lt;span class="s2"&gt;"Uncommitted changes detected. Commit or stash them first."&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;ok &lt;span class="s2"&gt;"Working tree clean"&lt;/span&gt;

&lt;span class="nv"&gt;CURRENT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'  %bwarning%b You are on branch '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'%s'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;', not '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'.\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$YELLOW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"  Continue anyway? (y/n) "&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="nt"&gt;-r&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$REPLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Aborted."&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="k"&gt;fi
else
    &lt;/span&gt;ok &lt;span class="s2"&gt;"On branch main"&lt;/span&gt;
&lt;span class="k"&gt;fi

if &lt;/span&gt;git tag | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;fail &lt;span class="s2"&gt;"Tag v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; already exists."&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;ok &lt;span class="s2"&gt;"Tag v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is available"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Confirm&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Proceed with release v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;? (y/n) "&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$REPLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Aborted."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Bump versions&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

step &lt;span class="s2"&gt;"Updating version numbers"&lt;/span&gt;

node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"
const fs = require('fs');
const version = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';

// package.json
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
pkg.version = version;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');

// src-tauri/tauri.conf.json
const conf = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json', 'utf-8'));
conf.version = version;
fs.writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(conf, null, 2) + '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');

// src-tauri/Cargo.toml
const cargo = fs.readFileSync('src-tauri/Cargo.toml', 'utf-8');
fs.writeFileSync('src-tauri/Cargo.toml',
    cargo.replace(/^(version&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;[^&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]*&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/m, &lt;/span&gt;&lt;span class="se"&gt;\`\\\$&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="se"&gt;\"\$&lt;/span&gt;&lt;span class="s2"&gt;{version}&lt;/span&gt;&lt;span class="se"&gt;\"\`&lt;/span&gt;&lt;span class="s2"&gt;)
);
"&lt;/span&gt;
ok &lt;span class="s2"&gt;"package.json"&lt;/span&gt;
ok &lt;span class="s2"&gt;"src-tauri/tauri.conf.json"&lt;/span&gt;
ok &lt;span class="s2"&gt;"src-tauri/Cargo.toml"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Regenerate Cargo.lock&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

step &lt;span class="s2"&gt;"Regenerating Cargo.lock"&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo generate-lockfile &lt;span class="nt"&gt;--quiet&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
ok &lt;span class="s2"&gt;"Cargo.lock"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Generate changelog entry and insert into CHANGELOG.md&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

step &lt;span class="s2"&gt;"Generating changelog"&lt;/span&gt;

&lt;span class="nv"&gt;LAST_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git describe &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nt"&gt;--abbrev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;RANGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LAST_TAG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..HEAD"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;RANGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"HEAD"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;CHANGELOG_ENTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"
const { execSync } = require('child_process');
const fs = require('fs');

const range = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RANGE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';
const version = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';
const today = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';

// Parse conventional commits
const log = execSync(
    'git log ' + range + ' --pretty=format:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --reverse',
    { encoding: 'utf-8' }
);
const lines = log.split('&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;').filter(Boolean);

const added = [];
const fixed = [];
const changed = [];

const pattern = /^(feat|fix|refactor|perf|build|style|docs|test|chore)(&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;.*?&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="s2"&gt;)?&lt;/span&gt;&lt;span class="se"&gt;\!&lt;/span&gt;&lt;span class="s2"&gt;?:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;(.+)&lt;/span&gt;&lt;span class="nv"&gt;$/&lt;/span&gt;&lt;span class="s2"&gt;;

for (const line of lines) {
    const m = line.match(pattern);
    if (!m) continue;
    const [, type, scope, msg] = m;
    const entry = scope
        ? '**' + scope.slice(1, -1) + '**: ' + msg
        : msg;
    switch (type) {
        case 'feat': added.push(entry); break;
        case 'fix': fixed.push(entry); break;
        case 'refactor': case 'perf': case 'style': changed.push(entry); break;
    }
}

// Build changelog section
let entry = '## [' + version + '] - ' + today;
let hasContent = false;

if (added.length) {
    entry += '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Added&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;' + added.map(e =&amp;gt; '- ' + e).join('&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');
    hasContent = true;
}
if (fixed.length) {
    entry += '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Fixed&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;' + fixed.map(e =&amp;gt; '- ' + e).join('&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');
    hasContent = true;
}
if (changed.length) {
    entry += '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Changed&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;' + changed.map(e =&amp;gt; '- ' + e).join('&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');
    hasContent = true;
}
if (!hasContent) {
    entry += '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Maintenance release.';
}

// Insert into CHANGELOG.md
const changelog = fs.readFileSync('CHANGELOG.md', 'utf-8');
const marker = '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;## [';
const idx = changelog.indexOf(marker);

if (idx !== -1) {
    const before = changelog.slice(0, idx);
    const after = changelog.slice(idx);
    fs.writeFileSync('CHANGELOG.md', before + '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;' + entry + '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;' + after);
} else {
    fs.writeFileSync('CHANGELOG.md', changelog.trimEnd() + '&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;' + entry + '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');
}

// Output entry for preview
process.stdout.write(entry);
"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

ok &lt;span class="s2"&gt;"CHANGELOG.md"&lt;/span&gt;

&lt;span class="c"&gt;# Show the generated changelog for review&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%b--- changelog preview ---%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGELOG_ENTRY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%b--- end preview ---%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Does the changelog look good? (y/n) "&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$REPLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Edit CHANGELOG.md manually, then run:"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  git add package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json CHANGELOG.md"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  git commit -m &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;chore: release v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  git tag -a v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -m &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Release v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  git push origin main --tags"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Git commit and tag&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

step &lt;span class="s2"&gt;"Creating release commit"&lt;/span&gt;

git add package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json CHANGELOG.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: release v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ok &lt;span class="s2"&gt;"Committed"&lt;/span&gt;

step &lt;span class="s2"&gt;"Creating tag v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ok &lt;span class="s2"&gt;"Tagged"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Done&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%b========================================%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%b  Released v%s%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%b========================================%b\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Next steps:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  1. Review the commit:  git show HEAD"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  2. Push to trigger CI: git push origin main --tags"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  CI will build cross-platform binaries and create a draft GitHub release."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"To undo this release:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  git tag -d v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; git reset --soft HEAD~1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x scripts/release.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Script Works
&lt;/h3&gt;

&lt;p&gt;Let's break down the key parts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Version Bumping
&lt;/h4&gt;

&lt;p&gt;The script updates version strings in three files simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt; -- the canonical version source, also used by &lt;code&gt;npm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src-tauri/tauri.conf.json&lt;/code&gt;&lt;/strong&gt; -- Tauri reads this for the app version. The &lt;code&gt;__VERSION__&lt;/code&gt; placeholder in your workflow's &lt;code&gt;tagName: v__VERSION__&lt;/code&gt; is resolved from here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src-tauri/Cargo.toml&lt;/code&gt;&lt;/strong&gt; -- the Rust crate version. Must match for consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three must stay in sync. The script uses a single &lt;code&gt;node -e&lt;/code&gt; invocation to update all of them atomically.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cargo.lock Regeneration
&lt;/h4&gt;

&lt;p&gt;After bumping &lt;code&gt;Cargo.toml&lt;/code&gt;, the lockfile is out of date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo generate-lockfile &lt;span class="nt"&gt;--quiet&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This regenerates it without compiling anything. If you skip this step, CI will see a lockfile mismatch and may produce unexpected results.&lt;/p&gt;

&lt;h4&gt;
  
  
  Changelog Generation
&lt;/h4&gt;

&lt;p&gt;The script parses your git history using &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Changelog Section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;refactor:&lt;/code&gt;, &lt;code&gt;perf:&lt;/code&gt;, &lt;code&gt;style:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;chore:&lt;/code&gt;, &lt;code&gt;docs:&lt;/code&gt;, &lt;code&gt;test:&lt;/code&gt;, &lt;code&gt;build:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scoped commits like &lt;code&gt;feat(ui): add dark mode&lt;/code&gt; become &lt;code&gt;**ui**: add dark mode&lt;/code&gt; in the changelog.&lt;/p&gt;

&lt;p&gt;The generated entry is inserted into &lt;code&gt;CHANGELOG.md&lt;/code&gt; before the previous version, maintaining the reverse-chronological order expected by &lt;a href="https://keepachangelog.com/" rel="noopener noreferrer"&gt;Keep a Changelog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You get a preview and a chance to bail out and edit manually before the commit is created.&lt;/p&gt;

&lt;h4&gt;
  
  
  Safe Pre-flight Checks
&lt;/h4&gt;

&lt;p&gt;Before doing anything destructive, the script verifies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Working tree is clean&lt;/strong&gt; -- no uncommitted changes that would be included accidentally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch check&lt;/strong&gt; -- warns if you're not on &lt;code&gt;main&lt;/code&gt; (you might be on a feature branch by accident)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag availability&lt;/strong&gt; -- confirms the target tag doesn't already exist&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  The Undo Escape Hatch
&lt;/h4&gt;

&lt;p&gt;If something went wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag &lt;span class="nt"&gt;-d&lt;/span&gt; v0.1.1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git reset &lt;span class="nt"&gt;--soft&lt;/span&gt; HEAD~1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deletes the local tag and undoes the commit while keeping your changes staged. Clean recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  npm Script Entry Point
&lt;/h3&gt;

&lt;p&gt;Add this to your &lt;code&gt;package.json&lt;/code&gt; for convenience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"release:new"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash scripts/release.sh"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; patch    &lt;span class="c"&gt;# 0.1.0 -&amp;gt; 0.1.1&lt;/span&gt;
npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; minor    &lt;span class="c"&gt;# 0.1.0 -&amp;gt; 0.2.0&lt;/span&gt;
npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; major    &lt;span class="c"&gt;# 0.1.0 -&amp;gt; 1.0.0&lt;/span&gt;
npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; 2.0.0    &lt;span class="c"&gt;# explicit version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Full Release Flow
&lt;/h2&gt;

&lt;p&gt;Here's the complete end-to-end process:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepare
&lt;/h3&gt;

&lt;p&gt;Make sure all your changes are committed and pushed. The release script requires a clean working tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status  &lt;span class="c"&gt;# Should be clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Run the Release Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; patch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Release
  current : 0.2.0
  next    : 0.3.0

==&amp;gt; Running pre-flight checks
  ok Working tree clean
  ok On branch main
  ok Tag v0.3.0 is available
Proceed with release v0.3.0? (y/n) y

==&amp;gt; Updating version numbers
  ok package.json
  ok src-tauri/tauri.conf.json
  ok src-tauri/Cargo.toml

==&amp;gt; Regenerating Cargo.lock
  ok Cargo.lock

==&amp;gt; Generating changelog
  ok CHANGELOG.md

--- changelog preview ---
## [0.3.0] - 2026-02-19

### Added

- **ui**: add dark mode toggle
- **i18n**: add Japanese translations

### Fixed

- **auth**: fix PIN validation edge case
--- end preview ---

Does the changelog look good? (y/n) y

==&amp;gt; Creating release commit
  ok Committed

==&amp;gt; Creating tag v0.3.0
  ok Tagged

========================================
  Released v0.3.0
========================================

Next steps:
  1. Review the commit:  git show HEAD
  2. Push to trigger CI: git push origin main --tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Review and Push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show HEAD         &lt;span class="c"&gt;# Review the release commit&lt;/span&gt;
git push origin main &lt;span class="nt"&gt;--tags&lt;/span&gt;  &lt;span class="c"&gt;# Trigger CI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Monitor the Build
&lt;/h3&gt;

&lt;p&gt;Go to your repository's &lt;strong&gt;Actions&lt;/strong&gt; tab. You'll see three jobs running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;release (macos-latest, aarch64-apple-darwin)&lt;/code&gt; -- ~10-15 min&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release (macos-latest, x86_64-apple-darwin)&lt;/code&gt; -- ~10-15 min&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release (windows-latest, x86_64-pc-windows-msvc)&lt;/code&gt; -- ~8-12 min&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The macOS builds take longer because of the notarization step -- Tauri uploads the signed app to Apple and waits for the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Review the Draft Release
&lt;/h3&gt;

&lt;p&gt;Once all three jobs complete, go to &lt;strong&gt;Releases&lt;/strong&gt; in your repository. You'll find a &lt;strong&gt;draft&lt;/strong&gt; release with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;YourApp_0.3.0_aarch64.dmg&lt;/code&gt; -- macOS Apple Silicon installer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YourApp_0.3.0_x64.dmg&lt;/code&gt; -- macOS Intel installer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YourApp_0.3.0_x64-setup.exe&lt;/code&gt; -- Windows installer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YourApp_0.3.0_x64-setup.nsis.zip&lt;/code&gt; -- Windows NSIS archive&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latest.json&lt;/code&gt; -- Updater manifest&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.sig&lt;/code&gt; files -- Updater signatures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Edit the release notes if needed, then click &lt;strong&gt;Publish release&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying Your Release
&lt;/h2&gt;

&lt;h3&gt;
  
  
  macOS Verification
&lt;/h3&gt;

&lt;p&gt;Download the &lt;code&gt;.dmg&lt;/code&gt; and check that the app is properly signed and notarized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check code signing&lt;/span&gt;
codesign &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--strict&lt;/span&gt; /Applications/YourApp.app

&lt;span class="c"&gt;# Check notarization&lt;/span&gt;
spctl &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; /Applications/YourApp.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;spctl&lt;/code&gt; command should output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Applications/YourApp.app: accepted
source=Notarized Developer ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;"rejected"&lt;/code&gt;, the notarization failed. Check the CI build logs for errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows Verification
&lt;/h3&gt;

&lt;p&gt;On Windows, right-click the &lt;code&gt;.exe&lt;/code&gt; installer &amp;gt; &lt;strong&gt;Properties &amp;gt; Digital Signatures&lt;/strong&gt; tab. You should see your certificate listed.&lt;/p&gt;

&lt;p&gt;If SmartScreen still shows a warning, your certificate is new and hasn't built reputation yet. This improves over time as more users install your app. You can accelerate this by submitting your binary to &lt;a href="https://www.microsoft.com/en-us/wdsi/filesubmission" rel="noopener noreferrer"&gt;Microsoft's file submission portal&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Resource not accessible by integration"
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; doesn't have write permissions. Go to &lt;strong&gt;Settings &amp;gt; Actions &amp;gt; General &amp;gt; Workflow permissions&lt;/strong&gt; and enable &lt;strong&gt;Read and write permissions&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS: "No signing identity found"
&lt;/h3&gt;

&lt;p&gt;The certificate wasn't imported correctly. Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt; secret doesn't contain valid base64&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APPLE_CERTIFICATE_PASSWORD&lt;/code&gt; is wrong&lt;/li&gt;
&lt;li&gt;The certificate has expired&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the "Import Apple signing certificate" step output -- it prints available identities. If the list is empty, the import failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS: Notarization fails with "invalid credentials"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Verify &lt;code&gt;APPLE_ID&lt;/code&gt; is your Apple account email&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;APPLE_PASSWORD&lt;/code&gt; is an &lt;strong&gt;app-specific password&lt;/strong&gt;, not your Apple ID password&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;APPLE_TEAM_ID&lt;/code&gt; is correct (check your &lt;a href="https://developer.apple.com/account" rel="noopener noreferrer"&gt;membership page&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Make sure 2FA is enabled on your Apple ID (required for app-specific passwords)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Windows: "relic: command not found"
&lt;/h3&gt;

&lt;p&gt;Relic needs to be installed on the Windows runner. Add this step before the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install relic&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'Windows'&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go install github.com/sassoftware/relic/v8@latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go is pre-installed on GitHub Actions Windows runners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows: Azure Key Vault 403 / access denied
&lt;/h3&gt;

&lt;p&gt;Check that your App Registration has both required roles on the Key Vault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key Vault Certificate User&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key Vault Crypto User&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also verify that the &lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt; hasn't expired.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build succeeds but no artifacts on the release
&lt;/h3&gt;

&lt;p&gt;This usually means the &lt;code&gt;tagName&lt;/code&gt; doesn't match. The &lt;code&gt;v__VERSION__&lt;/code&gt; placeholder is replaced with the version from &lt;code&gt;tauri.conf.json&lt;/code&gt;. If you tagged &lt;code&gt;v0.3.0&lt;/code&gt; but &lt;code&gt;tauri.conf.json&lt;/code&gt; says &lt;code&gt;0.2.0&lt;/code&gt;, the action creates a release for &lt;code&gt;v0.2.0&lt;/code&gt; (which doesn't match your tag) and things get confused.&lt;/p&gt;

&lt;p&gt;Always use the release script to keep versions in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow builds
&lt;/h3&gt;

&lt;p&gt;First builds are the slowest because there's no Rust compilation cache. The &lt;code&gt;swatinem/rust-cache@v2&lt;/code&gt; action caches the &lt;code&gt;target/&lt;/code&gt; directory, so subsequent builds should be significantly faster.&lt;/p&gt;

&lt;p&gt;For macOS, notarization adds 2-5 minutes per build. There's no way around this -- Apple needs time to scan your binary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Here's what we've set up across both parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;macOS code signing&lt;/strong&gt; with a Developer ID Application certificate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS notarization&lt;/strong&gt; via Apple ID + app-specific password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows code signing&lt;/strong&gt; via Azure Key Vault + relic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tauri updater signing&lt;/strong&gt; for secure in-app updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions workflow&lt;/strong&gt; that builds for 3 targets (macOS ARM, macOS Intel, Windows x64)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release automation script&lt;/strong&gt; that bumps versions, generates changelogs, and creates tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft releases&lt;/strong&gt; for review before publishing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: run &lt;code&gt;npm run release:new -- patch&lt;/code&gt;, push, wait ~15 minutes, review the draft, and publish. Your users get signed, notarized, verified installers on every platform.&lt;/p&gt;

&lt;p&gt;The entire pipeline shown here is used in production by &lt;a href="https://givemefortuna.com" rel="noopener noreferrer"&gt;Fortuna&lt;/a&gt;, an open-source personal wealth management app built with Tauri v2. Feel free to browse the &lt;a href="https://github.com/cosiato/Fortuna" rel="noopener noreferrer"&gt;repository&lt;/a&gt; for the full implementation and to participate in the project with a PR if you wish!&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-time setup&lt;/span&gt;
&lt;span class="c"&gt;# 1. Create Apple Developer ID certificate (see Part 1)&lt;/span&gt;
&lt;span class="c"&gt;# 2. Set up Azure Key Vault for Windows signing (see Part 1)&lt;/span&gt;
&lt;span class="c"&gt;# 3. Generate Tauri updater keys:&lt;/span&gt;
npx tauri signer generate &lt;span class="nt"&gt;-w&lt;/span&gt; ~/.tauri/myapp.key
&lt;span class="c"&gt;# 4. Add all 11 secrets to GitHub (see Part 1 summary)&lt;/span&gt;
&lt;span class="c"&gt;# 5. Enable Actions write permissions in repo settings&lt;/span&gt;

&lt;span class="c"&gt;# Every release&lt;/span&gt;
npm run release:new &lt;span class="nt"&gt;--&lt;/span&gt; patch       &lt;span class="c"&gt;# or minor, major, x.y.z&lt;/span&gt;
git push origin main &lt;span class="nt"&gt;--tags&lt;/span&gt;        &lt;span class="c"&gt;# triggers CI&lt;/span&gt;
&lt;span class="c"&gt;# Wait for builds, review draft release, publish&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>automation</category>
      <category>cicd</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Ship Your Tauri v2 App Like a Pro: Code Signing for macOS and Windows (Part 1/2)</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Fri, 20 Feb 2026 05:19:44 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-code-signing-for-macos-and-windows-part-12-3o9n</link>
      <guid>https://forem.com/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-code-signing-for-macos-and-windows-part-12-3o9n</guid>
      <description>&lt;p&gt;You've built a Tauri desktop app. It compiles, it runs, your friends are impressed. Nice! Then you send the &lt;code&gt;.dmg&lt;/code&gt; to someone and macOS says &lt;em&gt;"this app is damaged and can't be opened."&lt;/em&gt; Or Windows SmartScreen blocks the &lt;code&gt;.exe&lt;/code&gt; entirely. Welcome to the world of code signing.&lt;/p&gt;

&lt;p&gt;This two-part guide walks you through the entire release pipeline for a Tauri v2 application -- from code signing certificates to automated cross-platform builds on GitHub Actions. It's based on real-world experience shipping &lt;a href="https://givemefortuna.com" rel="noopener noreferrer"&gt;Fortuna&lt;/a&gt;, an &lt;a href="https://github.com/cosiato/Fortuna" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; offline wealth management desktop app built with Tauri v2, React, and Rust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; (this article) covers code signing setup for macOS and Windows.&lt;br&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; covers GitHub Actions CI/CD, release automation scripts, and the updater.&lt;/p&gt;

&lt;p&gt;By the end, pushing a git tag will automatically build signed &lt;code&gt;.dmg&lt;/code&gt; and &lt;code&gt;.exe&lt;/code&gt; installers and publish them as a GitHub Release.&lt;/p&gt;


&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Project Configuration Baseline&lt;/li&gt;
&lt;li&gt;
macOS Code Signing

&lt;ul&gt;
&lt;li&gt;Apple Developer Account&lt;/li&gt;
&lt;li&gt;Create a Developer ID Certificate&lt;/li&gt;
&lt;li&gt;Find Your Signing Identity&lt;/li&gt;
&lt;li&gt;Create an Entitlements File&lt;/li&gt;
&lt;li&gt;Configure tauri.conf.json for macOS&lt;/li&gt;
&lt;li&gt;Set Up Notarization&lt;/li&gt;
&lt;li&gt;Export Your Certificate for CI&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Windows Code Signing

&lt;ul&gt;
&lt;li&gt;Why Azure Key Vault?&lt;/li&gt;
&lt;li&gt;Create an Azure Account&lt;/li&gt;
&lt;li&gt;Set Up Azure Key Vault&lt;/li&gt;
&lt;li&gt;Create an App Registration&lt;/li&gt;
&lt;li&gt;Assign Permissions&lt;/li&gt;
&lt;li&gt;Install relic&lt;/li&gt;
&lt;li&gt;Create the relic Configuration&lt;/li&gt;
&lt;li&gt;Configure tauri.conf.json for Windows&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Tauri Updater Signing

&lt;ul&gt;
&lt;li&gt;Generate an Update Signing Keypair&lt;/li&gt;
&lt;li&gt;Configure the Updater&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Summary of Secrets&lt;/li&gt;
&lt;li&gt;What's Next&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A working Tauri v2 project that builds locally (&lt;code&gt;npm run tauri build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; (LTS)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rustup.rs/" rel="noopener noreferrer"&gt;Rust&lt;/a&gt; (stable toolchain)&lt;/li&gt;
&lt;li&gt;A macOS machine (required for Apple certificate creation)&lt;/li&gt;
&lt;li&gt;A GitHub repository for your project&lt;/li&gt;
&lt;li&gt;Some patience -- there are a lot of accounts and portals involved&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Project Configuration Baseline
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;src-tauri/tauri.conf.json&lt;/code&gt; should already have the basic bundle configuration. Here's what the relevant skeleton looks like before we add signing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.tauri.app/config/2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.yourcompany.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeDevCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"devUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:1420"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeBuildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"frontendDist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dmg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nsis"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/32x32.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/128x128.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/128x128@2x.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon.icns"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon.ico"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shortDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your app description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;targets&lt;/code&gt; array tells Tauri what to produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"app"&lt;/code&gt; -- the &lt;code&gt;.app&lt;/code&gt; bundle on macOS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"dmg"&lt;/code&gt; -- the macOS disk image installer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"nsis"&lt;/code&gt; -- the Windows &lt;code&gt;.exe&lt;/code&gt; installer (NSIS-based)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll fill in the &lt;code&gt;macOS&lt;/code&gt; and &lt;code&gt;windows&lt;/code&gt; sections as we go.&lt;/p&gt;




&lt;h2&gt;
  
  
  macOS Code Signing
&lt;/h2&gt;

&lt;p&gt;Apple requires apps distributed outside the App Store to be &lt;strong&gt;code signed&lt;/strong&gt; and &lt;strong&gt;notarized&lt;/strong&gt;. Without both, macOS will either show scary warnings or outright refuse to open your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Apple Developer Account
&lt;/h3&gt;

&lt;p&gt;You need a &lt;strong&gt;paid&lt;/strong&gt; Apple Developer account ($99/year) from &lt;a href="https://developer.apple.com" rel="noopener noreferrer"&gt;developer.apple.com&lt;/a&gt;. The free tier lets you develop and test but cannot notarize apps -- meaning users will see the "damaged app" dialog.&lt;/p&gt;

&lt;p&gt;Enroll at: &lt;a href="https://developer.apple.com/programs/enroll/" rel="noopener noreferrer"&gt;developer.apple.com/programs/enroll&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Developer ID Certificate
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;Developer ID Application&lt;/strong&gt; certificate is what you need for apps distributed outside the Mac App Store.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only the Account Holder role can create Developer ID certificates. If you're on a team, the account holder needs to do this step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On your Mac, open &lt;strong&gt;Keychain Access&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Keychain Access &amp;gt; Certificate Assistant &amp;gt; Request a Certificate From a Certificate Authority&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your email, leave CA Email blank, select &lt;strong&gt;Saved to disk&lt;/strong&gt;, and save the &lt;code&gt;.certSigningRequest&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Go to &lt;a href="https://developer.apple.com/account/resources/certificates/list" rel="noopener noreferrer"&gt;Apple Developer &amp;gt; Certificates&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button to create a new certificate&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Developer ID Application&lt;/strong&gt; and click Continue&lt;/li&gt;
&lt;li&gt;Upload your &lt;code&gt;.certSigningRequest&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Download the generated &lt;code&gt;.cer&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Double-click the &lt;code&gt;.cer&lt;/code&gt; file to install it in your keychain (make sure the &lt;strong&gt;login&lt;/strong&gt; keychain is selected)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Find Your Signing Identity
&lt;/h3&gt;

&lt;p&gt;After installing the certificate, find the exact identity string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;security find-identity &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; codesigning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) ABC123DEF456... "Developer ID Application: Your Name (TEAMID)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quoted string is your &lt;strong&gt;signing identity&lt;/strong&gt;. Note it down -- you'll need it for &lt;code&gt;tauri.conf.json&lt;/code&gt; and as a GitHub secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Create an Entitlements File
&lt;/h3&gt;

&lt;p&gt;Tauri apps use a WebView that requires JIT compilation. Create &lt;code&gt;src-tauri/Entitlements.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-jit&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two entitlements are required for the WebView to function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;allow-jit&lt;/code&gt; -- enables Just-In-Time compilation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow-unsigned-executable-memory&lt;/code&gt; -- allows the JavaScript engine to allocate executable memory&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Configure tauri.conf.json for macOS
&lt;/h3&gt;

&lt;p&gt;Add the macOS signing configuration to your &lt;code&gt;bundle&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signingIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Developer ID Application: Your Name (TEAMID)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Entitlements.plist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimumSystemVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dmg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"appPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"applicationFolderPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Field breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;signingIdentity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The identity string from step 3. Can also be set via &lt;code&gt;APPLE_SIGNING_IDENTITY&lt;/code&gt; env var.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entitlements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to the entitlements plist (relative to &lt;code&gt;src-tauri/&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minimumSystemVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum macOS version. &lt;code&gt;"11.0"&lt;/code&gt; (Big Sur) is a reasonable floor for modern apps.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dmg.appPosition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where the app icon sits in the DMG installer window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dmg.applicationFolderPosition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where the "Applications" shortcut sits in the DMG.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The DMG position values control the drag-to-install layout that users see when they open the &lt;code&gt;.dmg&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Set Up Notarization
&lt;/h3&gt;

&lt;p&gt;Notarization is Apple's automated security check. It's separate from code signing -- after Tauri signs your app, it uploads the binary to Apple's servers for analysis. If it passes, Apple staples a "ticket" to your app so macOS trusts it.&lt;/p&gt;

&lt;p&gt;Tauri handles notarization automatically during the build process. You just need to provide credentials via environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Apple ID + App-Specific Password (simpler)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the approach most indie developers use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://appleid.apple.com/" rel="noopener noreferrer"&gt;appleid.apple.com&lt;/a&gt; &amp;gt; Sign-In and Security &amp;gt; App-Specific Passwords&lt;/li&gt;
&lt;li&gt;Generate a new app-specific password (name it something like "Tauri Notarization")&lt;/li&gt;
&lt;li&gt;Find your Team ID at &lt;a href="https://developer.apple.com/account" rel="noopener noreferrer"&gt;developer.apple.com/account&lt;/a&gt; under Membership Details&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The environment variables you'll need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your Apple account email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The app-specific password (NOT your Apple ID password)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your 10-character Team ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Option B: App Store Connect API Key (more secure, recommended for teams)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://appstoreconnect.apple.com/access/integrations" rel="noopener noreferrer"&gt;App Store Connect &amp;gt; Users and Access &amp;gt; Integrations &amp;gt; Keys&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; to create a new key with &lt;strong&gt;Developer&lt;/strong&gt; access&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.p8&lt;/code&gt; private key file (you can only download it once)&lt;/li&gt;
&lt;li&gt;Note the Key ID and Issuer ID&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_API_ISSUER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Issuer ID shown above the keys table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Key ID from the table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_API_KEY_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to the downloaded &lt;code&gt;.p8&lt;/code&gt; key file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This guide uses Option A for simplicity. Either way works with Tauri's built-in notarization.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Export Your Certificate for CI
&lt;/h3&gt;

&lt;p&gt;Your Mac has the certificate in its keychain, but GitHub Actions runners need it too. You'll export it as a base64-encoded &lt;code&gt;.p12&lt;/code&gt; file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Keychain Access&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;My Certificates&lt;/strong&gt; in the left sidebar (under "login" keychain)&lt;/li&gt;
&lt;li&gt;Find your "Developer ID Application" certificate&lt;/li&gt;
&lt;li&gt;Expand it (click the arrow) -- you should see a private key underneath&lt;/li&gt;
&lt;li&gt;Right-click the &lt;strong&gt;private key&lt;/strong&gt; and select &lt;strong&gt;Export&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Save as &lt;code&gt;.p12&lt;/code&gt; format and set a strong password&lt;/li&gt;
&lt;li&gt;Convert to base64:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; certificate.p12 &lt;span class="nt"&gt;-o&lt;/span&gt; certificate-base64.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;certificate-base64.txt&lt;/code&gt; -- this is the value for the &lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt; secret&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Delete the &lt;code&gt;.p12&lt;/code&gt; and &lt;code&gt;certificate-base64.txt&lt;/code&gt; files after you've stored them as GitHub secrets. Never commit these files to your repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;macOS secrets summary:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GitHub Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base64 content of the exported &lt;code&gt;.p12&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_CERTIFICATE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password you set during &lt;code&gt;.p12&lt;/code&gt; export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_SIGNING_IDENTITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;e.g., &lt;code&gt;Developer ID Application: Your Name (TEAMID)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your 10-character Team ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your Apple account email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App-specific password for notarization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Windows Code Signing
&lt;/h2&gt;

&lt;p&gt;Windows code signing prevents SmartScreen from blocking your installer and tells users that your app comes from a verified publisher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Azure Key Vault?
&lt;/h3&gt;

&lt;p&gt;Since June 2023, certificate authorities no longer issue OV (Organization Validation) code signing certificates on exportable files. New certificates must be stored on hardware security modules (HSMs). The most accessible option for indie developers and small teams is &lt;strong&gt;Azure Key Vault&lt;/strong&gt;, which acts as a cloud-based HSM.&lt;/p&gt;

&lt;p&gt;We'll use &lt;a href="https://github.com/sassoftware/relic" rel="noopener noreferrer"&gt;relic&lt;/a&gt;, an open-source signing tool that can authenticate to Azure Key Vault and sign Windows executables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Alternative:&lt;/strong&gt; Azure Trusted Signing is another option, but it requires a more involved setup with Azure Code Signing accounts and profiles. The Key Vault approach is more straightforward.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. Create an Azure Account
&lt;/h3&gt;

&lt;p&gt;Sign up at &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;portal.azure.com&lt;/a&gt;. You'll need an active subscription -- the Pay-As-You-Go plan works fine. Key Vault costs are minimal (a few cents per signing operation).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Set Up Azure Key Vault
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the Azure Portal, search for &lt;strong&gt;Key Vault&lt;/strong&gt; and create one&lt;/li&gt;
&lt;li&gt;Pick a name (e.g., &lt;code&gt;app-signing-tauri&lt;/code&gt;), choose your region, select the &lt;strong&gt;Standard&lt;/strong&gt; pricing tier&lt;/li&gt;
&lt;li&gt;Create the vault&lt;/li&gt;
&lt;li&gt;Navigate to your vault &amp;gt; &lt;strong&gt;Objects &amp;gt; Certificates&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate/Import&lt;/strong&gt; to create a new certificate:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; Generate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Name:&lt;/strong&gt; e.g., &lt;code&gt;your-app-signing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type of CA:&lt;/strong&gt; Self-signed (or integrate with a CA if you have one)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject:&lt;/strong&gt; &lt;code&gt;CN=Your Company Name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validity Period:&lt;/strong&gt; 12 months (or your preference)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Type:&lt;/strong&gt; PKCS #12&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; and wait for it to provision&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on self-signed vs CA-issued:&lt;/strong&gt; A self-signed certificate from Azure Key Vault will still trigger SmartScreen warnings initially. To avoid this entirely, you need an EV (Extended Validation) certificate from a trusted CA stored in Key Vault. For most indie apps, SmartScreen reputation builds over time as more users install your app. You can also manually submit your binary to Microsoft's &lt;a href="https://www.microsoft.com/en-us/wdsi/filesubmission" rel="noopener noreferrer"&gt;file submission portal&lt;/a&gt; to speed this up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Create an App Registration
&lt;/h3&gt;

&lt;p&gt;Azure Key Vault uses Azure Active Directory for authentication. You need an "App Registration" -- essentially a service account.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Azure Portal, go to &lt;strong&gt;Microsoft Entra ID&lt;/strong&gt; (formerly Azure Active Directory)&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;App registrations &amp;gt; New registration&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it (e.g., &lt;code&gt;tauri-code-signing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Leave the redirect URI blank&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Register&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Application (client) ID&lt;/strong&gt; -- this is your &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Directory (tenant) ID&lt;/strong&gt; -- this is your &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Certificates &amp;amp; secrets &amp;gt; Client secrets &amp;gt; New client secret&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set a description and expiration&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt; and immediately copy the &lt;strong&gt;Value&lt;/strong&gt; -- this is your &lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The client secret value is only shown once. Copy it now or you'll need to create a new one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Assign Permissions
&lt;/h3&gt;

&lt;p&gt;Your app registration needs permission to use the Key Vault for signing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your Key Vault in the Azure Portal&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Access control (IAM)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add role assignment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Assign these two roles to your app registration:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key Vault Certificate User&lt;/strong&gt; -- allows reading the certificate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Vault Crypto User&lt;/strong&gt; -- allows signing operations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without both roles, signing will fail with a permissions error.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Install relic
&lt;/h3&gt;

&lt;p&gt;Relic is a Go-based signing tool that bridges Azure Key Vault and the code signing process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/sassoftware/relic/v8@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure &lt;code&gt;$GOPATH/bin&lt;/code&gt; is in your PATH. Verify with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relic &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;On the GitHub Actions Windows runner, Go is pre-installed. You'll install relic as a build step (covered in Part 2).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  6. Create the relic Configuration
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;src-tauri/relic.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;azure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure&lt;/span&gt;

&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;azure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;YOUR_VAULT_NAME&amp;gt;.vault.azure.net/certificates/&amp;lt;YOUR_CERTIFICATE_NAME&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;YOUR_VAULT_NAME&amp;gt;&lt;/code&gt; with your Key Vault name (e.g., &lt;code&gt;app-signing-tauri&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;YOUR_CERTIFICATE_NAME&amp;gt;&lt;/code&gt; with your certificate name (e.g., &lt;code&gt;your-app-signing&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This file is safe to commit -- it contains no secrets, only the vault and certificate identifiers. Authentication happens via environment variables at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Configure tauri.conf.json for Windows
&lt;/h3&gt;

&lt;p&gt;Add the Windows signing configuration to your &lt;code&gt;bundle&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"relic sign --file %1 --key azure --config relic.conf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"webviewInstallMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"downloadBootstrapper"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"installMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"both"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Field breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;signCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom signing command. &lt;code&gt;%1&lt;/code&gt; is replaced by the file path to sign. Tauri calls this for every binary and the installer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webviewInstallMode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How the installer handles the WebView2 runtime. &lt;code&gt;"downloadBootstrapper"&lt;/code&gt; downloads it on demand, keeping your installer small.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nsis.installMode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"both"&lt;/code&gt; means the installer can run as both per-user and per-machine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;signCommand&lt;/code&gt; approach is the most flexible -- Tauri will invoke this command for every file that needs signing, passing the file path as &lt;code&gt;%1&lt;/code&gt;. Relic then authenticates to Azure using the &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, and &lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt; environment variables and signs the file using the Key Vault certificate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows secrets summary:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GitHub Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application (client) ID from App Registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directory (tenant) ID from App Registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client secret value from App Registration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Tauri Updater Signing
&lt;/h2&gt;

&lt;p&gt;If you plan to use Tauri's built-in auto-updater, you need a separate signing keypair. This is unrelated to OS-level code signing -- it's used to verify that update payloads come from you and haven't been tampered with.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Generate an Update Signing Keypair
&lt;/h3&gt;

&lt;p&gt;Run the Tauri CLI to generate a keypair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tauri signer generate &lt;span class="nt"&gt;-w&lt;/span&gt; ~/.tauri/myapp.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.tauri/myapp.key&lt;/code&gt; -- your &lt;strong&gt;private key&lt;/strong&gt; (keep this secret)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.tauri/myapp.key.pub&lt;/code&gt; -- your &lt;strong&gt;public key&lt;/strong&gt; (embed this in your app)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI will prompt you for an optional password. If you set one, you'll need to provide it as &lt;code&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Store the private key securely and never commit it. The public key is safe to embed in your app config.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Configure the Updater
&lt;/h3&gt;

&lt;p&gt;Add the updater plugin configuration to &lt;code&gt;tauri.conf.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createUpdaterArtifacts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updater"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/YOUR_USERNAME/YOUR_REPO/releases/latest/download/latest.json"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PUBLIC_KEY_CONTENT_HERE"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Field breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createUpdaterArtifacts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tells the build to generate the &lt;code&gt;.sig&lt;/code&gt; signature files and &lt;code&gt;latest.json&lt;/code&gt; manifest alongside the installer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpoints&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where your app checks for updates. The GitHub Releases URL is the simplest approach.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pubkey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The content of your &lt;code&gt;.key.pub&lt;/code&gt; file. Used by the app to verify update signatures.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;tauri-action&lt;/code&gt; in your CI workflow will automatically upload the &lt;code&gt;latest.json&lt;/code&gt; file to the GitHub Release, which the updater plugin reads to detect available updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updater secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GitHub Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Content of the private key file (&lt;code&gt;~/.tauri/myapp.key&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password for the key (if you set one)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary of Secrets
&lt;/h2&gt;

&lt;p&gt;Here's the complete list of GitHub secrets you'll need to configure in your repository (Settings &amp;gt; Secrets and variables &amp;gt; Actions):&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS Signing + Notarization
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base64-encoded &lt;code&gt;.p12&lt;/code&gt; certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_CERTIFICATE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password for the &lt;code&gt;.p12&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_SIGNING_IDENTITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;e.g., &lt;code&gt;Developer ID Application: Your Name (TEAMID)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10-character Team ID from Apple Developer portal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apple account email for notarization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App-specific password for notarization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Windows Signing (Azure Key Vault)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App Registration client ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Azure directory tenant ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App Registration client secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Tauri Updater
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private key for update signing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAURI_SIGNING_PRIVATE_KEY_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password for the key (if set)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total: 11 secrets.&lt;/strong&gt; Yes, it's a lot. The good news is you only set them up once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Complete tauri.conf.json
&lt;/h2&gt;

&lt;p&gt;Here's what the full &lt;code&gt;bundle&lt;/code&gt; section looks like with everything configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.tauri.app/config/2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.yourcompany.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeDevCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"devUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:1420"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeBuildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"frontendDist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resizable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"csp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default-src 'self'; style-src 'self' 'unsafe-inline'"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createUpdaterArtifacts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dmg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nsis"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/32x32.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/128x128.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/128x128@2x.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon.icns"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon.ico"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shortDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your app description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signingIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Developer ID Application: Your Name (TEAMID)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Entitlements.plist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimumSystemVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dmg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"appPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"applicationFolderPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"relic sign --file %1 --key azure --config relic.conf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"webviewInstallMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"downloadBootstrapper"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"installMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"both"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updater"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/YOUR_USERNAME/YOUR_REPO/releases/latest/download/latest.json"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PUBLIC_KEY_HERE"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;At this point you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A macOS signing certificate ready for CI&lt;/li&gt;
&lt;li&gt;Azure Key Vault configured for Windows signing&lt;/li&gt;
&lt;li&gt;Updater signing keys generated&lt;/li&gt;
&lt;li&gt;All 11 secrets identified and ready to add to GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we'll wire all of this into a GitHub Actions workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds your app for macOS (ARM + Intel) and Windows&lt;/li&gt;
&lt;li&gt;Signs and notarizes automatically&lt;/li&gt;
&lt;li&gt;Uploads installers to a draft GitHub Release&lt;/li&gt;
&lt;li&gt;Generates updater artifacts for in-app updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll also build a release automation script that bumps versions, generates changelogs, and triggers the whole pipeline with a single command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/tomtomdu73/ship-your-tauri-v2-app-like-a-pro-github-actions-and-release-automation-part-22-2ef7"&gt;Continue to Part 2: GitHub Actions and Release Automation -&amp;gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>rust</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Collecting Fees and Rewards from Orca Whirlpool Positions</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Thu, 27 Nov 2025 05:26:48 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/collecting-fees-and-rewards-from-orca-whirlpool-positions-5af5</link>
      <guid>https://forem.com/tomtomdu73/collecting-fees-and-rewards-from-orca-whirlpool-positions-5af5</guid>
      <description>&lt;p&gt;A quick guide using Anchor and the &lt;code&gt;whirlpool_cpi&lt;/code&gt; crate&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Learn
&lt;/h2&gt;

&lt;p&gt;Orca Whirlpools is a concentrated liquidity AMM on Solana. When you provide liquidity, you earn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trading Fees&lt;/strong&gt;: A share of swaps within your price range (Token A + Token B)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewards&lt;/strong&gt;: Up to 3 incentive tokens configured by the pool operator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide shows how to collect both programmatically using CPI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Critical Step Everyone Misses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You must call &lt;code&gt;update_fees_and_rewards&lt;/code&gt; before collecting, or you will receive 0 tokens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The position account stores fees/rewards as checkpoints. The update instruction calculates what's owed since the last checkpoint and writes it to &lt;code&gt;fee_owed_a&lt;/code&gt;, &lt;code&gt;fee_owed_b&lt;/code&gt;, and &lt;code&gt;reward_infos[].amount_owed&lt;/code&gt;. Without this step, those fields remain at zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CRITICAL: Always call this BEFORE collect_fees or collect_reward&lt;/span&gt;
&lt;span class="nf"&gt;execute_update_fees_and_rewards_cpi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.whirlpool_program&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.whirlpool&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.position&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.tick_array_lower&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.tick_array_upper&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vault_seeds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collecting Fees
&lt;/h2&gt;

&lt;p&gt;After updating, call &lt;code&gt;collect_fees&lt;/code&gt; to receive both pool tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;whirlpool_cpi&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cpi&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collect_fees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collecting Rewards
&lt;/h2&gt;

&lt;p&gt;Rewards use an index-based system (0, 1, or 2). Check &lt;code&gt;whirlpool.rewardInfos[index]&lt;/code&gt; for active rewards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client-side: iterate through active reward slots&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rewardIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rewardIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rewardIndex&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rewardInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pool_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewardInfos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rewardIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;// Skip uninitialized slots&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rewardInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;11111111111111111111111111111111&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Collect this reward using its index, mint, and vault&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectRewardsOrcaPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rewardIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;rewardTokenMint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rewardInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rewardVault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rewardInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// ... other accounts&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always update first&lt;/strong&gt; - Call &lt;code&gt;update_fees_and_rewards&lt;/code&gt; or get 0 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fees are both tokens&lt;/strong&gt; - One call collects Token A and Token B&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewards are per-index&lt;/strong&gt; - Loop through &lt;code&gt;rewardInfos[0..2]&lt;/code&gt; and collect each active one&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;📖 &lt;strong&gt;&lt;a href="https://www.etherwavelabs.com/blog/the-guide-for-collecting-fees-and-rewards-from-orca-whirlpool-positions" rel="noopener noreferrer"&gt;Read the full guide&lt;/a&gt;&lt;/strong&gt; for complete CPI implementations, account structures, and client-side fee/reward quote calculations using the Orca SDK.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>rust</category>
      <category>web3</category>
    </item>
    <item>
      <title>🌐 AI Agents: The Silent Revolution in Your Tech Stack (That’s About to Get Loud)</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Mon, 24 Feb 2025 22:03:26 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/ai-agents-the-silent-revolution-in-your-tech-stack-thats-about-to-get-loud-2e04</link>
      <guid>https://forem.com/tomtomdu73/ai-agents-the-silent-revolution-in-your-tech-stack-thats-about-to-get-loud-2e04</guid>
      <description>&lt;p&gt;&lt;em&gt;Imagine tools that don’t just follow orders—but debate, adapt, and even outthink you. Spoiler: They’re already here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI agents aren’t coming. &lt;strong&gt;They’re colonizing workflows&lt;/strong&gt;, and your business is either riding the wave or drowning in legacy code. Forget clunky bots—these are digital alchemists turning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chaos into strategy&lt;/strong&gt; (Autonomous multi-step planning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data into intuition&lt;/strong&gt; (Context-aware memory that learns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silos into symphonies&lt;/strong&gt; (Seamless tool integration)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;“But wait—can they handle REAL work?”&lt;/em&gt;&lt;br&gt;
Ask the blockchain devs automating 80% of audits. Or the support teams sleeping through peak hours while their AI clones charm customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kicker?&lt;/strong&gt; This isn’t sci-fi—it’s 2024’s open-source reality. By the time you finish this teaser, another startup just deployed an agent that codes better than their interns.&lt;/p&gt;

&lt;p&gt;“Show me the future before I’m obsolete” → &lt;a href="https://www.etherwavelabs.com/blog/the-rise-of-ai-agents-transforming-business-operations-through-intelligent-automation" rel="noopener noreferrer"&gt;Read full article here&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building SpyClub: A Modern Telegram Mini App with Web3 Features 🚀</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Wed, 05 Feb 2025 22:56:10 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/building-spyclub-a-modern-telegram-mini-app-with-web3-features-2jlg</link>
      <guid>https://forem.com/tomtomdu73/building-spyclub-a-modern-telegram-mini-app-with-web3-features-2jlg</guid>
      <description>&lt;p&gt;Ever wondered how to create a blockchain-enabled game that runs smoothly within Telegram? Let's explore how we built SpyClub, a multiplayer word game that combines real-time gaming with crypto rewards!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes SpyClub Special? 🎮
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://t.me/Spyclubgame_bot/game" rel="noopener noreferrer"&gt;SpyClub&lt;/a&gt; isn't your typical word game. Players compete in real-time matches, earning &lt;a href="https://hashscan.io/mainnet/token/0.0.7716866" rel="noopener noreferrer"&gt;$Spycoin&lt;/a&gt; tokens while climbing leaderboards, with top performers receiving USDC rewards. It's where casual gaming meets Web3!&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Breakdown ⚡
&lt;/h2&gt;

&lt;p&gt;Here's our powerful tech stack:&lt;/p&gt;

&lt;p&gt;Frontend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React with Vite for lightning-fast performance&lt;/li&gt;
&lt;li&gt;Zustand for state management&lt;/li&gt;
&lt;li&gt;Tailwind CSS with shadcn/ui for slick UI&lt;/li&gt;
&lt;li&gt;Telegram Mini App SDK for platform integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fastify for high-performance API handling&lt;/li&gt;
&lt;li&gt;Supabase for database and real-time features&lt;/li&gt;
&lt;li&gt;Hedera for blockchain transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features in Action 💫
&lt;/h2&gt;

&lt;p&gt;Let's peek at some real implementation highlights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Game Room Management
export class GameRoom {
  private async startRound(round: number) {
    const wordSet = await this.getNewWordSet()

    this.channel.send({
      type: 'broadcast',
      event: 'game_update',
      payload: {
        type: 'new_round',
        round,
        clue: wordSet.clue,
        words: allWords,
        endTime: roundEndTime,
      },
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-time Magic ✨
&lt;/h2&gt;

&lt;p&gt;The game stays synchronized using Supabase's real-time channels, managing everything from player moves to score updates. We handle blockchain rewards through Hedera, ensuring secure and transparent token distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Tips for Mini App Development 💡
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always validate Telegram authentication data&lt;/li&gt;
&lt;li&gt;Handle real-time state synchronization carefully&lt;/li&gt;
&lt;li&gt;Implement retry mechanisms for blockchain operations&lt;/li&gt;
&lt;li&gt;Optimize for mobile-first experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Architecture Works 🎯
&lt;/h2&gt;

&lt;p&gt;-Scalable multiplayer system&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure blockchain integration&lt;/li&gt;
&lt;li&gt;Smooth user experience&lt;/li&gt;
&lt;li&gt;Real-time performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ready to Build Your Own? 🚀
&lt;/h2&gt;

&lt;p&gt;Whether you're creating a game, a DeFi app, or something entirely new, the patterns we've shared can jumpstart your Telegram Mini App development journey.&lt;/p&gt;

&lt;p&gt;Want to learn more about building blockchain-enabled applications? &lt;a href="https://www.etherwavelabs.com/" rel="noopener noreferrer"&gt;Let's connect&lt;/a&gt; and turn your ideas into reality! &lt;/p&gt;

</description>
      <category>web3</category>
      <category>telegramminiapp</category>
      <category>react</category>
    </item>
    <item>
      <title>Implementing SIWE with WalletConnect's AppKit in Next.js</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Fri, 23 Aug 2024 03:34:11 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/implementing-siwe-with-walletconnects-appkit-in-nextjs-4723</link>
      <guid>https://forem.com/tomtomdu73/implementing-siwe-with-walletconnects-appkit-in-nextjs-4723</guid>
      <description>&lt;p&gt;Web3 authentication is evolving, and Sign In With Ethereum (SIWE) is leading the charge. This article explores how to implement SIWE in a Next.js application using WalletConnect's AppKit, focusing on the "One Click Auth" feature and securing API routes with JWT tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding SIWE and AppKit
&lt;/h2&gt;

&lt;p&gt;Sign In With Ethereum (SIWE) is a standardized authentication method (EIP-4361) that allows users to prove ownership of their Ethereum address through a cryptographic signature. WalletConnect's AppKit simplifies this process with its "One-Click Auth" feature, enhancing both security and user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up&lt;/strong&gt;: Install necessary dependencies:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   yarn add @web3modal/siwe next-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configure SIWE Client&lt;/strong&gt;: Create a SIWE client configuration file to handle message parameters, nonce generation, and session management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set Up API Route&lt;/strong&gt;: Implement a NextAuth handler to manage authentication, verifying signatures and creating sessions using JWT tokens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secure API Routes&lt;/strong&gt;: Use a Next.js middleware to protect specified routes, requiring authentication for access and passing the user's wallet address to API routes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read &lt;a href="https://www.etherwavelabs.com/blog/secure-ethereum-sign-in-with-next-js-using-walletconnect-appkit" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for more details about the steps aforementioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enhanced security through cryptographic signatures&lt;/li&gt;
&lt;li&gt;Improved user experience with one-click authentication&lt;/li&gt;
&lt;li&gt;Seamless integration with Next.js applications&lt;/li&gt;
&lt;li&gt;Robust API route protection using JWT tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By implementing SIWE with WalletConnect's AppKit, developers can create secure, user-friendly decentralized applications that leverage the power of Web3 authentication.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Top 5 Web3 Wallet Solutions with Social Login to Boost User Onboarding in 2024</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Thu, 25 Apr 2024 21:19:04 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/top-5-web3-wallet-solutions-with-social-login-to-boost-user-onboarding-in-2024-3n9a</link>
      <guid>https://forem.com/tomtomdu73/top-5-web3-wallet-solutions-with-social-login-to-boost-user-onboarding-in-2024-3n9a</guid>
      <description>&lt;p&gt;As Web3 technologies continue to evolve, simplifying the user onboarding process is crucial for wider adoption. Traditional methods like social logins and phone number verifications are now being integrated into Web3 platforms. &lt;/p&gt;

&lt;p&gt;This blog post will compare five leading solutions—&lt;strong&gt;Particle Network, Dynamic, Privy, Web3Auth, and Capsule&lt;/strong&gt;—that facilitate user onboarding by using these familiar methods, while also maintaining the security and great user experience (with &lt;a href="https://dev.to/blog/enhancing-web3-ux-practical-steps-for-developers-to-implement-biconomy-s-account-abstraction"&gt;Account Abstraction&lt;/a&gt; and Embedded Wallet). You will find our comparative table at the end of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Privy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F330a595f6fe92759fe7304809b9caf4cf5d06030-600x314.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F330a595f6fe92759fe7304809b9caf4cf5d06030-600x314.jpg%26w%3D828%26q%3D75" alt="privy social login web3 proof2work" width="600" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.privy.io/" rel="noopener noreferrer"&gt;Privy&lt;/a&gt; is a web3 wallet designed to integrate web2 user experience standards into web3 applications. It features streamlined authentication flows, including sign-in options via email and social media, and provides powerful embedded wallets that support self-custody. Privy also includes scalable infrastructure to manage multiple external wallet connections, ensuring flexibility and enhanced user interaction.&lt;/p&gt;

&lt;p&gt;The platform highlights a focus on security, conducting regular audits across infrastructure, cryptography, operational, and application security aspects. Privy has completed several security audits with recognized firms and has a SOC 2 Type II attestation in progress.&lt;/p&gt;

&lt;p&gt;Privy is supported by notable investors such as Paradigm, Sequoia, BlueYard, Electric Capital, Archetype, and Protocol Labs, reflecting significant backing and trust in its vision and technology.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.privy.io/" rel="noopener noreferrer"&gt;See their wallet in action&lt;/a&gt; - &lt;a href="https://docs.privy.io/" rel="noopener noreferrer"&gt;Read developer documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dynamic
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F7bb1e0aab117324f8edaebf0afe12109ac039665-600x395.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F7bb1e0aab117324f8edaebf0afe12109ac039665-600x395.jpg%26w%3D828%26q%3D75" alt="dynamic web3 onboarding proof2work" width="600" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.dynamic.xyz/" rel="noopener noreferrer"&gt;Dynamic&lt;/a&gt; offers a robust web3 login solution that emphasizes ease of use for both users and developers. Features include multi-chain support, embedded wallets, and seamless integration with both EVM and non-EVM blockchains like Solana and Starknet. It supports a passwordless system using Passkeys and plans to introduce TSS-MPC technology for enhanced security. Prominent clients like Pudgy Penguins and Starkware showcase its wide acceptance.&lt;/p&gt;

&lt;p&gt;Dynamic’s development platform is backed by secure, audited technology, ensuring a safe environment for user data and transactions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.dynamic.xyz/" rel="noopener noreferrer"&gt;See their wallet in action&lt;/a&gt; - &lt;a href="https://docs.dynamic.xyz/introduction/welcome" rel="noopener noreferrer"&gt;Read developer documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Web3Auth
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Fe8088e2fd225fa0a8720d77c62bb80035e110f72-600x314.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Fe8088e2fd225fa0a8720d77c62bb80035e110f72-600x314.jpg%26w%3D828%26q%3D75" alt="web3auth social login proof2work" width="600" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://web3auth.io/" rel="noopener noreferrer"&gt;Web3Auth&lt;/a&gt; is a comprehensive Web3 login and wallet service designed to offer a seamless, secure, and decentralized user experience. Key features include Multi-Party Computation (MPC) for secure transaction signing, Account Abstraction for simplified user interactions, and customizable multi-factor authentication.&lt;/p&gt;

&lt;p&gt;Additionally, Web3Auth provides a fiat-to-crypto API, white-labeled wallet UIs, and compatibility across various blockchain ecosystems. The platform is committed to robust security with regular audits and compliance with major data protection regulations. Web3Auth is trusted by significant players in both Web2 and Web3 sectors, demonstrating its scalability and reliability in the digital wallet space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.web3auth.io/" rel="noopener noreferrer"&gt;See their wallet in action&lt;/a&gt; - &lt;a href="https://web3auth.io/docs/index.html" rel="noopener noreferrer"&gt;Read developer documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Particle Network
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Ff2550a53e1feb77cc504678f1826044abd0a1d58-600x352.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Ff2550a53e1feb77cc504678f1826044abd0a1d58-600x352.jpg%26w%3D828%26q%3D75" alt="particle network social login web3 proof2work" width="600" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://particle.network/" rel="noopener noreferrer"&gt;Particle Network&lt;/a&gt; offers a suite of Web3 products designed to enhance user experience and connectivity across blockchain environments. Key features include modular account abstraction compatible with ERC-4337, an embedded wallet for seamless in-dApp transactions, and a one-click user onboarding system compatible with various Web2 interfaces.&lt;/p&gt;

&lt;p&gt;Particle Network extends its functionality to Bitcoin through the BTC Connect service, enabling EVM-compatible operations with Bitcoin wallets.&lt;/p&gt;

&lt;p&gt;Security is prioritized with modular access to secure node services, while investments from influential entities in the tech space back the platform's robust capabilities. Particle's solutions are tailored to developers seeking versatile and secure Web3 integration tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aaparticle.vercel.app/" rel="noopener noreferrer"&gt;See their wallet in action&lt;/a&gt; - &lt;a href="https://developers.particle.network/" rel="noopener noreferrer"&gt;Read developer documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Capsule
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F9f60aeec418bcc5cd99a184c6afc918bc7712516-600x315.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252F9f60aeec418bcc5cd99a184c6afc918bc7712516-600x315.jpg%26w%3D828%26q%3D75" alt="capsule web3 social onboarding proof2work" width="600" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://usecapsule.com/" rel="noopener noreferrer"&gt;Capsule&lt;/a&gt; is a versatile Web3 wallet solution that offers a secure and user-friendly experience across multiple devices and chains. Its features include passwordless social logins, multi-party computation (MPC) for key management, and transaction permission engines to enhance security.&lt;/p&gt;

&lt;p&gt;Capsule also provides customizable wallet UIs and native on-and-off ramps for seamless integration. The wallet supports Ethereum, Polygon, Base, and Celo chains. For developers, Capsule offers easy-to-integrate SDKs compatible with modern Web3 standards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.beta.usecapsule.com/" rel="noopener noreferrer"&gt;See their wallet in action&lt;/a&gt; - &lt;a href="https://docs.usecapsule.com/" rel="noopener noreferrer"&gt;Read developer documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparative Table
&lt;/h2&gt;

&lt;p&gt;The table outlines specific criteria to compare web3 wallet platforms: Supported Chains, Account Abstraction, Security, Social Login, Wallet Login, Email/SMS Login, Embedded Wallets, and Price.&lt;/p&gt;

&lt;p&gt;For an exhaustive list of supported networks, please visit the individual websites of each project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Ffc5fcec0b3000e7bb7c6ce7c0917a47d7209d450-1240x483.jpg%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.proof2work.com%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fcdn.sanity.io%252Fimages%252Fmre6fyj5%252Fproduction%252Ffc5fcec0b3000e7bb7c6ce7c0917a47d7209d450-1240x483.jpg%26w%3D828%26q%3D75" alt="best web3 social login wallets compared 2024 proof2work" width="828" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Choosing the right Web3 onboarding solution depends on your specific needs, such as the type of blockchain network you use, the level of security you require for private keys, and the user authentication methods you prefer.&lt;/p&gt;

&lt;p&gt;By incorporating social logins and phone number integrations into Web3 platforms, these solutions not only enhance security but also significantly lower the barrier for user adoption, fostering a more inclusive and expansive digital economy.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article has been published initially on &lt;a href="https://www.proof2work.com/blog/top-5-web3-wallet-solutions-with-social-login-to-boost-user-onboarding-in-2024" rel="noopener noreferrer"&gt;proof2work.com&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>onboarding</category>
      <category>blockchain</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Setting Up a VPS on DigitalOcean to Build Your Blockchain Nodes</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Tue, 23 Apr 2024 19:10:35 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/setting-up-a-vps-on-digitalocean-to-build-your-blockchain-nodes-4iic</link>
      <guid>https://forem.com/tomtomdu73/setting-up-a-vps-on-digitalocean-to-build-your-blockchain-nodes-4iic</guid>
      <description>&lt;p&gt;Hello, fellow enthusiasts! If you're reading this, you're probably as excited about blockchain as I am. Today, we're going to take a significant step in our blockchain validator journey by setting up a Virtual Private Server (VPS) on DigitalOcean. Don't worry if this sounds daunting; I promise to keep things simple and jargon-free!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DigitalOcean?
&lt;/h2&gt;

&lt;p&gt;DigitalOcean is one of the leading cloud infrastructure providers, known for its simplicity and cost-effectiveness. For blockchain enthusiasts like us, it offers a reliable platform to experiment, learn, and grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy way to start your VPS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Sign Up &amp;amp; Login
&lt;/h3&gt;

&lt;p&gt;First things first, head over to &lt;a href="https://www.digitalocean.com/?refcode=073ad693a308&amp;amp;utm_campaign=Referral_Invite&amp;amp;utm_medium=Referral_Program&amp;amp;utm_source=CopyPaste" rel="noopener noreferrer"&gt;DigitalOcean's website&lt;/a&gt; (you get $200 free credit with this referal link!). If you're new, sign up; otherwise, log in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbn2pgxvwh1mlq7emt4i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbn2pgxvwh1mlq7emt4i.png" alt="login to digitalocean" width="551" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Droplet
&lt;/h3&gt;

&lt;p&gt;In DigitalOcean lingo, a &lt;strong&gt;'Droplet'&lt;/strong&gt; is essentially your VPS. Click on the 'Create' button, followed by 'Droplets'. There, you'll be presented with various configuration options.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Choose Your Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose a data center region&lt;/strong&gt; closest to you (or closest to your end user) for optimal performance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Select an OS:&lt;/strong&gt; I prefer Ubuntu with the latest LTS version, 22.04 at the time of writing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick a server size:&lt;/strong&gt; for our blockchain validator journey, I recommend starting with the Basic plan (Share CPU) with Regular SSD disk. As you grow, you can always scale up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;select vps size digitalocean&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Attach your SSH Keys to the Droplet
&lt;/h3&gt;

&lt;p&gt;You now need to set up SSH keys to securely access your VPS remotely. Sounds like Chinese to you? Don't get scared here, just click "New SSH keys" and carefully follow the instructions given on the right side of the modal panel. You can read more about how to setup your SSH keys &lt;a href="https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Additional Options &amp;amp; Setup
&lt;/h3&gt;

&lt;p&gt;You'll find additional options like backups, monitoring, database or extra volume. I only recommend selecting monitoring (it's free). Finally, create a new project and assign it to your droplet.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Launch &amp;amp; Celebrate!
&lt;/h3&gt;

&lt;p&gt;Click the "Create Droplet" button. In a few moments, your VPS will be up and running. Congratulations!&lt;/p&gt;

&lt;h2&gt;
  
  
  In Conclusion
&lt;/h2&gt;

&lt;p&gt;Setting up a VPS on DigitalOcean is more than just carving out a personal space in the digital realm; it's the foundational step towards building a validator node in the blockchain ecosystem. A validator node is crucial for ensuring the integrity and functionality of a blockchain network.&lt;/p&gt;

&lt;p&gt;By starting with a VPS, you're not just dipping your toes into the blockchain waters; you're gearing up to play a pivotal role in the decentralized future. As you embark on this journey, remember that every validator node begins with a reliable and secure server. So, here's to the first of many steps in your blockchain adventure. Until next time, happy building!&lt;/p&gt;

&lt;p&gt;Liked this article? Read more similar articles on &lt;a href="https://www.proof2work.com/blog/category/build" rel="noopener noreferrer"&gt;www.proof2work.com/blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vps</category>
      <category>server</category>
      <category>blockchain</category>
      <category>nodes</category>
    </item>
    <item>
      <title>Deploy Upgradeable Smart Contracts with Foundry and OpenZeppelin</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Thu, 04 Apr 2024 22:44:52 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/deploy-upgradeable-smart-contracts-with-foundry-and-openzeppelin-2hn1</link>
      <guid>https://forem.com/tomtomdu73/deploy-upgradeable-smart-contracts-with-foundry-and-openzeppelin-2hn1</guid>
      <description>&lt;p&gt;Today, I am gonna teach you how to deploy an upgradeable ERC20 token to Polygon Mumbai with Foundry and OpenZeppelin 🚀&lt;/p&gt;

&lt;p&gt;I will assume you already have your Foundry environment set up and have already written an upgradeable smart contract for your ERC20 token, that we will call &lt;em&gt;MyUpgradeableToken.sol&lt;/em&gt;. If you need help with this, you can &lt;a href="https://www.proof2work.com/blog/deploy-upgradeable-contract-with-foundry-and-openzeppelin" rel="noopener noreferrer"&gt;read my full article&lt;/a&gt; about deploying an upgradeable ERC20 token with Foundry.&lt;/p&gt;

&lt;p&gt;First, set up your environment to using OpenZeppelin's upgradeable contracts and Foundry extensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then modify the &lt;strong&gt;foundry.toml&lt;/strong&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]

[rpc_endpoints]
mumbai = "https://rpc.ankr.com/polygon_mumbai"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can create a new file called &lt;em&gt;01_Deploy.s.sol&lt;/em&gt; in the script directory. Don't forget to add your own &lt;strong&gt;PRIVATE_KEY&lt;/strong&gt; variable in the &lt;em&gt;.env&lt;/em&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Script.sol";
import "../src/MyUpgradeableToken.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract DeployScript is Script {

    function run() external returns (address, address) {
        //we need to declare the sender's private key here to sign the deploy transaction
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy the upgradeable contract
        address _proxyAddress = Upgrades.deployTransparentProxy(
            "MyUpgradeableToken.sol",
            msg.sender,
            abi.encodeCall(MyUpgradeableToken.initialize, (msg.sender))
        );

        // Get the implementation address
        address implementationAddress = Upgrades.getImplementationAddress(
            _proxyAddress
        );

        vm.stopBroadcast();

        return (implementationAddress, _proxyAddress);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you can deploy your smart contract to Polygon Mumbai!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge script script/01_Deploy.s.sol:DeployScript --sender ${YOUR_PUBLIC_KEY} --rpc-url mumbai --broadcast -vvvv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy deployment and stay tuned for more tutorials to build and navigate in web3 ! 🏄‍♂️&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>foundry</category>
      <category>blockchain</category>
      <category>openzeppelin</category>
    </item>
    <item>
      <title>How to easily optimize your SEO when hosting your site with Netlify</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Fri, 23 Jul 2021 21:18:07 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/how-to-easily-optimize-your-seo-when-hosting-your-site-with-netlify-2k1c</link>
      <guid>https://forem.com/tomtomdu73/how-to-easily-optimize-your-seo-when-hosting-your-site-with-netlify-2k1c</guid>
      <description>&lt;p&gt;I love Netlify for hosting my static sites. It's free, fast and the automatic deployments with the Github integration is a game changing. You still need to &lt;strong&gt;pay attention to a few points&lt;/strong&gt; if you want to get the best out of it, especially from a SEO point of view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgei9shtc45cq5vneszem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgei9shtc45cq5vneszem.png" alt="image" width="664" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Static websites made with Gatsby, NextJS or Hugo have everything to boost your SEO score and help you get the golden 100 mark on Lighthouse. &lt;/p&gt;

&lt;p&gt;Indeed, expect to see your speed performances soar, with insanely low FCP (First Contentful Paint) , LCP (Largest Contentful Paint) and CLS (Cumulative Layout Shift) values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9yjfjumgxoi8nl8p5pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9yjfjumgxoi8nl8p5pd.png" alt="image" width="725" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue
&lt;/h2&gt;

&lt;p&gt;However, a great Lighthouse score does not necessarily mean a better ranking and visibility on search engines. Especially if you host your site on Netlify. The reason? With its default settings, Netlify makes every page of your site available as a page on your custom domain and as a page in a subdomain inside &lt;em&gt;.netlify.app&lt;/em&gt;. And God knows how Google for instance does not like duplicate content! &lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Set _redirects file (important!)
&lt;/h3&gt;

&lt;p&gt;First, you need to tell Netlify to redirect your Netlify subdomain to your custom domain. You have two ways to do that: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;em&gt;static&lt;/em&gt; folder in your project root and save inside a &lt;strong&gt;_redirects&lt;/strong&gt; file with the following content, by replacing the site name accordingly.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://[yoursitename].netlify.app/* https://www.[yoursitename].com/:splat 301!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install &lt;a href="https://www.gatsbyjs.com/plugins/gatsby-plugin-netlify/" rel="noopener noreferrer"&gt;gatsby-plugin-netlify&lt;/a&gt; plugin, that will automatically generate the &lt;strong&gt;_redirects&lt;/strong&gt; file for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;rel:"canonical"&lt;/code&gt; on every page
&lt;/h3&gt;

&lt;p&gt;To prevent duplicates and tell Google which page is the original version, make sure to include the following &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag inside the &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt; of every pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link rel="canonical" href="{{ your-base-url }}{{ page-slug }}"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have too many pages on your site, doing it manually can quickly become tedious. I suggest you use &lt;a href="https://www.getgutenberg.io/" rel="noopener noreferrer"&gt;Gutenberg&lt;/a&gt; to automate this process.&lt;/p&gt;

&lt;p&gt;Here you are! Your site won't have duplicate content anymore and you should now stay far away from Google SEO penalties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other tips
&lt;/h2&gt;

&lt;p&gt;I would also suggest you to use the following Netlify plugins and npm packages to boost your static sites performances&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if you used Gatsby: &lt;a href="https://www.gatsbyjs.com/plugins/gatsby-plugin-minify/" rel="noopener noreferrer"&gt;&lt;strong&gt;gatsby-plugin-minify&lt;/strong&gt;&lt;/a&gt;, to minify all output HTML files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Optim&lt;/strong&gt; (Netlify plugin) to compress all your static images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline source&lt;/strong&gt; (Netlify plugin) to inline some sources and assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit sitemap&lt;/strong&gt; (Netlify plugin) to automatically push your updated sitemap to major search engines after each new build. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me - I'd be happy to help!&lt;/p&gt;

</description>
      <category>netlify</category>
      <category>wordpress</category>
      <category>gatsby</category>
    </item>
    <item>
      <title>Image as a Link with Contentful and Gatsby JS</title>
      <dc:creator>Thomas Cosialls</dc:creator>
      <pubDate>Sat, 08 May 2021 23:40:56 +0000</pubDate>
      <link>https://forem.com/tomtomdu73/image-as-a-link-with-contentful-and-gatsby-js-154f</link>
      <guid>https://forem.com/tomtomdu73/image-as-a-link-with-contentful-and-gatsby-js-154f</guid>
      <description>&lt;p&gt;As rich as the Rich Text functionality of Contentful might be, it doesn't include a simple way to create clickable images, i.e images embedded in a HTML link element. Here is a quick workaround with a Gatsby JS frontent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a custom model in Contentful
&lt;/h2&gt;

&lt;p&gt;We are creating a new custom model called &lt;strong&gt;ImageWithLink&lt;/strong&gt; that contains 3 simple fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;image (type &lt;em&gt;Media&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;url (type &lt;em&gt;Short text&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfxg0kzvzwgqvvgn8848.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfxg0kzvzwgqvvgn8848.png" alt="image" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can then use your ImageWithLink model in any Rich Text contents or as part of another model definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate with GatsbyJS
&lt;/h2&gt;

&lt;p&gt;I will only emphasize how to generate the custom image with link element when the ImageWithLink model is provided inside a Rich Text field. You need to make sure your GraphQL requests are working correctly and &lt;a href="https://www.contentful.com/developers/docs/tutorials/general/rich-text-and-gatsby/" rel="noopener noreferrer"&gt;that you imported all packages/functions needed&lt;/a&gt; to process Rich Text elements in Gatsby ;) I chose to use a fluid image in my example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[BLOCKS.EMBEDDED_ENTRY]: node =&amp;gt; {
    const {__typename} = node.data.target
    const target = node.data.target

    return &amp;lt;a href={target.url}&amp;gt;&amp;lt;img src={target.image.fluid.src} /&amp;gt;&amp;lt;/a&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this will help some of you !&lt;br&gt;
Best ❤️&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>contentful</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
