mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [DlgCreateGame] Heap-allocate create game dialog to prevent crash on parent deletion * [Tests] Run the short-write cases without forking a death test The three cases that force a write failure - CardDatabaseTest.Xml4ShortWriteKeepsInstalledFile, OracleDatabaseWriter.ShortWriteKeepsPreviouslyInstalledDatabase and SpoilerFileTest.ShortWriteLeavesExistingFileIntact - all forked a child, lowered RLIMIT_FSIZE and ignored SIGXFSZ. That child is killed by SIGSEGV on the macOS runners, so all three have been red on every macOS leg of master since they landed, while the same cases pass on Linux. Cap the soft limit in the test process instead: ctest already runs one case per process, and the guard restores the limit and the SIGXFSZ handler, so a whole binary keeps working. Same real EFBIG failure, no fork, and macOS keeps its coverage of the writers. * [CI] Derive DEBUG_TEST and RUN_TESTS in the nightly job The shared steps gate on both, but build-linux-nightly never set them, so its coverage leg skipped "Build debug and test" and then failed collecting a report from an empty build/ directory on every nightly and tag run. continue-on-error kept the run green, so the coverage artifact silently never existed. Derive both exactly as the pull request job does. --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
1221 lines
60 KiB
YAML
1221 lines
60 KiB
YAML
name: Build Desktop
|
|
|
|
permissions:
|
|
actions: write # needed to delete entries in GHA cache (update ccache)
|
|
attestations: write # needed to persist the attestation.
|
|
contents: write # needed for e.g. vcpkg dependency graph updates
|
|
id-token: write # needed for signing certificate in attestation
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
paths:
|
|
- '*/**' # matches all files not in root
|
|
- '!**.md'
|
|
- '!.github/**'
|
|
- '!.tx/**'
|
|
- '!doc/**'
|
|
- '.github/workflows/desktop-build.yml'
|
|
- 'CMakeLists.txt'
|
|
- 'vcpkg.json'
|
|
- 'vcpkg' # needed to match submodule bumps (gitlink)
|
|
tags:
|
|
- '*'
|
|
pull_request:
|
|
paths:
|
|
- '*/**' # matches all files not in root
|
|
- '!**.md'
|
|
- '!.github/**'
|
|
- '!.tx/**'
|
|
- '!doc/**'
|
|
- '.github/workflows/desktop-build.yml'
|
|
- 'CMakeLists.txt'
|
|
- 'vcpkg.json'
|
|
- 'vcpkg' # needed to match submodule bumps (gitlink)
|
|
# The legs the pull-request matrix leaves out run here instead, so the legacy distros and
|
|
# the slow macOS runner stay covered without gating a pull request on them. Tags still
|
|
# build every leg, because a tag is what ships.
|
|
schedule:
|
|
- cron: '23 3 * * *'
|
|
|
|
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on tag --> release)
|
|
concurrency:
|
|
# The event name is part of the group because a nightly run and a push run on master share
|
|
# refs/heads/master but must not cancel each other: the nightly would drop the push's vcpkg
|
|
# import, and a merge landing during the nightly would drop that night's coverage.
|
|
group: "${{ github.workflow }} @ ${{ github.ref_name }} (${{ github.event_name }})"
|
|
cancel-in-progress: ${{ github.ref_type != 'tag' }}
|
|
|
|
jobs:
|
|
configure:
|
|
name: Configure
|
|
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
|
outputs:
|
|
tag: ${{ steps.configure.outputs.tag }}
|
|
sha: ${{ steps.configure.outputs.sha }}
|
|
# MAJ.MIN.PATCH[-beta[.N]] of a tag build; empty otherwise. Mirrors what
|
|
# cmake/getversion.cmake derives as PROJECT_VERSION, which the client bakes as its
|
|
# Sentry release (cmake/CrashReporting.cmake) — symbol uploads must use the same
|
|
# string so stack traces de-symbolicate against this build's binaries.
|
|
sentry_release: ${{ steps.configure.outputs.sentry_release }}
|
|
|
|
steps:
|
|
- name: "Configure"
|
|
env:
|
|
RESOLVED_SHA: ${{ case(github.event_name == 'pull_request', github.event.pull_request.head.sha, github.sha) }}
|
|
id: configure
|
|
shell: bash
|
|
run: |
|
|
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
|
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
|
# Tag format is YYYY-MM-DD-{Release|Development}-MAJ.MIN.PATCH[-beta[.N]];
|
|
# PROJECT_VERSION is the tail after the type (see cmake/getversion.cmake).
|
|
echo "sentry_release=$(sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-(Release|Development)-//' <<< "$GITHUB_REF_NAME")" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: "Checkout"
|
|
if: steps.configure.outputs.tag != null
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0 # fetch all history for all branches and tags
|
|
|
|
- name: "Prepare release parameters"
|
|
id: prepare
|
|
if: steps.configure.outputs.tag != null
|
|
shell: bash
|
|
env:
|
|
TAG: ${{ steps.configure.outputs.tag }}
|
|
run: .ci/prep_release.sh
|
|
|
|
- name: "Create release"
|
|
if: steps.configure.outputs.tag != null
|
|
id: create_release
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
tag_name: ${{ steps.configure.outputs.tag }}
|
|
target: ${{ steps.configure.outputs.sha }}
|
|
release_name: ${{ steps.prepare.outputs.title }}
|
|
body_path: ${{ steps.prepare.outputs.body_path }}
|
|
prerelease: ${{ steps.prepare.outputs.is_beta }}
|
|
run: |
|
|
args=()
|
|
[[ $prerelease == yes ]] && args+=(--prerelease)
|
|
|
|
gh release create "$tag_name" --verify-tag --draft "${args[@]}" \
|
|
--target "$target" \
|
|
--title "$release_name" \
|
|
--notes-file "$body_path"
|
|
|
|
vcpkg-baseline:
|
|
name: vcpkg baseline
|
|
runs-on: ubuntu-slim
|
|
steps:
|
|
- name: "Checkout"
|
|
uses: actions/checkout@v7
|
|
|
|
# vcpkg-tool reads versions/baseline.json out of the commit named by
|
|
# `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule
|
|
# pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by
|
|
# `git add` in a PR - makes every vcpkg leg fail in configure with "failed to
|
|
# `git show` versions/baseline.json". That reads like a baseline or network
|
|
# problem, when the pointer is what has to move. Check the two agree here in
|
|
# seconds instead of on five build legs.
|
|
- name: "Check the submodule pointer against the manifest baseline"
|
|
shell: bash
|
|
run: |
|
|
pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)"
|
|
baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)"
|
|
if [[ -z "$pointer" || -z "$baseline" ]]; then
|
|
echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline"
|
|
exit 1
|
|
fi
|
|
if [[ "$pointer" != "$baseline" ]]; then
|
|
echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline"
|
|
echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg"
|
|
exit 1
|
|
fi
|
|
echo "vcpkg submodule and builtin-baseline agree on $baseline"
|
|
|
|
build-linux:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# The files in ".ci/$distro$version" correspond to the values given here
|
|
include:
|
|
- distro: Arch
|
|
|
|
allow-failure: yes
|
|
package: skip # We are packaged in Arch already
|
|
debug_test: yes # see the note on Fedora 44 below
|
|
|
|
- distro: Servatrice_Debian
|
|
version: 12
|
|
|
|
package: DEB
|
|
server_only: yes
|
|
test: skip
|
|
ccache_size: 400M # server only, no client to compile
|
|
|
|
- distro: Debian
|
|
version: 13
|
|
|
|
package: DEB
|
|
ccache_size: 850M # the Release build plus the suite it runs
|
|
|
|
- distro: Fedora
|
|
version: 44
|
|
|
|
package: RPM
|
|
# Arch and Fedora 44 are the two newest toolchains, and the -Wno-error= exceptions
|
|
# in CMakeLists.txt exist for exactly their warnings. They keep compiling Debug:
|
|
# only the Debug configuration passes -Werror, so these two are the legs where a
|
|
# new warning in either compiler still fails the build.
|
|
debug_test: yes
|
|
ccache_size: 900M # Debug for the suite and Release for the package
|
|
|
|
- distro: Ubuntu
|
|
version: 26.04
|
|
|
|
package: DEB
|
|
test: skip # The sanitizer leg below runs this distro's suite
|
|
|
|
# The one leg that runs the suite under AddressSanitizer and
|
|
# UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the
|
|
# uninstrumented suite, so nothing is lost by instrumenting this one.
|
|
#
|
|
# It packages nothing. A sanitized Debug build is not something to ship, and
|
|
# carrying the package build here meant 18 minutes of Release compile queued
|
|
# behind the instrumented one in a single job. The leg above publishes this
|
|
# distro's DEB.
|
|
- distro: Ubuntu
|
|
version: 26.04
|
|
variant: sanitize # separate image and ccache, same distro
|
|
|
|
package: skip
|
|
sanitize: yes
|
|
ccache_size: 800M # instrumented objects are larger
|
|
|
|
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
|
needs: configure
|
|
# The nightly legs live in build-linux-nightly below. A leg is in exactly one of the two
|
|
# matrices, so nothing that reviews a change waits for a distro nobody is changing for.
|
|
if: github.event_name != 'schedule'
|
|
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
|
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
|
timeout-minutes: 70
|
|
env:
|
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
|
CCACHE_EVICTION_AGE: 7d
|
|
# The bound is sized against the repository's 10 GB cache quota
|
|
# (https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy),
|
|
# not against what one leg would like. The quota currently holds about 1.1 GB of
|
|
# CodeQL trap, 1.1 GB of vcpkg, 0.7 GB of Qt and 2.1 GB of docker buildkit layers
|
|
# (mode=min in docker-release.yml, added here, brings the last one down to roughly
|
|
# 0.3 GB - the one input here that is an expectation rather than a measurement, since
|
|
# mode=min still writes one layer per pushed binary) plus the compiler caches of the nine legs a pull request runs. The five
|
|
# nightly-only legs keep no compiler cache (see PERSIST_CCACHE below), which is what
|
|
# makes these per-leg bounds affordable. Accounting for all of it: 6.45G of ccache
|
|
# bounds + 3.2G of the fixed entries = 9.65G against the 10G quota.
|
|
#
|
|
# A configuration needs roughly 500-850 MB on Linux, so the old uniform 600M bound sat
|
|
# below what a leg writes: ccache spent each build deleting the entries the next run
|
|
# asked for (measured: 2624-3720 cleanup passes per build, 8% of calls served). Each
|
|
# leg states the size it needs instead: one configuration for most, two for Fedora 44
|
|
# (Debug for the suite, Release for the package), the server alone for Servatrice.
|
|
# Raising these further means caching fewer legs, since GitHub evicts a whole entry
|
|
# when the total is exceeded.
|
|
#
|
|
# Every key listed in LIVE_CCACHE_KEYS in prune-caches below must match this matrix.
|
|
CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }}
|
|
CMAKE_GENERATOR: 'Ninja'
|
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
|
IMAGE_SUFFIX: ${{ matrix.variant }}
|
|
# Which build the suite runs in, decided once per leg here so the steps' if: conditions
|
|
# and their shells cannot drift apart. A step's own env is not visible to its if:, which
|
|
# is why these live at job level rather than on the release step.
|
|
# DEBUG_TEST the instrumented legs (symbols and instrumentation matter) and the two
|
|
# newest distro toolchains, which only fail on a warning in Debug.
|
|
# RUN_TESTS every other leg that tests runs the suite against the release build it may
|
|
# also package, so testing and packaging cost one compile instead of two.
|
|
DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }}
|
|
RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }}
|
|
# Whether this leg restores and saves its compiler cache. Every leg a pull request
|
|
# runs keeps one; the nightly-only legs do not, so that the quota is spent on the
|
|
# builds that review a change. A tag build runs both sets, and the nightly legs are
|
|
# cold there - accepted, because a tag is rare and only the pull-request legs gate.
|
|
PERSIST_CCACHE: 'true'
|
|
|
|
# Shared with build-linux-nightly so the two leg sets run the same steps.
|
|
steps: &linux-steps
|
|
- name: "Checkout"
|
|
uses: actions/checkout@v7
|
|
|
|
- name: "Restore compiler cache (ccache)"
|
|
id: restore_ccache
|
|
if: env.PERSIST_CCACHE == 'true'
|
|
uses: actions/cache/restore@v6
|
|
with:
|
|
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
|
path: ${{ env.CACHE }}/.ccache
|
|
|
|
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
|
shell: bash
|
|
run: source .ci/docker.sh --build
|
|
|
|
# The instrumented legs and the two newest distro toolchains build and test Debug; every
|
|
# other leg runs its suite against the release build below, so testing and packaging cost
|
|
# one compile instead of two.
|
|
- name: "Build debug and test"
|
|
if: env.DEBUG_TEST == 'true'
|
|
shell: bash
|
|
env:
|
|
sanitize: '${{ matrix.sanitize }}'
|
|
coverage: '${{ matrix.coverage }}'
|
|
run: |
|
|
source .ci/docker.sh
|
|
args=()
|
|
[[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
|
|
args+=(--ccache "$CCACHE_SIZE")
|
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
|
|
|
extra=()
|
|
[[ $sanitize == yes ]] && extra+=(--sanitize)
|
|
[[ $coverage == yes ]] && extra+=(--coverage)
|
|
|
|
RUN --server --debug --test "${extra[@]}" "${args[@]}"
|
|
|
|
# RUN bind-mounts the workspace into the container, so the report the build wrote into
|
|
# build/ is already on the host and no second container is needed to collect it.
|
|
- name: "Collect coverage report"
|
|
if: matrix.coverage == 'yes'
|
|
shell: bash
|
|
run: |
|
|
mkdir -p coverage
|
|
# --html-details writes one page per source file plus a stylesheet and script, and the
|
|
# index links to all of them by relative name. Copying the index alone would publish a
|
|
# report where every link 404s, so the whole family has to travel together.
|
|
cp build/coverage-details* coverage/
|
|
cp build/coverage.lcov build/coverage.xml build/coverage-summary.json coverage/
|
|
ls -la coverage/
|
|
|
|
- name: "Upload coverage artifact"
|
|
if: matrix.coverage == 'yes'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: coverage-report
|
|
path: coverage/
|
|
if-no-files-found: warn
|
|
|
|
# One build serves both purposes: the suite runs against the same configuration that
|
|
# gets packaged, so a packaging leg no longer pays for a full Debug compile it then
|
|
# throws away. macOS and Windows have always tested their release build.
|
|
#
|
|
# Instrumented and debug-test legs reach this step only when they also package; their
|
|
# suite already ran in the debug step above, so the tests are not repeated here.
|
|
- name: "Build release, test and package"
|
|
id: build
|
|
if: matrix.package != 'skip' || env.RUN_TESTS == 'true'
|
|
shell: bash
|
|
env:
|
|
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
|
package: '${{ matrix.package }}'
|
|
server_only: '${{ matrix.server_only }}'
|
|
# Only a leg that packages builds the shipping configuration; a release build that
|
|
# only runs tests stays free of the crash reporting SDK.
|
|
WITH_CRASH_REPORTING: ${{ matrix.package != 'skip' && 'ON' || '' }}
|
|
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
|
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
|
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
|
# and the symbol upload below just like full releases.
|
|
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
|
run: |
|
|
source .ci/docker.sh
|
|
args=()
|
|
[[ $server_only == yes ]] && args+=(--no-client)
|
|
[[ $RUN_TESTS == true ]] && args+=(--test)
|
|
[[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
|
|
args+=(--ccache "$CCACHE_SIZE")
|
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
|
args+=(--suffix "$SUFFIX")
|
|
# The container inherits no environment, so the DSN rides in explicitly. -e without
|
|
# =value passes it through (possibly empty); compile.sh only consumes it for Release
|
|
# configures, and empty keeps the mock fallback.
|
|
export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN"
|
|
|
|
# --package takes an optional type, so the flag and its argument have to travel
|
|
# together or the type would become the next flag.
|
|
package_args=()
|
|
[[ -n $package && $package != skip ]] && package_args+=(--package "$package")
|
|
|
|
# compile.sh runs the tests before the package target, so a failing suite stops the
|
|
# leg before anything is built into an installer.
|
|
RUN --server --release "${package_args[@]}" "${args[@]}"
|
|
|
|
- name: "Install sentry-cli"
|
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
|
# Downloads from Sentry's release registry: a Sentry outage or a broken install under
|
|
# Git Bash must not take down the release assets below.
|
|
continue-on-error: true
|
|
shell: bash
|
|
env:
|
|
SENTRY_CLI_VERSION: '3.8.0'
|
|
run: |
|
|
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
|
curl -sL https://sentry.io/get-cli/ | bash
|
|
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
|
|
|
- name: "Upload debug symbols to Sentry"
|
|
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
|
# A Sentry outage or rotated token must not take down the release assets below.
|
|
continue-on-error: true
|
|
shell: bash
|
|
env:
|
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
run: |
|
|
paths=()
|
|
# The binaries only, not the directories: a directory makes sentry-cli walk the
|
|
# whole build tree, where it finds (and uploads) object files and other fragments
|
|
# that can never match a stack frame. The packaged copy is split and stripped by
|
|
# cpack, but keeps this binary's GNU build-id, which is how Sentry matches it.
|
|
for app in cockatrice oracle servatrice; do
|
|
binary="build/$app/$app"
|
|
[[ -f $binary ]] && paths+=("$binary")
|
|
done
|
|
if [[ ${#paths[@]} == 0 ]]; then
|
|
# Fail loudly instead of skipping: see the Windows/macOS leg below — this step
|
|
# is the only way symbols reach Sentry and `continue-on-error` keeps the job
|
|
# green either way, so a silent skip looks exactly like a successful upload.
|
|
echo "::error::No debug symbol files found; nothing would be uploaded"
|
|
exit 1
|
|
fi
|
|
# Project slug on Sentry; must match the project the baked DSN points at.
|
|
# No --release here: `debug-files upload` has no such option (sentry-cli errors on
|
|
# the unknown flag and uploads nothing), and debug information files are matched to
|
|
# events by debug ID via the event's debug_meta, not by release. The release grouping
|
|
# comes from the client event itself (COCKATRICE_SENTRY_RELEASE).
|
|
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
|
|
|
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
|
- name: "Delete remote compiler cache (ccache)"
|
|
if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true' && steps.restore_ccache.outputs.cache-hit
|
|
continue-on-error: true
|
|
env:
|
|
CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
|
|
echo "Cache deleted successfully"
|
|
fi
|
|
|
|
- name: "Save updated compiler cache (ccache)"
|
|
if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true'
|
|
uses: actions/cache/save@v6
|
|
with:
|
|
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
|
path: ${{ env.CACHE }}/.ccache
|
|
|
|
- name: "Upload artifact"
|
|
id: upload_artifact
|
|
if: matrix.package != 'skip'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
archive: false
|
|
if-no-files-found: error
|
|
path: ${{ steps.build.outputs.path }}
|
|
|
|
- name: "Upload to release"
|
|
id: upload_release
|
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
|
shell: bash
|
|
env:
|
|
asset_name: ${{ steps.build.outputs.fullname }}
|
|
asset_path: ${{ steps.build.outputs.path }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
tag_name: ${{ needs.configure.outputs.tag }}
|
|
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
|
|
|
- name: "Attest binary provenance"
|
|
id: attestation
|
|
if: steps.upload_release.outcome == 'success'
|
|
uses: actions/attest@v4
|
|
with:
|
|
show-summary: false
|
|
subject-path: ${{ steps.build.outputs.path }}
|
|
|
|
- name: "Verify binary attestation"
|
|
if: steps.attestation.outcome == 'success'
|
|
shell: bash
|
|
env:
|
|
BUILD_PATH: ${{ steps.build.outputs.path }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
|
|
|
|
build-linux-nightly:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- distro: Debian
|
|
version: 12 # previous stable, still covered by the nightly
|
|
|
|
package: DEB
|
|
test: skip # Running tests on all distros is superfluous
|
|
|
|
- distro: Fedora
|
|
version: 43 # previous release, still covered by the nightly
|
|
|
|
package: RPM
|
|
test: skip # Running tests on all distros is superfluous
|
|
|
|
- distro: Ubuntu
|
|
version: 24.04 # previous LTS, still covered by the nightly
|
|
|
|
package: DEB
|
|
test: skip # Running tests on all distros is superfluous
|
|
|
|
# The one leg that runs instrumented for line coverage and publishes the report as
|
|
# a workflow artifact. It stays a separate leg rather than a second flag on the
|
|
# sanitizer leg because the two instruments answer different questions: the
|
|
# sanitizer leg reports what the run did wrong, the coverage leg reports what the
|
|
# run never touched at all. Combining them would also slow both down.
|
|
#
|
|
# It moved here from build-linux: gcovr.cfg sets no fail_under, so the leg cannot
|
|
# fail a build, and nothing consumes the report per change, which made it an
|
|
# artifact nobody read on every pull request. On the default branch it becomes a
|
|
# trend instead. Image and ccache keys come from distro, version and variant, so
|
|
# they are unchanged and the warm cache that leg built still matches.
|
|
- distro: Ubuntu
|
|
version: 26.04
|
|
variant: cover # separate image and ccache, same distro
|
|
|
|
coverage: yes
|
|
package: skip
|
|
# A report must not redden the nightly run: vcpkg-cache-import only imports the
|
|
# archives of a run whose conclusion is success.
|
|
allow-failure: yes
|
|
|
|
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
|
needs: configure
|
|
# A nightly and a tag build cover the legs a pull request or a merge does not wait for:
|
|
# the previous release of each distro, and the one instrument that only reports.
|
|
if: github.event_name == 'schedule' || github.ref_type == 'tag'
|
|
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
|
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
|
timeout-minutes: 70
|
|
env:
|
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
|
CCACHE_EVICTION_AGE: 7d
|
|
# These legs do not keep a compiler cache: they run once a day (and on tags) and gate
|
|
# nothing, so their share of the 10 GB quota goes to the pull-request legs instead.
|
|
# A cold nightly build costs minutes on a runner nobody waits for; a cold pull request
|
|
# build costs a review. CCACHE_SIZE is still passed so ccache trims its scratch dir.
|
|
PERSIST_CCACHE: 'false'
|
|
CCACHE_SIZE: 600M
|
|
CMAKE_GENERATOR: 'Ninja'
|
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
|
IMAGE_SUFFIX: ${{ matrix.variant }}
|
|
# The shared steps below gate on these, so they have to be derived here too. Left
|
|
# undefined they silently skipped the build the leg exists for: the coverage leg
|
|
# found build/ empty and its report collection failed on every nightly and tag run.
|
|
# Same derivation as the pull request job above.
|
|
DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }}
|
|
RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }}
|
|
steps: *linux-steps
|
|
|
|
build-vcpkg:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: macOS
|
|
target: 14 # EOL 2026-09-14
|
|
runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
|
|
|
|
ccache_eviction_age: 7d
|
|
cmake_generator: Ninja
|
|
make_package: 1
|
|
override_target: 14
|
|
package_suffix: "-macOS14"
|
|
qt_version: 6.11.*
|
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
|
soc: Apple
|
|
type: Release
|
|
use_ccache: 1
|
|
vcpkg_deployment_target: 14
|
|
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
|
|
- os: macOS
|
|
target: 15 # EOL 2027-??
|
|
runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
|
|
|
|
ccache_eviction_age: 7d
|
|
cmake_generator: Ninja
|
|
make_package: 1
|
|
override_target: 15
|
|
package_suffix: "-macOS15"
|
|
qt_version: 6.11.*
|
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
|
soc: Apple
|
|
type: Release
|
|
use_ccache: 1
|
|
vcpkg_deployment_target: 14
|
|
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
|
|
- os: macOS
|
|
target: 27
|
|
runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
|
|
|
|
ccache_eviction_age: 7d
|
|
cmake_generator: Ninja
|
|
qt_version: 6.11.*
|
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
|
soc: Apple
|
|
type: Debug
|
|
use_ccache: 1
|
|
vcpkg_deployment_target: 14
|
|
vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
|
|
|
|
- os: Windows
|
|
target: 10
|
|
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
|
|
|
|
# Ninja builds the whole dependency graph at once instead of one
|
|
# MSBuild node at a time, runs the compiler itself, and matches the
|
|
# Linux and macOS legs. Its median build phase measured ~17 min
|
|
# against ~21 for the Visual Studio generator (shared 4-core hosts
|
|
# swing either by minutes, so read medians, not single runs).
|
|
cmake_generator: Ninja
|
|
make_package: 1
|
|
package_suffix: "-Win10"
|
|
qt_version: 6.11.*
|
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
|
type: Release
|
|
# Scope is named after the vs2026 toolchain, not the generator: the
|
|
# ABI hash covers cl.exe, which is identical, so this leg keeps
|
|
# restoring the warm archives. Renaming would orphan them.
|
|
vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
|
|
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
|
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
|
needs: [configure, vcpkg-baseline]
|
|
# The nightly leg lives in build-vcpkg-nightly below, next to the Linux nightly legs.
|
|
if: github.event_name != 'schedule'
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 100
|
|
env:
|
|
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
|
# macOS configurations compile about twice the TUs of a Linux one, so each of these
|
|
# three legs gets a bound above what it has been measured to write. The four macOS
|
|
# legs used to be bounded at 600M from the same quota as the Linux ones; the three
|
|
# pull-request legs now get 700M each while the nightly Intel leg keeps none, and
|
|
# that is what fits (see CCACHE_SIZE in build-linux for the full arithmetic).
|
|
CCACHE_SIZE: 700M
|
|
PERSIST_CCACHE: 'true'
|
|
|
|
# Shared with build-vcpkg-nightly so the two leg sets run the same steps.
|
|
steps: &vcpkg-steps
|
|
- name: "Checkout"
|
|
uses: actions/checkout@v7
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: "[Windows] Add msbuild to PATH"
|
|
if: matrix.os == 'Windows'
|
|
uses: microsoft/setup-msbuild@v3
|
|
with:
|
|
msbuild-architecture: x64
|
|
|
|
- name: "[macOS] Setup ccache"
|
|
if: matrix.os == 'macOS' && matrix.use_ccache == 1
|
|
run: brew install ccache
|
|
|
|
- name: "[macOS] Restore compiler cache (ccache)"
|
|
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true'
|
|
id: restore_ccache
|
|
uses: actions/cache/restore@v6
|
|
with:
|
|
key: ccache-${{ matrix.os }}${{ matrix.override_target }}-${{ matrix.runner }}
|
|
path: ${{ env.CCACHE_DIR }}
|
|
|
|
- name: "[macOS] Install aqtinstall"
|
|
if: matrix.os == 'macOS'
|
|
run: pipx install aqtinstall
|
|
|
|
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
|
|
- name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input"
|
|
if: matrix.os == 'macOS'
|
|
env:
|
|
QT_VERSION: ${{ matrix.qt_version }}
|
|
id: resolve_qt_version
|
|
shell: bash
|
|
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
|
|
|
|
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}"
|
|
if: matrix.os == 'macOS'
|
|
id: restore_qt
|
|
uses: actions/cache/restore@v6
|
|
with:
|
|
key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin
|
|
path: ${{ github.workspace }}/Qt
|
|
|
|
# Using jurplel/install-qt-action to install Qt without using brew
|
|
# Qt build using vcpkg either just fails or takes too long to build
|
|
- name: "[macOS] Install fat Qt ${{ matrix.qt_version }}"
|
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
|
uses: jurplel/install-qt-action@v4
|
|
with:
|
|
cache: false
|
|
# cache-key-prefix: Qt
|
|
dir: ${{ github.workspace }} # thinning script depends on this location
|
|
modules: ${{ matrix.qt_modules }}
|
|
version: ${{ matrix.qt_version }}
|
|
|
|
- name: "[macOS] Create thin Qt libraries"
|
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
|
run: .ci/thin_macos_qtlib.sh
|
|
|
|
- name: "[macOS] Cache thin Qt libraries"
|
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master'
|
|
uses: actions/cache/save@v6
|
|
with:
|
|
key: ${{ steps.restore_qt.outputs.cache-primary-key }}
|
|
path: ${{ github.workspace }}/Qt
|
|
|
|
- name: "[Windows] Install Qt ${{ matrix.qt_version }}"
|
|
if: matrix.os == 'Windows'
|
|
uses: jurplel/install-qt-action@v4
|
|
with:
|
|
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
|
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
|
cache: ${{ github.ref == 'refs/heads/master' }}
|
|
cache-key-prefix: Qt
|
|
modules: ${{ matrix.qt_modules }}
|
|
version: ${{ matrix.qt_version }}
|
|
|
|
- name: "[Windows] Install NSIS"
|
|
if: matrix.os == 'Windows'
|
|
shell: bash
|
|
run: choco install nsis
|
|
|
|
# Ninja invokes cl.exe directly and needs the MSVC environment on PATH.
|
|
- name: "[Windows] Set up MSVC environment"
|
|
if: matrix.os == 'Windows'
|
|
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0
|
|
with:
|
|
arch: x64
|
|
|
|
- name: "Setup vcpkg cache"
|
|
id: vcpkg-cache
|
|
uses: TAServers/vcpkg-cache@v3
|
|
with:
|
|
# Every entry is keyed by vcpkg ABI hash, and one hash set exists per platform,
|
|
# target and toolchain. The action restores every entry it finds under the
|
|
# prefix, not just the hashes vcpkg asks for, so with one shared vcpkg/
|
|
# namespace every leg downloads all 66 archives of all 5 legs on every run.
|
|
# Each of those restores also refreshes the entry's last access date, which
|
|
# is why a dead ABI generation is never evicted by age. Naming the scope is
|
|
# what makes those generations identifiable, and prune-caches below able to
|
|
# delete them.
|
|
prefix: ${{ matrix.vcpkg_scope }}
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# The action only creates its directory when the prefix matched at least one
|
|
# entry, and it reads that directory unguarded when it saves at the end of the
|
|
# job. A scope nobody has written to yet therefore leaves the directory missing
|
|
# and fails the job, which is exactly the state every scope starts in. Make the
|
|
# directory up front so an empty scope is a cache miss instead of an error.
|
|
# always() covers the cache step itself failing: the action's post save still runs
|
|
# then, and it should report the real error instead of ENOENT. It cannot rescue a
|
|
# failure before the cache step, since a post step only runs for an action whose
|
|
# main step did. The fallback keeps this guard from failing when the cache step was
|
|
# skipped and left no path output.
|
|
- name: "Create vcpkg cache directory"
|
|
if: always()
|
|
shell: bash
|
|
env:
|
|
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
|
run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
|
|
|
|
# Uses environment variables, see compile.sh for more details
|
|
- name: "Build Cockatrice"
|
|
id: build
|
|
shell: bash
|
|
env:
|
|
BUILDTYPE: '${{ matrix.type }}'
|
|
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
|
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
|
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
|
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
|
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
|
# and the symbol upload below just like full releases.
|
|
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
|
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
|
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
|
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
|
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
|
MAKE_PACKAGE: '${{ matrix.make_package }}'
|
|
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
|
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
|
USE_CCACHE: ${{ matrix.use_ccache }}
|
|
# Only pushes to the default branch may write the shared binary cache: the
|
|
# archives a run compiles are only as trustworthy as the code they were
|
|
# built from, and only merged code may populate the scope that master and
|
|
# release builds restore and link. Pull requests (same-repository ones
|
|
# included) restore the shared cache but never write it; with the source
|
|
# left at read the collect step below finds nothing on PRs, so no
|
|
# pull-request-scoped entry is ever saved that no later run could restore.
|
|
# Forks pushing to their own default branch resolve github.ref against
|
|
# their own repository, so they keep a working cache of their own there.
|
|
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
|
|
VCPKG_DISABLE_METRICS: 1
|
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
|
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
|
# Release legs compile the sentry-native SDK; Debug legs stay SDK-free.
|
|
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
|
|
run: .ci/compile.sh --server --test --vcpkg
|
|
|
|
- name: "Install sentry-cli"
|
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
|
# Same as the Linux leg above: the install downloads from Sentry's registry, so a
|
|
# failure here must not block the release upload below.
|
|
continue-on-error: true
|
|
shell: bash
|
|
env:
|
|
SENTRY_CLI_VERSION: '3.8.0'
|
|
run: |
|
|
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
|
curl -sL https://sentry.io/get-cli/ | bash
|
|
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
|
|
|
- name: "Upload debug symbols to Sentry"
|
|
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
|
# A Sentry outage or rotated token must not take down the release assets below.
|
|
continue-on-error: true
|
|
shell: bash
|
|
env:
|
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
run: |
|
|
paths=()
|
|
if [[ '${{ matrix.os }}' == 'Windows' ]]; then
|
|
# Top-level CMakeLists.txt links /DEBUG even in release, so the linker PDBs
|
|
# land next to the binaries. Ninja is single-config, so that is
|
|
# build/<target>/ and NOT build/<target>/Release/ — the same globs the symbol
|
|
# artifact step below uses.
|
|
#
|
|
# The executables are uploaded as well, in the same invocation. On 64-bit
|
|
# Windows the Call Frame Information (.pdata/.xdata) lives in the PE and is
|
|
# not copied into the PDB, so a PDB on its own leaves Sentry reporting
|
|
# "Stack Unwinding: Missing" for the image. sentry-cli also needs to see the
|
|
# PE and its PDB together: only then does it associate the PE's code
|
|
# identifier with the PDB's debug identifier.
|
|
shopt -s nullglob
|
|
for target in cockatrice oracle servatrice; do
|
|
paths+=("build/$target"/*.exe "build/$target"/*.pdb)
|
|
done
|
|
shopt -u nullglob
|
|
|
|
# Qt's own runtime DLLs, taken from required-runtime.txt — the manifest
|
|
# .ci/compile.sh asserts the installer against — so the set uploaded is
|
|
# the set an install actually carries. The PE is the only carrier of
|
|
# unwind information for a 64-bit image, and sending it in the same
|
|
# invocation as a PDB is what lets sentry-cli associate the PE's code
|
|
# identifier with the PDB's debug identifier. Qt ships no PDBs with the
|
|
# SDK, so Qt frames resolve through the export table: names, no lines.
|
|
qt_dir=
|
|
qt_core_dir=
|
|
# Existence guards matter: the step runs under `bash -eo pipefail`, so a
|
|
# redirection-less sed over a missing file would abort the step outright
|
|
# and continue-on-error would hide it.
|
|
if [[ -f build/CMakeCache.txt ]]; then
|
|
qt_core_dir=$(sed -n 's/^Qt6Core_DIR:PATH=//p' build/CMakeCache.txt | head -1)
|
|
fi
|
|
if [[ -n $qt_core_dir ]]; then
|
|
qt_dir=$(dirname "$(dirname "$(dirname "$qt_core_dir")")")
|
|
fi
|
|
if [[ -z $qt_dir || ! -d $qt_dir ]]; then
|
|
# Warnings, not failures: the upload of our own binaries is what this
|
|
# step is for, and continue-on-error already means an upload failure
|
|
# cannot be told apart from a successful one.
|
|
echo "::warning::Qt runtime directory not resolved from build/CMakeCache.txt; Qt images get no unwind information"
|
|
elif [[ ! -f build/required-runtime.txt ]]; then
|
|
echo "::warning::build/required-runtime.txt is missing; cannot tell which Qt DLLs ship"
|
|
else
|
|
while IFS= read -r runtime; do
|
|
name=${runtime##*/}
|
|
if [[ $runtime == */* ]]; then
|
|
# A Qt plugin: the install root keeps it as plugins/<type>/<name>.
|
|
# `|| true`: an unmatched glob makes ls exit non-zero, and pipefail
|
|
# turns that into a step abort.
|
|
runtime_path=$(ls "$qt_dir/plugins"/*/"$name" 2>/dev/null | head -1) || true
|
|
else
|
|
runtime_path="$qt_dir/bin/$name"
|
|
fi
|
|
if [[ -f ${runtime_path:-} ]]; then
|
|
paths+=("$runtime_path")
|
|
else
|
|
echo "::warning::shipped Qt runtime $runtime not found under $qt_dir"
|
|
fi
|
|
done < <(grep -E '^(Qt6[^/]*\.dll|Plugins/)' build/required-runtime.txt)
|
|
fi
|
|
else
|
|
# Release builds carry DWARF (-g, kept by CrashReporting.cmake); macOS keeps its
|
|
# debug info in dSYM bundles, so materialize them before uploading.
|
|
for app in cockatrice oracle servatrice; do
|
|
binary="build/$app/$app.app/Contents/MacOS/$app"
|
|
if [[ -f $binary ]]; then
|
|
if [[ '${{ matrix.os }}' == 'macOS' ]]; then
|
|
dsymutil "$binary" -o "$binary.dSYM"
|
|
paths+=("$binary.dSYM")
|
|
# The dSYM holds the debug information, but the unwind information
|
|
# Sentry needs to walk a minidump stack lives in the Mach-O itself.
|
|
paths+=("$binary")
|
|
else
|
|
paths+=("$binary")
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
if [[ ${#paths[@]} == 0 ]]; then
|
|
# Fail loudly instead of skipping: this step is the only way symbols reach
|
|
# Sentry, and `continue-on-error` keeps the job green either way, so a
|
|
# silent skip is indistinguishable from a successful upload. It also hid a
|
|
# wrong Windows path (build/<target>/Release, which Ninja never creates).
|
|
echo "::error::No debug symbol files found; nothing would be uploaded"
|
|
exit 1
|
|
fi
|
|
# Project slug on Sentry; must match the project the baked DSN points at.
|
|
# No --release: see the Linux leg above — the flag does not exist on
|
|
# `debug-files upload` and DIFs are matched by debug ID, not release.
|
|
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
|
|
|
|
# CPack reports a failed makensis run as "Problem running NSIS command"
|
|
# plus a path to NSISOutput.log, and that log lives only on the runner, so
|
|
# the actual compile error is otherwise nowhere in the workflow output.
|
|
# Dump it, or a broken installer script is undiagnosable from CI alone.
|
|
# shell: bash matters here - the default pwsh resolves `find` to Windows'
|
|
# text-search find.exe, which errors instead of searching.
|
|
- name: "Dump NSIS compile log"
|
|
if: failure() && matrix.os == 'Windows'
|
|
shell: bash
|
|
run: find . -name NSISOutput.log -exec cat {} +
|
|
|
|
# A master run hands over the archives it had to build itself so that
|
|
# vcpkg-cache-import.yml, which runs in the default branch scope, can save them
|
|
# where later runs can restore them. Pull requests hand over nothing: their cache
|
|
# entries would be scoped to refs/pull/<n>/merge and stay unreadable for every
|
|
# other run, so writing them only burns the repository's quota.
|
|
# vcpkg_upload keeps this to one leg per scope: macOS 14, 15 and 26 share a scope
|
|
# by design, and all three rebuild the same archives, so uploading from each of them
|
|
# would publish three identical 269 MB artifacts under one artifact name and make
|
|
# the importer spawn a save job per copy.
|
|
- name: "Collect new vcpkg entries"
|
|
id: vcpkg_new
|
|
if: github.ref == 'refs/heads/master' && matrix.vcpkg_upload == 1
|
|
shell: bash
|
|
env:
|
|
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
|
|
GH_TOKEN: ${{ github.token }}
|
|
VCPKG_SCOPE: '${{ matrix.vcpkg_scope }}'
|
|
run: |
|
|
# The archive name is the ABI hash, so a missing key is exactly what vcpkg had
|
|
# to compile from source in this job.
|
|
gh api --paginate \
|
|
"repos/$GITHUB_REPOSITORY/actions/caches?ref=refs/heads/master&key=$VCPKG_SCOPE&per_page=100" \
|
|
--jq '.actions_caches[].key' | sort -u >"$RUNNER_TEMP/master-keys.txt"
|
|
find "$CACHE_DIR" -type f -name '*.zip' -exec basename {} .zip \; |
|
|
sed "s|^|$VCPKG_SCOPE|" | sort -u >"$RUNNER_TEMP/local-keys.txt"
|
|
comm -23 "$RUNNER_TEMP/local-keys.txt" "$RUNNER_TEMP/master-keys.txt" >"$RUNNER_TEMP/new-keys.txt"
|
|
|
|
mkdir -p "$RUNNER_TEMP/vcpkg-new"
|
|
: >"$RUNNER_TEMP/vcpkg-new/keys.txt"
|
|
while read -r key; do
|
|
hash="${key##*/}"
|
|
archive="$(find "$CACHE_DIR" -type f -name "$hash.zip" -print -quit)"
|
|
dir="$RUNNER_TEMP/vcpkg-new/${hash:0:2}" # vcpkg's own two character fanout
|
|
mkdir -p "$dir"
|
|
cp "$archive" "$dir/"
|
|
echo "$key ${hash:0:2}/$hash.zip" >>"$RUNNER_TEMP/vcpkg-new/keys.txt"
|
|
done <"$RUNNER_TEMP/new-keys.txt"
|
|
|
|
count="$(wc -l <"$RUNNER_TEMP/new-keys.txt")"
|
|
echo "vcpkg scope $VCPKG_SCOPE: $count entries to import"
|
|
echo "count=$count" >>"$GITHUB_OUTPUT"
|
|
echo "slug=$(sed -e 's|^vcpkg/||' -e 's|/$||' -e 's|/|-|g' <<<"$VCPKG_SCOPE")" >>"$GITHUB_OUTPUT"
|
|
|
|
# archive: false accepts a single file and nothing else, and a scope hands over one
|
|
# archive per ABI hash vcpkg had to rebuild, which is 23 files on a cold runner.
|
|
# Zipping the directory keeps the fanout at the artifact root, which is where
|
|
# vcpkg-cache-import.yml looks for keys.txt once download-artifact unpacks it.
|
|
- name: "Upload new vcpkg entries"
|
|
# An empty count means the step above did not run at all, on master and on forks
|
|
if: steps.vcpkg_new.outputs.count != '' && steps.vcpkg_new.outputs.count != '0'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
if-no-files-found: error # a count above zero with nothing staged is a bug
|
|
name: vcpkg-new-${{ steps.vcpkg_new.outputs.slug }}
|
|
path: ${{ runner.temp }}/vcpkg-new
|
|
retention-days: 1
|
|
|
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
|
- name: "[macOS] Delete remote compiler cache (ccache)"
|
|
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
|
continue-on-error: true
|
|
env:
|
|
CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
|
|
echo "Cache deleted successfully"
|
|
fi
|
|
|
|
- name: "[macOS] Cache updated compiler cache (ccache)"
|
|
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master'
|
|
uses: actions/cache/save@v6
|
|
with:
|
|
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
|
|
path: ${{ env.CCACHE_DIR }}
|
|
|
|
- name: "[macOS] Sign app bundle"
|
|
if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null
|
|
id: sign_macos
|
|
env:
|
|
BUILD_PATH: ${{ steps.build.outputs.path }}
|
|
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
|
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
|
run: |
|
|
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
|
then
|
|
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
|
/usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "$BUILD_PATH"
|
|
fi
|
|
|
|
- name: "[macOS] Notarize app bundle"
|
|
if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success'
|
|
env:
|
|
BUILD_PATH: ${{ steps.build.outputs.path }}
|
|
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
|
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
|
|
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
|
run: |
|
|
if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
|
|
then
|
|
# Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
|
|
echo "Create keychain profile"
|
|
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"
|
|
|
|
# We can't notarize an app bundle directly, but we need to compress it as an archive.
|
|
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
|
|
# notarization service
|
|
echo "Creating temp notarization archive"
|
|
ditto -c -k --keepParent "$BUILD_PATH" "notarization.zip"
|
|
|
|
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
|
|
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
|
|
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
|
|
# you're curious
|
|
echo "Notarize app"
|
|
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
|
|
|
|
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
|
|
# validated by macOS even when an internet connection is not available.
|
|
echo "Attach staple"
|
|
xcrun stapler staple "$BUILD_PATH"
|
|
fi
|
|
|
|
- name: "Upload artifact"
|
|
if: matrix.make_package
|
|
id: upload_artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
archive: false
|
|
if-no-files-found: error
|
|
path: ${{ steps.build.outputs.path }}
|
|
|
|
- name: "Upload to release"
|
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
|
id: upload_release
|
|
shell: bash
|
|
env:
|
|
asset_name: ${{ steps.build.outputs.fullname }}
|
|
asset_path: ${{ steps.build.outputs.path }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
tag_name: ${{ needs.configure.outputs.tag }}
|
|
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
|
|
|
# Ninja is single-config: binaries and their PDBs land directly in the
|
|
# target directories instead of a Release subdirectory.
|
|
#
|
|
# Archived for tag builds only, and ordered after the release upload so
|
|
# a missing PDB can never block a release from being published. On a tag
|
|
# build this artifact is the only durable copy of the symbols: the Sentry
|
|
# upload is continue-on-error, so without it a failed upload would lose
|
|
# the ability to symbolicate that release permanently. Other builds upload
|
|
# no symbols at all, so archiving them there would only cost storage.
|
|
#
|
|
# The executables travel with their PDBs: re-uploading the pair together is
|
|
# what lets sentry-cli associate the PE's code identifier with the PDB's
|
|
# debug identifier, and the PE is what carries the unwind information.
|
|
- name: "[Windows] Upload debug symbols (exe + pdb)"
|
|
if: matrix.os == 'Windows' && needs.configure.outputs.tag != null
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
if-no-files-found: error
|
|
name: ${{ steps.build.outputs.name }}-Symbols
|
|
path: |
|
|
build/cockatrice/*.exe
|
|
build/cockatrice/*.pdb
|
|
build/oracle/*.exe
|
|
build/oracle/*.pdb
|
|
build/servatrice/*.exe
|
|
build/servatrice/*.pdb
|
|
|
|
- name: "Attest binary provenance"
|
|
if: steps.upload_release.outcome == 'success'
|
|
id: attestation
|
|
uses: actions/attest@v4
|
|
with:
|
|
show-summary: false
|
|
subject-path: ${{ steps.build.outputs.path }}
|
|
|
|
- name: "Verify binary attestation"
|
|
if: steps.attestation.outcome == 'success'
|
|
shell: bash
|
|
env:
|
|
BUILD_PATH: ${{ steps.build.outputs.path }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
|
|
|
|
build-vcpkg-nightly:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: macOS
|
|
target: 13 # EOL 2025-09-15
|
|
runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
|
|
# macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout
|
|
|
|
ccache_eviction_age: 7d
|
|
cmake_generator: Ninja
|
|
make_package: 1
|
|
override_target: 13
|
|
package_suffix: "-macOS13_Intel"
|
|
qt_version: 6.11.*
|
|
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
|
soc: Intel
|
|
type: Release
|
|
use_ccache: 1
|
|
vcpkg_scope: 'vcpkg/macos13-x64-xcode26.3/'
|
|
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
|
|
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
|
needs: configure
|
|
# The EOL target on the slowest runner is too slow to gate a change, so it runs in the
|
|
# nightly and on tags only, where the vcpkg cache import also has to accept schedule runs.
|
|
if: github.event_name == 'schedule' || github.ref_type == 'tag'
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 100
|
|
env:
|
|
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
|
# Like build-linux-nightly: this leg keeps no compiler cache, so the quota stays with
|
|
# the pull-request legs (see PERSIST_CCACHE there).
|
|
PERSIST_CCACHE: 'false'
|
|
CCACHE_SIZE: 600M
|
|
steps: *vcpkg-steps
|
|
|
|
prune-caches:
|
|
name: Prune caches
|
|
# Only master writes caches at all, so only master may clean them up too
|
|
if: github.ref == 'refs/heads/master'
|
|
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
|
|
continue-on-error: true # freeing space must never fail a build
|
|
|
|
steps:
|
|
# The repository holds 10 GB of cache against entries that want more, and GitHub evicts
|
|
# the least recently used entry whenever that is exceeded. The vcpkg entries are the
|
|
# reason that never happens: the restore above touches every entry under the prefix on
|
|
# every run, so a dead ABI generation keeps its access date fresh forever and age based
|
|
# eviction can never reach it. Deleting by age alone cannot fix that, only knowing which
|
|
# scope an entry belongs to can. The same is true of a compiler cache of a leg that no
|
|
# longer keeps one: its restore would keep touching it, so the live key list below is
|
|
# what retires it.
|
|
#
|
|
# docker-release's image builds keep their layer cache in the same quota: one blob per
|
|
# layer, keyed buildkit-blob-1-<digest>, plus one index-<scope>-1-<hash> entry per image
|
|
# scope that lists them. They are written with mode=min, so only the final image stage
|
|
# is kept and the set stays small. A layer a build actually needs is downloaded, which
|
|
# refreshes its access date, so an old date means the builds have stopped using it and
|
|
# age is the right test. Dropping a blob an index still lists costs the next image build
|
|
# one rebuilt layer; docker-release gates nothing, so that is cheaper than keeping the
|
|
# entries forever.
|
|
- name: "Delete unusable cache entries"
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
# Must list every vcpkg_scope of the build-vcpkg and build-vcpkg-nightly
|
|
# matrices above, or the entries of a scope that is still in use get deleted
|
|
# on the next run.
|
|
LIVE_VCPKG_SCOPES: 'vcpkg/macos13-x64-xcode26.3/ vcpkg/macos14-arm64-xcode26.6/ vcpkg/windows10-x64-vs2026/'
|
|
# Must list the ccache key of every leg that keeps a compiler cache
|
|
# (PERSIST_CCACHE in build-linux and build-vcpkg), or a live entry is deleted.
|
|
# The nightly legs keep none, so their leftover entries are reclaimed here
|
|
# instead of waiting out the age rule.
|
|
LIVE_CCACHE_KEYS: 'ccache-Arch ccache-Servatrice_Debian12 ccache-Debian13 ccache-Fedora44 ccache-Ubuntu26.04 ccache-Ubuntu26.04sanitize ccache-macOS14-xcode-27 ccache-macOS15-xcode-27 ccache-macOS-xcode-27'
|
|
run: |
|
|
gh api --paginate "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" \
|
|
--jq '.actions_caches[] | [.id, .ref, .key, .last_accessed_at] | @tsv' >"$RUNNER_TEMP/caches.tsv"
|
|
|
|
cutoff="$(( $(date -u +%s) - 7 * 24 * 3600 ))"
|
|
live=" ${LIVE_VCPKG_SCOPES} "
|
|
live_ccache=" ${LIVE_CCACHE_KEYS} "
|
|
|
|
deleted=0
|
|
while IFS=$'\t' read -r id ref key accessed; do
|
|
[[ -n $id ]] || continue
|
|
# Only ever touch the key shapes our workflows create (buildkit-* and index-*
|
|
# come from docker-release.yml), so that an entry of some other workflow can
|
|
# never be deleted by accident
|
|
case "$key" in
|
|
vcpkg/* | ccache-* | Qt* | buildkit-* | index-*) ;;
|
|
*) continue ;;
|
|
esac
|
|
|
|
reason=""
|
|
if [[ $ref != refs/heads/master ]]; then
|
|
reason="scoped to $ref"
|
|
elif [[ $key == ccache-* ]]; then
|
|
# An exact token compare: ccache-Ubuntu26.04 is a prefix of
|
|
# ccache-Ubuntu26.04sanitize, and both keys are live
|
|
if [[ $live_ccache != *" $key "* ]]; then
|
|
reason="no pull-request leg keeps it"
|
|
fi
|
|
elif [[ $key == vcpkg/* ]]; then
|
|
# "<scope>/<abi hash>", cut the hash off the end to get the scope
|
|
scope="vcpkg/${key#vcpkg/}"
|
|
scope="${scope%/*}/"
|
|
if [[ $live != *" $scope "* ]]; then
|
|
reason="no matrix leg uses $scope"
|
|
fi
|
|
fi
|
|
if [[ -z $reason ]]; then
|
|
stamp="$(date -u -d "$accessed" +%s 2>/dev/null || echo 0)"
|
|
if [[ $stamp -ne 0 && $stamp -lt $cutoff ]]; then
|
|
reason="unused since $accessed"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n $reason ]]; then
|
|
echo "::notice::deleting $key, $reason"
|
|
gh api -X DELETE "repos/$GITHUB_REPOSITORY/actions/caches/$id"
|
|
deleted=$((deleted + 1))
|
|
fi
|
|
done <"$RUNNER_TEMP/caches.tsv"
|
|
|
|
echo "::notice::deleted $deleted of $(wc -l <"$RUNNER_TEMP/caches.tsv") cache entries"
|