Skip to content

Commit a33627e

Browse files
authored
Merge pull request #381 from nulib/deploy/staging
Deploy v2.11.1 to production
2 parents 476ec8b + cb6960f commit a33627e

272 files changed

Lines changed: 17122 additions & 43830 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/env bash
2+
# .github/scripts/generate_release_notes.sh
3+
#
4+
# Generates human-readable release notes for a DC API production release.
5+
# Finds PRs merged into deploy/staging since the last production tag, filters
6+
# noise, calls Claude via Bedrock to summarize, then creates a GitHub Release.
7+
#
8+
# Required env vars:
9+
# GITHUB_TOKEN - GitHub token with contents:write and pull-requests:read
10+
# AWS_REGION - AWS region for Bedrock (e.g. "us-east-1")
11+
# CURRENT_TAG - Tag being released (e.g. "v1.2.3")
12+
#
13+
# Optional env vars:
14+
# PREVIOUS_TAG - Previous release tag; auto-detected from git if not set
15+
# DRAFT_RELEASE - Set to "true" to create a draft GitHub Release
16+
# GITHUB_REPOSITORY - Set automatically by GitHub Actions (owner/repo)
17+
# DRY_RUN - Set to "true" to skip release creation and print output only
18+
#
19+
# Local testing (dry run):
20+
# export GITHUB_TOKEN=<your-pat>
21+
# export CURRENT_TAG=v1.2.3
22+
# export PREVIOUS_TAG=v1.1.0
23+
# export AWS_REGION=us-east-1
24+
# export GITHUB_REPOSITORY=nulib/dc-api-v2
25+
# export DRY_RUN=true
26+
# bash .github/scripts/generate_release_notes.sh
27+
28+
set -euo pipefail
29+
30+
REPO="${GITHUB_REPOSITORY}"
31+
CURRENT_TAG="${CURRENT_TAG:?CURRENT_TAG must be set}"
32+
PREVIOUS_TAG="${PREVIOUS_TAG:-}"
33+
MODEL_ID="us.anthropic.claude-sonnet-4-6"
34+
DRAFT_RELEASE="${DRAFT_RELEASE:-false}"
35+
DRY_RUN="${DRY_RUN:-false}"
36+
37+
FILTERED_COUNT=0
38+
PR_DETAIL_LIST=""
39+
SUMMARY=""
40+
41+
if [[ "$DRY_RUN" == "true" ]]; then
42+
echo "==> DRY RUN MODE — no GitHub Release will be created"
43+
fi
44+
45+
TEST_PR_LIST="${TEST_PR_LIST:-}"
46+
47+
echo "==> Generating release notes for ${CURRENT_TAG}"
48+
49+
if [[ -n "$TEST_PR_LIST" ]]; then
50+
echo "==> TEST MODE — using provided TEST_PR_LIST, skipping GitHub API"
51+
FILTERED_COUNT=$(echo "$TEST_PR_LIST" | grep -c "^-" || true)
52+
PR_DETAIL_LIST="$TEST_PR_LIST"
53+
54+
else
55+
# ---------------------------------------------------------------------------
56+
# 1. Find the previous production tag
57+
# ---------------------------------------------------------------------------
58+
if [[ -z "$PREVIOUS_TAG" ]]; then
59+
echo "==> Finding previous tag..."
60+
61+
PREVIOUS_TAG=$(git tag \
62+
--sort=-creatordate \
63+
--list "v*" \
64+
| grep -v "^${CURRENT_TAG}$" \
65+
| head -1 || true)
66+
67+
if [[ -z "$PREVIOUS_TAG" ]]; then
68+
echo "No previous tag found. Skipping release notes generation."
69+
exit 0
70+
fi
71+
else
72+
echo "==> Using provided previous tag: ${PREVIOUS_TAG}"
73+
fi
74+
75+
echo " Previous tag: ${PREVIOUS_TAG}"
76+
echo " Current tag: ${CURRENT_TAG}"
77+
78+
# ---------------------------------------------------------------------------
79+
# 2. Fetch merged PRs between the two tags via GitHub API
80+
# ---------------------------------------------------------------------------
81+
echo "==> Fetching merged PRs between ${PREVIOUS_TAG} and ${CURRENT_TAG}..."
82+
83+
PREVIOUS_TAG_DATE=$(git log -1 --format="%cI" "${PREVIOUS_TAG}")
84+
echo " Previous tag date: ${PREVIOUS_TAG_DATE}"
85+
86+
PR_RESPONSE=$(curl -s \
87+
-H "Authorization: token ${GITHUB_TOKEN}" \
88+
-H "Accept: application/vnd.github.v3+json" \
89+
"https://api.github.com/repos/${REPO}/pulls?state=closed&base=deploy/staging&sort=updated&direction=desc&per_page=100&since=${PREVIOUS_TAG_DATE}")
90+
91+
PR_LIST=$(echo "$PR_RESPONSE" | jq -r --arg since "$PREVIOUS_TAG_DATE" '
92+
[
93+
.[] |
94+
select(
95+
.merged_at != null and
96+
.merged_at > $since
97+
) |
98+
{
99+
number: .number,
100+
title: .title,
101+
labels: [.labels[].name],
102+
merged_at: .merged_at
103+
}
104+
] | sort_by(.merged_at)
105+
')
106+
107+
PR_COUNT=$(echo "$PR_LIST" | jq 'length')
108+
echo " Found ${PR_COUNT} merged PRs"
109+
110+
if [[ "$PR_COUNT" -eq 0 ]]; then
111+
echo "No PRs found for this release. Creating release with minimal notes."
112+
SUMMARY="No pull requests were found for this release."
113+
PR_DETAIL_LIST=""
114+
else
115+
# -------------------------------------------------------------------------
116+
# 3. Filter out noise PRs
117+
# -------------------------------------------------------------------------
118+
echo "==> Filtering noise PRs..."
119+
120+
FILTERED_PRS=$(echo "$PR_LIST" | jq -r '
121+
[
122+
.[] |
123+
select(
124+
(.title | ascii_downcase | test("^(dependabot|chore:|ci:|ignore:|build:|bump version|increment version|deploy v|dependency rollup|auto-release)") | not) and
125+
((.labels | map(ascii_downcase) | any(. == "dependencies" or . == "chore" or . == "ci" or . == "ignore")) | not)
126+
)
127+
]
128+
')
129+
130+
FILTERED_COUNT=$(echo "$FILTERED_PRS" | jq 'length')
131+
EXCLUDED_COUNT=$(( PR_COUNT - FILTERED_COUNT ))
132+
echo " Kept ${FILTERED_COUNT} PRs, excluded ${EXCLUDED_COUNT} noise PRs"
133+
134+
if [[ "$FILTERED_COUNT" -eq 0 ]]; then
135+
echo "All PRs were infrastructure/dependency updates. Creating release with minimal notes."
136+
SUMMARY="This release contains dependency updates and infrastructure improvements only."
137+
PR_DETAIL_LIST=""
138+
else
139+
PR_DETAIL_LIST=$(echo "$FILTERED_PRS" | jq -r '
140+
.[] | "- #\(.number): \(.title)"
141+
')
142+
fi
143+
fi
144+
145+
fi
146+
147+
# ---------------------------------------------------------------------------
148+
# 4. Call Claude via Bedrock to generate plain-language release notes
149+
# ---------------------------------------------------------------------------
150+
151+
if [[ -n "$PR_DETAIL_LIST" ]]; then
152+
echo "==> Calling Claude via Bedrock..."
153+
echo " PRs to summarize:"
154+
echo "$PR_DETAIL_LIST" | sed 's/^/ /'
155+
156+
PROMPT="You are writing release notes for DC API (dc-api-v2), the digital collections API service that powers Northwestern University Libraries' digital asset discovery and delivery platform.
157+
158+
Given the following list of pull request titles merged into this release, write concise, plain-language release notes suitable for both technical staff and library stakeholders. Focus on what changed from the user's perspective — new features, bug fixes, or improvements. Group related changes if it makes sense. Do not mention PR numbers, branch names, version numbers, or technical implementation details. Do not invent a title or header. Use plain prose or a short bullet list. Keep it under 200 words.
159+
160+
Pull requests in this release:
161+
${PR_DETAIL_LIST}
162+
163+
Write only the release notes text, nothing else."
164+
165+
REQUEST_PAYLOAD=$(jq -n \
166+
--arg prompt "$PROMPT" \
167+
'{
168+
anthropic_version: "bedrock-2023-05-31",
169+
max_tokens: 1024,
170+
messages: [
171+
{
172+
role: "user",
173+
content: $prompt
174+
}
175+
]
176+
}')
177+
178+
PAYLOAD_FILE=$(mktemp)
179+
echo "$REQUEST_PAYLOAD" > "$PAYLOAD_FILE"
180+
RESPONSE_FILE=$(mktemp)
181+
182+
aws bedrock-runtime invoke-model \
183+
--region "${AWS_REGION}" \
184+
--model-id "${MODEL_ID}" \
185+
--content-type "application/json" \
186+
--accept "application/json" \
187+
--body "fileb://${PAYLOAD_FILE}" \
188+
"$RESPONSE_FILE"
189+
190+
SUMMARY=$(jq -r '.content[0].text' "$RESPONSE_FILE")
191+
rm -f "$PAYLOAD_FILE" "$RESPONSE_FILE"
192+
193+
echo " Summary generated (${#SUMMARY} chars)"
194+
fi
195+
196+
RELEASE_BODY="${SUMMARY}"
197+
198+
# ---------------------------------------------------------------------------
199+
# 5. Create the GitHub Release (or print in dry run mode)
200+
# ---------------------------------------------------------------------------
201+
202+
if [[ "$DRY_RUN" == "true" ]]; then
203+
echo ""
204+
echo "==> DRY RUN: Would create GitHub Release with the following:"
205+
echo ""
206+
echo " Tag: ${CURRENT_TAG}"
207+
echo " Name: DC API ${CURRENT_TAG}"
208+
echo ""
209+
echo "--- RELEASE BODY ---"
210+
echo "${RELEASE_BODY}"
211+
echo "--- END RELEASE BODY ---"
212+
echo ""
213+
echo "==> DRY RUN complete. No release was created."
214+
215+
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
216+
{
217+
echo "current_tag=${CURRENT_TAG}"
218+
echo "release_url=DRY_RUN"
219+
echo "release_body<<__RELEASE_BODY__"
220+
echo "${RELEASE_BODY}"
221+
echo "__RELEASE_BODY__"
222+
} >> "$GITHUB_OUTPUT"
223+
fi
224+
exit 0
225+
fi
226+
227+
echo "==> Creating GitHub Release for ${CURRENT_TAG}..."
228+
229+
RELEASE_PAYLOAD=$(jq -n \
230+
--arg tag "$CURRENT_TAG" \
231+
--arg name "DC API ${CURRENT_TAG}" \
232+
--arg body "$RELEASE_BODY" \
233+
--argjson draft "$([[ "$DRAFT_RELEASE" == "true" ]] && echo "true" || echo "false")" \
234+
'{
235+
tag_name: $tag,
236+
name: $name,
237+
body: $body,
238+
draft: $draft,
239+
prerelease: false
240+
}')
241+
242+
RELEASE_RESPONSE=$(curl -s \
243+
-X POST \
244+
-H "Authorization: token ${GITHUB_TOKEN}" \
245+
-H "Accept: application/vnd.github.v3+json" \
246+
"https://api.github.com/repos/${REPO}/releases" \
247+
-d "$RELEASE_PAYLOAD")
248+
249+
RELEASE_URL=$(echo "$RELEASE_RESPONSE" | jq -r '.html_url')
250+
251+
if [[ "$RELEASE_URL" == "null" || -z "$RELEASE_URL" ]]; then
252+
echo "ERROR: Failed to create release. Response:"
253+
echo "$RELEASE_RESPONSE" | jq .
254+
exit 1
255+
fi
256+
257+
echo "==> Release created successfully: ${RELEASE_URL}"
258+
259+
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
260+
{
261+
echo "current_tag=${CURRENT_TAG}"
262+
echo "release_url=${RELEASE_URL}"
263+
echo "release_body<<__RELEASE_BODY__"
264+
echo "${RELEASE_BODY}"
265+
echo "__RELEASE_BODY__"
266+
} >> "$GITHUB_OUTPUT"
267+
fi

.github/workflows/deploy.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
repository: "nulib/tfvars"
5959
ref: main
6060
path: ".tfvars"
61+
- uses: oven-sh/setup-bun@v2
6162
- uses: actions/setup-python@v2
6263
with:
6364
python-version: "3.12"
@@ -106,3 +107,20 @@ jobs:
106107
github_token: ${{ steps.dispatch-token.outputs.token }}
107108
tags: true
108109
branch: main
110+
- name: Generate Release Notes
111+
if: ${{ github.ref == 'refs/heads/main' }}
112+
uses: actions/github-script@v6
113+
with:
114+
script: |
115+
github.rest.actions.createWorkflowDispatch({
116+
owner: context.repo.owner,
117+
repo: context.repo.repo,
118+
workflow_id: 'release_notes.yml',
119+
ref: 'main',
120+
inputs: {
121+
current_tag: `v${process.env.API_VERSION}`,
122+
previous_tag: '',
123+
draft: 'false',
124+
notify_teams: 'true',
125+
},
126+
})
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
name: Generate Release Notes
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
current_tag:
7+
description: "Tag being released (e.g. v1.2.3)"
8+
required: true
9+
previous_tag:
10+
description: "Previous release tag (leave empty to auto-detect)"
11+
required: false
12+
default: ""
13+
draft:
14+
description: "Create as draft release"
15+
type: boolean
16+
default: false
17+
notify_teams:
18+
description: "Send notification to Teams channel"
19+
type: boolean
20+
default: true
21+
permissions:
22+
id-token: write
23+
contents: write
24+
actions: write
25+
jobs:
26+
generate:
27+
runs-on: ubuntu-latest
28+
permissions:
29+
id-token: write
30+
contents: write
31+
actions: write
32+
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
33+
steps:
34+
- uses: actions/checkout@v3
35+
with:
36+
fetch-depth: 0
37+
fetch-tags: true
38+
- uses: aws-actions/configure-aws-credentials@master
39+
with:
40+
role-to-assume: arn:aws:iam::${{ secrets.AwsAccount }}:role/github-actions-role
41+
aws-region: us-east-1
42+
- name: Generate Release Notes
43+
id: generate
44+
run: .github/scripts/generate_release_notes.sh
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
CURRENT_TAG: ${{ inputs.current_tag }}
48+
PREVIOUS_TAG: ${{ inputs.previous_tag }}
49+
DRAFT_RELEASE: ${{ inputs.draft }}
50+
AWS_REGION: us-east-1
51+
- name: Notify Teams
52+
if: steps.generate.outputs.release_body != '' && inputs.notify_teams
53+
env:
54+
TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
55+
RELEASE_URL: ${{ steps.generate.outputs.release_url }}
56+
RELEASE_BODY: ${{ steps.generate.outputs.release_body }}
57+
CURRENT_TAG: ${{ steps.generate.outputs.current_tag }}
58+
IS_DRAFT: ${{ inputs.draft }}
59+
run: |
60+
if [[ "$IS_DRAFT" == "true" ]]; then
61+
TITLE="DC API ${CURRENT_TAG} (Draft Release)"
62+
else
63+
TITLE="DC API ${CURRENT_TAG} Released"
64+
fi
65+
66+
PAYLOAD=$(jq -n \
67+
--arg title "$TITLE" \
68+
--arg body "$RELEASE_BODY" \
69+
--arg url "$RELEASE_URL" \
70+
'{
71+
type: "message",
72+
attachments: [{
73+
contentType: "application/vnd.microsoft.card.adaptive",
74+
content: {
75+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
76+
type: "AdaptiveCard",
77+
version: "1.2",
78+
body: [
79+
{type: "TextBlock", size: "large", weight: "bolder", text: $title, wrap: true},
80+
{type: "TextBlock", text: $body, wrap: true}
81+
],
82+
actions: [
83+
{type: "Action.OpenUrl", title: "View Release", url: $url}
84+
]
85+
}
86+
}]
87+
}')
88+
89+
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
90+
-X POST \
91+
-H "Content-Type: application/json" \
92+
-d "$PAYLOAD" \
93+
"$TEAMS_WEBHOOK_URL")
94+
95+
if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "202" ]]; then
96+
echo "WARNING: Teams notification returned HTTP ${HTTP_STATUS}"
97+
else
98+
echo "==> Teams notification sent"
99+
fi

0 commit comments

Comments
 (0)