Skip to content

Commit ba482e8

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feat/ci-windows-only-and-macos-only-flags
# Conflicts: # .github/workflows/build-unitycloud.yml
2 parents 787ed69 + b8d655d commit ba482e8

25 files changed

Lines changed: 1080 additions & 135 deletions

.github/workflows/build-unitycloud.yml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,8 @@ jobs:
452452
runs-on: ubuntu-latest
453453
needs: prebuild
454454
if: needs.prebuild.outputs.should_build == 'true'
455-
timeout-minutes: 360
455+
# Safety ceiling around the 450m retry budget + surrounding steps.
456+
timeout-minutes: 510
456457
strategy:
457458
fail-fast: false
458459
matrix:
@@ -477,21 +478,25 @@ jobs:
477478
- name: Execute Unity Cloud build
478479
uses: nick-fields/retry@v3
479480
with:
480-
timeout_minutes: 180 # matches your GLOBAL_TIMEOUT = 10800s
481+
# Wraps QUEUE_TIMEOUT (240m) + BUILD_TIMEOUT (180m) + buffer.
482+
timeout_minutes: 450
481483
max_attempts: 2
482484
retry_on_exit_code: 99
483485
retry_wait_seconds: 30
484486
on_retry_command: |
485-
echo "::warning::🔁 Unity Cloud Build retry triggered at $(date '+%Y-%m-%d %H:%M:%S')"
487+
echo "::warning::🔁 Unity Cloud Build retry triggered at $(date '+%Y-%m-%d %H:%M:%S'). The next attempt will reattach to the persisted build if it is still in flight."
486488
command: |
487489
echo "🔧 Starting Unity Cloud Build attempt at $(date '+%Y-%m-%d %H:%M:%S')"
488490
python -u scripts/cloudbuild/build.py
489491
env:
490492
API_KEY: ${{ secrets.UNITY_CLOUD_API_KEY }}
491493
ORG_ID: ${{ secrets.UNITY_CLOUD_ORG_ID }}
492494
PROJECT_ID: ${{ secrets.UNITY_CLOUD_PROJECT_ID }}
493-
POLL_TIME: 60 # Set the polling time in seconds
494-
GLOBAL_TIMEOUT: 10800 # Set the global timeout in seconds (e.g., 3 hours)
495+
POLL_TIME: 60
496+
QUEUE_POLL_TIME: 120
497+
STALE_POLL_THRESHOLD: 600
498+
QUEUE_TIMEOUT: 14400
499+
BUILD_TIMEOUT: 10800
495500
TARGET: t_${{ matrix.target }}
496501
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
497502
COMMIT_SHA: ${{ needs.prebuild.outputs.commit_sha }}
@@ -833,14 +838,14 @@ jobs:
833838
path: shader_compilation_report.log
834839
if-no-files-found: warn
835840

836-
# Will run on cancel or timeout only
841+
# Also runs on failure() so an outer-step timeout doesn't leave a Unity-side build holding a slot.
837842
- name: Cancel Unity Cloud build
838-
if: ${{ cancelled() }}
843+
if: ${{ cancelled() || failure() }}
839844
env:
840845
API_KEY: ${{ secrets.UNITY_CLOUD_API_KEY }}
841846
ORG_ID: ${{ secrets.UNITY_CLOUD_ORG_ID }}
842847
PROJECT_ID: ${{ secrets.UNITY_CLOUD_PROJECT_ID }}
843-
run: python -u scripts/cloudbuild/build.py --cancel
848+
run: python -u scripts/cloudbuild/build.py --cancel || true
844849

845850
build-gate:
846851
name: Build Gate (Windows + macOS)

