Skip to content

Commit 11ea619

Browse files
authored
Merge pull request #71 from rajbos/main
Add deployment footer with branch name and deploy time information (#10)
2 parents 038a518 + dbb94d7 commit 11ea619

10 files changed

Lines changed: 312 additions & 0 deletions

build-for-pages.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ npm install
117117

118118
# Build the app
119119
echo "Building application..."
120+
# Set deploy time for build
121+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
122+
echo "Setting deploy time: $DEPLOY_TIME"
120123
npm run build
121124

122125
# Move the built files to the output directory

deploy-to-pages.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ cat > temp-vite.config.ts <<EOF
3030
$(cat vite.config.ts | sed "s/build: {/build: {\n base: '\/${REPO_NAME}\/',/")
3131
EOF
3232

33+
# Set deploy time for build
34+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
35+
echo "Setting deploy time: $DEPLOY_TIME"
36+
3337
# Build with temporary config
3438
mv temp-vite.config.ts vite.config.ts
3539
npm run build

deploy.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ rm -rf dist
1010

1111
# Build the frontend
1212
echo "Compiling frontend..."
13+
# Set deploy time for build
14+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
15+
echo "Setting deploy time: $DEPLOY_TIME"
16+
1317
npm install -f # force because there is a known mismatch of shadcn and react 19 - https://ui.shadcn.com/docs/react-19
1418
npm run build
1519

scripts/capture-deploy-info.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
# Capture deployment information
4+
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
5+
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
6+
DEPLOY_TIME=${DEPLOY_TIME:-$BUILD_TIME}
7+
8+
# Export as environment variables for Vite
9+
export VITE_GIT_BRANCH="$BRANCH_NAME"
10+
export VITE_BUILD_TIME="$BUILD_TIME"
11+
export VITE_DEPLOY_TIME="$DEPLOY_TIME"
12+
13+
echo "Deployment Info:"
14+
echo " Branch: $BRANCH_NAME"
15+
echo " Build Time: $BUILD_TIME"
16+
echo " Deploy Time: $DEPLOY_TIME"

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Separator } from "@/components/ui/separator";
1111
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
1212
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
1313
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
14+
import { DeploymentFooter } from "@/components/DeploymentFooter";
1415
import {
1516
AggregatedData,
1617
CopilotUsageData,
@@ -690,6 +691,7 @@ function App() {
690691
</div>
691692
)}
692693
<Toaster position="top-right" />
694+
<DeploymentFooter />
693695
</div>
694696
);
695697
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getDeploymentInfo } from "@/lib/deployment-info";
2+
3+
export function DeploymentFooter() {
4+
const deployInfo = getDeploymentInfo();
5+
6+
// Don't show footer if no deployment info is available
7+
if (deployInfo.branchName === 'unknown' && deployInfo.deployTime === 'unknown') {
8+
return null;
9+
}
10+
11+
const formatDeployTime = (deployTime: string) => {
12+
if (deployTime === 'unknown') return 'Unknown';
13+
try {
14+
// Try to parse and format the timestamp
15+
const date = new Date(deployTime);
16+
if (isNaN(date.getTime())) return deployTime; // Return as-is if not parseable
17+
return date.toLocaleString();
18+
} catch {
19+
return deployTime; // Return as-is if parsing fails
20+
}
21+
};
22+
23+
return (
24+
<footer className="mt-8 py-4 border-t border-border">
25+
<div className="container max-w-7xl mx-auto px-4">
26+
<div className="flex flex-col sm:flex-row justify-between items-center gap-2 text-xs text-muted-foreground">
27+
<div className="flex items-center gap-4">
28+
{deployInfo.branchName !== 'unknown' && (
29+
<span>
30+
Branch: <span className="font-mono text-foreground">{deployInfo.branchName}</span>
31+
</span>
32+
)}
33+
{deployInfo.deployTime !== 'unknown' && (
34+
<span>
35+
Deployed: <span className="text-foreground">{formatDeployTime(deployInfo.deployTime)}</span>
36+
</span>
37+
)}
38+
</div>
39+
{deployInfo.buildTime !== 'unknown' && (
40+
<span className="text-xs">
41+
Built: {formatDeployTime(deployInfo.buildTime)}
42+
</span>
43+
)}
44+
</div>
45+
</div>
46+
</footer>
47+
);
48+
}

