Skip to content

Check libsignal Updates #232

Check libsignal Updates

Check libsignal Updates #232

# 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 (GitHub 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
# 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'
env:
AI_MODELS_TOKEN: ${{ secrets.AI_MODELS_TOKEN }}
LATEST_VERSION: ${{ steps.check.outputs.latest_version }}
CURRENT_VERSION: ${{ steps.check.outputs.current_version }}
run: |
if make update-changelog ARGS="--version $LATEST_VERSION --from $CURRENT_VERSION --ci"; 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: 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
else
echo "success=false" >> $GITHUB_OUTPUT
echo "::warning::FRB codegen failed. libsignal API may have changed."
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' && 'regenerated FRB bindings' || '**NOT UPDATED** (codegen failed, manual update required)' }}
- `CHANGELOG.md` — ${{ steps.changelog.outputs.success == 'true' && 'AI-generated entry (GitHub Models)' || '**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