.github/workflows/claude-pr-review.yml

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
!contains(github.event.pull_request.labels.*.name, 'no review') &&
1717
!contains(github.event.pull_request.labels.*.name, 'auto-pr')
1818
runs-on: ubuntu-latest
19+
concurrency:
20+
group: claude-auto-review-${{ github.event.pull_request.number }}
21+
cancel-in-progress: true
1922
permissions:
2023
id-token: write
2124
contents: read
@@ -44,11 +47,12 @@ jobs:
4447
f.filename === 'CODEOWNERS'
4548
);
4649
if (touchesSensitive) {
47-
core.notice('Auto-review disabled: PR modifies .github/prompts, .github/workflows, or CODEOWNERS. Human review required.');
50+
core.notice('Sensitive paths modified (.github/prompts, .github/workflows, or CODEOWNERS): Claude will review but not auto-approve. Human DEV review required; QA can still be waived if Claude reports QA_REQUIRED: NO.');
4851
}
4952
}
5053
51-
core.setOutput('is-auto-review', (isAutoReview && !touchesSensitive).toString());
54+
core.setOutput('is-auto-review', isAutoReview.toString());
55+
core.setOutput('touches-sensitive', touchesSensitive.toString());
5256
5357
- name: Set status to pending
5458
if: steps.check-type.outputs.is-auto-review == 'true'
@@ -121,17 +125,18 @@ jobs:
121125
with:
122126
script: |
123127
const prNumber = ${{ github.event.pull_request.number }};
128+
const owner = context.repo.owner;
129+
const repo = context.repo.repo;
130+
const touchesSensitive = '${{ steps.check-type.outputs.touches-sensitive }}' === 'true';
124131
125132
const comments = await github.rest.issues.listComments({
126-
owner: context.repo.owner,
127-
repo: context.repo.repo,
128-
issue_number: prNumber,
129-
per_page: 100
133+
owner, repo, issue_number: prNumber, per_page: 100
130134
});
131135
132-
const claudeComment = comments.data
133-
.reverse()
134-
.find(c => c.user?.login === 'claude[bot]');
136+
const claudeComments = comments.data.filter(c =>
137+
c.user?.login === 'claude[bot]' && c.body?.includes('REVIEW_RESULT:')
138+
);
139+
const claudeComment = claudeComments[claudeComments.length - 1];
135140
136141
const output = claudeComment?.body ?? '';
137142
console.log('Claude comment preview:', output.slice(0, 300));
@@ -143,36 +148,82 @@ jobs:
143148
const hasReviewResult = output.includes('REVIEW_RESULT:');
144149
const passed = !failed && !errored;
145150
146-
if (passed && isSimple) {
147-
const approvalBody = qaRequired
148-
? 'Auto-approved by Claude — simple fix/chore with no blocking issues. QA approval is still required.'
149-
: 'Auto-approved by Claude — simple fix/chore with no blocking issues. No QA needed (non-runtime changes only).';
150-
151-
await github.rest.pulls.createReview({
152-
owner: context.repo.owner,
153-
repo: context.repo.repo,
154-
pull_number: prNumber,
155-
event: 'APPROVE',
156-
body: approvalBody
157-
});
158-
159-
const labels = ['claude-approved'];
160-
if (!qaRequired) {
161-
labels.push('no QA needed');
151+
const reviews = await github.rest.pulls.listReviews({
152+
owner, repo, pull_number: prNumber, per_page: 100
153+
});
154+
const ourApprovals = reviews.data.filter(r =>
155+
r.user?.login === 'github-actions[bot]' && r.state === 'APPROVED'
156+
);
157+
const prLabels = (context.payload.pull_request.labels || []).map(l => l.name);
158+
const COMPLEX_MARKER = '🔍 Claude reviewed this PR and found no blocking issues';
159+
const SENSITIVE_MARKER = '🔒 Claude reviewed this PR — sensitive paths modified';
160+
161+
const canAutoApprove = hasReviewResult && passed && isSimple && !touchesSensitive;
162+
const canWaiveQa = hasReviewResult && passed && isSimple && !qaRequired;
163+
164+
if (canAutoApprove) {
165+
if (ourApprovals.length === 0) {
166+
const approvalBody = qaRequired
167+
? 'Auto-approved by Claude — simple fix/chore with no blocking issues. QA approval is still required.'
168+
: 'Auto-approved by Claude — simple fix/chore with no blocking issues. No QA needed (non-runtime changes only).';
169+
await github.rest.pulls.createReview({
170+
owner, repo, pull_number: prNumber, event: 'APPROVE', body: approvalBody
171+
});
162172
}
163173
await github.rest.issues.addLabels({
164-
owner: context.repo.owner,
165-
repo: context.repo.repo,
166-
issue_number: prNumber,
167-
labels
168-
});
169-
} else if (passed && !isSimple) {
170-
await github.rest.issues.createComment({
171-
owner: context.repo.owner,
172-
repo: context.repo.repo,
173-
issue_number: prNumber,
174-
body: '🔍 Claude reviewed this PR and found no blocking issues, but assessed it as **complex** — human DEV review is still required before merging.'
174+
owner, repo, issue_number: prNumber, labels: ['claude-approved']
175175
});
176+
} else if (hasReviewResult) {
177+
for (const r of ourApprovals) {
178+
await github.rest.pulls.dismissReview({
179+
owner, repo, pull_number: prNumber, review_id: r.id,
180+
message: 'Dismissed: latest Claude review no longer auto-approves this PR.'
181+
}).catch(e => console.log(`dismissReview: ${e.message}`));
182+
}
183+
if (prLabels.includes('claude-approved')) {
184+
await github.rest.issues.removeLabel({
185+
owner, repo, issue_number: prNumber, name: 'claude-approved'
186+
}).catch(e => console.log(`removeLabel claude-approved: ${e.message}`));
187+
}
188+
if (touchesSensitive && passed && isSimple && !comments.data.some(c =>
189+
c.user?.login === 'github-actions[bot]' && c.body?.startsWith(SENSITIVE_MARKER))) {
190+
const qaNote = qaRequired
191+
? 'QA approval is still required.'
192+
: 'No QA needed (Claude reported `QA_REQUIRED: NO`).';
193+
await github.rest.issues.createComment({
194+
owner, repo, issue_number: prNumber,
195+
body: `${SENSITIVE_MARKER} (\`.github/prompts\`, \`.github/workflows\`, or \`CODEOWNERS\`). Claude will not auto-approve these PRs — human DEV review is required. ${qaNote}`
196+
});
197+
} else if (!touchesSensitive && passed && !isSimple && !comments.data.some(c =>
198+
c.user?.login === 'github-actions[bot]' && c.body?.startsWith(COMPLEX_MARKER))) {
199+
await github.rest.issues.createComment({
200+
owner, repo, issue_number: prNumber,
201+
body: COMPLEX_MARKER + ', but assessed it as **complex** — human DEV review is still required before merging.'
202+
});
203+
}
204+
}
205+
206+
if (canWaiveQa) {
207+
if (!prLabels.includes('no QA needed')) {
208+
await github.rest.issues.addLabels({
209+
owner, repo, issue_number: prNumber, labels: ['no QA needed']
210+
});
211+
}
212+
} else if (hasReviewResult && prLabels.includes('no QA needed')) {
213+
await github.rest.issues.removeLabel({
214+
owner, repo, issue_number: prNumber, name: 'no QA needed'
215+
}).catch(e => console.log(`removeLabel no QA needed: ${e.message}`));
216+
}
217+
218+
for (const c of claudeComments.slice(0, -1)) {
219+
await github.graphql(
220+
`mutation($id: ID!) {
221+
minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) {
222+
minimizedComment { isMinimized }
223+
}
224+
}`,
225+
{ id: c.node_id }
226+
).catch(e => console.log(`minimizeComment ${c.id}: ${e.message}`));
176227
}
177228
178229
let description;
@@ -184,6 +235,10 @@ jobs:
184235
description = 'Claude review encountered an error';
185236
} else if (failed) {
186237
description = 'Claude found issues that must be resolved';
238+
} else if (touchesSensitive && isSimple) {
239+
description = canWaiveQa
240+
? 'No blocking issues — sensitive paths, DEV review required (no QA needed)'
241+
: 'No blocking issues — sensitive paths, DEV review required';
187242
} else if (isSimple) {
188243
description = 'No blocking issues found — PR auto-approved';
189244
} else {
@@ -261,6 +316,9 @@ jobs:
261316
needs: check-member
262317
if: needs.check-member.outputs.is-member == 'true'
263318
runs-on: ubuntu-latest
319+
concurrency:
320+
group: claude-on-demand-review-${{ github.event.issue.number }}
321+
cancel-in-progress: true
264322
permissions:
265323
id-token: write
266324
contents: read
@@ -373,9 +431,10 @@ jobs:
373431
per_page: 100
374432
});
375433
376-
const claudeComment = comments.data
377-
.reverse()
378-
.find(c => c.user?.login === 'claude[bot]');
434+
const claudeComments = comments.data.filter(c =>
435+
c.user?.login === 'claude[bot]' && c.body?.includes('REVIEW_RESULT:')
436+
);
437+
const claudeComment = claudeComments[claudeComments.length - 1];
379438
380439
const output = claudeComment?.body ?? '';
381440
console.log('Claude comment preview:', output.slice(0, 200));
@@ -384,6 +443,17 @@ jobs:
384443
const errored = '${{ steps.claude.outcome }}' === 'failure';
385444
const hasReviewResult = output.includes('REVIEW_RESULT:');
386445
446+
for (const c of claudeComments.slice(0, -1)) {
447+
await github.graphql(
448+
`mutation($id: ID!) {
449+
minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) {
450+
minimizedComment { isMinimized }
451+
}
452+
}`,
453+
{ id: c.node_id }
454+
).catch(e => console.log(`minimizeComment ${c.id}: ${e.message}`));
455+
}
456+
387457
let description;
388458
if (errored && !output) {
389459
description = 'Claude review failed — credit balance too low or API error';

.github/workflows/dependency-security-review.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ on:
1616
- '.github/workflows/**'
1717
- '.github/prompts/**'
1818

19+
concurrency:
20+
group: dependency-security-review-${{ github.event.pull_request.number || github.run_id }}
21+
cancel-in-progress: true
22+
1923
jobs:
2024
detect-and-review:
2125
if: github.event.pull_request.draft == false
@@ -182,9 +186,21 @@ jobs:
182186
issue_number: ${{ github.event.pull_request.number }},
183187
});
184188
185-
const claudeComment = comments
186-
.reverse()
187-
.find(c => c.user?.login === 'claude[bot]');
189+
const claudeComments = comments.filter(c =>
190+
c.user?.login === 'claude[bot]' && c.body?.includes('DEPENDENCY_REVIEW:')
191+
);
192+
const claudeComment = claudeComments[claudeComments.length - 1];
193+
194+
for (const c of claudeComments.slice(0, -1)) {
195+
await github.graphql(
196+
`mutation($id: ID!) {
197+
minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) {
198+
minimizedComment { isMinimized }
199+
}
200+
}`,
201+
{ id: c.node_id }
202+
).catch(e => console.log(`minimizeComment ${c.id}: ${e.message}`));
203+
}
188204
189205
const output = claudeComment?.body ?? '';
190206
const errored = '${{ steps.claude.outcome }}' === 'failure';

0 commit comments

Comments
 (0)