Skip to content

Commit 3ccbdc3

Browse files
Merge branch '1.3.x' into 1.4.x
2 parents 3a92d4e + 3ca2982 commit 3ccbdc3

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Post Release Workflow
2+
3+
on:
4+
workflow_dispatch: # Enables manual trigger
5+
6+
jobs:
7+
generate-release-notes:
8+
name: Generate Release Notes
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Check out the repository
13+
uses: actions/checkout@v3
14+
15+
- name: Download Changelog Generator
16+
run: |
17+
curl -L -o github-changelog-generator.jar https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.11/github-changelog-generator.jar
18+
19+
- name: Generate release notes
20+
id: generate_notes
21+
run: |
22+
java -jar github-changelog-generator.jar \
23+
${GITHUB_REF_NAME#v} \
24+
changelog.md \
25+
--changelog.repository="${{ github.repository }}" \
26+
--github.token="${{ secrets.GITHUB_TOKEN }}"
27+
28+
- name: Run script to process Markdown file
29+
run: python .github/workflows/process_changelog.py
30+
31+
- name: Update release text
32+
run: |
33+
echo -e "::Info::Original changelog\n\n"
34+
cat changelog.md
35+
36+
echo -e "\n\n"
37+
echo -e "::Info::Processed changelog\n\n"
38+
cat changelog-output.md
39+
gh release edit ${{ github.ref_name }} --notes-file changelog-output.md
40+
env:
41+
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
42+
43+
close-milestone:
44+
name: Close Milestone
45+
runs-on: ubuntu-latest
46+
needs: generate-release-notes
47+
steps:
48+
- name: Close milestone
49+
run: |
50+
# Extract version without 'v' prefix
51+
milestone_name=${GITHUB_REF_NAME#v}
52+
53+
echo "Closing milestone: $milestone_name"
54+
55+
# List milestones and find the ID
56+
milestone_id=$(gh api "/repos/${{ github.repository }}/milestones?state=open" \
57+
--jq ".[] | select(.title == \"$milestone_name\").number")
58+
59+
if [ -z "$milestone_id" ]; then
60+
echo "::error::Milestone '$milestone_name' not found"
61+
exit 1
62+
fi
63+
64+
# Close the milestone
65+
gh api --method PATCH "/repos/${{ github.repository }}/milestones/$milestone_id" \
66+
-f state=closed
67+
68+
echo "Successfully closed milestone: $milestone_name"
69+
env:
70+
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
71+
72+
notify:
73+
name: Send Notifications
74+
runs-on: ubuntu-latest
75+
needs: close-milestone
76+
77+
steps:
78+
- name: Announce Release on `Spring-Releases` space
79+
run: |
80+
milestone_name=${GITHUB_REF_NAME#v}
81+
curl --location --request POST '${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }}' \
82+
--header 'Content-Type: application/json' \
83+
--data-raw "{ text: \"${{ github.event.repository.name }}-announcing ${milestone_name}\"}"
84+
85+
- name: Post on Bluesky
86+
env:
87+
BSKY_IDENTIFIER: ${{ secrets.BLUESKY_HANDLE }}
88+
BSKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
89+
run: |
90+
# First get the session token
91+
SESSION_TOKEN=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
92+
-H "Content-Type: application/json" \
93+
-d "{\"identifier\":\"$BSKY_IDENTIFIER\",\"password\":\"$BSKY_PASSWORD\"}" | \
94+
jq -r .accessJwt)
95+
96+
# Create post content
97+
VERSION=${GITHUB_REF_NAME#v}
98+
POST_TEXT="${{ github.event.repository.name }} ${VERSION} has been released!\n\nCheck out the changelog: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}"
99+
100+
# Create the post
101+
curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
102+
-H "Content-Type: application/json" \
103+
-H "Authorization: Bearer ${SESSION_TOKEN}" \
104+
-d "{
105+
\"repo\": \"$BSKY_IDENTIFIER\",
106+
\"collection\": \"app.bsky.feed.post\",
107+
\"record\": {
108+
\"\$type\": \"app.bsky.feed.post\",
109+
\"text\": \"$POST_TEXT\",
110+
\"createdAt\": \"$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")\"
111+
}
112+
}"
+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import re
2+
import subprocess
3+
4+
input_file = "changelog.md"
5+
output_file = "changelog-output.md"
6+
7+
def fetch_test_and_optional_dependencies():
8+
# Fetch the list of all subprojects
9+
result = subprocess.run(
10+
["./gradlew", "projects"],
11+
stdout=subprocess.PIPE,
12+
stderr=subprocess.PIPE,
13+
text=True,
14+
)
15+
subprojects = []
16+
for line in result.stdout.splitlines():
17+
match = re.match(r".*Project (':.+')", line)
18+
if match:
19+
subprojects.append(match.group(1).strip("'"))
20+
21+
print(f"Found the following subprojects\n\n {subprojects}\n\n")
22+
test_optional_dependencies = set()
23+
implementation_dependencies = set()
24+
25+
print("Will fetch non transitive dependencies for all subprojects...")
26+
# Run dependencies task for all subprojects in a single Gradle command
27+
if subprojects:
28+
dependencies_command = ["./gradlew"] + [f"{subproject}:dependencies" for subproject in subprojects]
29+
result = subprocess.run(
30+
dependencies_command,
31+
stdout=subprocess.PIPE,
32+
stderr=subprocess.PIPE,
33+
text=True,
34+
)
35+
in_test_section = False
36+
in_optional_section = False
37+
in_implementation_section = False
38+
39+
for line in result.stdout.splitlines():
40+
if "project :" in line:
41+
continue
42+
43+
# Detect gradle plugin
44+
if "classpath" in line:
45+
in_optional_section = True
46+
continue
47+
48+
# Detect test dependencies section
49+
if "testCompileClasspath" in line or "testImplementation" in line:
50+
in_test_section = True
51+
continue
52+
if "runtimeClasspath" in line or line.strip() == "":
53+
in_test_section = False
54+
55+
# Detect optional dependencies section
56+
if "compileOnly" in line:
57+
in_optional_section = True
58+
continue
59+
if line.strip() == "":
60+
in_optional_section = False
61+
62+
# Detect implementation dependencies section
63+
if "implementation" in line or "compileClasspath" in line:
64+
in_implementation_section = True
65+
continue
66+
if line.strip() == "":
67+
in_implementation_section = False
68+
69+
# Parse dependencies explicitly declared with +--- or \---
70+
match = re.match(r"[\\+|\\\\]--- ([^:]+):([^:]+):([^ ]+)", line)
71+
if match:
72+
group_id, artifact_id, _ = match.groups()
73+
dependency_key = f"{group_id}:{artifact_id}"
74+
if in_test_section or in_optional_section:
75+
test_optional_dependencies.add(dependency_key)
76+
if in_implementation_section:
77+
implementation_dependencies.add(dependency_key)
78+
79+
# Remove dependencies from test/optional if they are also in implementation
80+
final_exclusions = test_optional_dependencies - implementation_dependencies
81+
82+
print(f"Dependencies in either test or optional scope to be excluded from changelog processing:\n\n{final_exclusions}\n\n")
83+
return final_exclusions
84+
85+
def process_dependency_upgrades(lines, exclude_dependencies):
86+
dependencies = {}
87+
regex = re.compile(r"- Bump (.+?) from ([\d\.]+) to ([\d\.]+) \[(#[\d]+)\]\((.+)\)")
88+
for line in lines:
89+
match = regex.match(line)
90+
if match:
91+
unit, old_version, new_version, pr_number, link = match.groups()
92+
if unit not in exclude_dependencies:
93+
if unit not in dependencies:
94+
dependencies[unit] = {"lowest": old_version, "highest": new_version, "pr_number": pr_number, "link": link}
95+
else:
96+
dependencies[unit]["lowest"] = min(dependencies[unit]["lowest"], old_version)
97+
dependencies[unit]["highest"] = max(dependencies[unit]["highest"], new_version)
98+
sorted_units = sorted(dependencies.keys())
99+
return [f"- Bump {unit} from {dependencies[unit]['lowest']} to {dependencies[unit]['highest']} [{dependencies[unit]['pr_number']}]({dependencies[unit]['link']})" for unit in sorted_units]
100+
101+
with open(input_file, "r") as file:
102+
lines = file.readlines()
103+
104+
# Fetch test and optional dependencies from all projects
105+
print("Fetching test and optional dependencies from the project and its subprojects...")
106+
exclude_dependencies = fetch_test_and_optional_dependencies()
107+
108+
# Step 1: Copy all content until the hammer line
109+
header = []
110+
dependency_lines = []
111+
footer = []
112+
in_dependency_section = False
113+
114+
print("Parsing changelog until the dependency upgrades section...")
115+
116+
for line in lines:
117+
if line.startswith("## :hammer: Dependency Upgrades"):
118+
in_dependency_section = True
119+
header.append(line)
120+
header.append("\n")
121+
break
122+
header.append(line)
123+
124+
print("Parsing dependency upgrade section...")
125+
126+
# Step 2: Parse dependency upgrades
127+
if in_dependency_section:
128+
for line in lines[len(header):]:
129+
if line.startswith("## :heart: Contributors"):
130+
break
131+
dependency_lines.append(line)
132+
133+
print("Parsing changelog to find everything after the dependency upgrade section...")
134+
# Find the footer starting from the heart line
135+
footer_start_index = next((i for i, line in enumerate(lines) if line.startswith("## :heart: Contributors")), None)
136+
if footer_start_index is not None:
137+
footer = lines[footer_start_index:]
138+
139+
print("Processing the dependency upgrades section...")
140+
processed_dependencies = process_dependency_upgrades(dependency_lines, exclude_dependencies)
141+
142+
print("Writing output...")
143+
# Step 3: Write the output file
144+
with open(output_file, "w") as file:
145+
file.writelines(header)
146+
file.writelines(f"{line}\n" for line in processed_dependencies)
147+
file.writelines("\n")
148+
file.writelines(footer)

0 commit comments

Comments
 (0)