src/lib/deployment-info.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Deployment information that gets injected at build time
2+
export interface DeploymentInfo {
3+
branchName: string;
4+
deployTime: string;
5+
buildTime: string;
6+
}
7+
8+
// Get deployment information from environment variables injected at build time
9+
export function getDeploymentInfo(): DeploymentInfo {
10+
return {
11+
branchName: import.meta.env.VITE_GIT_BRANCH || 'unknown',
12+
deployTime: import.meta.env.VITE_DEPLOY_TIME || 'unknown',
13+
buildTime: import.meta.env.VITE_BUILD_TIME || 'unknown',
14+
};
15+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { render, screen, cleanup } from '@testing-library/react';
3+
import { DeploymentFooter } from '@/components/DeploymentFooter';
4+
5+
// Mock the deployment info module
6+
vi.mock('@/lib/deployment-info', () => ({
7+
getDeploymentInfo: vi.fn(),
8+
}));
9+
10+
import * as deploymentInfoModule from '@/lib/deployment-info';
11+
12+
describe('DeploymentFooter', () => {
13+
const mockGetDeploymentInfo = vi.mocked(deploymentInfoModule.getDeploymentInfo);
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
});
18+
19+
afterEach(() => {
20+
vi.restoreAllMocks();
21+
cleanup();
22+
});
23+
24+
it('should display branch name and deploy time when deployment info is available', () => {
25+
const mockDeployInfo = {
26+
branchName: 'main',
27+
deployTime: '2025-06-28T10:00:00Z',
28+
buildTime: '2025-06-28T10:00:00Z',
29+
};
30+
31+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
32+
33+
render(<DeploymentFooter />);
34+
35+
expect(screen.getByText('Branch:')).toBeInTheDocument();
36+
expect(screen.getByText('main')).toBeInTheDocument();
37+
expect(screen.getByText('Deployed:')).toBeInTheDocument();
38+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
39+
});
40+
41+
it('should display feature branch name correctly', () => {
42+
const mockDeployInfo = {
43+
branchName: 'feature/footer-implementation',
44+
deployTime: '2025-06-28T10:00:00Z',
45+
buildTime: '2025-06-28T10:00:00Z',
46+
};
47+
48+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
49+
50+
render(<DeploymentFooter />);
51+
52+
expect(screen.getByText('feature/footer-implementation')).toBeInTheDocument();
53+
});
54+
55+
it('should not render when deployment info is unknown', () => {
56+
const mockDeployInfo = {
57+
branchName: 'unknown',
58+
deployTime: 'unknown',
59+
buildTime: 'unknown',
60+
};
61+
62+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
63+
64+
const { container } = render(<DeploymentFooter />);
65+
66+
expect(container.firstChild).toBeNull();
67+
});
68+
69+
it('should render when only branch name is available', () => {
70+
const mockDeployInfo = {
71+
branchName: 'main',
72+
deployTime: 'unknown',
73+
buildTime: 'unknown',
74+
};
75+
76+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
77+
78+
render(<DeploymentFooter />);
79+
80+
expect(screen.getByText('Branch:')).toBeInTheDocument();
81+
expect(screen.getByText('main')).toBeInTheDocument();
82+
expect(screen.queryByText('Deployed:')).not.toBeInTheDocument();
83+
expect(screen.queryByText('Built:')).not.toBeInTheDocument();
84+
});
85+
86+
it('should render when only deploy time is available', () => {
87+
const mockDeployInfo = {
88+
branchName: 'unknown',
89+
deployTime: '2025-06-28T10:00:00Z',
90+
buildTime: '2025-06-28T10:00:00Z',
91+
};
92+
93+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
94+
95+
render(<DeploymentFooter />);
96+
97+
expect(screen.queryByText('Branch:')).not.toBeInTheDocument();
98+
expect(screen.getByText('Deployed:')).toBeInTheDocument();
99+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
100+
});
101+
102+
it('should format date correctly when valid ISO date is provided', () => {
103+
const mockDeployInfo = {
104+
branchName: 'main',
105+
deployTime: '2025-06-28T10:30:45Z',
106+
buildTime: '2025-06-28T10:30:45Z',
107+
};
108+
109+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
110+
111+
render(<DeploymentFooter />);
112+
113+
// Check that the dates are formatted (should have been converted from ISO)
114+
expect(screen.getByText((content) => content.includes('Deployed:'))).toBeInTheDocument();
115+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
116+
117+
// Check that the formatted dates contain digits (meaning they were processed)
118+
// Using getAllByText since there are multiple elements with the date
119+
const elementsWithDate = screen.getAllByText((content) => content.includes('28') || content.includes('2025'));
120+
expect(elementsWithDate.length).toBeGreaterThan(0);
121+
});
122+
123+
it('should handle invalid date strings gracefully', () => {
124+
const mockDeployInfo = {
125+
branchName: 'main',
126+
deployTime: 'invalid-date',
127+
buildTime: 'another-invalid-date',
128+
};
129+
130+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
131+
132+
render(<DeploymentFooter />);
133+
134+
// Should display the raw string when date parsing fails
135+
expect(screen.getByText('invalid-date')).toBeInTheDocument();
136+
expect(screen.getByText((content) => content.includes('another-invalid-date'))).toBeInTheDocument();
137+
});
138+
139+
it('should have correct CSS classes for styling', () => {
140+
const mockDeployInfo = {
141+
branchName: 'test-styling-branch',
142+
deployTime: '2025-06-28T10:00:00Z',
143+
buildTime: '2025-06-28T10:00:00Z',
144+
};
145+
146+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
147+
148+
const { container } = render(<DeploymentFooter />);
149+
150+
const footer = container.querySelector('footer');
151+
expect(footer).toHaveClass('mt-8', 'py-4', 'border-t', 'border-border');
152+
153+
const branchElement = screen.getByText('test-styling-branch');
154+
expect(branchElement).toHaveClass('font-mono', 'text-foreground');
155+
});
156+
});

