Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions .github/workflows/auto_update.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ jobs:
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
if: inputs.platform == 'all platforms' || inputs.platform == matrix.os
with:
python-version: "3.11"
- name: Install update verification dependencies
if: inputs.platform == 'all platforms' || inputs.platform == matrix.os
run: python -m pip install -r scripts/requirements-update-validation.txt
- name: Resolve exact published release
if: inputs.platform == 'all platforms' || inputs.platform == matrix.os
shell: bash
Expand Down Expand Up @@ -114,11 +121,8 @@ jobs:
EXT=exe; if [ "$OS" = macos ]; then EXT=dmg; fi
ASSET="AppFlowy-$VERSION-$OS-$PACKAGE_ARCH.$EXT"
gh release download "$TAG" --repo "$REPOSITORY" --pattern "$ASSET" --output "app.$EXT"
python3 - "$ASSET" "app.$EXT" <<'PYCODE'
import json, pathlib, sys
assets = [a for a in json.load(open('release.json', encoding='utf-8'))['assets'] if a['name'] == sys.argv[1]]
assert len(assets) == 1 and assets[0]['size'] == pathlib.Path(sys.argv[2]).stat().st_size > 0
PYCODE
python3 scripts/verify_native_update.py release-asset \
--release-json release.json --asset-name "$ASSET" --installer "app.$EXT"
- name: Sign macOS installer and read its actual build version
if: matrix.os == 'macos' && (inputs.platform == 'all platforms' || inputs.platform == 'macos')
shell: bash
Expand All @@ -129,18 +133,32 @@ jobs:
curl --fail --location https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz -o sparkle.tar.xz
echo '5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be sparkle.tar.xz' | shasum -a 256 -c -
mkdir sparkle && tar -xf sparkle.tar.xz -C sparkle
printf '%s' "$UPDATE_PRIVATE_KEY" | sparkle/bin/sign_update --ed-key-file - -p app.dmg > signature.txt
mkdir mount
hdiutil attach -readonly -nobrowse -mountpoint "$PWD/mount" app.dmg
hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "$PWD/mount" app.dmg
trap 'hdiutil detach "$PWD/mount"' EXIT
python3 - <<'PYCODE'
import glob, os, plistlib
plists = glob.glob('mount/*.app/Contents/Info.plist')
assert len(plists) == 1, 'Expected one application'
with open(plists[0], 'rb') as source: info = plistlib.load(source)
assert info['CFBundleShortVersionString'] == os.environ['VERSION']
with open(os.environ['GITHUB_ENV'], 'a') as output: output.write(f"BUILD_VERSION={info['CFBundleVersion']}\n")
PYCODE
apps=(mount/*.app)
test "${#apps[@]}" -eq 1
BUILD_VERSION=$(python3 scripts/verify_native_update.py macos-bundle \
--bundle "${apps[0]}" --version "$VERSION")
codesign --verify --strict --deep "${apps[0]}"
spctl --assess --type execute --verbose=2 "${apps[0]}"
printf '%s' "$UPDATE_PRIVATE_KEY" | sparkle/bin/sign_update --ed-key-file - -p app.dmg > signature.txt
python3 scripts/verify_native_update.py signature \
--platform macos --installer app.dmg --signature-file signature.txt
echo "BUILD_VERSION=$BUILD_VERSION" >> "$GITHUB_ENV"
- name: Verify Windows Authenticode signature
if: matrix.os == 'windows' && (inputs.platform == 'all platforms' || inputs.platform == 'windows')
shell: pwsh
run: |
$signature = Get-AuthenticodeSignature -LiteralPath app.exe
if ($signature.Status -ne 'Valid') {
throw "Published Windows installer has an invalid Authenticode signature: $($signature.Status)"
}
Write-Host "Verified installer signer: $($signature.SignerCertificate.Subject)"
$version = (Get-Item -LiteralPath app.exe).VersionInfo.ProductVersion.Trim()
if ($version -ne $env:VERSION) {
throw "Windows installer version $version does not match release $env:VERSION"
}
- name: Sign Windows installer
if: matrix.os == 'windows' && (inputs.platform == 'all platforms' || inputs.platform == 'windows')
shell: bash
Expand All @@ -154,6 +172,8 @@ jobs:
printf '%s' "$UPDATE_PRIVATE_KEY" > update-key.pem
openssl dgst -sha1 -sign update-key.pem -out signature.bin app.exe
openssl base64 -A -in signature.bin > signature.txt
python3 scripts/verify_native_update.py signature \
--platform windows --installer app.exe --signature-file signature.txt
echo "BUILD_VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Read release compatibility manifest
if: inputs.platform == 'all platforms' || inputs.platform == matrix.os
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ jobs:
"$arch" "$MACOS_APP_RELEASE_PATH/$arch/AppFlowy.app"
done

- name: Verify macOS update compatibility
run: |
set -euo pipefail
for arch in universal arm64 x86_64; do
python .builder/scripts/verify_native_update.py macos-bundle \
--bundle "$MACOS_APP_RELEASE_PATH/$arch/AppFlowy.app" \
--version "$BUILD_VERSION"
done

- name: Codesign AppFlowy
run: |
if [ -z "${{ secrets.MACOS_CERTIFICATE_BASE64 }}" ]; then
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/test-update-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test update validation

on:
pull_request:
paths:
- 'scripts/**'
- '.github/workflows/auto_update.yaml'
- '.github/workflows/macos.yaml'
- '.github/workflows/windows.yaml'
- '.github/workflows/test-update-validation.yml'
push:
branches: [main]
paths:
- 'scripts/**'
- '.github/workflows/auto_update.yaml'
- '.github/workflows/macos.yaml'
- '.github/workflows/windows.yaml'
- '.github/workflows/test-update-validation.yml'

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: python -m pip install -r scripts/requirements-update-validation.txt
- run: python -m unittest discover -s scripts -p 'test_*.py' -v
23 changes: 18 additions & 5 deletions .github/workflows/windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,19 @@ jobs:

steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: ${{ inputs.repo }}
ref: ${{ inputs.branch }}
token: ${{ secrets.PRIVATE_REPO_TOKEN }}

- name: Checkout packaging tools
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ github.sha }}
path: .builder

- name: Install Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -112,13 +119,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}

- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: x86_64-pc-windows-msvc
override: true
targets: x86_64-pc-windows-msvc
components: rustfmt
profile: minimal

- uses: davidB/rust-cargo-make@v1
with:
Expand Down Expand Up @@ -190,6 +195,14 @@ jobs:
Copy-Item "C:\Windows\System32\vcruntime140.dll" "${{ env.WINDOWS_APP_RELEASE_PATH }}\AppFlowy\"
Copy-Item "C:\Windows\System32\vcruntime140_1.dll" "${{ env.WINDOWS_APP_RELEASE_PATH }}\AppFlowy\"

- name: Verify Windows update compatibility
run: |
python -m pip install -r .builder/scripts/requirements-update-validation.txt
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python .builder/scripts/verify_native_update.py windows-bundle `
--executable "$env:WINDOWS_APP_RELEASE_PATH\AppFlowy\AppFlowy.exe"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Archive Asset
uses: vimtor/action-zip@v1
with:
Expand Down
9 changes: 4 additions & 5 deletions scripts/generate_native_appcast.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
#!/usr/bin/env python3
"""Generate one version-pinned appcast from the final signed installer metadata."""
import argparse
import base64
import datetime
import pathlib
import re
import xml.etree.ElementTree as ET

from verify_native_update import verify_installer_signature

SPARKLE = "http://www.andymatuschak.org/xml-namespaces/sparkle"
ET.register_namespace("sparkle", SPARKLE)


def generate(tag, platform, arch, installer, signature, build_version, repository="AppFlowy-IO/AppFlowy", minimum_server_version=None, critical=False, package_arch=None):
def generate(tag, platform, arch, installer, signature, build_version, repository="AppFlowy-IO/AppFlowy", minimum_server_version=None, critical=False, package_arch=None, *, public_key=None):
version = tag.removeprefix("v")
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise ValueError("A stable release tag is required")
Expand All @@ -25,9 +26,7 @@ def generate(tag, platform, arch, installer, signature, build_version, repositor
if not re.fullmatch(r"[\w.-]+/[\w.-]+", repository):
raise ValueError("Invalid repository")
signature = signature.strip()
decoded = base64.b64decode(signature, validate=True)
if not decoded or (platform == "macos" and len(decoded) != 64):
raise ValueError("Missing or malformed installer signature")
verify_installer_signature(platform, installer, signature, public_key)
size = installer.stat().st_size
if size <= 0:
raise ValueError("Empty installer")
Expand Down
2 changes: 2 additions & 0 deletions scripts/requirements-update-validation.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cryptography==50.0.2
PyYAML==6.0.3
Loading
Loading