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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: release

on:
workflow_run:
workflows: ["build"]
types: [completed]
branches:
- master

permissions:
contents: write
pull-requests: read

concurrency:
group: release-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

jobs:
release:
if: >-
${{
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master'
}}
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}

- name: Skip if head is already tagged
id: points_at_tag
shell: bash
run: |
tags="$(git tag --points-at "${{ github.event.workflow_run.head_sha }}")"
if [ -n "$tags" ]; then
echo "already_tagged=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "already_tagged=false" >> "$GITHUB_OUTPUT"

- name: Find associated pull request
if: steps.points_at_tag.outputs.already_tagged != 'true'
id: pr
uses: actions/github-script@v7
with:
script: |
const response = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.payload.workflow_run.head_sha,
});

const mergedPulls = response.data
.filter((pull) => pull.merged_at)
.sort((left, right) => new Date(right.merged_at) - new Date(left.merged_at));

if (!mergedPulls.length) {
core.setOutput("found", "false");
return;
}

const pull = mergedPulls[0];
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull.number,
per_page: 100,
});

core.setOutput("found", "true");
core.setOutput("number", String(pull.number));
core.setOutput("title", pull.title);
core.setOutput("body", pull.body || "");
core.setOutput("labels", JSON.stringify(pull.labels.map((label) => label.name)));
core.setOutput("commits", JSON.stringify(commits.map((commit) => commit.commit.message.split("\n")[0])));

- name: Skip if no merged pull request was found
if: >-
${{
steps.points_at_tag.outputs.already_tagged != 'true' &&
steps.pr.outputs.found != 'true'
}}
shell: bash
run: |
echo "The push to master is not associated with a merged pull request, so no release will be created."

- name: Read latest release tag
if: >-
${{
steps.points_at_tag.outputs.already_tagged != 'true' &&
steps.pr.outputs.found == 'true'
}}
id: latest_tag
shell: bash
run: |
latest_tag="$(git tag --sort=-version:refname | head -n 1)"
if [ -z "$latest_tag" ]; then
latest_tag="v0.0.0"
fi
echo "value=$latest_tag" >> "$GITHUB_OUTPUT"

- name: Compute next version
if: >-
${{
steps.points_at_tag.outputs.already_tagged != 'true' &&
steps.pr.outputs.found == 'true'
}}
id: version
env:
LABELS_JSON: ${{ steps.pr.outputs.labels }}
PR_TITLE: ${{ steps.pr.outputs.title }}
PR_BODY: ${{ steps.pr.outputs.body }}
COMMITS_JSON: ${{ steps.pr.outputs.commits }}
shell: bash
run: |
python3 scripts/next_release.py \
--current-version "${{ steps.latest_tag.outputs.value }}" \
--labels-json "$LABELS_JSON" \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--commits-json "$COMMITS_JSON" >> "$GITHUB_OUTPUT"

- name: Create GitHub release
if: >-
${{
steps.points_at_tag.outputs.already_tagged != 'true' &&
steps.pr.outputs.found == 'true'
}}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.next_tag }}
target_commitish: ${{ github.event.workflow_run.head_sha }}
generate_release_notes: true
name: ${{ steps.version.outputs.next_tag }}
14 changes: 13 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
cmake_minimum_required(VERSION 3.22 FATAL_ERROR)
cmake_policy(VERSION 3.22)
project(CONSOLEPP VERSION 0.1.1)

option(CONOSOLEPP_COVERAGE "Build with code coverage options")
option(CONSOLEPP_SANITIZE "Build using sanitizers" "")
Expand All @@ -13,6 +12,19 @@ message("Building Console++ with sanitizers: ${CONSOLEPP_SANITIZE}")
message("Building Console++ with tests: ${CONSOLEPP_WITH_TESTS}")
message("Building Console++ with only documentation: ${CONSOLEPP_DOC_ONLY}")

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules")

if (CONSOLEPP_VERSION)
include(function-parse_version)
parse_version("${CONSOLEPP_VERSION}" "CONSOLEPP_")
else()
include(function-git_version)
git_version(CONSOLEPP)
endif()

message(STATUS "Console++ Version: ${CONSOLEPP_VERSION}")
project(CONSOLEPP VERSION ${CONSOLEPP_VERSION})

# For producing automatically-generated documentation, we use Doxygen.
find_package(Doxygen)

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ cmake -S . -B build \
When using manifest mode (`vcpkg.json` in this repository), configure will
trigger dependency installation automatically.

## Automated Releases

