# Deployment History

> Deployment feed on a connecting rail: version, environment and status (deployed, failed, building, rolled back) with commit and author meta.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/deployment-history).

- Page, with a live preview: https://shadcn.exchange/r/hirael/deployment-history
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/deployment-history-light-93a90456ece75f07.png
- Registry item: https://hirael.com/r/deployment-history.json
- Categories: blocks, cloud
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/deployment-history.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `spinner`

## Files

### `registry/hirael/bases/radix/blocks/deployment-history/deployment-history.tsx`

```tsx
// Deployment History from Hirael <https://hirael.com/blocks/cloud/deployment-history>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import { CheckCircle2, Clock, GitCommitHorizontal, Loader2, RotateCcw, XCircle } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Spinner } from '@/registry/hirael/bases/radix/ui/spinner';

export type DeploymentState = 'success' | 'failed' | 'building' | 'rolled-back' | 'queued';

const stateMeta: Record<DeploymentState, { icon: React.ElementType; text: string; ring: string; label: string }> = {
  success: {
    icon: CheckCircle2,
    text: 'text-success',
    ring: 'border-success/40',
    label: 'Deployed',
  },
  failed: {
    icon: XCircle,
    text: 'text-destructive',
    ring: 'border-destructive/40',
    label: 'Failed',
  },
  building: {
    icon: Loader2,
    text: 'text-info',
    ring: 'border-info/40',
    label: 'Building',
  },
  'rolled-back': {
    icon: RotateCcw,
    text: 'text-warning',
    ring: 'border-warning/40',
    label: 'Rolled back',
  },
  queued: {
    icon: Clock,
    text: 'text-muted-foreground',
    ring: 'border-border',
    label: 'Queued',
  },
};

type DeploymentHistoryProps = React.ComponentProps<'ol'>;

const DeploymentHistory = ({ className, ...props }: DeploymentHistoryProps) => {
  return <ol data-slot="deployment-history" className={cn('flex flex-col', className)} {...props} />;
};

interface DeploymentHistoryItemProps extends React.ComponentProps<'li'> {
  state: DeploymentState;
  version: React.ReactNode;
  environment?: React.ReactNode;
  /** Hide the connecting rail below (pass on the last item). */
  last?: boolean;
}

const DeploymentHistoryItem = ({
  state,
  version,
  environment,
  last,
  className,
  children,
  ...props
}: DeploymentHistoryItemProps) => {
  const meta = stateMeta[state];
  const Icon = meta.icon;

  return (
    <li
      data-slot="deployment-history-item"
      data-state={state}
      className={cn('relative flex gap-3 ps-1', className)}
      {...props}
    >
      <div className="flex flex-col items-center">
        <span className={cn('flex size-7 shrink-0 items-center justify-center rounded-full border bg-card', meta.ring)}>
          <Icon
            className={cn('size-4', meta.text, state === 'building' && 'animate-spin motion-reduce:animate-none')}
            aria-hidden
          />
        </span>
        {!last ? <span className="w-px flex-1 bg-border" aria-hidden /> : null}
      </div>
      <div className="flex min-w-0 flex-1 flex-col gap-1 pb-6">
        <div className="flex flex-wrap items-center gap-2">
          <span className="text-sm font-medium text-foreground">{version}</span>
          {environment ? (
            <span className="rounded-sm bg-accent px-1.5 py-0.5 text-xs text-muted-foreground uppercase">
              {environment}
            </span>
          ) : null}
          <span className={cn('text-xs font-medium', meta.text)}>{meta.label}</span>
        </div>
        {children}
      </div>
    </li>
  );
};

type DeploymentHistoryMetaProps = React.ComponentProps<'div'>;

const DeploymentHistoryMeta = ({ className, ...props }: DeploymentHistoryMetaProps) => {
  return (
    <div
      data-slot="deployment-history-meta"
      className={cn('flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground', className)}
      {...props}
    />
  );
};

interface DeploymentHistoryCommitProps extends React.ComponentProps<'span'> {
  sha: React.ReactNode;
}

const DeploymentHistoryCommit = ({ sha, className, children, ...props }: DeploymentHistoryCommitProps) => {
  return (
    <span data-slot="deployment-history-commit" className={cn('inline-flex items-center gap-1', className)} {...props}>
      <GitCommitHorizontal className="size-3.5" aria-hidden />
      <span>{sha}</span>
      {children}
    </span>
  );
};

export { DeploymentHistory, DeploymentHistoryItem, DeploymentHistoryMeta, DeploymentHistoryCommit };

const ENTER =
  'animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
const SWAP =
  'animate-in fade-in slide-in-from-bottom-1 duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';

type Deployment = {
  id: string;
  state: DeploymentState;
  version: string;
  environment: string;
  sha: string;
  note: string;
  when: string;
  duration?: string;
};

const DEPLOYMENTS: Deployment[] = [
  {
    id: 'dpl-5',
    state: 'building',
    version: 'v2.4.1',
    environment: 'staging',
    sha: 'a1b9c4d',
    note: 'by maya',
    when: 'just now',
  },
  {
    id: 'dpl-4',
    state: 'success',
    version: 'v2.4.0',
    environment: 'production',
    sha: 'e84f2c7',
    note: 'by omar',
    when: '2h ago',
    duration: '48s',
  },
  {
    id: 'dpl-3',
    state: 'success',
    version: 'v2.3.9',
    environment: 'production',
    sha: '7f2e10a',
    note: 'by lena',
    when: 'yesterday',
    duration: '52s',
  },
  {
    id: 'dpl-2',
    state: 'rolled-back',
    version: 'v2.3.8',
    environment: 'production',
    sha: 'c03be91',
    note: 'reverted failing migration',
    when: '2d ago',
  },
  {
    id: 'dpl-1',
    state: 'failed',
    version: 'v2.3.7',
    environment: 'staging',
    sha: '11de4f0',
    note: 'build step exited 1',
    when: '2d ago',
  },
];

type RollbackStep = 'idle' | 'confirm' | 'pending';

const DeploymentHistoryBlock = () => {
  const [deployments, setDeployments] = React.useState(DEPLOYMENTS);
  const [target, setTarget] = React.useState<string | null>(null);
  const [step, setStep] = React.useState<RollbackStep>('idle');
  const [addedId, setAddedId] = React.useState<string | null>(null);
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);

  React.useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current);
    },
    [],
  );

  const live = deployments.find(
    (deployment) => deployment.environment === 'production' && deployment.state === 'success',
  );

  const rollback = (deployment: Deployment) => {
    setStep('pending');
    timer.current = setTimeout(() => {
      const id = `dpl-${deployments.length + 1}`;
      setDeployments((current) => [
        {
          ...deployment,
          id,
          state: 'success',
          note: `rolled back from ${live?.version ?? 'latest'}`,
          when: 'just now',
          duration: '31s',
        },
        ...current.map((item) => (item.id === live?.id ? { ...item, state: 'rolled-back' as const } : item)),
      ]);
      setAddedId(id);
      setTarget(null);
      setStep('idle');
    }, 1800);
  };

  return (
    <section data-slot="deployment-history-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <div className={cn(ENTER, 'w-full max-w-lg')}>
        <DeploymentHistory>
          {deployments.map((deployment, index) => {
            const canRollback =
              deployment.environment === 'production' &&
              deployment.state === 'success' &&
              deployment.version !== live?.version;
            const isTarget = target === deployment.id;

            return (
              <DeploymentHistoryItem
                key={deployment.id}
                state={deployment.state}
                version={deployment.version}
                environment={deployment.environment}
                last={index === deployments.length - 1}
                className={cn(deployment.id === addedId && SWAP)}
              >
                <DeploymentHistoryMeta>
                  <DeploymentHistoryCommit sha={deployment.sha} />
                  <span>{deployment.note}</span>
                  <span>{deployment.when}</span>
                  {deployment.duration ? <span className="tabular-nums">{deployment.duration}</span> : null}
                </DeploymentHistoryMeta>

                {canRollback ? (
                  <div data-slot="deployment-history-actions" className="pt-1.5">
                    {!isTarget ? (
                      <Button
                        variant="outline"
                        size="xs"
                        disabled={step !== 'idle'}
                        onClick={() => {
                          setTarget(deployment.id);
                          setStep('confirm');
                        }}
                      >
                        <RotateCcw aria-hidden />
                        Roll back to {deployment.version}
                      </Button>
                    ) : (
                      <div
                        key={step}
                        className={cn(
                          SWAP,
                          'flex flex-wrap items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2',
                        )}
                      >
                        {step === 'pending' ? (
                          <span role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
                            <Spinner aria-hidden className="size-3.5 motion-reduce:animate-none" />
                            Rolling production back to {deployment.version}
                          </span>
                        ) : (
                          <>
                            <span className="me-auto text-xs text-foreground">
                              Replace {live?.version} in production with {deployment.version}?
                            </span>
                            <Button
                              variant="ghost"
                              size="xs"
                              onClick={() => {
                                setTarget(null);
                                setStep('idle');
                              }}
                            >
                              Cancel
                            </Button>
                            <Button size="xs" onClick={() => rollback(deployment)}>
                              Roll back
                            </Button>
                          </>
                        )}
                      </div>
                    )}
                  </div>
                ) : null}
              </DeploymentHistoryItem>
            );
          })}
        </DeploymentHistory>
      </div>
    </section>
  );
};

export default DeploymentHistoryBlock;
```
