Skip to content

Commit f0e6142

Browse files
committed
feat: initial RimMind shared workflows - reusable release and test workflows
0 parents  commit f0e6142

5 files changed

Lines changed: 668 additions & 0 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
name: Generate Tag Release
2+
run-name: ${{ inputs.tag-name }}
3+
4+
on:
5+
workflow_call:
6+
inputs:
7+
tag-name:
8+
description: 'The tag that triggered the release (e.g. v1.2.3)'
9+
required: true
10+
type: string
11+
deepseek-base-url:
12+
description: 'DeepSeek API base URL'
13+
required: false
14+
type: string
15+
default: 'https://api.deepseek.com'
16+
deepseek-model:
17+
description: 'DeepSeek model name'
18+
required: false
19+
type: string
20+
default: 'deepseek-chat'
21+
exclude-patterns:
22+
description: 'Additional rsync exclude patterns (space-separated)'
23+
required: false
24+
type: string
25+
default: ''
26+
secrets:
27+
DEEPSEEK_API_KEY:
28+
required: true
29+
DEEPSEEK_BASE_URL:
30+
required: false
31+
GITHUB_TOKEN:
32+
required: true
33+
34+
permissions:
35+
contents: write
36+
pull-requests: read
37+
38+
jobs:
39+
release:
40+
name: Generate Release & Package Mod
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Checkout Caller Repo
44+
uses: actions/checkout@v4
45+
with:
46+
fetch-depth: 0
47+
48+
- name: Checkout Shared Workflows
49+
uses: actions/checkout@v4
50+
with:
51+
repository: RimWorld-RimMind-Mod/Workflows
52+
path: .shared-workflows
53+
54+
- name: Determine Git Range
55+
id: git-range
56+
run: |
57+
CURRENT_TAG="${{ inputs.tag-name }}"
58+
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^" 2>/dev/null || echo "")
59+
echo "::group::[Debug] Tag 计算结果"
60+
echo "当前 Tag: $CURRENT_TAG"
61+
if [ -z "$PREVIOUS_TAG" ]; then
62+
echo "未找到上一个 Tag,将获取全部历史"
63+
else
64+
echo "上一个 Tag: $PREVIOUS_TAG"
65+
echo "对比区间: ${PREVIOUS_TAG}...${CURRENT_TAG}"
66+
fi
67+
echo "::endgroup::"
68+
echo "current=$CURRENT_TAG" >> $GITHUB_OUTPUT
69+
echo "base=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
70+
71+
- name: Collect Raw Commits
72+
run: |
73+
BASE="${{ steps.git-range.outputs.base }}"
74+
HEAD="${{ steps.git-range.outputs.current }}"
75+
if [ -z "$BASE" ]; then
76+
git log --pretty=format:"* %h - %s (%an)" "$HEAD" > /tmp/commits.txt
77+
else
78+
git log --pretty=format:"* %h - %s (%an)" "${BASE}..${HEAD}" > /tmp/commits.txt
79+
fi
80+
echo "::group::[Debug] 原始 Commit 列表"
81+
cat /tmp/commits.txt
82+
echo "::endgroup::"
83+
84+
- name: Collect PR Data
85+
uses: actions/github-script@v7
86+
with:
87+
script: |
88+
const base = '${{ steps.git-range.outputs.base }}';
89+
const head = '${{ steps.git-range.outputs.current }}';
90+
const compareOptions = {
91+
owner: context.repo.owner,
92+
repo: context.repo.repo,
93+
head: head
94+
};
95+
if (base) compareOptions.base = base;
96+
const { data: compare } = await github.rest.repos.compareCommits(compareOptions);
97+
console.log(`::notice::共检测到 ${compare.commits.length} 个 Commits`);
98+
const prNumbers = new Set();
99+
for (const commit of compare.commits) {
100+
const message = commit.commit.message;
101+
const mergeMatch = message.match(/Merge pull request #(\d+)/);
102+
if (mergeMatch) prNumbers.add(parseInt(mergeMatch[1]));
103+
const suffixMatch = message.match(/\(#(\d+)\)$/);
104+
if (suffixMatch) prNumbers.add(parseInt(suffixMatch[1]));
105+
const fixMatch = message.match(/(?:fixes|closes|resolves) #(\d+)/i);
106+
if (fixMatch) prNumbers.add(parseInt(fixMatch[1]));
107+
}
108+
const prDetails = [];
109+
for (const num of prNumbers) {
110+
try {
111+
const { data: pr } = await github.rest.pulls.get({
112+
owner: context.repo.owner,
113+
repo: context.repo.repo,
114+
pull_number: num
115+
});
116+
prDetails.push({
117+
number: pr.number,
118+
title: pr.title,
119+
body: pr.body,
120+
author: pr.user.login,
121+
labels: pr.labels.map(l => l.name)
122+
});
123+
} catch (e) {
124+
console.log(`::warning::获取 PR #${num} 失败: ${e.message}`);
125+
}
126+
}
127+
const fs = require('fs');
128+
fs.writeFileSync('/tmp/prs.json', JSON.stringify(prDetails, null, 2));
129+
130+
- name: Setup Python
131+
uses: actions/setup-python@v5
132+
with:
133+
python-version: '3.11'
134+
135+
- name: Install Dependencies
136+
run: pip install openai
137+
138+
- name: Generate AI Release Notes
139+
env:
140+
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
141+
DEEPSEEK_BASE_URL: ${{ secrets.DEEPSEEK_BASE_URL || inputs.deepseek-base-url }}
142+
DEEPSEEK_MODEL: ${{ inputs.deepseek-model }}
143+
run: |
144+
python .shared-workflows/scripts/analyze_release.py \
145+
--tag "${{ steps.git-range.outputs.current }}" \
146+
--prs-file /tmp/prs.json \
147+
--commits-txt /tmp/commits.txt
148+
149+
- name: Package Mod
150+
id: package
151+
run: |
152+
TAG="${{ steps.git-range.outputs.current }}"
153+
if [ -f "About/About.xml" ]; then
154+
MOD_NAME=$(grep -oP '(?<=<name>)[^<]+' About/About.xml | head -1)
155+
MOD_SAFE=$(echo "$MOD_NAME" | sed 's/[^a-zA-Z0-9 ]//g' | sed 's/ /-/g' | sed 's/-\+/-/g')
156+
else
157+
MOD_SAFE="${{ github.event.repository.name }}"
158+
fi
159+
[ -z "$MOD_SAFE" ] && MOD_SAFE="mod"
160+
echo "mod_safe=$MOD_SAFE" >> $GITHUB_OUTPUT
161+
EXTRA_EXCLUDES=""
162+
if [ -n "${{ inputs.exclude-patterns }}" ]; then
163+
for pattern in ${{ inputs.exclude-patterns }}; do
164+
EXTRA_EXCLUDES="$EXTRA_EXCLUDES --exclude='$pattern'"
165+
done
166+
fi
167+
mkdir -p "/tmp/${MOD_SAFE}"
168+
rsync -av --delete \
169+
--exclude='.git/' \
170+
--exclude='.github/' \
171+
--exclude='Source/' \
172+
--exclude='Tests/' \
173+
--exclude='.claude/' \
174+
--exclude='.trae/' \
175+
--exclude='docs/' \
176+
--exclude='*.csproj' \
177+
--exclude='*.sln' \
178+
--exclude='.gitignore' \
179+
--exclude='.gitattributes' \
180+
--exclude='*.md' \
181+
--exclude='.shared-workflows/' \
182+
$EXTRA_EXCLUDES \
183+
"./" "/tmp/${MOD_SAFE}/"
184+
cd /tmp
185+
zip -r "${MOD_SAFE}-${TAG}.zip" "${MOD_SAFE}/"
186+
echo "::notice::打包完成: ${MOD_SAFE}-${TAG}.zip"
187+
188+
- name: Check Prerelease
189+
id: prerelease
190+
run: |
191+
TAG="${{ steps.git-range.outputs.current }}"
192+
if echo "$TAG" | grep -qiE 'alpha|beta|rc|preview'; then
193+
echo "is_prerelease=true" >> $GITHUB_OUTPUT
194+
else
195+
echo "is_prerelease=false" >> $GITHUB_OUTPUT
196+
fi
197+
198+
- name: Publish GitHub Release
199+
uses: softprops/action-gh-release@v2
200+
with:
201+
tag_name: ${{ steps.git-range.outputs.current }}
202+
name: Release ${{ steps.git-range.outputs.current }}
203+
body_path: /tmp/release_notes.md
204+
prerelease: ${{ steps.prerelease.outputs.is_prerelease == 'true' }}
205+
files: /tmp/${{ steps.package.outputs.mod_safe }}-${{ steps.git-range.outputs.current }}.zip
206+
generate_release_notes: false
207+
env:
208+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎.github/workflows/test.yml‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: Reusable Test
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
dotnet-version:
7+
description: .NET SDK version
8+
required: false
9+
type: string
10+
default: '10.0.x'
11+
test-dir:
12+
description: Directory containing unit test project
13+
required: false
14+
type: string
15+
default: 'Tests'
16+
arch-test-dir:
17+
description: Directory containing architecture test project
18+
required: false
19+
type: string
20+
default: 'ArchTests'
21+
arch-phases:
22+
description: Comma-separated list of arch-test phases to run (A,B,C,D,E,Full)
23+
required: false
24+
type: string
25+
default: 'A,B,C,D,E,Full'
26+
27+
jobs:
28+
test:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Check Tests directory
34+
id: check_tests
35+
run: |
36+
if [ -d "${{ inputs.test-dir }}" ]; then
37+
csproj=$(find ${{ inputs.test-dir }} -maxdepth 1 -name '*.Tests.csproj' -print -quit 2>/dev/null)
38+
if [ -n "$csproj" ]; then
39+
echo "has_tests=true" >> $GITHUB_OUTPUT
40+
echo "csproj=$(basename $csproj)" >> $GITHUB_OUTPUT
41+
else
42+
echo "has_tests=false" >> $GITHUB_OUTPUT
43+
fi
44+
else
45+
echo "has_tests=false" >> $GITHUB_OUTPUT
46+
fi
47+
48+
- name: Setup .NET
49+
if: steps.check_tests.outputs.has_tests == 'true'
50+
uses: actions/setup-dotnet@v4
51+
with:
52+
dotnet-version: ${{ inputs.dotnet-version }}
53+
54+
- name: Restore dependencies
55+
if: steps.check_tests.outputs.has_tests == 'true'
56+
run: dotnet restore ${{ inputs.test-dir }}/${{ steps.check_tests.outputs.csproj }}
57+
58+
- name: Build
59+
if: steps.check_tests.outputs.has_tests == 'true'
60+
run: dotnet build ${{ inputs.test-dir }}/${{ steps.check_tests.outputs.csproj }} --no-restore -c Release
61+
62+
- name: Test
63+
if: steps.check_tests.outputs.has_tests == 'true'
64+
run: dotnet test ${{ inputs.test-dir }}/${{ steps.check_tests.outputs.csproj }} --no-build -c Release --verbosity normal
65+
66+
arch-tests:
67+
runs-on: ubuntu-latest
68+
strategy:
69+
fail-fast: false
70+
matrix:
71+
phase: [A, B, C, D, E, Full]
72+
steps:
73+
- uses: actions/checkout@v4
74+
75+
- name: Check if phase is enabled
76+
id: phase-check
77+
run: |
78+
PHASES="${{ inputs.arch-phases }}"
79+
if echo "$PHASES" | grep -qw "${{ matrix.phase }}"; then
80+
echo "enabled=true" >> $GITHUB_OUTPUT
81+
else
82+
echo "enabled=false" >> $GITHUB_OUTPUT
83+
fi
84+
85+
- name: Check ArchTests directory
86+
if: steps.phase-check.outputs.enabled == 'true'
87+
id: check_arch
88+
run: |
89+
if [ -d "${{ inputs.arch-test-dir }}" ]; then
90+
csproj=$(find ${{ inputs.arch-test-dir }} -maxdepth 1 -name '*.ArchTests.csproj' -print -quit 2>/dev/null)
91+
if [ -n "$csproj" ]; then
92+
echo "has_arch=true" >> $GITHUB_OUTPUT
93+
echo "csproj=$(basename $csproj)" >> $GITHUB_OUTPUT
94+
else
95+
echo "has_arch=false" >> $GITHUB_OUTPUT
96+
fi
97+
else
98+
echo "has_arch=false" >> $GITHUB_OUTPUT
99+
fi
100+
101+
- name: Setup .NET
102+
if: steps.phase-check.outputs.enabled == 'true' && steps.check_arch.outputs.has_arch == 'true'
103+
uses: actions/setup-dotnet@v4
104+
with:
105+
dotnet-version: ${{ inputs.dotnet-version }}
106+
107+
- name: Restore dependencies
108+
if: steps.phase-check.outputs.enabled == 'true' && steps.check_arch.outputs.has_arch == 'true'
109+
run: dotnet restore ${{ inputs.arch-test-dir }}/${{ steps.check_arch.outputs.csproj }}
110+
111+
- name: Build ArchTests
112+
if: steps.phase-check.outputs.enabled == 'true' && steps.check_arch.outputs.has_arch == 'true'
113+
run: dotnet build ${{ inputs.arch-test-dir }}/${{ steps.check_arch.outputs.csproj }} --no-restore -c Release
114+
115+
- name: Run ArchTests Phase ${{ matrix.phase }}
116+
if: steps.phase-check.outputs.enabled == 'true' && steps.check_arch.outputs.has_arch == 'true'
117+
run: |
118+
FILTER=""
119+
case "${{ matrix.phase }}" in
120+
A) FILTER="Phase=A|Phase=General" ;;
121+
B) FILTER="Phase=A|Phase=B|Phase=General" ;;
122+
C) FILTER="Phase=A|Phase=B|Phase=C|Phase=General" ;;
123+
D) FILTER="Phase=A|Phase=B|Phase=C|Phase=D|Phase=General" ;;
124+
E) FILTER="Phase=A|Phase=B|Phase=C|Phase=D|Phase=E|Phase=General" ;;
125+
Full) FILTER="" ;;
126+
esac
127+
if [ -n "$FILTER" ]; then
128+
dotnet test ${{ inputs.arch-test-dir }}/${{ steps.check_arch.outputs.csproj }} --no-build -c Release --filter "$FILTER" --verbosity normal
129+
else
130+
dotnet test ${{ inputs.arch-test-dir }}/${{ steps.check_arch.outputs.csproj }} --no-build -c Release --verbosity normal
131+
fi

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.pyc
2+
__pycache__/

0 commit comments

Comments
 (0)