- {filteredLogs.map((entry, idx) => {
+ {filteredLogs.map((entry) => {
if (props.variant === "service-logs") {
const e = entry as ServiceLogEntry;
return (
@@ -989,7 +992,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as RequestEntry;
return (
@@ -999,7 +1002,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as ServerLogEntry;
return (
@@ -1009,7 +1012,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as BuildLogEntry;
return (
@@ -1018,7 +1021,7 @@ export function LogViewer(props: LogViewerProps) {
const e = entry as BuildLogEntry;
return (
diff --git a/web/components/service/details/pending-changes-banner.tsx b/web/components/service/details/pending-changes-banner.tsx
index f584ba3..3cfd102 100644
--- a/web/components/service/details/pending-changes-banner.tsx
+++ b/web/components/service/details/pending-changes-banner.tsx
@@ -85,7 +85,7 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({
{changes.map((change, i) => (
diff --git a/web/components/service/details/rollout-history.tsx b/web/components/service/details/rollout-history.tsx
index 1c39224..eafaeaa 100644
--- a/web/components/service/details/rollout-history.tsx
+++ b/web/components/service/details/rollout-history.tsx
@@ -68,7 +68,7 @@ function StatusBadge({ status }: { status: RolloutStatus }) {
return (
{config.label}
diff --git a/web/components/service/details/secrets-section.tsx b/web/components/service/details/secrets-section.tsx
index 2d0a51e..bb829df 100644
--- a/web/components/service/details/secrets-section.tsx
+++ b/web/components/service/details/secrets-section.tsx
@@ -278,7 +278,7 @@ export const SecretsSection = memo(function SecretsSection({
)}
{pendingVars.map((variable, index) => (
diff --git a/web/components/ui/command.tsx b/web/components/ui/command.tsx
deleted file mode 100644
index 9f7152b..0000000
--- a/web/components/ui/command.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { Command as CommandPrimitive } from "cmdk";
-
-import { cn } from "@/lib/utils";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
-import { SearchIcon, CheckIcon } from "lucide-react";
-
-function Command({
- className,
- ...props
-}: React.ComponentProps
) {
- return (
-
- );
-}
-
-function CommandDialog({
- title = "Command Palette",
- description = "Search for a command to run...",
- children,
- className,
- showCloseButton = false,
- ...props
-}: Omit, "children"> & {
- title?: string;
- description?: string;
- className?: string;
- showCloseButton?: boolean;
- children: React.ReactNode;
-}) {
- return (
-
- );
-}
-
-function CommandInput({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-function CommandList({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandEmpty({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandGroup({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandSeparator({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandItem({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- {children}
-
-
- );
-}
-
-function CommandShortcut({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
- );
-}
-
-export {
- Command,
- CommandDialog,
- CommandInput,
- CommandList,
- CommandEmpty,
- CommandGroup,
- CommandItem,
- CommandShortcut,
- CommandSeparator,
-};
diff --git a/web/components/ui/input-group.tsx b/web/components/ui/input-group.tsx
deleted file mode 100644
index b74357c..0000000
--- a/web/components/ui/input-group.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-
-function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
- return (
- [data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
- className,
- )}
- {...props}
- />
- );
-}
-
-const inputGroupAddonVariants = cva(
- "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
- {
- variants: {
- align: {
- "inline-start":
- "pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first",
- "inline-end":
- "pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last",
- "block-start":
- "px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
- "block-end":
- "px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
- },
- },
- defaultVariants: {
- align: "inline-start",
- },
- },
-);
-
-function InputGroupAddon({
- className,
- align = "inline-start",
- ...props
-}: React.ComponentProps<"div"> & VariantProps
) {
- return (
- {
- if ((e.target as HTMLElement).closest("button")) {
- return;
- }
- e.currentTarget.parentElement?.querySelector("input")?.focus();
- }}
- {...props}
- />
- );
-}
-
-const inputGroupButtonVariants = cva(
- "gap-2 text-sm shadow-none flex items-center",
- {
- variants: {
- size: {
- xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
- sm: "",
- "icon-xs":
- "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
- "icon-sm": "size-8 p-0 has-[>svg]:p-0",
- },
- },
- defaultVariants: {
- size: "xs",
- },
- },
-);
-
-function InputGroupButton({
- className,
- type = "button",
- variant = "ghost",
- size = "xs",
- ...props
-}: Omit
, "size" | "type"> &
- VariantProps & {
- type?: "button" | "submit" | "reset";
- }) {
- return (
-
- );
-}
-
-function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- );
-}
-
-function InputGroupInput({
- className,
- ...props
-}: React.ComponentProps<"input">) {
- return (
-
- );
-}
-
-function InputGroupTextarea({
- className,
- ...props
-}: React.ComponentProps<"textarea">) {
- return (
-
- );
-}
-
-export {
- InputGroup,
- InputGroupAddon,
- InputGroupButton,
- InputGroupText,
- InputGroupInput,
- InputGroupTextarea,
-};
diff --git a/web/lib/email/templates/alert.tsx b/web/lib/email/templates/alert.tsx
index 6730d2d..7dfd5d1 100644
--- a/web/lib/email/templates/alert.tsx
+++ b/web/lib/email/templates/alert.tsx
@@ -38,8 +38,8 @@ export function Alert({
{description}
- {details.map((detail, index) => (
-
+ {details.map((detail) => (
+
{detail.label}
{detail.value}
diff --git a/web/lib/email/templates/test-email.tsx b/web/lib/email/templates/test-email.tsx
deleted file mode 100644
index e3ea2b4..0000000
--- a/web/lib/email/templates/test-email.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Heading, Text } from "@react-email/components";
-import { BaseEmail } from "./base";
-
-type TestEmailProps = {
- baseUrl?: string;
-};
-
-export function TestEmail({ baseUrl }: TestEmailProps) {
- return (
-
- SMTP Configuration Test
-
- This is a test email to verify your SMTP configuration is working
- correctly.
-
-
- If you received this email, your email settings are configured properly.
-
-
- );
-}
-
-const heading = {
- fontSize: "24px",
- fontWeight: "600",
- lineHeight: "32px",
- color: "#1a1a1a",
- marginBottom: "24px",
-};
-
-const paragraph = {
- fontSize: "14px",
- lineHeight: "24px",
- color: "#525f7f",
- marginBottom: "16px",
-};
diff --git a/web/lib/github.ts b/web/lib/github.ts
index e6b2e07..5b07b7b 100644
--- a/web/lib/github.ts
+++ b/web/lib/github.ts
@@ -122,6 +122,12 @@ export function buildCloneUrl(token: string, repoFullName: string): string {
return `https://x-access-token:${token}@github.com/${repoFullName}.git`;
}
+function validateRepoFullName(repoFullName: string): void {
+ if (!/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repoFullName)) {
+ throw new Error("Invalid repository name");
+ }
+}
+
type DeploymentState =
| "pending"
| "in_progress"
@@ -136,6 +142,7 @@ export async function createGitHubDeployment(
environment: string,
description: string,
): Promise
{
+ validateRepoFullName(repoFullName);
const token = await getInstallationToken(installationId);
const response = await fetch(
@@ -178,6 +185,7 @@ export async function updateGitHubDeploymentStatus(
environmentUrl?: string;
},
): Promise {
+ validateRepoFullName(repoFullName);
const token = await getInstallationToken(installationId);
const body: Record = {