From 8bed6b0899caf5224a4d7ca6c679b79ce4b36929 Mon Sep 17 00:00:00 2001 From: Volnei Munhoz Date: Tue, 30 Dec 2025 20:38:43 -0300 Subject: [PATCH 1/4] Spread biome errors (#26329) --- biome.json | 103 ++++++++++++++---- lint-staged.config.mjs | 10 +- .../coss-ui/src/components/breadcrumb.tsx | 3 +- packages/coss-ui/tsconfig.json | 1 + packages/features/package.json | 1 - 5 files changed, 87 insertions(+), 31 deletions(-) diff --git a/biome.json b/biome.json index 055095ed7786f7..e12691bf428d36 100644 --- a/biome.json +++ b/biome.json @@ -27,6 +27,7 @@ "!!**/dist", "!!**/build", "!!**/public", + "!!companion", "!!public", "!!apps/web/public/embed", "!!packages/prisma/zod", @@ -59,19 +60,89 @@ }, "linter": { "enabled": true, + "domains": { + "next": "recommended", + "react": "recommended" + }, "rules": { "recommended": true, - "correctness": "warn", - "suspicious": "warn", - "complexity": "warn", - "performance": "warn", - "nursery": "warn", - "a11y": "warn", - "style": "warn", - "security": "warn" + "a11y": "off" } }, "overrides": [ + { + "includes": ["**/*.tsx"], + "javascript": { + "jsxRuntime": "transparent" + } + }, + { + "includes": [ + "apps/web/app/**/page.tsx", + "apps/web/app/**/layout.tsx", + "apps/web/app/pages/**/*.tsx" + ], + "linter": { + "rules": { + "style": { + "noDefaultExport": "off" + } + } + } + }, + { + "includes": ["**/*"], + "linter": { + "rules": { + "nursery": { + "useQwikValidLexicalScope": "warn", + "useExplicitType": "warn", + "noReactForwardRef": "warn", + "noTernary": "warn", + "noUnresolvedImports": "warn" + }, + "correctness": { + "useImportExtensions": "warn", + "noProcessGlobal": "warn", + "useExhaustiveDependencies": "warn", + "noUnknownProperty": "warn", + "noUnreachable": "warn", + "noEmptyPattern": "warn", + "noSwitchDeclarations": "warn", + "useHookAtTopLevel": "warn", + "useJsxKeyInIterable": "warn", + "noUnsafeOptionalChaining": "warn" + }, + "style": { + "useExportsLast": "warn", + "noProcessEnv": "warn" + }, + "complexity": { + "noExcessiveLinesPerFunction": "warn" + }, + "performance": { + "noImgElement": "warn" + }, + "suspicious": { + "noReactSpecificProps": "warn", + "noDoubleEquals": "warn", + "noAssignInExpressions": "warn", + "noExplicitAny": "warn", + "useIterableCallbackReturn": "warn", + "noImplicitAnyLet": "warn", + "noRedeclare": "warn", + "noControlCharactersInRegex": "warn", + "noArrayIndexKey": "warn", + "noShadowRestrictedNames": "warn", + "noAsyncPromiseExecutor": "warn" + }, + "security": { + "noBlankTarget": "warn", + "noDangerouslySetInnerHtml": "warn" + } + } + } + }, { "includes": ["packages/lib/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"], "linter": { @@ -167,22 +238,6 @@ } } } - }, - { - "includes": ["companion/**/*.{ts,tsx,js,jsx}"], - "javascript": { - "formatter": { - "lineWidth": 100 - } - } - }, - { - "includes": ["apps/website/lib/utils/wordlist/wordlist.ts"], - "javascript": { - "formatter": { - "quoteProperties": "preserve" - } - } } ] } diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index fe534bd17bca87..e31db660989d23 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -1,10 +1,12 @@ -const skipWarnings = ["1", "true", "yes", "on"].includes((process.env.SKIP_WARNINGS ?? "").toLowerCase()); +const skipWarnings = ["1", "true", "yes", "on"].includes( + (process.env.SKIP_WARNINGS ?? "").toLowerCase() +); export default { "(apps|packages|companion)/**/*.{js,ts,jsx,tsx}": (files) => skipWarnings - ? `biome lint --write ${files.join(" ")}` - : `biome lint --write --error-on-warnings ${files.join(" ")}`, - "*.json": (files) => `biome format --write ${files.join(" ")}`, + ? `biome lint ${files.join(" ")}` + : `biome lint --error-on-warnings ${files.join(" ")}`, + "*.json": (files) => `biome format ${files.join(" ")}`, "packages/prisma/schema.prisma": ["prisma format"], }; diff --git a/packages/coss-ui/src/components/breadcrumb.tsx b/packages/coss-ui/src/components/breadcrumb.tsx index d325b54d6856d5..c3dc54a4a35f75 100644 --- a/packages/coss-ui/src/components/breadcrumb.tsx +++ b/packages/coss-ui/src/components/breadcrumb.tsx @@ -14,7 +14,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
    ) { return ( - // biome-ignore lint(a11y/useFocusableInteractive): known Date: Wed, 31 Dec 2025 00:16:52 -0300 Subject: [PATCH 2/4] fix(ci): improve security for external contributions (#26327) - Remove pull_request_review trigger - Add trust-check job to validate PR authors before running CI - Create run-ci.yml workflow for maintainer approval via label - Use workflow run timestamp to prevent backdating attacks - Add per_page and in-progress checks for robustness --- .github/workflows/pr.yml | 148 ++++++++++++++++++----------------- .github/workflows/run-ci.yml | 74 ++++++++++++++++++ 2 files changed, 152 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/run-ci.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 06dc5e2b8d10b4..d15fcc6c45c4b7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,3 +1,4 @@ +# ⚠️ SECURITY: Do not add steps that checkout PR code or run local actions before trust-check job completes. name: PR Update on: @@ -6,9 +7,6 @@ on: branches: - main - gh-actions-test-branch - # Allow CI to run when a maintainer approves an external contributor's PR - pull_request_review: - types: [submitted] workflow_dispatch: @@ -17,19 +15,19 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}${{ github.event_name == 'pull_request_review' && github.event.review.state != 'approved' && '-noop' || '' }} - cancel-in-progress: ${{ github.event_name != 'pull_request_review' || github.event.review.state == 'approved' }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - # Security gate: Check if PR is from a trusted contributor or has been approved by a core team member + # Security gate: Check if PR is from a trusted contributor or approved via run-ci label # This MUST run before any job that checks out PR code and executes it with secrets trust-check: name: Trust Check runs-on: blacksmith-2vcpu-ubuntu-2404 - # Skip if this is a non-approval review event (e.g., comment or changes_requested) - if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved' permissions: pull-requests: read + actions: read + issues: read outputs: is-trusted: ${{ steps.check-trust.outputs.is-trusted }} steps: @@ -39,9 +37,8 @@ jobs: with: script: | const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; - const pr = context.payload.pull_request; - if (!pr) { + if (!context.payload.pull_request) { if (context.eventName === 'workflow_dispatch') { console.log('workflow_dispatch event - assuming trusted (manual trigger)'); core.setOutput('is-trusted', true); @@ -54,10 +51,17 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; + + // Fetch fresh PR data - payload labels may be stale on re-runs + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: context.payload.pull_request.number, + }); + const prNumber = pr.number; const headSha = pr.head.sha; - // Check if user has write access or higher (admin, maintain, write) async function hasWriteAccess(username) { try { const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ @@ -65,85 +69,84 @@ jobs: repo, username, }); - const hasAccess = ['admin', 'maintain', 'write'].includes(permission.permission); - console.log(`User ${username}: hasWriteAccess=${hasAccess}`); - return hasAccess; + return ['admin', 'maintain', 'write'].includes(permission.permission); } catch (e) { - console.log(`Could not check permission for ${username}: ${e.message}`); + console.log(`Permission check failed for ${username}: ${e.message}`); return false; } } - console.log(`PR #${prNumber} by ${pr.user.login}`); - console.log(`Author association: ${pr.author_association}`); - console.log(`Head SHA: ${headSha}`); + console.log(`PR #${prNumber} by ${pr.user.login} (${pr.author_association})`); // Check 1: Is the author a trusted contributor? - const isTrustedAuthor = trustedAssociations.includes(pr.author_association); - if (isTrustedAuthor) { - console.log(`Author ${pr.user.login} is trusted (${pr.author_association})`); + if (trustedAssociations.includes(pr.author_association)) { + console.log(`Author has trusted association: ${pr.author_association}`); core.setOutput('is-trusted', true); return; } // Check 2: Verify write access via API (author_association can be unreliable) if (await hasWriteAccess(pr.user.login)) { - console.log(`Author ${pr.user.login} verified as having write access`); + console.log(`Author has write access`); core.setOutput('is-trusted', true); return; } - console.log(`Author ${pr.user.login} does not have write access, checking for approval...`); - - // Check 3: Has someone with write access approved the current commit? - const reviews = await github.paginate(github.rest.pulls.listReviews, { - owner, - repo, - pull_number: prNumber, - per_page: 100, - }); - - // Group reviews by reviewer and get their latest state for the current commit - const reviewerStates = new Map(); - for (const review of reviews) { - // Only consider reviews for the current head SHA - if (review.commit_id !== headSha) { - continue; - } - - // Only track APPROVED and CHANGES_REQUESTED states - if (!['APPROVED', 'CHANGES_REQUESTED'].includes(review.state)) { - continue; - } + // Check 3: Was 'run-ci' label added AFTER this SHA was pushed by someone with write access? + // This enables re-runs triggered by the run-ci.yml workflow + // NOTE: We use workflow run created_at instead of commit timestamp because + // git commit timestamps can be arbitrarily backdated by attackers + if (pr.labels?.some(l => l.name === 'run-ci')) { + const events = await github.paginate(github.rest.issues.listEvents, { + owner, + repo, + issue_number: prNumber, + per_page: 100, + }); - const reviewer = review.user.login; - const existing = reviewerStates.get(reviewer); + const labelEvent = events + .filter(e => e.event === 'labeled' && e.label?.name === 'run-ci') + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0]; - // Keep the latest review (higher ID = more recent) - if (!existing || review.id > existing.id) { - reviewerStates.set(reviewer, { - id: review.id, - state: review.state, - login: reviewer, + if (labelEvent) { + // Get workflow runs to find when this SHA was first pushed + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: 'pr.yml', + head_sha: headSha, + per_page: 100, }); - } - } - - // Check if any approver has write access - for (const [reviewer, reviewData] of reviewerStates) { - if (reviewData.state !== 'APPROVED') { - continue; - } - if (await hasWriteAccess(reviewer)) { - console.log(`PR approved by ${reviewer} (has write access) for commit ${headSha}`); - core.setOutput('is-trusted', true); - return; + // Filter runs to this PR (in case same SHA exists in multiple PRs) + const matchingRuns = runs.filter(run => + !run.pull_requests?.length || run.pull_requests.some(p => p.number === prNumber) + ); + + if (matchingRuns.length > 0) { + const labelTime = new Date(labelEvent.created_at); + // Use the oldest run's created_at as the push time + const originalRun = matchingRuns[matchingRuns.length - 1]; + const pushTime = new Date(originalRun.created_at); + + if (labelTime > pushTime) { + const adder = labelEvent.actor.login; + if (await hasWriteAccess(adder)) { + console.log(`Approved via 'run-ci' label added by ${adder} after push (label: ${labelTime.toISOString()}, push: ${pushTime.toISOString()})`); + core.setOutput('is-trusted', true); + return; + } + console.log(`Label 'run-ci' added by ${adder} (no write access)`); + } else { + console.log(`Label 'run-ci' is stale (label: ${labelTime.toISOString()}, push: ${pushTime.toISOString()})`); + } + } else { + console.log('No workflow runs found for this SHA - cannot validate label timing'); + } } - console.log(`Reviewer ${reviewer} does not have write access`); } - console.log('PR requires approval from someone with write access before CI can run'); + console.log('External contribution requires "run-ci" label from a maintainer'); core.setOutput('is-trusted', false); prepare: @@ -412,13 +415,18 @@ jobs: if: always() runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - name: Fail if PR is not trusted (external contributor without approval) + - name: Fail if trust-check did not succeed + run: | + echo "::error::Trust check did not complete successfully (result: ${{ needs.trust-check.result }}). Please re-run the workflow." + exit 1 + if: needs.trust-check.result != 'success' + - name: Fail if PR is not trusted (external contributor without run-ci label) run: | - echo "::error::This PR is from an external contributor and requires approval from a core team member before CI can run." - echo "A maintainer with write access must approve this PR to trigger CI checks." + echo "::error::This PR is from an external contributor and requires the 'run-ci' label before CI can run." + echo "A maintainer must review the code and add the 'run-ci' label to trigger CI checks." exit 1 if: needs.trust-check.outputs.is-trusted != 'true' && needs.trust-check.result == 'success' - - name: fail if conditional jobs failed + - name: Fail if conditional jobs failed run: exit 1 if: | ( diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml new file mode 100644 index 00000000000000..a82c106d362cfe --- /dev/null +++ b/.github/workflows/run-ci.yml @@ -0,0 +1,74 @@ +name: Run CI + +on: + pull_request_target: + types: [labeled] + +permissions: + actions: write + contents: read + +jobs: + trigger: + name: Trigger CI + if: github.event.label.name == 'run-ci' + runs-on: ubuntu-latest + steps: + - name: Verify and trigger CI + uses: actions/github-script@v7 + with: + script: | + const adder = context.payload.sender.login; + const pr = context.payload.pull_request; + + // Verify label adder has write access + const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: adder, + }); + + if (!['admin', 'maintain', 'write'].includes(perm.permission)) { + core.setFailed(`${adder} does not have write access`); + return; + } + + console.log(`Label added by ${adder} (${perm.permission})`); + + // Find the latest pr.yml run for this PR's head SHA + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'pr.yml', + head_sha: pr.head.sha, + per_page: 5, + }); + + // Filter runs to this PR (in case same SHA exists in multiple PRs) + const matchingRuns = runs.workflow_runs.filter(run => + !run.pull_requests?.length || run.pull_requests.some(p => p.number === pr.number) + ); + + if (matchingRuns.length === 0) { + core.setFailed(`No PR workflow found for SHA ${pr.head.sha}`); + return; + } + + const latestRun = matchingRuns[0]; + + // Check if workflow is still running - can't re-run in-progress workflows + if (latestRun.status === 'in_progress' || latestRun.status === 'queued') { + core.setFailed(`Workflow is still running (status: ${latestRun.status}). Wait for it to complete or cancel it first.`); + return; + } + + console.log(`Re-running workflow ${latestRun.id} (was: ${latestRun.conclusion})`); + + // Re-run preserves original context (PR, SHA, etc.) + await github.rest.actions.reRunWorkflow({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: latestRun.id, + }); + + console.log(`Triggered: ${latestRun.html_url}`); From fee19654b0c987c5399e73619c60d648a78e6314 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 31 Dec 2025 09:23:38 +0530 Subject: [PATCH 3/4] fix: meetin url (#26321) --- .../features/tasker/tasks/sendWorkflowEmails.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/features/tasker/tasks/sendWorkflowEmails.ts b/packages/features/tasker/tasks/sendWorkflowEmails.ts index dbd5ac6870e09a..2520bdbf0e2634 100644 --- a/packages/features/tasker/tasks/sendWorkflowEmails.ts +++ b/packages/features/tasker/tasks/sendWorkflowEmails.ts @@ -6,6 +6,7 @@ import { BookingRepository } from "@calcom/features/bookings/repositories/Bookin import { BookingSeatRepository } from "@calcom/features/bookings/repositories/BookingSeatRepository"; import { EmailWorkflowService } from "@calcom/features/ee/workflows/lib/service/EmailWorkflowService"; import { WorkflowReminderRepository } from "@calcom/features/ee/workflows/repositories/WorkflowReminderRepository"; +import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { prisma } from "@calcom/prisma"; export const ZSendWorkflowEmailsSchemaEager = z.object({ @@ -53,12 +54,23 @@ export async function sendWorkflowEmails(payload: string): Promise { if (!calendarEvent) { throw new Error("Calendar event could not be built"); } + + // Check if videoCallUrl exists in booking metadata and add it to evt.metadata + const bookingMetadata = bookingMetadataSchema.parse(booking.metadata || {}); + const metadata = bookingMetadata?.videoCallUrl + ? { + videoCallUrl: bookingMetadata.videoCallUrl, + } + : undefined; + + const evtWithMetadata = { ...calendarEvent, metadata }; + const workflowReminderRepository = new WorkflowReminderRepository(prisma); const bookingSeatRepository = new BookingSeatRepository(prisma); const emailWorkflowService = new EmailWorkflowService(workflowReminderRepository, bookingSeatRepository); await emailWorkflowService.handleSendEmailWorkflowTask({ - evt: calendarEvent, + evt: evtWithMetadata, workflowReminderId: mailData.workflowReminderId, }); From 582e547beac94c75239159144a3a42fcbbc04496 Mon Sep 17 00:00:00 2001 From: Tamal Chakraborty <134912393+Tamalckb531@users.noreply.github.com> Date: Wed, 31 Dec 2025 10:02:27 +0600 Subject: [PATCH 4/4] Fix: Changed Delete My Account button from primary to Destructive (#26326) --- apps/web/modules/settings/my-account/profile-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/modules/settings/my-account/profile-view.tsx b/apps/web/modules/settings/my-account/profile-view.tsx index dbce2bf6109392..c4b3c0261bbf5d 100644 --- a/apps/web/modules/settings/my-account/profile-view.tsx +++ b/apps/web/modules/settings/my-account/profile-view.tsx @@ -364,7 +364,7 @@ const ProfileView = ({ user }: Props) => {