vite.config.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@ import { defineConfig } from 'vite';
22
import tailwindcss from "@tailwindcss/vite";
33
import react from '@vitejs/plugin-react-swc';
44
import { resolve } from 'path';
5+
import { execSync } from 'child_process';
6+
7+
// Get deployment information at build time
8+
const getDeploymentInfo = () => {
9+
try {
10+
const branchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
11+
const buildTime = new Date().toISOString();
12+
const deployTime = process.env.DEPLOY_TIME || buildTime;
13+
14+
return {
15+
VITE_GIT_BRANCH: branchName,
16+
VITE_BUILD_TIME: buildTime,
17+
VITE_DEPLOY_TIME: deployTime,
18+
};
19+
} catch (error) {
20+
console.warn('Could not get git information:', error.message);
21+
return {
22+
VITE_GIT_BRANCH: 'unknown',
23+
VITE_BUILD_TIME: new Date().toISOString(),
24+
VITE_DEPLOY_TIME: process.env.DEPLOY_TIME || new Date().toISOString(),
25+
};
26+
}
27+
};
528

629
// Get the GitHub repository name from package.json to use as base path
730
// This makes assets load correctly on GitHub Pages
@@ -28,6 +51,15 @@ const getBasePath = () => {
2851
export default defineConfig({
2952
plugins: [react(), tailwindcss()],
3053
base: getBasePath(),
54+
define: {
55+
// Inject deployment info as constants
56+
...Object.fromEntries(
57+
Object.entries(getDeploymentInfo()).map(([key, value]) => [
58+
`import.meta.env.${key}`,
59+
JSON.stringify(value)
60+
])
61+
),
62+
},
3163
build: {
3264
outDir: 'dist',
3365
sourcemap: false,

vite.github-pages.config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,45 @@ import { defineConfig } from "vite";
22
import tailwindcss from "@tailwindcss/vite";
33
import react from "@vitejs/plugin-react";
44
import { resolve } from 'path';
5+
import { execSync } from 'child_process';
6+
7+
// Get deployment information at build time
8+
const getDeploymentInfo = () => {
9+
try {
10+
const branchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
11+
const buildTime = new Date().toISOString();
12+
const deployTime = process.env.DEPLOY_TIME || buildTime;
13+
14+
return {
15+
VITE_GIT_BRANCH: branchName,
16+
VITE_BUILD_TIME: buildTime,
17+
VITE_DEPLOY_TIME: deployTime,
18+
};
19+
} catch (error) {
20+
console.warn('Could not get git information:', error.message);
21+
return {
22+
VITE_GIT_BRANCH: 'unknown',
23+
VITE_BUILD_TIME: new Date().toISOString(),
24+
VITE_DEPLOY_TIME: process.env.DEPLOY_TIME || new Date().toISOString(),
25+
};
26+
}
27+
};
528

629
// Simple Vite config for GitHub Pages deployment
730
export default defineConfig({
831
plugins: [
932
react(),
1033
tailwindcss(),
1134
],
35+
define: {
36+
// Inject deployment info as constants
37+
...Object.fromEntries(
38+
Object.entries(getDeploymentInfo()).map(([key, value]) => [
39+
`import.meta.env.${key}`,
40+
JSON.stringify(value)
41+
])
42+
),
43+
},
1244
build: {
1345
outDir: 'dist',
1446
// Set external dependencies that shouldn't be bundled

0 commit comments

Comments
 (0)