Skip to content

Commit b53f8bd

Browse files
committed
Add CI/CD
1 parent 199fbba commit b53f8bd

File tree

12 files changed

+436
-2
lines changed

12 files changed

+436
-2
lines changed

.dockerignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Dockerfile
2+
.dockerignore
3+
node_modules
4+
yarn-debug.log
5+
README.md
6+
.next
7+
.git
8+
.env*.local
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Production Build Workflow
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
jobs:
9+
docker:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Set most recent tag
15+
id: vars
16+
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
17+
18+
- uses: docker/setup-buildx-action@v3
19+
name: Set up Docker Buildx
20+
21+
- uses: docker/login-action@v3
22+
name: Login to DockerHub
23+
with:
24+
username: ${{ secrets.DOCKERHUB_USERNAME }}
25+
password: ${{ secrets.DOCKERHUB_TOKEN }}
26+
27+
- uses: docker/build-push-action@v6
28+
name: Build and Push WebApp Image
29+
id: docker_build
30+
with:
31+
context: .
32+
push: true
33+
tags: orgsinfo/webapp:${{ step.vars.outputs.tag }}
34+
file: Dockerfile.production
35+
36+
- name: WebApp Image Digest
37+
run: echo ${{ steps.docker_build.outputs.digest }}
38+
39+
deploy:
40+
needs: docker
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- name: Set most recent tag
46+
id: vars
47+
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
48+
49+
- name: Deploy the app to the cluster
50+
uses: nickgronow/kubectl@master
51+
with:
52+
config_data: ${{ secrets.KUBE_CONFIG_DATA }}
53+
args: set image deployment/webapp-production webapp-prod-app=orgsinfo/webapp:${{ steps.vars.outputs.tag }} --namespace=production
54+
55+
- name: Verify deployment
56+
uses: nickgronow/kubectl@master
57+
with:
58+
config_data: ${{ secrets.KUBE_CONFIG_DATA }}
59+
args: rollout status deployment/webapp-production --namespace=production

.github/workflows/build-staging.yml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Staging Build Workflow
2+
3+
on:
4+
push:
5+
branches:
6+
- "staging"
7+
8+
jobs:
9+
docker:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: docker/setup-buildx-action@v3
15+
name: Set up Docker Buildx
16+
17+
- uses: docker/login-action@v3
18+
name: Login to DockerHub
19+
with:
20+
username: ${{ secrets.DOCKERHUB_USERNAME }}
21+
password: ${{ secrets.DOCKERHUB_TOKEN }}
22+
23+
- uses: docker/build-push-action@v6
24+
name: Build and Push WebApp Image
25+
id: docker_build
26+
with:
27+
context: .
28+
push: true
29+
tags: orgsinfo/webapp:latest
30+
file: Dockerfile.staging
31+
32+
- name: WebApp Image Digest
33+
run: echo ${{ steps.docker_build.outputs.digest }}
34+
35+
deploy:
36+
needs: docker
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- name: Deploy the webapp to the cluster
42+
uses: nickgronow/kubectl@master
43+
with:
44+
config_data: ${{ secrets.KUBE_CONFIG_DATA }}
45+
args: delete pod --selector="app=webapp-staging-app" --namespace=staging

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ yarn-error.log*
3131
*.tsbuildinfo
3232
next-env.d.ts
3333

34+
# env
35+
.env*.local
36+
3437
# added
3538
/src/app/cannon
3639
/src/services/TokenService.ts
37-
.vscode
40+
.vscode

Dockerfile.production

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
FROM node:20-alpine AS base
2+
3+
# 1. Install dependencies only when needed
4+
FROM base AS deps
5+
6+
WORKDIR /app
7+
COPY package.json yarn.lock ./
8+
RUN yarn --frozen-lockfile
9+
10+
11+
# 2. Build the application
12+
FROM base AS builder
13+
14+
ENV NEXT_PUBLIC_CANNON_URL="https://cannon.sinfo.org"
15+
16+
WORKDIR /app
17+
COPY --from=deps /app/node_modules ./node_modules
18+
COPY . .
19+
RUN yarn run build
20+
21+
22+
# 3. Production image, copy all the files and run next
23+
FROM base AS runner
24+
25+
WORKDIR /app
26+
ENV NODE_ENV=production
27+
28+
# Disable Next.js telemetry during runtime
29+
ENV NEXT_TELEMETRY_DISABLED=1
30+
31+
RUN addgroup -g 1001 -S nodejs
32+
RUN adduser -S nextjs -u 1001
33+
34+
COPY --from=builder /app/public ./public
35+
36+
# Set the correct permission for prerender cache
37+
RUN mkdir .next
38+
RUN chown nextjs:nodejs .next
39+
40+
# Automatically leverage output traces to reduce image size
41+
# https://nextjs.org/docs/advanced-features/output-file-tracing
42+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
43+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
44+
45+
USER nextjs
46+
47+
EXPOSE 3000
48+
49+
ENV PORT=3000
50+
ENV HOSTNAME="0.0.0.0"
51+
52+
CMD ["server.js"]
53+

