Skip to content

Check libsignal Updates #249

Check libsignal Updates

Check libsignal Updates #249

# 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