@@ -25,69 +25,204 @@ jobs:
2525 - name : Resolve affected shards
2626 id : resolve
2727 shell : bash
28+ env :
29+ EVENT_NAME : ${{ github.event_name }}
30+ BASE_SHA : ${{ github.event.pull_request.base.sha || '' }}
31+ HEAD_SHA : ${{ github.sha }}
32+ BASE_REF : ${{ github.base_ref }}
2833 run : |
2934 set -euo pipefail
3035
31- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
32- shards=(root apps packages)
33- else
34- base_sha="${{ github.event.pull_request.base.sha }}"
35- head_sha="${{ github.sha }}"
36- mapfile -t changed_files < <(git diff --name-only "$base_sha" "$head_sha")
37-
38- run_all=false
39- root=false
40- apps=false
41- packages=false
42-
43- for file in "${changed_files[@]}"; do
44- [ -z "$file" ] && continue
45-
46- case "$file" in
47- apps/*) apps=true ;;
48- packages/*) packages=true ;;
49- tailwindcss-patch.config.*|tailwindcss-mangle.config.*) root=true ;;
50- esac
51-
52- case "$file" in
53- packages/tailwindcss-patch/*|pnpm-lock.yaml|pnpm-workspace.yaml)
54- run_all=true
55- ;;
56- esac
57- done
58-
59- if [ "$run_all" = true ]; then
60- shards=(root apps packages)
61- else
62- shards=()
63- [ "$root" = true ] && shards+=(root)
64- [ "$apps" = true ] && shards+=(apps)
65- [ "$packages" = true ] && shards+=(packages)
66- fi
67- fi
36+ node - <<'NODE'
37+ const fs = require('node:fs')
38+ const { execFileSync } = require('node:child_process')
6839
69- if [ "${#shards[@]}" -eq 0 ]; then
70- echo "has_changes=false" >> "$GITHUB_OUTPUT"
71- echo "shards=none" >> "$GITHUB_OUTPUT"
72- echo 'matrix={"shard":[]}' >> "$GITHUB_OUTPUT"
73- exit 0
74- fi
40+ const defaultConfig = {
41+ runAllPatterns: [
42+ 'packages/tailwindcss-patch/**',
43+ 'pnpm-lock.yaml',
44+ 'pnpm-workspace.yaml',
45+ 'package.json',
46+ 'turbo.json',
47+ '.npmrc',
48+ '.nvmrc',
49+ '.node-version',
50+ '.github/workflows/**',
51+ ],
52+ shards: [
53+ {
54+ name: 'root',
55+ reportFile: '.tw-patch/migrate-report-root.json',
56+ matchPatterns: ['tailwindcss-patch.config.*', 'tailwindcss-mangle.config.*'],
57+ },
58+ { name: 'apps', reportFile: '.tw-patch/migrate-report-apps.json', matchPatterns: ['apps/**'] },
59+ {
60+ name: 'packages',
61+ reportFile: '.tw-patch/migrate-report-packages.json',
62+ matchPatterns: ['packages/**'],
63+ },
64+ ],
65+ }
66+
67+ const configPath = '.tw-patch/ci-shards.json'
7568
76- shard_csv="$(IFS=,; echo "${shards[*]}")"
77- matrix_json="$(SHARDS="$shard_csv" node -e '
78- const names = (process.env.SHARDS || "").split(",").filter(Boolean)
79- const reportMap = {
80- root: ".tw-patch/migrate-report-root.json",
81- apps: ".tw-patch/migrate-report-apps.json",
82- packages: ".tw-patch/migrate-report-packages.json",
69+ const normalizeConfig = (value) => {
70+ if (!value || typeof value !== 'object') return defaultConfig
71+ const runAllPatterns = Array.isArray(value.runAllPatterns)
72+ ? value.runAllPatterns.filter((p) => typeof p === 'string' && p.length > 0)
73+ : defaultConfig.runAllPatterns
74+ const shards = Array.isArray(value.shards)
75+ ? value.shards
76+ .filter((s) => s && typeof s === 'object')
77+ .map((s) => ({
78+ name: typeof s.name === 'string' ? s.name : '',
79+ reportFile: typeof s.reportFile === 'string' ? s.reportFile : '',
80+ matchPatterns: Array.isArray(s.matchPatterns)
81+ ? s.matchPatterns.filter((p) => typeof p === 'string' && p.length > 0)
82+ : [],
83+ }))
84+ .filter((s) => s.name && s.reportFile && s.matchPatterns.length > 0)
85+ : defaultConfig.shards
86+
87+ if (shards.length === 0) return defaultConfig
88+ return { runAllPatterns, shards }
89+ }
90+
91+ const loadConfig = () => {
92+ if (!fs.existsSync(configPath)) return defaultConfig
93+ try {
94+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'))
95+ const config = normalizeConfig(parsed)
96+ console.log(`::notice::Loaded shard config from ${configPath}`)
97+ return config
98+ } catch (error) {
99+ console.log(`::warning::Invalid ${configPath}, fallback to defaults: ${error.message}`)
100+ return defaultConfig
83101 }
84- const shard = names.map(name => ({ name, report_file: reportMap[name] }))
85- process.stdout.write(JSON.stringify({ shard }))
86- ')"
102+ }
103+
104+ const globToRegExp = (pattern) => {
105+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
106+ const withGlob = escaped
107+ .replace(/\*\*/g, '__TW_PATCH_GLOBSTAR__')
108+ .replace(/\*/g, '[^/]*')
109+ .replace(/__TW_PATCH_GLOBSTAR__/g, '.*')
110+ .replace(/\?/g, '.')
111+ return new RegExp(`^${withGlob}$`)
112+ }
113+
114+ const matchesAny = (value, patterns) => patterns.some((pattern) => globToRegExp(pattern).test(value))
115+
116+ const runGit = (args) => execFileSync('git', args, { encoding: 'utf8' }).trim()
117+
118+ const hasCommit = (sha) => {
119+ if (!sha) return false
120+ try {
121+ execFileSync('git', ['cat-file', '-e', `${sha}^{commit}`], { stdio: 'ignore' })
122+ return true
123+ } catch {
124+ return false
125+ }
126+ }
127+
128+ const config = loadConfig()
129+ const allShardNames = config.shards.map((s) => s.name)
130+ const eventName = process.env.EVENT_NAME || ''
131+ const headSha = process.env.HEAD_SHA || 'HEAD'
132+ const baseRef = process.env.BASE_REF || ''
133+ const outputPath = process.env.GITHUB_OUTPUT
134+
135+ const writeOutputs = (payload) => {
136+ const lines = [
137+ `has_changes=${payload.hasChanges ? 'true' : 'false'}`,
138+ `shards=${payload.shards.length > 0 ? payload.shards.join(',') : 'none'}`,
139+ `matrix=${JSON.stringify({ shard: payload.matrix })}`,
140+ ]
141+ fs.appendFileSync(outputPath, `${lines.join('\n')}\n`)
142+ }
143+
144+ if (eventName === 'workflow_dispatch') {
145+ console.log('::notice::workflow_dispatch => run all shards')
146+ writeOutputs({
147+ hasChanges: true,
148+ shards: allShardNames,
149+ matrix: config.shards.map((s) => ({ name: s.name, report_file: s.reportFile })),
150+ })
151+ process.exit(0)
152+ }
153+
154+ let baseSha = process.env.BASE_SHA || ''
155+ if (!hasCommit(baseSha) && baseRef) {
156+ try {
157+ baseSha = runGit(['merge-base', 'HEAD', `origin/${baseRef}`])
158+ console.log(`::notice::Fallback base resolved by merge-base: ${baseSha}`)
159+ } catch {
160+ console.log('::warning::Unable to resolve PR base by merge-base, run all shards')
161+ writeOutputs({
162+ hasChanges: true,
163+ shards: allShardNames,
164+ matrix: config.shards.map((s) => ({ name: s.name, report_file: s.reportFile })),
165+ })
166+ process.exit(0)
167+ }
168+ }
169+
170+ if (!baseSha) {
171+ console.log('::warning::Missing base sha, run all shards')
172+ writeOutputs({
173+ hasChanges: true,
174+ shards: allShardNames,
175+ matrix: config.shards.map((s) => ({ name: s.name, report_file: s.reportFile })),
176+ })
177+ process.exit(0)
178+ }
179+
180+ let changedFiles = []
181+ try {
182+ changedFiles = runGit(['diff', '--name-only', baseSha, headSha])
183+ .split('\n')
184+ .map((f) => f.trim())
185+ .filter(Boolean)
186+ } catch (error) {
187+ console.log(`::warning::git diff failed (${error.message}), run all shards`)
188+ writeOutputs({
189+ hasChanges: true,
190+ shards: allShardNames,
191+ matrix: config.shards.map((s) => ({ name: s.name, report_file: s.reportFile })),
192+ })
193+ process.exit(0)
194+ }
195+
196+ if (changedFiles.length === 0) {
197+ console.log('::notice::No changed files in PR diff')
198+ writeOutputs({ hasChanges: false, shards: [], matrix: [] })
199+ process.exit(0)
200+ }
201+
202+ if (changedFiles.some((file) => matchesAny(file, config.runAllPatterns))) {
203+ console.log('::notice::Matched runAllPatterns => run all shards')
204+ writeOutputs({
205+ hasChanges: true,
206+ shards: allShardNames,
207+ matrix: config.shards.map((s) => ({ name: s.name, report_file: s.reportFile })),
208+ })
209+ process.exit(0)
210+ }
211+
212+ const matched = config.shards.filter((shard) => changedFiles.some((file) => matchesAny(file, shard.matchPatterns)))
213+
214+ if (matched.length === 0) {
215+ console.log('::notice::Changes do not affect configured shard patterns')
216+ writeOutputs({ hasChanges: false, shards: [], matrix: [] })
217+ process.exit(0)
218+ }
87219
88- echo "has_changes=true" >> "$GITHUB_OUTPUT"
89- echo "shards=$shard_csv" >> "$GITHUB_OUTPUT"
90- echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT"
220+ writeOutputs({
221+ hasChanges: true,
222+ shards: matched.map((s) => s.name),
223+ matrix: matched.map((s) => ({ name: s.name, report_file: s.reportFile })),
224+ })
225+ NODE
91226
92227 validate-migration-report :
93228 needs : detect-shards
0 commit comments