Dockerfile.staging

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
FROM node:20-alpine AS base
2+
3+
# 1. Install dependencies only when needed
4+
FROM base AS deps
5+
6+
WORKDIR /app
7+
COPY package.json yarn.lock ./
8+
RUN yarn --frozen-lockfile
9+
10+
11+
# 2. Build the application
12+
FROM base AS builder
13+
14+
ENV NEXT_PUBLIC_CANNON_URL="https://cannon-staging.sinfo.org"
15+
16+
WORKDIR /app
17+
COPY --from=deps /app/node_modules ./node_modules
18+
COPY . .
19+
RUN yarn run build
20+
21+
22+
# 3. Production image, copy all the files and run next
23+
FROM base AS runner
24+
25+
WORKDIR /app
26+
ENV NODE_ENV=production
27+
28+
# Disable Next.js telemetry during runtime
29+
ENV NEXT_TELEMETRY_DISABLED=1
30+
31+
RUN addgroup -g 1001 -S nodejs
32+
RUN adduser -S nextjs -u 1001
33+
34+
COPY --from=builder /app/public ./public
35+
36+
# Set the correct permission for prerender cache
37+
RUN mkdir .next
38+
RUN chown nextjs:nodejs .next
39+
40+
# Automatically leverage output traces to reduce image size
41+
# https://nextjs.org/docs/advanced-features/output-file-tracing
42+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
43+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
44+
45+
USER nextjs
46+
47+
EXPOSE 3000
48+
49+
ENV PORT=3000
50+
ENV HOSTNAME="0.0.0.0"
51+
52+
CMD ["server.js"]
53+

next.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3+
output: "standalone",
34
images: {
45
remotePatterns: [
56
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import EventDayButton from "@/components/EventDayButton";
4+
import GridList from "@/components/GridList";
5+
import List from "@/components/List";
6+
import { PrizeTile } from "@/components/prize";
7+
import { SessionTile } from "@/components/session";
8+
import { getEventFullDate } from "@/utils/utils";
9+
import { useSearchParams } from "next/navigation";
10+
import { useEffect, useMemo, useState } from "react";
11+
12+
interface DailyPrizesTable {
13+
prizes: Prize[];
14+
}
15+
16+
export default function DailyPrizesTable({ prizes }: DailyPrizesTable) {
17+
const [showingDay, setShowingDay] = useState<string | null>(null);
18+
19+
const prizesByDay = useMemo(() => {
20+
const sortedPrizes = prizes.sort((a, b) => a.name.localeCompare(b.name));
21+
return sortedPrizes.reduce(
22+
(acc, sp) => {
23+
const days = sp.days!.map((d) => d.split("T")[0]);
24+
return days.reduce(
25+
(a, d) => ({ ...a, [d]: [...(a[d] || []), sp] }),
26+
acc,
27+
);
28+
},
29+
{} as Record<string, Prize[]>,
30+
);
31+
}, [prizes]);
32+
33+
const sortedDays = useMemo(
34+
() => Object.keys(prizesByDay).sort(),
35+
[prizesByDay],
36+
);
37+
38+
useEffect(() => {
39+
const today = new Date().toISOString().split("T")[0];
40+
setShowingDay(sortedDays.find((d) => d === today) || sortedDays[0]);
41+
}, [sortedDays]);
42+
43+
return (
44+
<>
45+
<GridList>
46+
{sortedDays.map((d) => (
47+
<EventDayButton
48+
key={`event-day-${d}`}
49+
date={d}
50+
onClick={() => setShowingDay(d)}
51+
selected={showingDay === d}
52+
/>
53+
))}
54+
</GridList>
55+
{sortedDays
56+
.filter((d) => !showingDay || d === showingDay)
57+
.map((d) => (
58+
<List key={d} description={getEventFullDate(d)}>
59+
{prizesByDay[d].map((p) => (
60+
<PrizeTile key={p.id} prize={p} />
61+
))}
62+
</List>
63+
))}
64+
</>
65+
);
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import List from "@/components/List";
4+
import { SessionTile } from "@/components/session";
5+
import { useState } from "react";
6+
7+
interface PrizeSessionsProps {
8+
sessions: SINFOSession[];
9+
compressedSize?: number;
10+
}
11+
12+
export default function PrizeSessions({
13+
sessions,
14+
compressedSize = 3,
15+
}: PrizeSessionsProps) {
16+
const [expandList, setExpandList] = useState(false);
17+
18+
return (
19+
<div className="flex flex-col gap-y-2 py-2">
20+
<span className="text-sm text-gray-600">
21+
To win the prize above, go to a session below:
22+
</span>
23+
{sessions.slice(0, expandList ? undefined : compressedSize).map((s) => (
24+
<SessionTile key={s.id} session={s} />
25+
))}
26+
{sessions.length > compressedSize && (
27+
<label
28+
role="button"
29+
className="text-sm font-semibold hover:cursor-pointer"
30+
onClick={() => setExpandList((e) => !e)}
31+
>
32+
{expandList ? "Show less" : "Show more sessions"}
33+
</label>
34+
)}
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)