Pushes to `master` that come from a merged pull request and pass the `build`
workflow automatically publish a GitHub Release. The release version is chosen
using the following precedence:

- pull request labels `semver:major`, `semver:minor`, `semver:patch`
- Conventional Commit cues from the PR title or commit subjects
- contextual hints in the PR title/body such as `breaking` or `feature`
- `patch` when nothing else matches

Downstream consumers continue to get the standard GitHub release experience,
including the automatically generated source archives for each tag.

## Consume From CMake (Installed Package)

```cmake
Expand Down
106 changes: 106 additions & 0 deletions cmake/Modules/function-git_version.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# A function to parse verson numbers into their parts
include("${CMAKE_CURRENT_LIST_DIR}/function-parse_version.cmake")

# A function to detect a components version number from git tags.
function( GIT_VERSION VAR_PREFIX)

set( GIT_INFO ID AUTHOR DATE BRANCH DESC )

set( GIT_COMMIT_ID_DEFAULT "UNKNOWN" )
set( GIT_COMMIT_AUTHOR_DEFAULT "UNKNOWN" )
set( GIT_COMMIT_DATE_DEFAULT "UNKNOWN" )
set( GIT_COMMIT_BRANCH_DEFAULT "UNKNOWN" )
set( GIT_COMMIT_DESC_DEFAULT "None" )

# we need git of course...
find_package( Git )
if( GIT_FOUND )
# make sure that this is actually a git repo
execute_process( COMMAND ${GIT_EXECUTABLE} status
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE IsGitRepo
OUTPUT_VARIABLE OutputTrash
ERROR_VARIABLE ErrorTrash)

if( ${IsGitRepo} EQUAL 0 )
if( "${GIT_COMMIT_ID}" STREQUAL "" )
execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --sq HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_ID
ERROR_VARIABLE Trash)
endif()
if( "${GIT_COMMIT_AUTHOR}" STREQUAL "" )
execute_process( COMMAND ${GIT_EXECUTABLE} log -n1 --pretty="%an" HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_AUTHOR
ERROR_VARIABLE Trash)
endif()
if( "${GIT_COMMIT_DATE}" STREQUAL "" )
execute_process( COMMAND ${GIT_EXECUTABLE} log -n1 --pretty="%aD" HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_DATE
ERROR_VARIABLE Trash)
endif()
if( "${GIT_COMMIT_BRANCH}" STREQUAL "" )
execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_BRANCH
ERROR_VARIABLE Trash)
endif()
if( "${GIT_COMMIT_DESC}" STREQUAL "" )
# check to see if there are any tags
execute_process( COMMAND ${GIT_EXECUTABLE} tag
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE Tmp
ERROR_VARIABLE Trash)
if( NOT "${Tmp}" STREQUAL "" )
# at least one tag (appears to) exist
execute_process( COMMAND ${GIT_EXECUTABLE} describe --tags HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_DESC
ERROR_VARIABLE Trash)
else()
# no tag found. assume 0.0
set( GIT_COMMIT_DESC "0.0" )
endif()
endif()
else()
message( WARNING "Source directory is not a git repo." )
set( GIT_COMMIT_DESC "0.0.0" )
set( GIT_COMMIT_BRANCH "none" )
endif()
else()
message( WARNING "Could not find git command" )
set( GIT_COMMIT_DESC "0.0" )
set( GIT_COMMIT_BRANCH "none" )
endif()

# set defaults for any items that were not found
foreach( item ${GIT_INFO} )
if( "${GIT_COMMIT_${item}}" STREQUAL "" )
set( GIT_COMMIT_${item} ${GIT_COMMIT_${item}_DEFAULT} )
endif()
endforeach( item )

# remove quotes, backslashes, and new lines
# these cause problems when trying to embed version numbers into source files.
foreach( item ${GIT_INFO} )
string( REPLACE "'" "" GIT_COMMIT_${item} ${GIT_COMMIT_${item}} )
string( REPLACE "\"" "" GIT_COMMIT_${item} ${GIT_COMMIT_${item}} )
string( REPLACE "\n" "" GIT_COMMIT_${item} ${GIT_COMMIT_${item}} )
endforeach( item )

# set full version string
set(VERSION_FULL "${GIT_COMMIT_DESC}-${GIT_COMMIT_BRANCH}" )

parse_version( ${VERSION_FULL} "" )

# now set version information in the parent scope
set( ${VAR_PREFIX}_VERSION_FULL ${VERSION_FULL} PARENT_SCOPE )
set( ${VAR_PREFIX}_VERSION ${VERSION} PARENT_SCOPE )
set( ${VAR_PREFIX}_VERSION_MAJOR ${VERSION_MAJOR} PARENT_SCOPE )
set( ${VAR_PREFIX}_VERSION_MINOR ${VERSION_MINOR} PARENT_SCOPE )
set( ${VAR_PREFIX}_VERSION_PATCH ${VERSION_PATCH} PARENT_SCOPE )

endfunction(GIT_VERSION)

95 changes: 95 additions & 0 deletions cmake/Modules/function-parse_version.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# A function to parse verson numbers into their parts
function( PARSE_VERSION VERSION PREFIX )

# look for major, minor, and patch numbers in the version string
# this is required if the developer uses CPack for example.
# need to consider two possible formats:
# Major.Minor.Patch
# or
# Major.Minor-PatchStr
# the first format is the standard format used for version numbers,
# but would have to be set by the developer as a tag name.
# however, if the developer just sets a major and minor version with
# a tag, then git will add the number of commits since the tag. for example
# 0.1-10-devel
# indicates that this is the 10th commit on the devel branch after 0.1 patch.
#
# so, we assume
# version components may be separated by '.' or '-'
# version components must be numerical (this is what cmake supoorts)
#
# we parse by matching a version component and then removing it from the
# version string.

# set full version string to the raw string passed in
set( VERSION_FULL ${VERSION} )

# first number in version string will be the major version number
# version string may be preceeded by a 'v'
set( RE "^v*([0-9]+)[\\.\\-](.*)$" )
if( ${VERSION} MATCHES ${RE} )
set( VERSION_MAJOR ${CMAKE_MATCH_1} )
string( REGEX REPLACE ${RE} "\\2" VERSION "${VERSION}" )
else()
set( VERSION_MAJOR "" )
endif()

# second number in version string will be minor version number
set( RE "^([0-9]+)[\\.\\-](.*)$" )
if( ${VERSION} MATCHES ${RE} )
set( VERSION_MINOR ${CMAKE_MATCH_1} )
string( REGEX REPLACE ${RE} "\\2" VERSION "${VERSION}" )
else()
set( VERSION_MINOR "" )
endif()

# third number in version string will be patch version number
set( RE "^([0-9]+)[\\.\\-](.*)$" )
if( ${VERSION} MATCHES ${RE} )
set( VERSION_PATCH ${CMAKE_MATCH_1} )
string( REGEX REPLACE ${RE} "\\2" VERSION "${VERSION}" )
else()
set( VERSION_PATCH "" )
endif()

# fourth number in version string will be teak version number
# teak number should be the last number
set( RE "^([0-9]+)(.*)$" )
if( ${VERSION} MATCHES ${RE} )
set( VERSION_TWEAK ${CMAKE_MATCH_1} )
string( REGEX REPLACE ${RE} "\\2" VERSION "${VERSION}" )
else()
set( VERSION_TWEAK "" )
endif()


# if the version string passed in coudl be parsed,
# construct a cmake-compatible version string
# to put in ${PREFIX}VERSION. otherwise, just
# set ${PREFIX}VERSION to the string that was passed in.
if( NOT ${VERSION_MAJOR} EQUAL "" )
set(VERSION ${VERSION_MAJOR})
else()
set(VERSION ${VERSION_FULL})
endif()
if( NOT ${VERSION_MINOR} EQUAL "" )
set(VERSION ${VERSION}.${VERSION_MINOR})
endif()
if( NOT ${VERSION_PATCH} EQUAL "" )
set(VERSION ${VERSION}.${VERSION_PATCH})
endif()
if( NOT ${VERSION_TWEAK} EQUAL "" )
set(VERSION ${VERSION}.${VERSION_TWEAK})
endif()

# set variables in parent's scope
set( ${PREFIX}VERSION_FULL ${VERSION_FULL} PARENT_SCOPE )
set( ${PREFIX}VERSION ${VERSION} PARENT_SCOPE )
set( ${PREFIX}VERSION_MAJOR ${VERSION_MAJOR} PARENT_SCOPE )
set( ${PREFIX}VERSION_MINOR ${VERSION_MINOR} PARENT_SCOPE )
set( ${PREFIX}VERSION_PATCH ${VERSION_PATCH} PARENT_SCOPE )
set( ${PREFIX}VERSION_TWEAK ${VERSION_TWEAK} PARENT_SCOPE )


endfunction(PARSE_VERSION)

Loading
Loading