diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ba200a46..686a26bb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,39 +1,117 @@ -name: release +# This code is borrowed from https://github.com/kubernetes-sigs/cluster-api/blob/main/.github/workflows/release.yaml +name: Create Release on: push: - # Sequence of patterns matched against refs/tags - tags: - - "v*" + branches: + - main + paths: + - 'releasenotes/*.md' permissions: {} jobs: - build: - name: tag release + push_release_tags: + permissions: + contents: write runs-on: ubuntu-latest - + outputs: + release_tag: ${{ steps.release-version.outputs.release_version }} + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=v4.1.7 + with: + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # tag=v45.0.1 + - name: Get release version + id: release-version + run: | + if [[ ${{ steps.changed-files.outputs.all_changed_files_count }} != 1 ]]; then + echo "1 release notes file should be changed to create a release tag, found ${{ steps.changed-files.outputs.all_changed_files_count }}" + exit 1 + fi + for changed_file in ${{ steps.changed-files.outputs.all_changed_files }}; do + export RELEASE_VERSION=$(echo "${changed_file}" | grep -oP '(?<=/)[^/]+(?=\.md)') + echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_ENV} + echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_OUTPUT} + if [[ "${RELEASE_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "Valid semver: ${RELEASE_VERSION}" + else + echo "Invalid semver: ${RELEASE_VERSION}" + exit 1 + fi + done + - name: Determine the release branch to use + run: | + if [[ ${RELEASE_VERSION} =~ beta ]] || [[ ${RELEASE_VERSION} =~ alpha ]]; then + export RELEASE_BRANCH=main + echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> ${GITHUB_ENV} + echo "This is a beta or alpha release, will use release branch ${RELEASE_BRANCH}" + else + export RELEASE_BRANCH=release-$(echo ${RELEASE_VERSION} | sed -E 's/^v([0-9]+)\.([0-9]+)\..*$/\1.\2/') + echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> ${GITHUB_ENV} + echo "This is not a beta or alpha release, will use release branch ${RELEASE_BRANCH}" + fi + - name: Create or checkout release branch + run: | + if git show-ref --verify --quiet "refs/remotes/origin/${RELEASE_BRANCH}"; then + echo "Branch ${RELEASE_BRANCH} already exists" + git checkout "${RELEASE_BRANCH}" + else + git checkout -b "${RELEASE_BRANCH}" + git push origin "${RELEASE_BRANCH}" + echo "Created branch ${RELEASE_BRANCH}" + fi + - name: Validate tag does not already exist + run: | + if [[ -n "$(git tag -l "${RELEASE_VERSION}")" ]]; then + echo "Tag ${RELEASE_VERSION} already exists, exiting" + exit 1 + fi + - name: Create Release Tag + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + git tag -a ${RELEASE_VERSION} -m ${RELEASE_VERSION} + git tag test/${RELEASE_VERSION} + git push origin ${RELEASE_VERSION} + git push origin test/${RELEASE_VERSION} + echo "Created tags ${RELEASE_VERSION} and test/${RELEASE_VERSION}" + release: + name: create draft release + runs-on: ubuntu-latest + needs: push_release_tags permissions: contents: write - - if: github.repository == 'metal3-io/ip-address-manager' steps: - - name: Export RELEASE_TAG var - run: echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV - - name: checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - name: Install go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 - with: - go-version: '1.22' - - name: Generate release artifacts and notes - run: | - make release - - name: Release - uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9 - with: - draft: true - files: out/* - body_path: releasenotes/${{ env.RELEASE_TAG }}.md + - name: Set env + run: echo "RELEASE_TAG=${RELEASE_TAG}" >> ${GITHUB_ENV} + env: + RELEASE_TAG: ${{needs.push_release_tags.outputs.release_tag}} + - name: checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=v4.1.7 + with: + fetch-depth: 0 + ref: ${{ env.RELEASE_TAG }} + - name: Calculate go version + run: echo "go_version=$(make go-version)" >> ${GITHUB_ENV} + - name: Set up Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # tag=v5.0.2 + with: + go-version: ${{ env.go_version }} + - name: generate release artifacts + run: | + make release + - name: get release notes + run: | + curl -L "https://raw.githubusercontent.com/${{ github.repository }}/main/CHANGELOG/${{ env.RELEASE_TAG }}.md" \ + -o "${{ env.RELEASE_TAG }}.md" + - name: Release + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # tag=v2.0.8 + with: + draft: true + files: out/* + body_path: ${{ env.RELEASE_TAG }}.md + tag_name: ${{ env.RELEASE_TAG }} diff --git a/.gitignore b/.gitignore index 26dd0216..49d9c9d1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,11 +24,13 @@ tilt_config.json tilt.d/* out -releasenotes # Development containers (https://containers.dev/) .devcontainer +# vscode +.vscode + # Common editor / temporary files *~ *.tmp diff --git a/Makefile b/Makefile index 5ae8d56a..58dffe32 100644 --- a/Makefile +++ b/Makefile @@ -289,11 +289,8 @@ kind-reset: ## Destroys the "ipam" kind cluster. ## Release ## -------------------------------------- -RELEASE_TAG ?= $(shell git describe --abbrev=0 2>/dev/null) RELEASE_DIR := out RELEASE_NOTES_DIR := releasenotes -PREVIOUS_TAG ?= $(shell git tag -l | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+" | sort -V | grep -B1 $(RELEASE_TAG) | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$$" | head -n 1 2>/dev/null) - $(RELEASE_DIR): mkdir -p $(RELEASE_DIR)/ @@ -304,9 +301,13 @@ $(RELEASE_NOTES_DIR): release-manifests: $(KUSTOMIZE) $(RELEASE_DIR) ## Builds the manifests to publish with a release $(KUSTOMIZE) build config/default > $(RELEASE_DIR)/ipam-components.yaml +.PHONY: release-notes-tool +release-notes-tool: + go build -C hack/tools -o $(BIN_DIR) -tags tools github.com/metal3-io/ip-address-manager/hack/tools/release + .PHONY: release-notes -release-notes: $(RELEASE_NOTES_DIR) $(RELEASE_NOTES) - go run ./hack/tools/release/notes.go --from=$(PREVIOUS_TAG) > $(RELEASE_NOTES_DIR)/$(RELEASE_TAG).md +release-notes: $(RELEASE_NOTES_DIR) $(RELEASE_NOTES) release-notes-tool + $(TOOLS_BIN_DIR)/release --releaseTag=$(RELEASE_TAG) > $(RELEASE_NOTES_DIR)/$(RELEASE_TAG).md .PHONY: release release: diff --git a/docs/releasing.md b/docs/releasing.md index 5ff60168..b1db7848 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -53,22 +53,27 @@ Clone the repository: or if using existing repository, verify your intended remote is set to `metal3-io`: `git remote -v`. For this document, we assume it is `origin`. -- If creating a new minor branch, identify the commit you wish to create the - branch from, and create a branch `release-1.x`: - `git checkout -b release-1.x` and push it to remote: - `git push origin release-1.x` to create it -- If creating a new patch release, use existing branch `release-1.x`: - `git checkout origin/release-1.x` +### Creating Release Notes -### Tags +- Switch to the main branch: `git checkout main` -First we create a primary release tag, that triggers release note creation and -image building processes. +- Create a new branch for the release notes**: + `git checkout -b release-notes-1.x.x origin/main` -- Create a signed, annotated tag with: `git tag -s -a v1.x.y -m v1.x.y` -- Push the tags to the GitHub repository: `git push origin v1.x.y` +- Generate the release notes: `RELEASE_TAG=v1.x.x make release-notes` + - Replace `v1.x.x` with the new release tag you're creating. + - This command generates the release notes here + `releasenotes/.md` . -This triggers two things: +- Commit and push your changes, push the new branch and create a pull request. +IMPORTANT_NOTE: + - The commit and PR title should be 🚀 Release v1.x.y. + - Important! The commit should only contain the release notes file, nothing + else, otherwise automation will not work. + +- Ask maintainers and release team members to review your pull request. + +Once PR is merged it triggers three things: - GitHub action workflow for automated release process creates a draft release in GitHub repository with correct content, comparing the pushed tag to @@ -83,6 +88,11 @@ This triggers two things: [Quay tags page](https://quay.io/repository/metal3-io/ip-address-manager?tab=tags). If the release tag build is not visible, check if the action has failed and retrigger as necessary. +- GH job `create draft release` creates draft release. Don't publish the release + until release tag is visible in + [Quay tags page](https://quay.io/repository/metal3-io/ip-address-manager?tab=tags). + If the release tag build is not visible, check if the action has failed and + retrigger as necessary. We also need to create one or more tags for the Go modules ecosystem: diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 200d4538..8140b04b 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -10,7 +10,11 @@ require ( ) require ( + github.com/blang/semver v3.5.1+incompatible github.com/golang/mock v1.6.0 + github.com/google/go-github v17.0.0+incompatible + github.com/pkg/errors v0.9.1 + golang.org/x/oauth2 v0.10.0 sigs.k8s.io/kustomize/kustomize/v5 v5.4.3 ) @@ -28,6 +32,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -40,7 +45,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/onsi/gomega v1.31.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -51,6 +55,7 @@ require ( golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.18.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/hack/tools/go.sum b/hack/tools/go.sum index b60e21f6..b2af4278 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -1,3 +1,5 @@ +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -33,13 +35,19 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -134,12 +142,15 @@ golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -160,6 +171,7 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -174,6 +186,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hack/tools/release/notes.go b/hack/tools/release/notes.go index 74651fad..d62da7d6 100644 --- a/hack/tools/release/notes.go +++ b/hack/tools/release/notes.go @@ -21,11 +21,18 @@ package main import ( "bytes" + "context" "flag" "fmt" + "log" "os" "os/exec" "strings" + + "github.com/blang/semver" + "github.com/google/go-github/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" ) /* @@ -43,10 +50,10 @@ const ( other = ":seedling: Others" unknown = ":question: Sort these by hand" superseded = ":recycle: Superseded or Reverted" -) - -const ( + repoOwner = "metal3-io" + repoName = "ip-address-manager" warningTemplate = ":rotating_light: This is a %s. Use it only for testing purposes. If you find any bugs, file an [issue](https://github.com/metal3-io/ip-address-manager/issues/new/).\n\n" + ) var ( @@ -59,8 +66,7 @@ var ( unknown, superseded, } - - fromTag = flag.String("from", "", "The tag or commit to start from.") + toTag = flag.String("releaseTag", "", "The tag or commit to end to.") ) func main() { @@ -68,25 +74,42 @@ func main() { os.Exit(run()) } -func latestTag() string { - cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") - out, err := cmd.Output() - if err != nil { - return firstCommit() +func latestTag() (string, error) { + if toTag != nil && *toTag != "" { + return *toTag, nil } - return string(bytes.TrimSpace(out)) + return "", errors.New("RELEASE_TAG is not set") } -func lastTag() string { - if fromTag != nil && *fromTag != "" { - return *fromTag - } - cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") - out, err := cmd.Output() - if err != nil { - return firstCommit() +// lastTag returns the tag to start collecting commits from based on the latestTag. +// For pre-releases and minor releases, it returns the latest minor release tag +// (e.g., for v1.9.0, v1.9.0-beta.0, or v1.9.0-rc.0, it returns v1.8.0). +// For patch releases, it returns the latest patch release tag (e.g., for v1.9.1 it returns v1.9.0). +func lastTag(latestTag string) (string, error) { + if isBeta(latestTag) || isRC(latestTag) || isMinor(latestTag) { + if index := strings.LastIndex(latestTag, "-"); index != -1 { + latestTag = latestTag[:index] + } + latestTag = strings.TrimPrefix(latestTag, "v") + + semVersion, err := semver.New(latestTag) + if err != nil { + return "", errors.Wrapf(err, "parsing semver for %s", latestTag) + } + semVersion.Minor-- + lastReleaseTag := fmt.Sprintf("v%s", semVersion.String()) + return lastReleaseTag, nil + } else { + latestTag = strings.TrimPrefix(latestTag, "v") + + semVersion, err := semver.New(latestTag) + if err != nil { + return "", errors.Wrapf(err, "parsing semver for %s", latestTag) + } + semVersion.Patch-- + lastReleaseTag := fmt.Sprintf("v%s", semVersion.String()) + return lastReleaseTag, nil } - return string(bytes.TrimSpace(out)) } func isBeta(tag string) bool { @@ -97,6 +120,10 @@ func isRC(tag string) bool { return strings.Contains(tag, "-rc.") } +func isMinor(tag string) bool { + return strings.HasSuffix(tag, ".0") +} + func firstCommit() string { cmd := exec.Command("git", "rev-list", "--max-parents=0", "HEAD") out, err := cmd.Output() @@ -107,9 +134,20 @@ func firstCommit() string { } func run() int { - lastTag := lastTag() - latestTag := latestTag() - cmd := exec.Command("git", "rev-list", lastTag+"..HEAD", "--merges", "--pretty=format:%B") // #nosec G204:gosec + latestTag, err := latestTag() + if err != nil{ + log.Fatal("Failed to get latestTag \n") + } + lastTag, err := lastTag(latestTag) + if err != nil{ + log.Fatal("Failed to get lastTag \n") + } + + commitHash,err := getCommitHashFromNewTag(latestTag) + if err != nil{ + log.Fatalf("Failed to get commit has from latestTag %s",latestTag) + } + cmd := exec.Command("git", "rev-list", lastTag+".."+commitHash, "--merges", "--pretty=format:%B") // #nosec G204:gosec merges := map[string][]string{ features: {}, @@ -173,6 +211,8 @@ func run() int { key = warning body = strings.TrimPrefix(body, ":warning:") body = strings.TrimPrefix(body, "⚠️") + case strings.HasPrefix(body, ":rocket:"), strings.HasPrefix(body, "🚀"): + continue default: key = unknown } @@ -242,3 +282,39 @@ func formatMerge(line, prNumber string) string { } return fmt.Sprintf("%s (%s)", line, prNumber) } + +// getCommitHashFromNewTag returns the latest commit hash for the specified tag. +// For minor and pre releases, it returns the main branch's latest commit. +// For patch releases, it returns the latest commit on the corresponding release branch. +func getCommitHashFromNewTag(newTag string) (string, error) { + trimmedTag := newTag + branch := "main" + if !isRC(newTag) && !isBeta(newTag) && !isMinor(newTag){ + trimmedTag = strings.TrimPrefix(trimmedTag, "v") + if index := strings.LastIndex(trimmedTag, "."); index != -1 { + trimmedTag = trimmedTag[:index] + } + branch = fmt.Sprintf("release-%s", trimmedTag) + } + + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return "", errors.New("GITHUB_TOKEN is required") + } else { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + ref, _, err := client.Git.GetRef(ctx, repoOwner, repoName, "refs/heads/"+branch) + if err != nil { + return "", err + log.Fatalf("Error fetching ref: %v", err) + } + commitHash := ref.GetObject().GetSHA() + return commitHash, nil + } +} +