Skip to content

Commit 1abe8a8

Browse files
committed
Merge branch 'master' into bring-pkgs
2 parents 1f71bab + 931afef commit 1abe8a8

File tree

18 files changed

+467
-91
lines changed

18 files changed

+467
-91
lines changed

.github/workflows/branch-monitor.yml

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
name: Branch Monitor
2+
3+
on:
4+
push:
5+
branches: ['master']
6+
7+
env:
8+
ISSUE_NUMBER: ${{ vars.MONITOR_ISSUE_NUMBER }}
9+
10+
concurrency:
11+
group: branch-monitor-master
12+
cancel-in-progress: true
13+
14+
jobs:
15+
monitor:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
issues: write
19+
checks: read
20+
timeout-minutes: 60
21+
22+
steps:
23+
- name: Monitor Branch Status
24+
uses: actions/github-script@v7
25+
with:
26+
script: |
27+
const branchName = process.env.GITHUB_REF_NAME;
28+
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
29+
30+
if (!issueNumber) {
31+
console.log('ERROR: ISSUE_NUMBER environment variable is not set. Please configure MONITOR_ISSUE_NUMBER in repository variables.');
32+
process.exit(1);
33+
}
34+
35+
const currentJobName = process.env.GITHUB_JOB;
36+
const COMMENT_MARKER = '<!-- branch-monitor-failure-comment -->';
37+
const POLL_INTERVAL = 30; // seconds
38+
const MAX_POLLS = 120; // 60 minutes total
39+
40+
// Get commit data from the push event
41+
const commitSha = context.sha;
42+
const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${commitSha}`;
43+
const commitMessage = context.payload.head_commit.message;
44+
const commitAuthor = context.payload.head_commit.author.username
45+
? `@${context.payload.head_commit.author.username}`
46+
: context.payload.head_commit.author.name;
47+
const commitDate = context.payload.head_commit.timestamp;
48+
console.log(JSON.stringify(context, null, 2));
49+
50+
console.log(`Monitoring checks for commit ${commitSha} on branch ${branchName}`);
51+
52+
// Poll for check completion
53+
let polls = 0;
54+
let overallStatus = 'pending';
55+
56+
while (polls < MAX_POLLS) {
57+
polls++;
58+
console.log(`Poll ${polls}/${MAX_POLLS} - checking status...`);
59+
60+
// Get combined status (external checks like CircleCI)
61+
const { data: combinedStatus } = await github.rest.repos.getCombinedStatusForRef({
62+
owner: context.repo.owner,
63+
repo: context.repo.repo,
64+
ref: commitSha
65+
});
66+
67+
// Get check runs (GitHub Actions and modern checks)
68+
const { data: checks } = await github.rest.checks.listForRef({
69+
owner: context.repo.owner,
70+
repo: context.repo.repo,
71+
ref: commitSha,
72+
per_page: 100
73+
});
74+
75+
// Combine into single array with normalized structure
76+
const allChecks = [
77+
// External statuses
78+
...combinedStatus.statuses.map(status => ({
79+
name: status.context,
80+
state: status.state,
81+
type: 'status'
82+
})),
83+
// GitHub check runs
84+
...checks.check_runs.map(check => ({
85+
name: check.name,
86+
state: check.conclusion || check.status,
87+
type: 'check'
88+
}))
89+
];
90+
91+
console.log(`Found ${allChecks.length} total checks for commit ${commitSha}`);
92+
allChecks.forEach(check => {
93+
console.log(`- ${check.type}: ${check.name}: ${check.state}`);
94+
});
95+
96+
// Filter out the current workflow run (this monitor job)
97+
const currentJobName = process.env.GITHUB_JOB;
98+
const otherChecks = allChecks.filter(check => check.name !== currentJobName);
99+
100+
console.log(`Filtered out current job '${currentJobName}', ${otherChecks.length} other checks remaining`);
101+
102+
if (otherChecks.length === 0) {
103+
console.log('No other checks found, continuing to poll...');
104+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL * 1000));
105+
continue;
106+
}
107+
108+
// Check if all other checks are completed
109+
const otherStates = otherChecks.map(check => check.state);
110+
const completedStates = otherStates.filter(s =>
111+
s === 'success' || s === 'failure' || s === 'error' || s === 'cancelled' || s === 'timed_out' || s === 'skipped'
112+
);
113+
const allOtherCompleted = completedStates.length === otherChecks.length;
114+
115+
console.log(`Completed: ${completedStates.length}/${otherChecks.length} other checks`);
116+
console.log(`All other checks completed: ${allOtherCompleted}`);
117+
118+
if (allOtherCompleted) {
119+
// Determine overall status from completed checks only
120+
if (completedStates.every(s => s === 'success' || s === 'skipped')) {
121+
overallStatus = 'success';
122+
} else if (completedStates.some(s => s === 'failure' || s === 'timed_out' || s === 'cancelled' || s === 'error')) {
123+
overallStatus = 'failure';
124+
} else {
125+
overallStatus = 'success'; // fallback
126+
}
127+
console.log(`All other checks completed with overall status: ${overallStatus}`);
128+
break;
129+
} else {
130+
console.log(`Still waiting for more checks to complete, waiting ${POLL_INTERVAL}s...`);
131+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL * 1000));
132+
}
133+
}
134+
135+
if (polls >= MAX_POLLS) {
136+
console.log('Timeout reached, using current status');
137+
overallStatus = 'timeout';
138+
}
139+
140+
// Generate status display
141+
const statusEmojis = {
142+
success: '✅',
143+
failure: '❌',
144+
timeout: '⏰',
145+
pending: '⏳'
146+
};
147+
148+
const emoji = statusEmojis[overallStatus] || '❓';
149+
const shortSha = commitSha.substring(0, 7);
150+
const date = new Date(commitDate).toISOString();
151+
152+
const issueTitle = `Status Monitor for the \`${branchName}\` branch`;
153+
const issueBody = `<!-- This issue is automatically updated by the branch-monitor workflow. Do not edit manually as changes will be overwritten. -->
154+
155+
This issue automatically tracks the CI status of the \`${branchName}\` branch. It monitors all checks and updates whenever new commits are pushed.
156+
157+
**Latest Commit**: [\`${shortSha}\`](${commitUrl}) ${commitMessage.split('\n')[0]} ${emoji} **${overallStatus}**
158+
159+
---
160+
*Last updated: ${new Date().toISOString()}*`;
161+
162+
// Update issue title and body
163+
await github.rest.issues.update({
164+
owner: context.repo.owner,
165+
repo: context.repo.repo,
166+
issue_number: issueNumber,
167+
title: issueTitle,
168+
body: issueBody
169+
});
170+
171+
console.log(`Updated issue #${issueNumber} with status: ${overallStatus}`);
172+
173+
// Handle failure comments
174+
const hasFailure = overallStatus === 'failure';
175+
176+
// Get existing comments
177+
const { data: comments } = await github.rest.issues.listComments({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
issue_number: issueNumber
181+
});
182+
183+
const existingFailureComment = comments.find(comment =>
184+
comment.body.includes(COMMENT_MARKER)
185+
);
186+
187+
if (hasFailure && !existingFailureComment) {
188+
// Post failure comment
189+
const failureComment = `${COMMENT_MARKER}
190+
🚨 **Build Failure Detected**
191+
192+
The latest commit on branch \`${branchName}\` has failed checks:
193+
- **Commit**: [\`${shortSha}\`](${commitUrl})
194+
- **Message**: ${commitMessage.split('\n')[0]}
195+
- **Author**: ${commitAuthor}
196+
197+
Please investigate and fix the failing checks.`;
198+
199+
await github.rest.issues.createComment({
200+
owner: context.repo.owner,
201+
repo: context.repo.repo,
202+
issue_number: issueNumber,
203+
body: failureComment
204+
});
205+
206+
console.log('Posted failure comment');
207+
} else if (!hasFailure && existingFailureComment) {
208+
// Remove failure comment when status is green
209+
await github.rest.issues.deleteComment({
210+
owner: context.repo.owner,
211+
repo: context.repo.repo,
212+
comment_id: existingFailureComment.id
213+
});
214+
215+
console.log('Removed failure comment - status is now green');
216+
}

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ jobs:
2929
cache: 'pnpm' # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-dependencies
3030
- run: pnpm release:prepare
3131
- run: pnpm release:build
32-
- run: pnpm pkg-pr-new-release
32+
- run: pnpm dlx pkg-pr-new publish $(pnpm -s pkg-pr-new-packages) --pnpm --comment=off --peerDeps --compact

