Check libsignal Updates #245
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Automatically check for new libsignal releases and create PR with all updates | |
| # | |
| # This workflow: | |
| # 1. Runs daily (or manually) to check for new libsignal releases | |
| # 2. Compares with current upstream dependency tag in rust/Cargo.toml | |
| # 3. If newer version found: | |
| # - Updates rust/Cargo.toml with new libsignal tag | |
| # - Runs cargo update to update Cargo.lock | |
| # - Regenerates FRB bindings (make codegen) | |
| # - Updates CHANGELOG.md with AI-generated entry (provider per AI_MODELS) | |
| # - Creates a PR with all changes | |
| name: Check libsignal Updates | |
| on: | |
| schedule: | |
| # Run daily at 09:00 UTC | |
| - cron: '0 9 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| force_update: | |
| description: 'Force update even if version is the same' | |
| required: false | |
| default: false | |
| type: boolean | |
| target_version: | |
| description: 'Specific version to update to (leave empty for latest)' | |
| required: false | |
| default: '' | |
| type: string | |
| jobs: | |
| check-updates: | |
| runs-on: ubuntu-latest | |
| # All writes (checkout push, PR creation, branch push) go through the App | |
| # token; the default GITHUB_TOKEN needs no write access here. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Generate GitHub App Token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ vars.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v7 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Setup FVM and Flutter | |
| uses: ./.github/actions/setup-fvm | |
| - name: Check for updates | |
| id: check | |
| env: | |
| TARGET_VERSION: ${{ inputs.target_version }} | |
| FORCE_UPDATE: ${{ inputs.force_update }} | |
| run: | | |
| # Build arguments | |
| ARGS="--ci" | |
| if [ -n "$TARGET_VERSION" ]; then | |
| # Manual input is interpolated into ARGS and reaches a shell before | |
| # the Dart checker can validate it. Reject anything that is not the | |
| # exact upstream tag form here as well. | |
| if [[ ! "$TARGET_VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then | |
| echo "::error::target_version must use the exact vX.Y.Z form." | |
| exit 2 | |
| fi | |
| ARGS="$ARGS --version $TARGET_VERSION" | |
| fi | |
| if [ "$FORCE_UPDATE" = "true" ]; then | |
| ARGS="$ARGS --force" | |
| fi | |
| echo "Running: make check-new-libsignal-version ARGS=\"$ARGS\"" | |
| # `|| true` is unavoidable, and the exit code is unusable as a signal: | |
| # the checker exits 1 for "update available", and GNU make collapses any | |
| # non-zero recipe status into its own exit 2 (verified: a recipe exiting | |
| # 1 makes `make` exit 2), so 2 cannot be told apart from a crash. | |
| make check-new-libsignal-version ARGS="$ARGS" || true | |
| # What can tell them apart is the outputs file: the checker writes it | |
| # before signalling, and not at all when it throws. Without this gate a | |
| # broken checker (rate limit, API change, network) looks exactly like | |
| # "already up to date" and the workflow stays green forever while | |
| # silently never opening another update PR. | |
| if ! grep -q '^needs_update=' "$GITHUB_OUTPUT"; then | |
| echo "::error::Update checker wrote no result — it failed before reporting. Check the step log above; do NOT read this as 'up to date'." | |
| exit 1 | |
| fi | |
| echo "Check completed. Outputs:" | |
| cat $GITHUB_OUTPUT || true | |
| # Idempotency gate: if an open PR for this exact version already exists, | |
| # do nothing. Without this, every scheduled run regenerates the update | |
| # (cargo update + AI changelog are not byte-deterministic) and | |
| # force-pushes the PR branch — adding daily noise commits and wiping any | |
| # manual commits pushed to the PR. force_update bypasses the gate. | |
| - name: Check for existing update PR | |
| id: existing | |
| if: steps.check.outputs.needs_update == 'true' | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| LATEST_VERSION: ${{ steps.check.outputs.latest_version }} | |
| FORCE_UPDATE: ${{ inputs.force_update }} | |
| run: | | |
| BRANCH="update-libsignal-${LATEST_VERSION}" | |
| OPEN_PRS=$(gh pr list --head "$BRANCH" --state open --json number --jq 'length') | |
| if [ "$OPEN_PRS" != "0" ] && [ "$FORCE_UPDATE" != "true" ]; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "::notice::Open update PR for branch $BRANCH already exists — skipping to avoid force-pushing over it." | |
| else | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update rust/Cargo.toml | |
| id: cargo-toml | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| env: | |
| TARGET_VERSION: ${{ inputs.target_version }} | |
| FORCE_UPDATE: ${{ inputs.force_update }} | |
| run: | | |
| # Build arguments for update | |
| ARGS="--update --ci" | |
| if [ -n "$TARGET_VERSION" ]; then | |
| # Manual input is interpolated into ARGS and reaches a shell before | |
| # the Dart checker can validate it. Reject anything that is not the | |
| # exact upstream tag form here as well. | |
| if [[ ! "$TARGET_VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then | |
| echo "::error::target_version must use the exact vX.Y.Z form." | |
| exit 2 | |
| fi | |
| ARGS="$ARGS --version $TARGET_VERSION" | |
| fi | |
| if [ "$FORCE_UPDATE" = "true" ]; then | |
| ARGS="$ARGS --force" | |
| fi | |
| echo "Running: make check-new-libsignal-version ARGS=\"$ARGS\"" | |
| if make check-new-libsignal-version ARGS="$ARGS"; then | |
| echo "success=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "success=false" >> $GITHUB_OUTPUT | |
| echo "::error::Failed to update rust/Cargo.toml" | |
| fi | |
| - name: Setup Rust | |
| id: setup-rust | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| continue-on-error: true | |
| uses: ./.github/actions/setup-rust | |
| - name: Setup protoc | |
| id: setup-protoc | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| continue-on-error: true | |
| uses: ./.github/actions/setup-protoc | |
| - name: Update Cargo.lock | |
| id: cargo-lock | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| run: | | |
| if make rust-update; then | |
| echo "success=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "success=false" >> $GITHUB_OUTPUT | |
| echo "::warning::Failed to update Cargo.lock" | |
| fi | |
| # The codegen binary is not preinstalled on runners; without this step | |
| # `make codegen` fails with exit 127 on every run. Pinned so CI and | |
| # local runs generate identical bindings (cached by rust-cache). | |
| - name: Install FRB codegen | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| continue-on-error: true | |
| run: make setup-frb-codegen | |
| - name: Get Dart dependencies | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| continue-on-error: true | |
| run: make get | |
| - name: Regenerate FRB bindings | |
| id: codegen | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| run: | | |
| if make codegen; then | |
| echo "success=true" >> $GITHUB_OUTPUT | |
| # Whether the FFI surface actually moved, captured as a fact the | |
| # CHANGELOG step and the reviewer can both rely on. | |
| # | |
| # On a plain dependency bump this should always be `unchanged`: | |
| # codegen reads rust/src/api, which the bump does not touch. A | |
| # `changed` here is therefore a tripwire — something in the | |
| # upstream types this crate re-exports moved — and it is also the | |
| # input to the stage-1 SemVer decision in `make release-frb`. | |
| if git diff --quiet -- lib/src/rust/; then | |
| echo "bindings=unchanged" >> $GITHUB_OUTPUT | |
| else | |
| echo "bindings=changed" >> $GITHUB_OUTPUT | |
| echo "::warning::codegen changed lib/src/rust/ — the FFI surface moved. Review before merging; the libsignal_frb bump is not a patch." | |
| fi | |
| else | |
| echo "success=false" >> $GITHUB_OUTPUT | |
| echo "bindings=not-run" >> $GITHUB_OUTPUT | |
| echo "::warning::FRB codegen failed. libsignal API may have changed." | |
| fi | |
| # Runs AFTER codegen on purpose. The entry states whether the FFI | |
| # surface moved, and that has to be a result rather than a guess: | |
| # while this step ran first, the model had no codegen output and | |
| # inferred one from the phrasing of earlier entries. | |
| # | |
| # Records the libsignal dependency change (libsignal Highlight + Changed). | |
| # Does NOT touch the libsignal_frb crate version — that is bumped later at | |
| # release time (`make release-frb`), which also stamps the frb Highlight. | |
| - name: Update CHANGELOG.md | |
| id: changelog | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| # Every key is step-scoped, as the single token before them was. These | |
| # are org-level secrets shared by unrelated repositories, and a | |
| # job-level `env:` would hand each of them to every other step in this | |
| # job — including the ones that run third-party actions. | |
| # | |
| # AI_MODELS is a variable, not a secret: which model writes the entry | |
| # is operational configuration, and keeping it out of the code is what | |
| # makes a provider switch a settings change rather than a template | |
| # release rolled out across every generated project. | |
| env: | |
| AI_MODELS: ${{ vars.AI_MODELS }} | |
| AI_EFFORT: ${{ vars.AI_EFFORT }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| LATEST_VERSION: ${{ steps.check.outputs.latest_version }} | |
| CURRENT_VERSION: ${{ steps.check.outputs.current_version }} | |
| BINDINGS: ${{ steps.codegen.outputs.bindings || 'not-run' }} | |
| run: | | |
| if make update-changelog ARGS="--version $LATEST_VERSION --from $CURRENT_VERSION --ci --codegen $BINDINGS --ci-output $GITHUB_OUTPUT"; then | |
| echo "success=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "success=false" >> $GITHUB_OUTPUT | |
| echo "::warning::AI changelog update failed. Please update CHANGELOG.md manually." | |
| fi | |
| - name: Create Pull Request | |
| if: steps.check.outputs.needs_update == 'true' && steps.existing.outputs.skip != 'true' | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| sign-commits: true | |
| commit-message: | | |
| chore(deps): update libsignal to ${{ steps.check.outputs.latest_version }} | |
| title: "Update libsignal to ${{ steps.check.outputs.latest_version }}" | |
| body: | | |
| ## Automated libsignal Update | |
| This PR updates the libsignal dependency version. | |
| ### Version Change | |
| | Component | Old | New | | |
| |-----------|-----|-----| | |
| | libsignal | ${{ steps.check.outputs.current_version }} | ${{ steps.check.outputs.latest_version }} | | |
| ${{ steps.check.outputs.is_prerelease == 'true' && '> **Pre-release**: This is a release candidate. Consider waiting for stable release unless you need specific features.' || '' }} | |
| ${{ steps.cargo-toml.outputs.success == 'false' && '> **Critical**: Failed to update rust/Cargo.toml. Manual update required.' || '' }} | |
| ${{ steps.cargo-lock.outputs.success == 'false' && '> **Warning**: Failed to update Cargo.lock. Run `make rust-update` manually.' || '' }} | |
| ${{ steps.codegen.outputs.success == 'false' && '> **Error**: FRB codegen failed. libsignal API may have changed. Manual intervention required.' || '' }} | |
| ${{ steps.changelog.outputs.success == 'false' && '> **Warning**: AI changelog update failed. Please update CHANGELOG.md manually.' || '' }} | |
| > **Note**: This PR does NOT bump the `libsignal_frb` crate version and does NOT build native binaries. It only updates the libsignal dependency. Cut a native release later with `make release-frb` (stage 1), then the Dart release (stage 2). See CLAUDE.md → Release Flow. | |
| ### Files Updated Automatically | |
| - `rust/Cargo.toml` — ${{ steps.cargo-toml.outputs.success == 'true' && 'libsignal dependency tags only (the `libsignal_frb` crate version is intentionally NOT bumped here)' || '**NOT UPDATED** (update failed)' }} | |
| - `rust/Cargo.lock` — ${{ steps.cargo-lock.outputs.success == 'true' && 'updated dependencies' || '**NOT UPDATED** (cargo update failed)' }} | |
| - `lib/src/rust/` — ${{ steps.codegen.outputs.success == 'true' && format('regenerated FRB bindings — FFI surface **{0}**', steps.codegen.outputs.bindings == 'changed' && 'MOVED (review: the `libsignal_frb` bump is not a patch)' || 'unchanged') || '**NOT UPDATED** (codegen failed, manual update required)' }} | |
| - `CHANGELOG.md` — ${{ steps.changelog.outputs.success == 'true' && format('AI-generated entry (written by `{0}`)', steps.changelog.outputs.ai_provider) || '**NOT UPDATED** (AI failed, manual update required)' }} | |
| - `README.md` — ${{ steps.cargo-toml.outputs.success == 'true' && 'version badge' || '**NOT UPDATED**' }} | |
| - `.copier-answers.yml` — ${{ steps.cargo-toml.outputs.success == 'true' && 'upstream_version updated' || '**NOT UPDATED**' }} | |
| - `CLAUDE.md` — ${{ steps.cargo-toml.outputs.success == 'true' && 'example in documentation' || '**NOT UPDATED**' }} | |
| ### Release Notes | |
| See [libsignal ${{ steps.check.outputs.latest_version }} release notes](${{ steps.check.outputs.release_url }}) | |
| ### Before Merge | |
| ${{ steps.cargo-toml.outputs.success == 'false' && '1. **UPDATE rust/Cargo.toml** — automatic update failed, update manually' || '' }} | |
| ${{ steps.cargo-lock.outputs.success == 'false' && '1. **RUN `make rust-update`** — Cargo.lock update failed' || '' }} | |
| 1. ${{ steps.codegen.outputs.success == 'false' && '**FIX FRB BINDINGS** — codegen failed, check libsignal API changes' || 'Review FRB bindings — check if API changes require Dart code updates' }} | |
| 2. **Review CHANGELOG.md** — ${{ steps.changelog.outputs.success == 'true' && 'verify AI-generated entry is accurate' || '**ADD CHANGELOG ENTRY MANUALLY**' }} | |
| ### After Merge | |
| 1. **Run tests locally** (optional, CI will run them): | |
| ```bash | |
| make test | |
| ``` | |
| 2. This update accumulates on `main` with no new native binary. When | |
| you are ready to ship, run the two-stage release: | |
| - **Stage 1 — native crate:** `make release-frb ARGS="--version X.Y.Z"` | |
| (bumps `libsignal_frb`, tags `libsignal_frb-X.Y.Z`, builds binaries) | |
| - **Stage 2 — Dart package:** bump `pubspec.yaml`, move CHANGELOG | |
| `[Unreleased]` → version, tag `vX.Y.Z` (release-package skill) | |
| --- | |
| *This PR was created automatically by the libsignal update checker.* | |
| branch: update-libsignal-${{ steps.check.outputs.latest_version }} | |
| delete-branch: true | |
| labels: | | |
| dependencies | |
| automated | |
| ${{ steps.check.outputs.is_prerelease == 'true' && 'pre-release' || '' }} | |
| ${{ steps.cargo-toml.outputs.success == 'false' && 'cargo-toml-failed' || '' }} | |
| ${{ steps.cargo-lock.outputs.success == 'false' && 'cargo-lock-failed' || '' }} | |
| ${{ steps.codegen.outputs.success == 'false' && 'codegen-failed' || '' }} | |
| ${{ steps.changelog.outputs.success == 'false' && 'changelog-needed' || '' }} | |
| - name: Summary | |
| env: | |
| LATEST_VERSION: ${{ steps.check.outputs.latest_version }} | |
| CURRENT_VERSION: ${{ steps.check.outputs.current_version }} | |
| IS_PRERELEASE: ${{ steps.check.outputs.is_prerelease }} | |
| NEEDS_UPDATE: ${{ steps.check.outputs.needs_update }} | |
| SKIP_EXISTING: ${{ steps.existing.outputs.skip }} | |
| CARGO_TOML_OK: ${{ steps.cargo-toml.outputs.success }} | |
| CARGO_LOCK_OK: ${{ steps.cargo-lock.outputs.success }} | |
| CODEGEN_OK: ${{ steps.codegen.outputs.success }} | |
| CHANGELOG_OK: ${{ steps.changelog.outputs.success }} | |
| run: | | |
| if [ "$SKIP_EXISTING" = "true" ]; then | |
| echo "## Update PR Already Open" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "An open PR for **${LATEST_VERSION}** already exists — nothing to do." >> $GITHUB_STEP_SUMMARY | |
| echo "Re-run with \`force_update\` to regenerate it (this force-pushes the PR branch)." >> $GITHUB_STEP_SUMMARY | |
| elif [ "$NEEDS_UPDATE" = "true" ]; then | |
| echo "## Update Available" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Current libsignal | ${CURRENT_VERSION} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| New libsignal | ${LATEST_VERSION} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Pre-release | ${IS_PRERELEASE} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Cargo.toml | ${CARGO_TOML_OK} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Cargo.lock | ${CARGO_LOCK_OK} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| FRB Codegen | ${CODEGEN_OK} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| AI Changelog | ${CHANGELOG_OK} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "A pull request has been created for review." >> $GITHUB_STEP_SUMMARY | |
| if [ "$CARGO_TOML_OK" = "false" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "> **Critical**: Failed to update rust/Cargo.toml. Manual update required." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "$CARGO_LOCK_OK" = "false" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "> **Warning**: Failed to update Cargo.lock. Run \`make rust-update\` manually." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "$CODEGEN_OK" = "false" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "> **Error**: FRB codegen failed. libsignal API may have changed. Manual intervention required." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "$CHANGELOG_OK" = "false" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "> **Warning**: AI changelog update failed. Please update CHANGELOG.md manually." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "## Up to Date" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Current version **${CURRENT_VERSION}** is the latest." >> $GITHUB_STEP_SUMMARY | |
| fi |