diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..30ee8fa3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,142 @@ +name: Release + +# Tag-triggered release. +# +# How to cut a release: +# +# git tag v0.3.3 # pick the next version; must start with "v" +# git push origin v0.3.3 +# +# Pushing a tag that looks like vX.Y.Z starts this workflow. It builds the +# project in Release, packages everything into a .zip with CPack, and publishes +# a GitHub Release named after the tag with the .zip attached. +# +# The build version comes from this same tag: the top-level CMakeLists.txt runs +# `git describe --tags --match "v*"` and extracts the numeric X.Y.Z. The release +# is always named after the full tag, but note that any pre-release suffix +# (e.g. the `-rc1` in v0.3.3-rc1) is NOT carried into the built PROJECT_VERSION, +# which would be 0.3.3 in that example. There are no version files to keep in +# sync. + +on: + push: + tags: + # GitHub tag filters use glob (minimatch) syntax, not regex, so we cannot + # use `+` quantifiers. Requiring the two dots enforces the vX.Y.Z shape and + # avoids firing on tags like v1 or v12foo. The trailing segment's `*` still + # allows pre-release suffixes such as v0.3.3-rc1. (This does not reject + # v1.2.3.4; add a runtime semver check if that strictness is needed.) + - "v[0-9]*.[0-9]*.[0-9]*" + +permissions: + contents: write # needed to create the GitHub Release + +env: + # Pin the architecture/triplet everywhere so vcpkg, CMake, and the packaged + # artifact all agree on x64 and we never accidentally publish a 32-bit build. + VCPKG_DEFAULT_TRIPLET: x64-windows + VCPKG_TARGET_TRIPLET: x64-windows + +jobs: + release: + runs-on: windows-latest + steps: + - name: Validate tag is strict semver + # The on.push.tags glob can only require the vX.Y.Z *shape*; it can't + # enforce digits-only segments, so tags like v1foo.2.3 or v1.2.3.4 would + # still start this job. Reject anything that isn't strict + # vMAJOR.MINOR.PATCH (with an optional -prerelease / +build suffix) so we + # never publish an unintended release. + shell: bash + run: | + tag="${{ github.ref_name }}" + # This is the official semver.org recommended regex, adapted to POSIX + # ERE (bash [[ =~ ]]): non-capturing groups become plain groups and \d + # becomes [0-9]. Enforcing the full grammar means: + # * MAJOR/MINOR/PATCH are numeric with no leading zeroes (0 | [1-9]...) + # * each dot-separated pre-release/build identifier is non-empty and + # well-formed, so junk like v1.2.3-alpha..1 or v1.2.3+build..5 (empty + # identifiers) is rejected even though it "looks" close to semver. + # * a pre-release numeric identifier has no leading zeroes, but build + # identifiers may (per spec). + semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' + if [[ ! "$tag" =~ $semver ]]; then + echo "::error::Tag '$tag' is not a strict vX.Y.Z[-prerelease][+build] semver tag; refusing to release." + exit 1 + fi + echo "Tag '$tag' accepted." + + - uses: actions/checkout@v5 + with: + submodules: 'recursive' + # Full history + tags so the CMake `git describe` version resolves to + # the tag that triggered this run. + fetch-depth: 0 + fetch-tags: true + + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + echo "dist-dir=${{ github.workspace }}/dist" >> "$GITHUB_OUTPUT" + + - name: Bootstrap vcpkg + shell: pwsh + run: ${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat + + - name: Run vcpkg + shell: pwsh + run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --triplet x64-windows + + - name: Configure CMake + # The Visual Studio generator selected by -A x64 is multi-config, so the + # build type is chosen later via --config / --build-config / -C Release + # rather than CMAKE_BUILD_TYPE. Only BUILD_TESTING controls test builds + # in this project's CMake. + run: > + cmake + -A x64 + -D CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -D VCPKG_TARGET_TRIPLET=x64-windows + -D BUILD_TESTING=ON + -B ${{ steps.strings.outputs.build-output-dir }} + -S ${{ github.workspace }} + + - name: Build + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release --parallel + + - name: Test + # Don't publish a release artifact that fails the suite the rest of CI runs. + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: ctest --build-config Release --output-on-failure + + - name: Package (CPack -> .zip) + run: > + cpack + --config ${{ steps.strings.outputs.build-output-dir }}/CPackConfig.cmake + -C Release + -B ${{ steps.strings.outputs.dist-dir }} + + - name: Publish GitHub Release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # github.ref_name is the tag that triggered this run, e.g. v0.3.3. + # A SemVer pre-release is a '-' that comes before any '+build' metadata + # (e.g. v0.3.3-rc1). Build metadata may itself contain hyphens + # (e.g. v1.2.3+build-foo), so strip everything from the first '+' + # before testing for a pre-release hyphen to avoid false positives. + tag="${{ github.ref_name }}" + version_no_build="${tag%%+*}" + PRERELEASE_FLAG="" + case "$version_no_build" in + *-*) PRERELEASE_FLAG="--prerelease" ;; + esac + + gh release create "${{ github.ref_name }}" \ + "${{ steps.strings.outputs.dist-dir }}"/*.zip \ + --title "${{ github.ref_name }}" \ + --generate-notes \ + $PRERELEASE_FLAG diff --git a/CMakeLists.txt b/CMakeLists.txt index 44fcb979..b7c99822 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,3 +201,21 @@ install( DESTINATION ${SYSCONFIG_INSTALL_DIR} ) + +# Packaging (CPack). This generates ${CMAKE_BINARY_DIR}/CPackConfig.cmake so that +# `cpack` can build release artifacts in CI. Keep the generator list portable: +# ZIP needs no extra tooling on the runner, unlike NSIS/DEB. +set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") +set(CPACK_PACKAGE_VENDOR "Microsoft") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "m C++ libraries") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") +set(CPACK_VERBATIM_VARIABLES ON) + +if(WIN32) + set(CPACK_GENERATOR "ZIP") +else() + set(CPACK_GENERATOR "TGZ") +endif() + +include(CPack)