2020 - " examples/**"
2121 - " .changeset/**"
2222
23+ concurrency :
24+ group : ${{ github.workflow }}-${{ github.ref }}
25+ cancel-in-progress : true
26+
2327env :
2428 NODE_OPTIONS : " --max-old-space-size=4096"
2529 NX_VERBOSE_LOGGING : true
@@ -30,18 +34,23 @@ jobs:
3034 format :
3135 runs-on : ubuntu-latest
3236 timeout-minutes : 10
37+ permissions :
38+ contents : write
3339
3440 steps :
3541 - name : Checkout
36- uses : actions/checkout@v3
42+ uses : actions/checkout@v4
43+ with :
44+ ref : ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
45+ token : ${{ secrets.GITHUB_TOKEN }}
3746
3847 - name : Setup pnpm
3948 uses : pnpm/action-setup@v4
4049 with :
4150 version : " 10.13.1"
4251
4352 - name : Use Node.js 20
44- uses : actions/setup-node@v3
53+ uses : actions/setup-node@v4
4554 with :
4655 node-version : 20.x
4756 cache : " pnpm"
@@ -50,23 +59,48 @@ jobs:
5059 - name : Install dependencies
5160 run : pnpm install --frozen-lockfile
5261
53- - name : Run format check
54- run : pnpm run check-format
62+ - name : Run formatter (check on push, fix on PR)
63+ run : |
64+ if [ "${{ github.event_name }}" = "pull_request" ]; then
65+ if ! pnpm run format; then
66+ echo "::warning::Formatter exited with error — auto-fix may be incomplete"
67+ fi
68+ if [ -n "$(git diff --name-only)" ]; then
69+ echo "format_fixed=true" >> $GITHUB_ENV
70+ fi
71+ fi
72+ pnpm run check-format
73+
74+ - name : Commit formatting fixes
75+ if : >-
76+ env.format_fixed == 'true' &&
77+ github.event_name == 'pull_request' &&
78+ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
79+ run : |
80+ if [ -z "$(git diff --name-only)" ]; then
81+ echo "No formatting changes to commit"
82+ exit 0
83+ fi
84+ git config user.name "github-actions[bot]"
85+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
86+ git diff --name-only -z | xargs -0 git add
87+ git commit -m "style: auto-fix formatting"
88+ git push
5589
5690 oxlint :
5791 runs-on : ubuntu-latest
5892 timeout-minutes : 10
5993 steps :
6094 - name : Checkout
61- uses : actions/checkout@v3
95+ uses : actions/checkout@v4
6296
6397 - name : Setup pnpm
6498 uses : pnpm/action-setup@v4
6599 with :
66100 version : " 10.13.1"
67101
68102 - name : Use Node.js 20
69- uses : actions/setup-node@v3
103+ uses : actions/setup-node@v4
70104 with :
71105 node-version : 20.x
72106 cache : " pnpm"
@@ -83,15 +117,15 @@ jobs:
83117 timeout-minutes : 10
84118 steps :
85119 - name : Checkout
86- uses : actions/checkout@v3
120+ uses : actions/checkout@v4
87121
88122 - name : Setup pnpm
89123 uses : pnpm/action-setup@v4
90124 with :
91125 version : " 10.13.1"
92126
93127 - name : Use Node.js 20
94- uses : actions/setup-node@v3
128+ uses : actions/setup-node@v4
95129 with :
96130 node-version : 20.x
97131 cache : " pnpm"
@@ -109,3 +143,76 @@ jobs:
109143
110144 - name : Run publint and attw
111145 run : pnpm run check:packages
146+
147+ commitlint :
148+ runs-on : ubuntu-latest
149+ timeout-minutes : 10
150+ permissions :
151+ pull-requests : write
152+ steps :
153+ - name : Checkout
154+ uses : actions/checkout@v4
155+ with :
156+ fetch-depth : 0
157+
158+ - name : Setup pnpm
159+ uses : pnpm/action-setup@v4
160+ with :
161+ version : " 10.13.1"
162+
163+ - name : Use Node.js 20
164+ uses : actions/setup-node@v4
165+ with :
166+ node-version : 20.x
167+ cache : " pnpm"
168+ cache-dependency-path : " **/pnpm-lock.yaml"
169+
170+ - name : Install dependencies
171+ run : pnpm install --frozen-lockfile
172+
173+ - name : Validate current commit (last commit) with commitlint
174+ if : github.event_name == 'push'
175+ run : npx commitlint --last --verbose
176+
177+ - name : Validate PR commits with commitlint
178+ id : commitlint
179+ if : github.event_name == 'pull_request'
180+ continue-on-error : true
181+ run : npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 2>&1 | tee /tmp/commitlint-output.txt
182+
183+ - name : Post fix suggestion on failure
184+ if : github.event_name == 'pull_request' && steps.commitlint.outcome == 'failure'
185+ continue-on-error : true
186+ uses : actions/github-script@v7
187+ with :
188+ script : |
189+ const fs = require('fs');
190+ const output = fs.readFileSync('/tmp/commitlint-output.txt', 'utf8');
191+ const body = `### ❌ Commitlint failed\n\nCommit messages must follow [Conventional Commits](https://www.conventionalcommits.org/).\n\n**Valid prefixes:** \`feat:\`, \`fix:\`, \`docs:\`, \`style:\`, \`refactor:\`, \`test:\`, \`chore:\`, \`ci:\`, \`perf:\`, \`build:\`\n\n**Example:** \`feat: add user authentication\`\n\n<details><summary>Full output</summary>\n\n\`\`\`\n${output}\n\`\`\`\n</details>\n\nTo fix, amend your commit messages:\n\`\`\`bash\ngit rebase -i HEAD~N # N = number of commits to fix\n# Change 'pick' to 'reword' for bad commits\n\`\`\``;
192+
193+ // Find existing comment to update
194+ const { data: comments } = await github.rest.issues.listComments({
195+ owner: context.repo.owner,
196+ repo: context.repo.repo,
197+ issue_number: context.issue.number,
198+ });
199+ const existing = comments.find(c => c.body.includes('Commitlint failed'));
200+ if (existing) {
201+ await github.rest.issues.updateComment({
202+ owner: context.repo.owner,
203+ repo: context.repo.repo,
204+ comment_id: existing.id,
205+ body,
206+ });
207+ } else {
208+ await github.rest.issues.createComment({
209+ owner: context.repo.owner,
210+ repo: context.repo.repo,
211+ issue_number: context.issue.number,
212+ body,
213+ });
214+ }
215+
216+ - name : Fail if commitlint failed
217+ if : github.event_name == 'pull_request' && steps.commitlint.outcome == 'failure'
218+ run : exit 1
0 commit comments