Skip to content

Commit 4a6eb91

Browse files
authored
Merge pull request #14 from ordermentum/feature/reusable-release-lifecycle
Add reusable release-lifecycle workflow
2 parents 290060e + 78a446f commit 4a6eb91

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
name: Release Lifecycle (Reusable)
2+
3+
on:
4+
workflow_call:
5+
secrets:
6+
SLACK_BOT:
7+
required: true
8+
CLAUDE_CODE_OAUTH_TOKEN:
9+
required: true
10+
11+
jobs:
12+
assess-and-draft:
13+
if: github.event_name != 'release'
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: write
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Determine previous release tag
24+
id: prev-tag
25+
run: |
26+
# gh release view without a tag argument defaults to the "Latest Release"
27+
PREV_TAG=$(gh release view --repo "${{ github.repository }}" --json tagName -q .tagName 2>/dev/null || true)
28+
if [ -z "$PREV_TAG" ]; then
29+
echo "No latest release found. Using initial commit."
30+
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
31+
fi
32+
echo "PREV_TAG=$PREV_TAG" >> "$GITHUB_OUTPUT"
33+
echo "Previous release tag: $PREV_TAG"
34+
env:
35+
GH_TOKEN: ${{ github.token }}
36+
37+
- name: Build comparison URL
38+
id: compare
39+
run: |
40+
COMPARE_URL="${{ github.server_url }}/${{ github.repository }}/compare/${{ steps.prev-tag.outputs.PREV_TAG }}...${{ github.ref_name }}"
41+
echo "COMPARE_URL=$COMPARE_URL" >> "$GITHUB_OUTPUT"
42+
echo "Comparison URL: $COMPARE_URL"
43+
44+
# ---------------------------------------------------------------
45+
# Claude Risk Assessment
46+
#
47+
# Uses the Claude Code CLI (not claude-code-action, which does
48+
# not support the `create` event type).
49+
#
50+
# Auth: CLAUDE_CODE_OAUTH_TOKEN env var (org/repo secret).
51+
# The prompt is piped via stdin heredoc to avoid shell quoting
52+
# issues with multiline arguments.
53+
# ---------------------------------------------------------------
54+
- name: Run Claude Risk Assessment
55+
continue-on-error: true
56+
run: |
57+
cat <<'PROMPT' | npx -y @anthropic-ai/claude-code --print \
58+
--model claude-sonnet-4-5-20250929 \
59+
--allowedTools "Bash(git diff:*)" \
60+
--allowedTools "Bash(git log:*)" \
61+
--allowedTools "Bash(git show:*)" \
62+
--allowedTools "Bash(cat:*)" \
63+
--allowedTools "Read" \
64+
- > /tmp/risk-assessment.txt
65+
You are assessing the risk of a production release.
66+
67+
Repository: ${{ github.repository }}
68+
Release branch: ${{ github.ref_name }}
69+
Previous release: ${{ steps.prev-tag.outputs.PREV_TAG }}
70+
Comparison: ${{ steps.compare.outputs.COMPARE_URL }}
71+
72+
Run git diff ${{ steps.prev-tag.outputs.PREV_TAG }}..HEAD --stat and git log ${{ steps.prev-tag.outputs.PREV_TAG }}..HEAD --oneline to understand the delta.
73+
74+
Provide a concise risk assessment covering:
75+
- Deployment risks (database migrations, breaking changes, new dependencies)
76+
- Security concerns
77+
- High-risk areas of change
78+
79+
Do NOT give feedback on code style or conventions. Be concise and actionable.
80+
Format your output as standard markdown.
81+
PROMPT
82+
env:
83+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
84+
85+
- name: Publish assessment to GitHub Actions summary
86+
run: |
87+
if [ -f /tmp/risk-assessment.txt ]; then
88+
ASSESSMENT=$(cat /tmp/risk-assessment.txt)
89+
else
90+
ASSESSMENT="⚠️ Claude risk assessment did not produce output. Please review the delta manually."
91+
fi
92+
{
93+
echo "## 🚀 Release Risk Assessment — \`${{ github.ref_name }}\`"
94+
echo ""
95+
echo "| | |"
96+
echo "|---|---|"
97+
echo "| **Previous release** | \`${{ steps.prev-tag.outputs.PREV_TAG }}\` |"
98+
echo "| **Compare** | [${{ steps.prev-tag.outputs.PREV_TAG }}...${{ github.ref_name }}](${{ steps.compare.outputs.COMPARE_URL }}) |"
99+
echo ""
100+
echo "$ASSESSMENT"
101+
} >> "$GITHUB_STEP_SUMMARY"
102+
103+
# ---------------------------------------------------------------
104+
# Draft GitHub Release (upsert)
105+
#
106+
# Tag is derived from the branch name:
107+
# release/2026-03-11.3 → v2026-03-11.3
108+
#
109+
# Creates a new draft if none exists; updates (regenerates notes)
110+
# if one already exists for this tag. Appends a link to the risk
111+
# assessment run at the bottom of the release notes.
112+
#
113+
# NOTE: Draft releases use temporary `untagged-*` URLs until
114+
# published, so we get the real URL from `gh release view`.
115+
# ---------------------------------------------------------------
116+
- name: Upsert draft GitHub Release
117+
id: release
118+
if: startsWith(github.ref_name, 'release/')
119+
run: |
120+
TAG="v${BRANCH#release/}"
121+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
122+
# Generate release notes via the API (works for both create and update)
123+
GENERATED_NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
124+
-f tag_name="$TAG" -f target_commitish="$BRANCH" \
125+
-f previous_tag_name="${{ steps.prev-tag.outputs.PREV_TAG }}" \
126+
--jq .body 2>/dev/null || echo "")
127+
128+
if gh release view "$TAG" --repo "${{ github.repository }}" &>/dev/null; then
129+
gh release edit "$TAG" \
130+
--repo "${{ github.repository }}" \
131+
--target "$BRANCH" \
132+
--draft \
133+
--notes "$GENERATED_NOTES"
134+
echo "Updated existing draft release for $TAG"
135+
else
136+
gh release create "$TAG" \
137+
--repo "${{ github.repository }}" \
138+
--target "$BRANCH" \
139+
--title "$TAG" \
140+
--draft \
141+
--notes "$GENERATED_NOTES"
142+
echo "Created draft release for $TAG"
143+
fi
144+
145+
# Append risk assessment link to release notes
146+
NOTES=$(gh release view "$TAG" --repo "${{ github.repository }}" --json body -q .body)
147+
NOTES="${NOTES}
148+
149+
---
150+
📋 [Risk Assessment](${RUN_URL})"
151+
gh release edit "$TAG" --repo "${{ github.repository }}" --notes "$NOTES"
152+
153+
# Get the actual release URL (works for drafts with untagged-* paths)
154+
RELEASE_URL=$(gh release view "$TAG" --repo "${{ github.repository }}" --json url -q .url)
155+
echo "RELEASE_URL=$RELEASE_URL" >> "$GITHUB_OUTPUT"
156+
env:
157+
GH_TOKEN: ${{ github.token }}
158+
BRANCH: ${{ github.ref_name }}
159+
160+
# ---------------------------------------------------------------
161+
# Slack notification
162+
#
163+
# Uses a Slack bot token (xoxb-*) with chat:write scope, NOT an
164+
# incoming webhook. This allows targeting any channel by ID.
165+
#
166+
# Channel: C0A7TSCLA01 (release-tran)
167+
# ---------------------------------------------------------------
168+
- name: Post to Slack
169+
run: |
170+
TAG="v${BRANCH#release/}"
171+
REPO_NAME="${{ github.repository }}"
172+
REPO_SHORT="${REPO_NAME##*/}"
173+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
174+
175+
# URL-encode branch/tag for Buildkite filter links
176+
BRANCH_ENCODED=$(jq -rn --arg b "$BRANCH" '$b | @uri')
177+
TAG_ENCODED=$(jq -rn --arg t "$TAG" '$t | @uri')
178+
BK_BASE="https://buildkite.com/${{ github.repository_owner }}/${REPO_SHORT}/builds"
179+
180+
# Permanent URLs (work after branch is deleted and release is published)
181+
COMPARE_TAG_URL="${{ github.server_url }}/${{ github.repository }}/compare/${{ steps.prev-tag.outputs.PREV_TAG }}...${TAG}"
182+
RELEASE_TAG_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/${TAG}"
183+
184+
MESSAGE="🚀 *Release Risk Assessment — ${REPO_SHORT}* · \`${{ github.ref_name }}\`
185+
*Previous release:* \`${{ steps.prev-tag.outputs.PREV_TAG }}\`
186+
<${{ steps.compare.outputs.COMPARE_URL }}|Compare (branch)> · <${COMPARE_TAG_URL}|Compare (tag)>
187+
<${{ steps.release.outputs.RELEASE_URL }}|Draft release> · <${RELEASE_TAG_URL}|Release ${TAG}>
188+
<${RUN_URL}|Risk assessment> · <${BK_BASE}?branch=${BRANCH_ENCODED}|Branch build> · <${BK_BASE}?branch=${TAG_ENCODED}|Tag build>"
189+
190+
curl -sf -X POST https://slack.com/api/chat.postMessage \
191+
-H "Authorization: Bearer $SLACK_BOT" \
192+
-H "Content-Type: application/json" \
193+
-d "$(jq -n --arg channel "C0A7TSCLA01" --arg text "$MESSAGE" '{channel: $channel, text: $text}')"
194+
env:
195+
SLACK_BOT: ${{ secrets.SLACK_BOT }}
196+
BRANCH: ${{ github.ref_name }}
197+
198+
cleanup-release-branch:
199+
if: github.event_name == 'release'
200+
runs-on: ubuntu-latest
201+
permissions:
202+
contents: write
203+
steps:
204+
- name: Delete release branch
205+
run: |
206+
TAG="${{ github.event.release.tag_name }}"
207+
BRANCH="release/${TAG#v}"
208+
echo "Deleting branch $BRANCH (tag $TAG has been created)"
209+
gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/${BRANCH}" || echo "Branch $BRANCH not found or already deleted"
210+
env:
211+
GH_TOKEN: ${{ github.token }}

0 commit comments

Comments
 (0)