diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e89a700c7..bfad6e346 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,16 +22,28 @@ jobs: with: fetch-depth: 0 + - name: Set env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Validate release tag ${{ env.RELEASE_VERSION }} + run: | + expected_tag=$(./scripts/get-git-tag-name.sh version.go) + actual_tag=${{ env.RELEASE_VERSION }} + + if [ "$actual_tag" = "$expected_tag" ]; then + echo "Git tag release string is as expected." + else + echo "Error: Versions are not equal. Actual: $actual_tag, Expected: $expected_tag" + exit 1 + fi + - name: setup go ${{ env.GO_VERSION }} uses: actions/setup-go@v2 with: go-version: '${{ env.GO_VERSION }}' - - name: Set env - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: build release for all architectures - run: SKIP_VERSION_CHECK=1 make release tag=${{ env.RELEASE_VERSION }} + run: make release tag=${{ env.RELEASE_VERSION }} - name: Create Release uses: lightninglabs/gh-actions/action-gh-release@2021.01.25.00 diff --git a/Makefile b/Makefile index b80964f03..f3d0501f1 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,9 @@ GOACC_BIN := $(GO_BIN)/go-acc GOIMPORTS_BIN := $(GO_BIN)/gosimports MIGRATE_BIN := $(GO_BIN)/migrate +# VERSION_GO_FILE is the golang file which defines the current project version. +VERSION_GO_FILE := "version.go" + COMMIT := $(shell git describe --tags --dirty) GOBUILD := GOEXPERIMENT=loopvar GO111MODULE=on go build -v @@ -115,6 +118,17 @@ release: $(VERSION_CHECK) ./scripts/release.sh build-release "$(VERSION_TAG)" "$(BUILD_SYSTEM)" "$(RELEASE_TAGS)" "$(RELEASE_LDFLAGS)" +release-tag: + @$(call print, "Adding release tag.") + + tag=$$(./scripts/get-git-tag-name.sh ${VERSION_GO_FILE}); \ + exit_status=$$?; \ + if [ $$exit_status -ne 0 ]; then \ + echo "Script encountered an error with exit status $$exit_status."; \ + fi; \ + echo "Adding git tag: $$tag"; \ + git tag -as -m "Tag generated using command \`make release-tag\`." "$$tag"; + docker-release: @$(call print, "Building release helper docker image.") if [ "$(tag)" = "" ]; then echo "Must specify tag=!"; exit 1; fi diff --git a/make/release_flags.mk b/make/release_flags.mk index 6af613434..45defe716 100644 --- a/make/release_flags.mk +++ b/make/release_flags.mk @@ -1,6 +1,13 @@ +# One can either specify a git tag as the version suffix or one that is +# generated from the current date. VERSION_TAG = $(shell date +%Y%m%d)-01 VERSION_CHECK = @$(call print, "Building master with date version tag") +ifneq ($(tag),) +VERSION_TAG = $(tag) +VERSION_CHECK = ./scripts/release.sh check-tag "$(VERSION_TAG)" "$(VERSION_GO_FILE)" +endif + DOCKER_RELEASE_HELPER = docker run \ -it \ --rm \ @@ -22,13 +29,6 @@ windows-amd64 RELEASE_TAGS = monitoring -# One can either specify a git tag as the version suffix or one is generated -# from the current date. -ifneq ($(tag),) -VERSION_TAG = $(tag) -VERSION_CHECK = ./scripts/release.sh check-tag "$(VERSION_TAG)" -endif - # By default we will build all systems. But with the 'sys' tag, a specific # system can be specified. This is useful to release for a subset of # systems/architectures. diff --git a/scripts/get-git-tag-name.sh b/scripts/get-git-tag-name.sh new file mode 100755 index 000000000..b31849412 --- /dev/null +++ b/scripts/get-git-tag-name.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# This script derives a git tag name from the version fields found in a given Go +# file. It also checks if the derived git tag name is a valid SemVer compliant +# version string. + +# get_git_tag_name reads the version fields from the given file and then +# constructs and returns a git tag name. +get_git_tag_name() { + local file_path="$1" + + # Check if the file exists + if [ ! -f "$file_path" ]; then + echo "Error: File not found at $file_path" >&2 + exit 1 + fi + + # Read and parse the version fields. We interpret these fields using regex + # matching which effectively serves as a basic sanity check. + local app_major + app_major=$(grep -oP 'AppMajor\s*uint\s*=\s*\K\d+' "$file_path") + + local app_minor + app_minor=$(grep -oP 'AppMinor\s*uint\s*=\s*\K\d+' "$file_path") + + local app_patch + app_patch=$(grep -oP 'AppPatch\s*uint\s*=\s*\K\d+' "$file_path") + + local app_status + app_status=$(grep -oP 'AppStatus\s*=\s*"\K([a-z]*)' "$file_path") + + local app_pre_release + app_pre_release=$(grep -oP 'AppPreRelease\s*=\s*"\K([a-z0-9]*)' "$file_path") + + # Parse the GitTagIncludeStatus field. + local git_tag_include_status + git_tag_include_status=false + + if grep -q 'GitTagIncludeStatus = true' "$file_path"; then + git_tag_include_status=true + elif grep -q 'GitTagIncludeStatus = false' "$file_path"; then + git_tag_include_status=false + else + echo "Error: GitTagIncludeStatus is not present in the Go version file." + exit 1 + fi + + # Construct the git tag name with conditional inclusion of app_status and + # app_pre_release. + tag_name="v${app_major}.${app_minor}.${app_patch}" + + # Append app_status if git_tag_include_status is true and app_status if + # specified. + if [ "$git_tag_include_status" = true ] && [ -n "$app_status" ]; then + tag_name+="-${app_status}" + + # Append app_pre_release if specified. + if [ -n "$app_pre_release" ]; then + tag_name+=".${app_pre_release}" + fi + else + # If the app_status field is not specified, then append + # app_pre_release (if specified) using a dash prefix. + if [ -n "$app_pre_release" ]; then + tag_name+="-${app_pre_release}" + fi + fi + + echo "$tag_name" +} + +file_path="$1" +echo "Reading version fields from file: $file_path" >&2 +tag_name=$(get_git_tag_name "$file_path") +echo "Derived git tag name: $tag_name" >&2 + +echo "$tag_name" diff --git a/scripts/release.sh b/scripts/release.sh index 152b0fdcf..3d28a9542 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -80,9 +80,10 @@ function red() { # check_tag_correct makes sure the given git tag is checked out and the git tree # is not dirty. -# arguments: +# arguments: function check_tag_correct() { local tag=$1 + local version_file_path=$2 # For automated builds we can skip this check as they will only be triggered # on tags. @@ -102,31 +103,16 @@ function check_tag_correct() { echo "Tag $tag checked out. Git commit: $commit_hash" fi - # Build tapd to extract version. - go build ${PKG}/cmd/tapd + # Ensure that the git tag matches the version string derived from the version + # file. + local expected_tag + expected_tag=$(./scripts/get-git-tag-name.sh "$version_file_path") - # Extract version command output. - tapd_version_output=$(./tapd --version) - - # Use a regex to isolate the version string. - if [[ $tapd_version_output =~ $TAPD_VERSION_REGEX ]]; then - # Prepend 'v' to match git tag naming scheme. - tapd_version="v${BASH_REMATCH[1]}" - green "version: $tapd_version" - - # If the tapd reported version contains a suffix, remove it, so we can match - # the tag properly. - # shellcheck disable=SC2001 - tapd_version=$(echo "$tapd_version" | sed -e 's/-\(alpha\|beta\)\(\.rc[0-9]\+\)\?//g') - - # Match git tag with tapd version. - if [[ $tag != "${tapd_version}" ]]; then - red "tapd version $tapd_version does not match tag $tag" - exit 1 - fi - else - red "malformed tapd version output" + if [[ $tag != "$expected_tag" ]]; then + red "Error: tag $tag does not match git tag version string derived from $version_file_path" exit 1 + else + green "tag $tag matches git tag version string derived from $version_file_path" fi } diff --git a/version.go b/version.go index d7714f433..e561bef62 100644 --- a/version.go +++ b/version.go @@ -31,9 +31,9 @@ var ( GoVersion string ) -// semanticAlphabet is the set of characters that are permitted for use in an -// AppPreRelease. -const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-." +// versionFieldsAlphabet is the set of characters that are permitted for use in +// a version string field. +const versionFieldsAlphabet = "0123456789abcdefghijklmnopqrstuvwxyz" // These constants define the application version and follow the semantic // versioning 2.0.0 spec (http://semver.org/). @@ -47,9 +47,21 @@ const ( // AppPatch defines the application patch for this binary. AppPatch uint = 2 - // AppPreRelease MUST only contain characters from semanticAlphabet - // per the semantic versioning spec. - AppPreRelease = "alpha" + // AppStatus defines the release status of this binary (e.g. beta). + AppStatus = "alpha" + + // AppPreRelease defines the pre-release version of this binary. + // It MUST only contain characters from the semantic versioning spec. + AppPreRelease = "" + + // GitTagIncludeStatus indicates whether the status should be included + // in the git tag name. + // + // Including the app version status in the git tag may be problematic + // for golang projects when importing them as dependencies. We therefore + // include this flag to allow toggling the status on and off in a + // standardised way across our projects. + GitTagIncludeStatus = false // defaultAgentName is the default name of the software that is added as // the first part of the user agent string. @@ -66,8 +78,10 @@ var agentName = defaultAgentName // the software tapd is bundled in (for example LiT). This function panics if // the agent name contains characters outside of the allowed semantic alphabet. func SetAgentName(newAgentName string) { + agentNameAlphabet := versionFieldsAlphabet + "-. " + for _, r := range newAgentName { - if !strings.ContainsRune(semanticAlphabet, r) { + if !strings.ContainsRune(agentNameAlphabet, r) { panic(fmt.Errorf("rune: %v is not in the semantic "+ "alphabet", r)) } @@ -81,7 +95,7 @@ func SetAgentName(newAgentName string) { func UserAgent(initiator string) string { // We'll only allow "safe" characters in the initiator portion of the // user agent string and spaces only if surrounded by other characters. - initiatorAlphabet := semanticAlphabet + ". " + initiatorAlphabet := versionFieldsAlphabet + "-. " cleanInitiator := normalizeVerString( strings.TrimSpace(initiator), initiatorAlphabet, ) @@ -104,12 +118,22 @@ func UserAgent(initiator string) string { } func init() { + // Assert that AppStatus is valid according to the semantic versioning + // guidelines for pre-release version and build metadata strings. In + // particular, it MUST only contain characters in versionFieldsAlphabet. + for _, r := range AppStatus { + if !strings.ContainsRune(versionFieldsAlphabet, r) { + panic(fmt.Errorf("rune: %v is not in the semantic "+ + "alphabet", r)) + } + } + // Assert that AppPreRelease is valid according to the semantic // versioning guidelines for pre-release version and build metadata - // strings. In particular it MUST only contain characters in - // semanticAlphabet. + // strings. In particular, it MUST only contain characters in + // versionFieldsAlphabet. for _, r := range AppPreRelease { - if !strings.ContainsRune(semanticAlphabet, r) { + if !strings.ContainsRune(versionFieldsAlphabet, r) { panic(fmt.Errorf("rune: %v is not in the semantic "+ "alphabet", r)) } @@ -162,12 +186,28 @@ func semanticVersion() string { // Start with the major, minor, and patch versions. version := fmt.Sprintf("%d.%d.%d", AppMajor, AppMinor, AppPatch) - // Append pre-release version if there is one. The hyphen called for - // by the semantic versioning spec is automatically appended and should - // not be contained in the pre-release string. The pre-release version + // If defined, we will now sanitise the release status string. The + // hyphen called for by the semantic versioning spec is automatically + // appended and should not be contained in the status string. The status // is not appended if it contains invalid characters. - preRelease := normalizeVerString(AppPreRelease, semanticAlphabet) - if preRelease != "" { + appStatus := normalizeVerString(AppStatus, versionFieldsAlphabet) + + // If defined, we will now sanitise the pre-release version string. The + // hyphen called for by the semantic versioning spec is automatically + // appended and should not be contained in the pre-release string. + // The pre-release version is not appended if it contains invalid + // characters. + preRelease := normalizeVerString(AppPreRelease, versionFieldsAlphabet) + + // Append any status and pre-release strings to the version string. + switch { + case appStatus != "" && preRelease != "": + version = fmt.Sprintf( + "%s-%s.%s", version, appStatus, preRelease, + ) + case appStatus != "": + version = fmt.Sprintf("%s-%s", version, appStatus) + case preRelease != "": version = fmt.Sprintf("%s-%s", version, preRelease) }