Skip to content

fix(secretstore): fix silent failures when libsecret can't unlock #131

fix(secretstore): fix silent failures when libsecret can't unlock

fix(secretstore): fix silent failures when libsecret can't unlock #131

Workflow file for this run

# Copyright (C) 2021-2026 Saturneric <eric@bktus.com>
#
# This file is part of GpgFrontend.
#
# GpgFrontend is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GpgFrontend is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
#
# The initial version of the source code is inherited from
# the gpg4usb project, which is under GPL-3.0-or-later.
#
# All the source code of GpgFrontend was modified and released by
# Saturneric <eric@bktus.com> starting on May 12, 2021.
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: Build
on:
push:
branches:
- main
tags:
- "v*"
paths-ignore:
- "resource/lfs/locale/**"
- "**.md"
pull_request:
branches:
- main
paths-ignore:
- "resource/lfs/locale/**"
- "**.md"
workflow_dispatch:
inputs:
build_mode:
description: "Build mode"
required: true
default: "nightly"
type: choice
options:
- nightly
- release
env:
BUILD_TYPE: RelWithDebInfo
GNUPG_VERSION: "2.5.20"
jobs:
build:
strategy:
matrix:
os:
[
"ubuntu-22.04",
"ubuntu-24.04-arm",
"macos-15-intel",
"macos-15",
"macos-26-intel",
"macos-26",
"windows-2022",
]
# Portable vs installed is a compile-time decision
# (GPGFRONTEND_BUILD_PORTABLE decides where the profile, and with it the
# user's keys, lives), so each flavour needs its own configure + build.
# They run as separate matrix jobs on purpose: the generated build
# headers land in the source tree, so two flavours cannot share one
# checkout.
flavor: ["installed", "portable"]
exclude:
# macOS ships as a signed, notarized .app bundle installed to
# /Applications; a portable bundle has no meaning there (and the App
# Store / sandbox variants forbid it outright).
- os: "macos-15-intel"
flavor: "portable"
- os: "macos-15"
flavor: "portable"
- os: "macos-26-intel"
flavor: "portable"
- os: "macos-26"
flavor: "portable"
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- name: Set git to use LF(Windows) or CRLF(MacOS) line endings
run: |
git config --global core.autocrlf false
git config --global core.eol lf
if: runner.os == 'Windows' || runner.os == 'macOS'
- uses: actions/checkout@v6
with:
lfs: "false"
submodules: recursive
- name: Setup Build Mode
shell: bash
run: |
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# Identifier the artifacts are named after. On Linux the runner label
# ("ubuntu-24.04-arm") is the wrong thing to publish: the build host's
# distro is not what an AppImage runs on, and the "-arm" suffix would
# read "arm-aarch64" next to the architecture. Just say "linux" — the
# architecture already keeps the two images apart. Windows and macOS
# keep their runner label.
if [[ "${{ runner.os }}" == "Linux" ]]; then
echo "OS_IDENTIFIER=linux" >> $GITHUB_ENV
else
echo "OS_IDENTIFIER=${{ matrix.os }}" >> $GITHUB_ENV
fi
# Build flavour: "portable" keeps the profile beside the application,
# "installed" uses the OS user-data location. Compile-time only.
# "installed" is the default flavour, so only "portable" is spelled
# out in artifact names.
if [[ "${{ matrix.flavor }}" == "portable" ]]; then
echo "GPGFRONTEND_BUILD_PORTABLE=ON" >> $GITHUB_ENV
echo "FLAVOR_SUFFIX=-portable" >> $GITHUB_ENV
else
echo "GPGFRONTEND_BUILD_PORTABLE=OFF" >> $GITHUB_ENV
echo "FLAVOR_SUFFIX=" >> $GITHUB_ENV
fi
# Single-branch (trunk + tags) model:
# - a version tag (v*) -> stable release build
# - a push to main -> nightly build
# - a pull request -> PR validation build
# - workflow_dispatch -> honour the chosen input
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
BUILD_MODE="${{ github.event.inputs.build_mode }}"
if [[ "${BUILD_MODE}" == "release" ]]; then
BUILD_TYPE_EFFECTIVE="Release"
else
BUILD_TYPE_EFFECTIVE="RelWithDebInfo"
fi
elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
BUILD_MODE="pr"
BUILD_TYPE_EFFECTIVE="RelWithDebInfo"
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
BUILD_MODE="release"
BUILD_TYPE_EFFECTIVE="Release"
else
BUILD_MODE="nightly"
BUILD_TYPE_EFFECTIVE="RelWithDebInfo"
fi
# Stable release builds drop the "Testing" suffix from the app name.
if [[ "${BUILD_MODE}" == "release" ]]; then
GPGFRONTEND_BUILD_STABLE="ON"
else
GPGFRONTEND_BUILD_STABLE="OFF"
fi
echo "BUILD_MODE=${BUILD_MODE}" >> $GITHUB_ENV
echo "BUILD_TYPE=${BUILD_TYPE_EFFECTIVE}" >> $GITHUB_ENV
echo "GPGFRONTEND_BUILD_STABLE=${GPGFRONTEND_BUILD_STABLE}" >> $GITHUB_ENV
echo "BUILD_TYPE_LOWER=$(echo ${BUILD_TYPE_EFFECTIVE} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "SANDBOX_CMAKE_FLAG=" >> $GITHUB_ENV
echo "CODE_SIGNING_IDENTITY=${{secrets.DEVELOPER_ID_CODE_SIGN_IDENTITY}}" >> $GITHUB_ENV
echo "PROVISIONING_PROFILE_UUID=${{secrets.DEVELOPER_ID_PROVISIONING_PROFILE_UUID}}" >> $GITHUB_ENV
echo "Build mode: ${BUILD_MODE}"
echo "Build type: ${BUILD_TYPE_EFFECTIVE}"
echo "Build flavor: ${{ matrix.flavor }}"
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.flavor }}-${{ env.BUILD_TYPE }}
- name: Install Dependence (Linux)
run: |
sudo apt-get update
sudo apt-get -y install build-essential binutils git autoconf automake gettext texinfo
sudo apt-get -y install gcc g++ ninja-build
sudo apt-get -y install libarchive-dev libssl-dev libsodium-dev
sudo apt-get -y install gpgsm libxcb-xinerama0 libxcb-icccm4-dev libcups2-dev libdrm-dev libegl1-mesa-dev
sudo apt-get -y install libfuse2 libgcrypt20-dev libnss3-dev libpci-dev libpulse-dev libudev-dev libxtst-dev
sudo apt-get -y install libglu1-mesa-dev libfontconfig1-dev libx11-xcb-dev libxcb-image0 gyp
sudo apt-get -y install libglu1-mesa-dev libfontconfig1-dev libx11-xcb-dev libxcb-* libxkbcommon-x11-0
sudo apt-get -y install libwayland-cursor0 libwayland-egl1
# libsecret is dlopen'd, never linked. Installed only so the AppImage
# can carry a copy built against the same glib it bundles.
sudo apt-get -y install libsecret-1-0
if: runner.os == 'Linux'
- name: Codesign Configuration (macOS)
run: |
APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12
PP_PATH=$RUNNER_TEMP/${{secrets.DEVELOPER_ID_PROVISIONING_PROFILE_UUID}}.provisionprofile
echo -n "${{secrets.DEVELOPER_ID_PROVISIONING_PROFILE_DATA}}" | base64 --decode -o $PP_PATH
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Import the certificates and provisioning profile into the keychain
echo -n "${{secrets.DEVELOP_ID_CERT}}" | base64 --decode -o $APP_CERT_PATH
security create-keychain -p gpgfrontend build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p gpgfrontend build.keychain
security import $APP_CERT_PATH -k build.keychain -P ${{secrets.DEVELOP_ID_CERT_PWD}} -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k gpgfrontend build.keychain
security set-keychain-settings -lut 3600
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_DIR"
cp "$PP_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/${{env.PROVISIONING_PROFILE_UUID}}.provisionprofile"
if: runner.os == 'macOS'
- name: Install Qt6
uses: jurplel/install-qt-action@v4
with:
version: "6.10.3"
cache: "true"
if: runner.os == 'Linux' || runner.os == 'macOS'
- name: Install Dependence (macOS)
run: |
brew install --formula automake texinfo libarchive googletest create-dmg libsodium
if: runner.os == 'macOS'
- name: Set up MinGW (Windows)
uses: msys2/setup-msys2@v2
id: msys2
with:
update: false
release: false
cache: true
install: >-
git
zip
unzip
msys2-devel
base-devel
msys2-runtime-devel
mingw-w64-x86_64-gcc
mingw-w64-x86_64-make
mingw-w64-x86_64-cmake
mingw-w64-x86_64-qt6-base
mingw-w64-x86_64-qt6-tools
mingw-w64-x86_64-ninja
mingw-w64-x86_64-libarchive
mingw-w64-x86_64-gtest
mingw-w64-x86_64-autotools
mingw-w64-x86_64-texinfo
mingw-w64-x86_64-libassuan
mingw-w64-x86_64-ccache
mingw-w64-x86_64-rust
mingw-w64-x86_64-libsodium
if: runner.os == 'Windows'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
if: runner.os == 'Linux' || runner.os == 'macOS'
# The Rust crate does not see the portable flag, so both flavours produce
# the same cargo output and deliberately share one cache entry.
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: |
rust -> build/cargo
shared-key: rust-${{ runner.os }}-${{ matrix.os }}-${{ env.BUILD_TYPE }}
cache-on-failure: true
if: runner.os == 'Linux' || runner.os == 'macOS'
# rust-cache cannot locate the msys2/mingw cargo, so cache the registry and
# Corrosion's target dir directly for the Windows build.
- name: Cache Cargo (Windows)
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
${{github.workspace}}/build/cargo
key: cargo-win-${{ matrix.os }}-${{ env.BUILD_TYPE }}-${{ hashFiles('rust/Cargo.lock') }}
restore-keys: |
cargo-win-${{ matrix.os }}-${{ env.BUILD_TYPE }}-
if: runner.os == 'Windows'
- name: Build GpgME (macOS)
run: |
cd ${{github.workspace}}/third_party/gpgme
export CC="ccache gcc"
export CXX="ccache g++"
./autogen.sh
mkdir -p build && cd build
../configure --enable-static \
--disable-shared \
--enable-silent-rules \
--disable-dependency-tracking \
--enable-languages=cl \
--disable-gpgconf-test \
--disable-gpg-test \
--disable-gpgsm-test \
--disable-g13-test
make -j$(sysctl -n hw.logicalcpu)
sudo make install
ccache -s
if: runner.os == 'macOS'
- name: Build GpgME (Windows)
shell: msys2 {0}
run: |
cd $(cygpath -u "${{github.workspace}}")
cd third_party/gpgme
export CC="ccache gcc"
export CXX="ccache g++"
export CFLAGS="${CFLAGS} -Wno-int-conversion -Wno-incompatible-pointer-types"
./autogen.sh
mkdir -p build && cd build
../configure --enable-maintainer-mode \
--enable-static \
--disable-shared \
--enable-silent-rules \
--disable-dependency-tracking \
--enable-languages=cl \
--disable-gpgconf-test \
--disable-gpg-test \
--disable-gpgsm-test \
--disable-g13-test \
--enable-w32-glib
make -j$(nproc)
make install
ccache -s
if: runner.os == 'Windows'
- name: Cache googletest (Linux)
uses: actions/cache@v4
with:
path: ${{github.workspace}}/third_party/googletest
key: gtest-${{ matrix.os }}-v1.15.2
if: runner.os == 'Linux'
- name: Build googletest (Linux)
run: |
if [ ! -f "${{github.workspace}}/third_party/googletest/build/build.ninja" ]; then
rm -rf ${{github.workspace}}/third_party/googletest
git clone --depth 1 --branch v1.15.2 https://github.com/google/googletest.git ${{github.workspace}}/third_party/googletest
cd ${{github.workspace}}/third_party/googletest
mkdir build && cd build
cmake -G Ninja -DBUILD_SHARED_LIBS=ON \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
..
ninja
else
echo "Reusing cached googletest build"
fi
cd ${{github.workspace}}/third_party/googletest/build
sudo ninja install
if: runner.os == 'Linux'
- name: Build GpgFrontend (macOS)
run: |
MACOS_MAJOR=$(sw_vers -productVersion | cut -d. -f1)
MACOS_MINOR=$(sw_vers -productVersion | cut -d. -f2)
if [[ "$MACOS_MAJOR" == "13" ]]; then
DEPLOY_TARGET="13.0"
elif [[ "$MACOS_MAJOR" == "14" ]]; then
DEPLOY_TARGET="14.0"
elif [[ "$MACOS_MAJOR" == "15" ]]; then
DEPLOY_TARGET="15.0"
elif [[ "$MACOS_MAJOR" == "26" ]]; then
DEPLOY_TARGET="26.0"
else
DEPLOY_TARGET="${MACOS_MAJOR}.${MACOS_MINOR}"
fi
echo "Set MacOS Deployment Target: $DEPLOY_TARGET"
cmake -B ${{github.workspace}}/build -G Xcode \
-DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOY_TARGET} \
-DGPGFRONTEND_XCODE_CODE_SIGN_IDENTITY="${{env.CODE_SIGNING_IDENTITY}}" \
-DGPGFRONTEND_XCODE_TEAM_ID="${{secrets.GPGFRONTEND_XCODE_TEAM_ID}}" \
-DGPGFRONTEND_XCODE_PROVISIONING_PROFILE_UUID="${{env.PROVISIONING_PROFILE_UUID}}" \
-DGPGFRONTEND_BUILD_STABLE=${{env.GPGFRONTEND_BUILD_STABLE}} \
-DGPGFRONTEND_BUILD_PORTABLE=OFF \
-DGPGFRONTEND_LINK_GPGME_INTO_CORE=ON
xcodebuild -list -project ${{github.workspace}}/build/GpgFrontend.xcodeproj
echo "CODE_SIGNING_IDENTITY=${{env.CODE_SIGNING_IDENTITY}}"
echo "PROFILE_UUID=${{env.PROVISIONING_PROFILE_UUID}}"
ls -la "$HOME/Library/MobileDevice/Provisioning Profiles/"
security cms -D -i "$HOME/Library/MobileDevice/Provisioning Profiles/${{env.PROVISIONING_PROFILE_UUID}}.provisionprofile" | plutil -p -
security find-identity -v -p codesigning
cd ${{github.workspace}}/build/
xcodebuild -project ${{github.workspace}}/build/GpgFrontend.xcodeproj \
-scheme GpgFrontend \
-configuration "${{env.BUILD_TYPE}}" \
-archivePath ${{github.workspace}}/build/GpgFrontend.xcarchive \
archive \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="${{secrets.GPGFRONTEND_XCODE_TEAM_ID}}" \
CODE_SIGN_IDENTITY="${{env.CODE_SIGNING_IDENTITY}}"
mkdir ${{github.workspace}}/build/package
xcodebuild -exportArchive -archivePath ${{github.workspace}}/build/GpgFrontend.xcarchive \
-exportOptionsPlist ${{github.workspace}}/build/ExportOptions.plist \
-exportPath ${{github.workspace}}/build/package/
if: runner.os == 'macOS'
- name: Deploy Qt & Code Sign (macOS)
run: |
# Use macdeployqt to deploy the Qt frameworks and plugins, and sign
# the app bundle in one step.
macdeployqt ${{github.workspace}}/build/package/GpgFrontend.app \
-verbose=2 \
-always-overwrite \
-codesign="${{env.CODE_SIGNING_IDENTITY}}"
# Sign the app bundle with the specified identity and provisioning profile.
codesign -s "${{env.CODE_SIGNING_IDENTITY}}" \
-f --deep --options=runtime --timestamp \
${{github.workspace}}/build/package/GpgFrontend.app
codesign --verify --deep --strict --verbose=4 \
${{github.workspace}}/build/package/GpgFrontend.app
if: runner.os == 'macOS'
- name: Package & Sign App Bundle (macOS)
run: |
security -v unlock-keychain -p gpgfrontend
ditto -c -k --keepParent ${{github.workspace}}/build/package/GpgFrontend.app ${{github.workspace}}/build/GpgFrontend.app.zip
hdiutil create ${{github.workspace}}/build/tmp.dmg -ov \
-volname "GpgFrontend" -fs HFS+ -srcfolder ${{github.workspace}}/build/package/
mkdir ${{github.workspace}}/build/upload-artifact
create-dmg --codesign "${{secrets.DEVELOPER_ID_CODE_SIGN_IDENTITY}}" --volicon "${{github.workspace}}/resource/lfs/icns/GpgFrontend.icns" --volname GpgFrontend --app-drop-link 600 185 --window-size 800 400 ${{github.workspace}}/build/upload-artifact/GpgFrontend.dmg ${{github.workspace}}/build/package/GpgFrontend.app
mv ${{github.workspace}}/build/upload-artifact/GpgFrontend.dmg \
${{github.workspace}}/build/upload-artifact/GpgFrontend-${{env.OS_IDENTIFIER}}-${{env.SHORT_SHA}}.dmg
mv ${{github.workspace}}/build/GpgFrontend.app.zip \
${{github.workspace}}/build/GpgFrontend-${{env.OS_IDENTIFIER}}-${{env.SHORT_SHA}}.zip
if: runner.os == 'macOS'
- name: Notarize macOS Build (macOS)
run: |
xcrun notarytool submit \
--apple-id ${{secrets.APPLE_DEVELOPER_ID}} \
--team-id ${{secrets.APPLE_DEVELOPER_TEAM_ID}} \
--password ${{secrets.APPLE_DEVELOPER_ID_SECRET}} \
${{github.workspace}}/build/GpgFrontend-${{env.OS_IDENTIFIER}}-${{env.SHORT_SHA}}.zip
if: runner.os == 'macOS'
- name: Build GpgFrontend (Linux)
run: |
export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:$LD_LIBRARY_PATH
cmake -B ${{github.workspace}}/build -G Ninja \
-DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
-DGPGFRONTEND_BUILD_APP_IMAGE=ON \
-DGPGFRONTEND_BUILD_STABLE=${{env.GPGFRONTEND_BUILD_STABLE}} \
-DGPGFRONTEND_BUILD_PORTABLE=${{env.GPGFRONTEND_BUILD_PORTABLE}} \
-DGPGFRONTEND_LINK_GPGME_INTO_CORE=ON
cmake --build ${{ github.workspace }}/build \
--config ${{ env.BUILD_TYPE }} \
--parallel \
--verbose
ccache -s
if: runner.os == 'Linux'
- name: Package App Image (Linux)
run: |
QT_PLUGIN_DIR=$(qmake -query QT_INSTALL_PLUGINS)
echo "Found Qt plugin dir: $QT_PLUGIN_DIR"
# enter the sqldrivers directory
cd $QT_PLUGIN_DIR/sqldrivers
# remove all non-sqlite drivers to reduce the size of the final AppImage
find . -type f ! -name '*sqlite*' -delete
ls -l
# return to the root of the repository
cd ${{github.workspace}}
mkdir ${{github.workspace}}/build/upload-artifact
cd ${{github.workspace}}/build/upload-artifact
ARCH=$(uname -m)
if [[ "$ARCH" == "x86_64" ]]; then
wget -c -nv https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage
mv linuxdeployqt-continuous-x86_64.AppImage linuxdeployqt-continuous.AppImage
EXTRA_ARGS=""
elif [[ "$ARCH" == "aarch64" ]]; then
wget -c -nv https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-aarch64.AppImage
mv linuxdeployqt-continuous-aarch64.AppImage linuxdeployqt-continuous.AppImage
mkdir -p ${{github.workspace}}/build/artifacts/AppDir/usr/share/doc/libc6/
touch ${{github.workspace}}/build/artifacts/AppDir/usr/share/doc/libc6/copyright
EXTRA_ARGS="-unsupported-allow-new-glibc"
fi
APP_DIR="${{github.workspace}}/build/artifacts/AppDir"
# The AppImage bundles libglib/libgobject/libgio and its AppRun puts
# them ahead of the host's copies, so a host libsecret built against a
# newer glib cannot resolve its own symbols and the system keychain
# simply disappears -- see linuxdeployqt issue 544. Carrying our own
# copy is what makes the dependency closure self-consistent. It has to
# be staged before linuxdeployqt runs: a file dropped in afterwards
# gets neither an rpath nor its own dependencies deployed.
LIBSECRET_SRC="/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libsecret-1.so.0"
test -f "$LIBSECRET_SRC"
cp -L "$LIBSECRET_SRC" "$APP_DIR/usr/lib/libsecret-1.so.0"
chmod u+x linuxdeployqt-continuous.AppImage
export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:$LD_LIBRARY_PATH
./linuxdeployqt-continuous.AppImage \
${{github.workspace}}/build/artifacts/AppDir/usr/share/applications/*.desktop \
$EXTRA_ARGS \
-no-translations \
-extra-plugins=iconengines,platforms,sqldrivers/libqsqlite.so \
-appimage \
-executable=$APP_DIR/usr/lib/libsecret-1.so.0 \
-executable-dir=${{github.workspace}}/build/artifacts/AppDir/usr/lib/modules
# Without the rpath patch the staged copy cannot find its own
# dependencies, which is the bug this whole step exists to fix, so it
# fails the build rather than shipping a silent regression.
echo "--- deployed credential-store closure ---"
ls -l "$APP_DIR/usr/lib" | grep -E 'secret|glib|gobject|gio|gcrypt' || true
readelf -d "$APP_DIR/usr/lib/libsecret-1.so.0" | grep -E 'RUNPATH|RPATH|SONAME|NEEDED' || true
readelf -d "$APP_DIR/usr/lib/libsecret-1.so.0" | grep -q 'ORIGIN' \
|| { echo "libsecret was not rpath-patched by linuxdeployqt"; exit 1; }
# linuxdeployqt names the image after the .desktop entry, so both
# flavours would come out as Gpg_Frontend-<arch>.AppImage and collide
# once the release job merges every runner's artifacts into one
# directory. Rename to the same scheme the other platforms use.
rm -f linuxdeployqt-continuous.AppImage
for image in Gpg_Frontend*.AppImage; do
mv "$image" \
"GpgFrontend-${{env.SHORT_SHA}}-${{env.OS_IDENTIFIER}}-${ARCH}${{env.FLAVOR_SUFFIX}}.AppImage"
done
ls -l
if: runner.os == 'Linux'
- name: Build GpgFrontend (Windows)
shell: msys2 {0}
run: |
cd $(cygpath -u "${{github.workspace}}")
cmake -G "Ninja" -S . -B build \
-DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
-DGPGFRONTEND_BUILD_STABLE=${{env.GPGFRONTEND_BUILD_STABLE}} \
-DGPGFRONTEND_BUILD_PORTABLE=${{env.GPGFRONTEND_BUILD_PORTABLE}} \
-DGPGFRONTEND_LINK_GPGME_INTO_CORE=ON
cmake --build build \
--config ${{ env.BUILD_TYPE }} \
--parallel \
--verbose
ccache -s
if: runner.os == 'Windows'
- name: Download GnuPG Binary Release (Windows)
shell: msys2 {0}
run: |
export URL="https://ftp.bktus.com/GnuPG/${{env.GNUPG_VERSION}}"
export KEY_URL="https://bktus.com/pgp/saturneric-code-signing.asc"
export KEY_FPR="12F7E8858CF15BEC9975FF3C5CA3DA246843FD03"
export KEY_FILE="saturneric-code-signing.asc"
export FILE="gnupg.zip"
export CHECKSUM_FILE="SHA256SUMS.txt"
export SIGNATURE_FILE="gnupg.zip.sig"
export GNUPGHOME=$(mktemp -d)
cd $(cygpath -u "${{github.workspace}}")
mkdir -p build/downloads
curl -fL --retry 3 -o build/downloads/$FILE $URL/$FILE
curl -fL --retry 3 -o build/downloads/$CHECKSUM_FILE $URL/$CHECKSUM_FILE
curl -fL --retry 3 -o build/downloads/$KEY_FILE $KEY_URL
curl -fL --retry 3 -o build/downloads/$SIGNATURE_FILE $URL/$SIGNATURE_FILE
gpg --import build/downloads/$KEY_FILE
# Trust is pinned to this exact fingerprint, not to whatever the
# downloaded key file happens to contain.
if ! KEY_INFO=$(gpg --batch --with-colons --list-keys "$KEY_FPR"); then
echo "Imported key does not match fingerprint $KEY_FPR!" >&2
exit 1
fi
EXPIRES=$(echo "$KEY_INFO" | awk -F: '/^pub:/ {print $7; exit}')
if [ -n "$EXPIRES" ]; then
echo "Signing key expires: $(date -u -d "@$EXPIRES")"
if [ "$EXPIRES" -le "$(date +%s)" ]; then
echo "Signing key has expired!" >&2
exit 1
fi
else
echo "Signing key has no expiration date"
fi
# VALIDSIG carries the primary key fingerprint as its last field, so
# this also rejects a valid signature from any other imported key.
if ! gpg --status-fd 1 --verify build/downloads/$SIGNATURE_FILE \
build/downloads/$FILE | grep "VALIDSIG" | grep -q "$KEY_FPR"; then
echo "GnuPG signature verification failed!" >&2
exit 1
fi
CHECKSUM=$(grep "$FILE\$" build/downloads/$CHECKSUM_FILE | awk '{print $1}')
ACTUAL_CHECKSUM=$(sha256sum build/downloads/$FILE | awk '{print $1}')
echo "Expected Checksum: $CHECKSUM"
echo "Actual Checksum: $ACTUAL_CHECKSUM"
if [ "$CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
echo "Checksum verification failed!" >&2
exit 1
fi
mkdir -p build/artifacts/gnupg
unzip -o build/downloads/$FILE -d build/artifacts/gnupg/
ls -l build/artifacts/gnupg/
if: runner.os == 'Windows'
# Payload staging is flavour-independent: the portable ZIP and the MSI are
# both built from this same tree, only from a differently configured build.
- name: Stage Payload (Windows)
shell: msys2 {0}
run: |
cd $(cygpath -u "${{github.workspace}}")
cp PrivacyPolicy.md build/artifacts/
cp README.md build/artifacts/
cp SECURITY.md build/artifacts/
cp TRANSLATORS build/artifacts/
cp COPYING build/artifacts/
cp gpgfrontend.ico build/artifacts/bin/
rm -rf build/artifacts/bin/*.a
rm -rf build/artifacts/bin/modules/*.a
mv build/artifacts/bin/modules build/artifacts/modules
cd build
# Deploy every gf_* library rather than a hand-kept list, so a newly
# registered library (gf_res was the last one) cannot be forgotten.
for lib in ./artifacts/bin/libgf_*.dll; do
windeployqt6 --no-translations --force "$lib"
done
windeployqt6 --no-translations --force ./artifacts/bin/GpgFrontend.exe
# A module may need Qt modules the app itself never links (Qt6Xml,
# Qt6Network, ...), so each one still has to be scanned. But --dir
# sends what it needs into bin/, which is the first directory the
# loader searches, instead of duplicating the whole Qt runtime next
# to every module.
for module in ./artifacts/modules/*.dll; do
windeployqt6 --no-translations --force --dir ./artifacts/bin "$module"
done
mkdir -p upload-artifact
if: runner.os == 'Windows'
- name: Package Portable Archive (Windows)
shell: msys2 {0}
run: |
cd $(cygpath -u "${{github.workspace}}")/build/artifacts
zip -r ../upload-artifact/GpgFrontend-${{env.SHORT_SHA}}-${{env.OS_IDENTIFIER}}-x86_64-portable.zip *
if: runner.os == 'Windows' && matrix.flavor == 'portable'
- name: Build MSI Installer (Windows)
shell: pwsh
run: |
# Version always tracks the CMake project() version, no wxs edit needed.
$match = Select-String -Path CMakeLists.txt `
-Pattern '^\s*VERSION\s+([0-9]+\.[0-9]+\.[0-9]+)' | Select-Object -First 1
if (-not $match) { throw "Could not extract project version from CMakeLists.txt" }
$version = $match.Matches[0].Groups[1].Value
Write-Host "Project version: $version"
# Toolset and extension versions must match: an unpinned extension resolves
# to the latest major (7.x), which a WiX 5 host cannot load (wixext5 vs wixext7).
$wixVersion = '5.0.2'
dotnet tool install --global wix --version $wixVersion
wix extension add -g WixToolset.UI.wixext/$wixVersion
wix extension add -g WixToolset.Util.wixext/$wixVersion
New-Item -ItemType Directory -Force -Path "${{github.workspace}}/build/upload-artifact" | Out-Null
# -arch x64 is mandatory: the package is MsiPackageType=x64.
# PayloadDir / ProductVersion / IconSource / BrandingDir override the wxs
# defaults for this runner (its defaults assume a build run from resource/wix).
wix build -arch x64 `
-ext WixToolset.UI.wixext/$wixVersion `
-ext WixToolset.Util.wixext/$wixVersion `
-d PayloadDir="${{github.workspace}}/build/artifacts" `
-d ProductVersion="$version" `
-d IconSource="${{github.workspace}}/gpgfrontend.ico" `
-d BrandingDir="${{github.workspace}}/resource/wix" `
-o "${{github.workspace}}/build/upload-artifact/GpgFrontend-${{env.SHORT_SHA}}-${{env.OS_IDENTIFIER}}-x86_64.msi" `
resource/wix/Package.wxs
# wix drops a .wixpdb next to the .msi; it is build metadata, not a deliverable.
Remove-Item -Force -ErrorAction SilentlyContinue `
"${{github.workspace}}/build/upload-artifact/*.wixpdb"
# An MSI installs into Program Files and keeps its data in the user
# profile, so it is only ever built from the installed flavour.
if: runner.os == 'Windows' && matrix.flavor == 'installed'
- name: Upload Artifact (Linux)
uses: actions/upload-artifact@v7
with:
name: gpgfrontend-${{matrix.os}}-${{matrix.flavor}}-${{env.BUILD_TYPE_LOWER}}-${{env.SHORT_SHA}}
path: ${{github.workspace}}/build/upload-artifact/GpgFrontend-*.AppImage*
if: runner.os == 'Linux'
- name: Upload Artifact (macOS)
uses: actions/upload-artifact@v7
with:
name: gpgfrontend-${{env.OS_IDENTIFIER}}-${{matrix.flavor}}-${{env.BUILD_TYPE_LOWER}}-${{env.SHORT_SHA}}
path: ${{github.workspace}}/build/upload-artifact/*
if: runner.os == 'macOS'
- name: Upload Artifact (Windows)
uses: actions/upload-artifact@v7
with:
name: gpgfrontend-${{matrix.os}}-${{matrix.flavor}}-${{env.BUILD_TYPE_LOWER}}-${{env.SHORT_SHA}}
path: ${{github.workspace}}/build/upload-artifact/*
if: runner.os == 'Windows'
release:
needs: build
runs-on: ubuntu-latest
# Only publish the rolling nightly release from pushes to the main branch.
# Stable releases (version tags) are packaged manually/offline.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: nightly-release-approval
steps:
- uses: actions/checkout@v6
with:
lfs: "false"
submodules: recursive
- name: Re-create nightly tag
env:
GH_TOKEN: ${{ github.token }}
run: |
cd ${{github.workspace}}
gh release delete nightly --repo saturneric/GpgFrontend --cleanup-tag --yes || true
git tag -f nightly $GITHUB_SHA
git push origin nightly --force
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
pattern: gpgfrontend-*
merge-multiple: true
- name: Ignore Non-Artifact Files
run: |
# Remove MAS specific .pkg files if they exist, since they are not
# needed for the release and we only want to keep the main artifacts
# (dmg, AppImage, zip).
find artifacts/ -type f -name '*.pkg' -delete
- name: Generate SHA256 checksums
run: |
sha256sum artifacts/* > artifacts/SHA256SUMS.txt
cat artifacts/SHA256SUMS.txt
- name: List files
run: ls -rl artifacts/
- name: Generate Nightly Release Title
id: release_title
run: echo "title=Nightly Build $(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Update Nightly Release
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
name: ${{ steps.release_title.outputs.title }}
draft: false
prerelease: true
body_path: ${{ github.workspace }}/.github/NIGHTLY_RELEASE.md
files: |
artifacts/*