1+ name : datastructures-algorithms-tag-and-release
2+
3+ # Triggers only when a PR to 'main' is closed
4+ on :
5+ pull_request :
6+ types : [closed] # Run when a PR is closed
7+ branches :
8+ - ' main' # Only when the PR's target branch is 'main'
9+
10+ # Permissions required for the tag/release job
11+ permissions :
12+ contents : write # To create/push tags AND create releases
13+ pull-requests : read # To read the PR body
14+
15+ jobs :
16+ # --- Job 1: Lint, Build, and Test ---
17+ lint-build-test :
18+ # This job only runs if the PR was actually merged
19+ if : github.event.pull_request.merged == true
20+ runs-on : windows-latest
21+
22+ steps :
23+ - name : Checkout code
24+ uses : actions/checkout@v4 # Use v4
25+
26+ - name : Setup MSVC
27+ uses : ilammy/msvc-dev-cmd@v1
28+ with :
29+ arch : x64
30+
31+ - name : Install dependencies
32+ shell : pwsh
33+ run : |
34+ choco install cmake -y
35+ choco install ninja -y
36+ choco install llvm -y
37+
38+ - name : Configure CMake
39+ shell : pwsh
40+ run : cmake -S . -B build -G "Ninja" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
41+
42+ - name : Run clang-tidy
43+ shell : pwsh
44+ run : |
45+ clang-tidy --version
46+ $files = Get-ChildItem -Recurse -Path source -Include *.cpp,*.cc,*.cxx -File
47+
48+ # These source/.* files would be checked using .clang-tidy maintained at projectroot
49+ foreach ($file in $files) {
50+ Write-Host "Running clang-tidy on source $($file.FullName)"
51+ clang-tidy -p build "$($file.FullName)" --warnings-as-errors=*
52+ }
53+
54+ - name : Build
55+ shell : pwsh
56+ run : cmake --build build
57+
58+ - name : Run tests
59+ shell : pwsh
60+ run : ctest --test-dir build --output-on-failure
61+
62+ # --- Job 2: Create Tag and Release ---
63+ create-tag-and-release :
64+ # This job MUST wait for the 'lint-build-test' job to succeed
65+ needs : lint-build-test
66+
67+ # This job only runs if:
68+ # 1. The PR was actually merged (implicit from 'needs', but good to be explicit).
69+ # 2. The PR was from a branch named exactly 'release'.
70+ if : github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'release'
71+ runs-on : ubuntu-latest
72+
73+ steps :
74+ # Step 1: Check out the code on the 'main' branch
75+ # fetch-depth: 0 is crucial to get all tags and history
76+ - name : Checkout main branch
77+ uses : actions/checkout@v4
78+ with :
79+ ref : ' main'
80+ fetch-depth : 0
81+
82+ # Step 2: Configure Git with bot credentials
83+ - name : Configure Git
84+ run : |
85+ git config --global user.name "github-actions[bot]"
86+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
87+
88+ # Step 3: Parse the PR body to find the version bump type
89+ - name : Determine Version Bump
90+ id : version_bump
91+ env :
92+ PR_BODY : ${{ github.event.pull_request.body }}
93+ run : |
94+ if echo "$PR_BODY" | grep -q '\[x\] Major'; then
95+ echo "bump_type=major" >> $GITHUB_OUTPUT
96+ elif echo "$PR_BODY" | grep -q '\[x\] Minor'; then
97+ echo "bump_type=minor" >> $GITHUB_OUTPUT
98+ elif echo "$PR_BODY" | grep -q '\[x\] Patch'; then
99+ echo "bump_type=patch" >> $GITHUB_OUTPUT
100+ else
101+ echo "No version bump type selected in PR body. ([x] Major, [x] Minor, or [x] Patch)."
102+ exit 1
103+ fi
104+
105+ # Step 4: Get the latest tag
106+ - name : Get latest tag
107+ id : last_tag
108+ run : |
109+ # Fetch all tags from remote just in case
110+ git fetch --tags
111+
112+ # Find the latest tag matching vX.Y.Z format, sort by version, get the highest
113+ LATEST_TAG=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n 1)
114+
115+ if [ -z "$LATEST_TAG" ]; then
116+ echo "No previous vX.Y.Z tag found. Starting from v0.0.0."
117+ LATEST_TAG="v0.0.0"
118+ fi
119+
120+ echo "Last tag: $LATEST_TAG"
121+ echo "last_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
122+
123+ # Step 5: Calculate the new tag based on the bump type
124+ - name : Calculate new tag
125+ id : new_tag
126+ run : |
127+ BUMP_TYPE=${{ steps.version_bump.outputs.bump_type }}
128+ LAST_TAG=${{ steps.last_tag.outputs.last_tag }}
129+
130+ # Strip 'v' prefix
131+ VERSION=${LAST_TAG#v}
132+
133+ # Split into parts
134+ IFS='.' read -r -a parts <<< "$VERSION"
135+ MAJOR=${parts[0]}
136+ MINOR=${parts[1]}
137+ PATCH=${parts[2]}
138+
139+ # Increment based on bump type
140+ case "$BUMP_TYPE" in
141+ "major")
142+ MAJOR=$((MAJOR + 1))
143+ MINOR=0
144+ PATCH=0
145+ ;;
146+ "minor")
147+ MINOR=$((MINOR + 1))
148+ PATCH=0
149+ ;;
150+ "patch")
151+ PATCH=$((PATCH + 1))
152+ ;;
153+ esac
154+
155+ NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
156+ echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
157+ echo "New tag will be: $NEW_TAG"
158+
159+ # Step 6: Create and push the new tag
160+ - name : Create and push new tag
161+ run : |
162+ NEW_TAG=${{ steps.new_tag.outputs.new_tag }}
163+ MERGE_COMMIT_SHA=${{ github.event.pull_request.merge_commit_sha }}
164+
165+ echo "Tagging commit $MERGE_COMMIT_SHA as $NEW_TAG"
166+ git tag $NEW_TAG $MERGE_COMMIT_SHA
167+ git push origin $NEW_TAG
168+
169+ # Step 7: Extract Release Summary from PR Body
170+ - name : Extract Release Summary
171+ id : extract_summary
172+ env :
173+ PR_BODY : ${{ github.event.pull_request.body }}
174+ run : |
175+ # Use an awk "state machine" to extract text between the two markers.
176+ # We use "<<<" (a "here string") to safely pass the multiline $PR_BODY to awk.
177+ # We must escape the '*' characters with '\' for the regex.
178+
179+ # 1. /\*\*Release Summary\*\*/{f=1; next} : When we see the START marker, set flag 'f' to 1 and skip to the next line.
180+ # 2. /\*\*Version Bump Type\*\*/{f=0} : When we see the END marker, set flag 'f' to 0.
181+ # 3. f : If flag 'f' is 1 (true), print the current line.
182+ SUMMARY=$(awk '/\*\*Release Summary\*\*/{f=1; next} /\*\*Version Bump Type\*\*/{f=0} f' <<< "$PR_BODY")
183+
184+ # Use multiline string syntax for GITHUB_OUTPUT
185+ echo "summary<<EOF" >> $GITHUB_OUTPUT
186+ echo "$SUMMARY" >> $GITHUB_OUTPUT
187+ echo "EOF" >> $GITHUB_OUTPUT
188+
189+ # Step 8: Create GitHub Release
190+ - name : Create GitHub Release
191+ env :
192+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
193+ NEW_TAG : ${{ steps.new_tag.outputs.new_tag }}
194+ SUMMARY : ${{ steps.extract_summary.outputs.summary }}
195+ PR_TITLE : ${{ github.event.pull_request.title }}
196+ run : |
197+ # Check if summary is empty, and provide a default
198+ if [ -z "$SUMMARY" ]; then
199+ echo "Release summary was empty. Using PR title as notes."
200+ RELEASE_NOTES="$PR_TITLE"
201+ else
202+ RELEASE_NOTES="$SUMMARY"
203+ fi
204+
205+ # Create the release using the GitHub CLI
206+ # --latest: Marks this as the latest official release
207+ # --title: Set to just the tag name as requested
208+ gh release create $NEW_TAG \
209+ --title "$NEW_TAG" \
210+ --notes "$RELEASE_NOTES" \
211+ --target ${{ github.event.pull_request.merge_commit_sha }} \
212+ --latest
0 commit comments