apps/code-infra-dashboard/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ Collection of helpers useful when working on [MUI](https://github.com/mui).
99
Bugfixes and feature suggestions are greatly appreciated. Though this project is highly opinionated so feature requests from external contributors likely won't be accepted if no core member has a use for them.
1010

1111
```bash
12-
$ pnpm --ignore-workspace install
13-
$ pnpm -F code-infra-dashboard dev
12+
pnpm install
13+
pnpm -F @app/code-infra-dashboard dev
1414
```

apps/code-infra-dashboard/netlify.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
publish = "build"
33

44
# Default build command.
5-
command = "pnpm -F code-infra-dashboard build"
5+
command = "pnpm -F @app/code-infra-dashboard build"
66

77
# Decide when to build Netlify
88
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF apps/code-infra-dashboard packages/bundle-size-checker pnpm-lock.yaml"
99

1010
[dev]
1111
framework = "#custom"
12-
command = "pnpm -F code-infra-dashboard start"
12+
command = "pnpm -F @app/code-infra-dashboard start"
1313
targetPort = 3000
1414

1515
[build.environment]

apps/code-infra-dashboard/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "code-infra-dashboard",
2+
"name": "@app/code-infra-dashboard",
33
"private": true,
44
"dependencies": {
55
"@emotion/react": "^11.14.0",
@@ -31,7 +31,7 @@
3131
"dev": "pnpm --package netlify-cli@15 dlx netlify dev",
3232
"preview": "vite preview",
3333
"test": "echo 'No tests yet' && exit 1",
34-
"test:types": "tsc"
34+
"typescript": "tsc"
3535
},
3636
"browserslist": {
3737
"production": [

apps/code-infra-dashboard/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "es5",
3+
"target": "ES2022",
44
"lib": ["dom", "dom.iterable", "esnext"],
55
"allowJs": true,
66
"skipLibCheck": true,
@@ -16,7 +16,6 @@
1616
"noEmit": true,
1717
"jsx": "react-jsx",
1818
"outDir": "./build",
19-
"downlevelIteration": true,
2019
"composite": true
2120
},
2221
"include": ["src", "functions"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import mysql from 'mysql2/promise';
2+
import SSH2Promise from 'ssh2-promise';
3+
4+
export type QueryStoreDatabaseExecute = (connection: mysql.Connection) => Promise<any>;
5+
6+
export async function queryStoreDatabase(execute: QueryStoreDatabaseExecute) {
7+
if (!process.env.STORE_PRODUCTION_READ_PASSWORD) {
8+
throw new Error(`Env variable STORE_PRODUCTION_READ_PASSWORD not configured`);
9+
}
10+
if (!process.env.BASTION_SSH_KEY) {
11+
throw new Error(`Env variable BASTION_SSH_KEY not configured`);
12+
}
13+
14+
const ssh = new SSH2Promise({
15+
host: process.env.BASTION_HOST,
16+
port: 22,
17+
username: process.env.BASTION_USERNAME,
18+
privateKey: process.env.BASTION_SSH_KEY.replace(/\\n/g, '\n'),
19+
});
20+
21+
const tunnel = await ssh.addTunnel({
22+
remoteAddr: process.env.STORE_PRODUCTION_READ_HOST,
23+
remotePort: 3306,
24+
});
25+
26+
const connection = await mysql.createConnection({
27+
host: 'localhost',
28+
port: tunnel.localPort,
29+
user: process.env.STORE_PRODUCTION_READ_USERNAME,
30+
password: process.env.STORE_PRODUCTION_READ_PASSWORD,
31+
database: process.env.STORE_PRODUCTION_READ_DATABASE,
32+
});
33+
34+
try {
35+
return await execute(connection);
36+
} finally {
37+
await connection.end().catch(() => {});
38+
await ssh.close().catch(() => {});
39+
}
40+
}

apps/tools-public/toolpad/resources/updateMuiPaidSupport.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import dayjs from 'dayjs';
33
import { sheets } from '@googleapis/sheets';
44
import { JWT } from 'google-auth-library';
55
import { Octokit } from '@octokit/core';
6+
import { queryStoreDatabase } from './queryStoreDatabase';
67

78
function findRowIndexByValue(sheet, value) {
89
for (let i = 0; i < sheet.length; i += 1) {
@@ -67,6 +68,17 @@ async function updateGitHubIssueLabels(repo, issueId) {
6768
};
6869
}
6970

71+
async function queryPurchasedSupportKey(supportKey: string) {
72+
return queryStoreDatabase(async (connection) => {
73+
const [rows] = await connection.execute(
74+
'select count(*) as found from wp3u_x_addons where license_key = ? and expire_at > now()',
75+
[supportKey],
76+
);
77+
const totalFound = rows?.[0]?.found ?? 0;
78+
return totalFound >= 1;
79+
}).catch(() => false);
80+
}
81+
7082
export async function updateMuiPaidSupport(issueId: string, repo: string, supportKey: string) {
7183
if (!process.env.GOOGLE_SHEET_TOKEN) {
7284
throw new Error('Env variable GOOGLE_SHEET_TOKEN not configured');
@@ -90,6 +102,11 @@ export async function updateMuiPaidSupport(issueId: string, repo: string, suppor
90102
};
91103
}
92104

105+
const isPurchasedSupportKey = await queryPurchasedSupportKey(supportKey);
106+
if (isPurchasedSupportKey) {
107+
return updateGitHubIssueLabels(repo, issueId);
108+
}
109+
93110
const googleAuth = new JWT({
94111
95112
key: process.env.GOOGLE_SHEET_TOKEN.replace(/\\n/g, '\n'),

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
"eslint:ci": "eslint . --report-unused-disable-directives --ext .js,.ts,.tsx --max-warnings 0",
88
"prettier": "pretty-quick --ignore-path .eslintignore",
99
"prettier:all": "prettier --write . --ignore-path .eslintignore",
10-
"update-netlify-ignore": "node ./update-netlify-ignore.js code-infra-dashboard",
10+
"update-netlify-ignore": "node ./update-netlify-ignore.js @app/code-infra-dashboard",
1111
"test": "vitest",
1212
"typescript": "tsc --build --verbose",
1313
"release:prepare": "pnpm install && pnpm release:build",
1414
"release:version": "lerna version --no-changelog --no-push --no-git-tag-version --no-private",
1515
"release:build": "pnpm --filter \"./packages/**\" run build",
1616
"release:publish": "pnpm publish --recursive --tag latest",
17-
"pkg-pr-new-packages": "pnpm ls -r --parseable --depth -1 -F \"./packages/**\"",
18-
"pkg-pr-new-release": "pnpm dlx pkg-pr-new publish $(pnpm -s pkg-pr-new-packages) --pnpm --comment=off --peerDeps --compact"
17+
"pkg-pr-new-packages": "pnpm ls -r --parseable --depth -1 -F \"./packages/**\""
1918
},
2019
"devDependencies": {
2120
"@actions/core": "^1.10.1",

0 commit comments

Comments
 (0)