Skip to content

Commit 4aab2e3

Browse files
ci: auto-deploy to Vercel on push to main via amondnet/vercel-action
- Switch deploy job from raw Vercel CLI to amondnet/vercel-action@v25 - Only deploys on push to main (not on PRs) - build job still runs on all PRs for type-check & build validation - Fix env.ts: use literal process.env.NEXT_PUBLIC_* keys (not dynamic bracket access) - Fix contact page social handles to derive from siteConfig URLs
1 parent a8e6ea0 commit 4aab2e3

File tree

3 files changed

+70
-99
lines changed

3 files changed

+70
-99
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI
1+
name: CI / Deploy
22

33
on:
44
push:
@@ -11,32 +11,27 @@ concurrency:
1111
cancel-in-progress: true
1212

1313
jobs:
14+
# ── 1. Type-check & Build ─────────────────────────────────────────────────
1415
build:
1516
name: Type-check & Build
1617
runs-on: ubuntu-latest
1718

18-
strategy:
19-
matrix:
20-
node-version: [20.x]
21-
2219
steps:
2320
- name: Checkout repository
2421
uses: actions/checkout@v4
2522

26-
- name: Set up Node.js ${{ matrix.node-version }}
23+
- name: Set up Node.js
2724
uses: actions/setup-node@v4
2825
with:
29-
node-version: ${{ matrix.node-version }}
26+
node-version: "20"
3027
cache: "npm"
3128

3229
- name: Install dependencies
3330
run: npm ci
3431

35-
# Fail fast on TypeScript errors before spending time on a full build
3632
- name: Type-check
3733
run: npx tsc --noEmit
3834

39-
# Build requires env vars — inject dummy values so Next.js doesn't crash
4035
- name: Build
4136
run: npm run build
4237
env:
@@ -45,15 +40,36 @@ jobs:
4540
NEXT_PUBLIC_SITE_TAGLINE: "Freelance DevOps & Cloud Consulting"
4641
NEXT_PUBLIC_SITE_DESCRIPTION: "Production-grade cloud infrastructure, Kubernetes, and CI/CD — built by a senior DevOps engineer."
4742
NEXT_PUBLIC_SITE_OG_IMAGE: /og-image.png
48-
NEXT_PUBLIC_CONTACT_EMAIL: contact@cloudforgeops.com
49-
NEXT_PUBLIC_SOCIAL_GITHUB: https://github.com/cloudforgeops
50-
NEXT_PUBLIC_SOCIAL_LINKEDIN: https://linkedin.com/in/deepaksagar
43+
NEXT_PUBLIC_CONTACT_EMAIL: sagardeepak2002@gmail.com
44+
NEXT_PUBLIC_SOCIAL_GITHUB: https://github.com/sagarDeepakDevOps
45+
NEXT_PUBLIC_SOCIAL_LINKEDIN: https://linkedin.com/in/sagardeepak2002
5146
NEXT_PUBLIC_OWNER_NAME: Deepak Sagar
5247
NEXT_PUBLIC_OWNER_TITLE: "Freelance DevOps & Cloud Engineer"
5348
NEXT_PUBLIC_OWNER_EXPERIENCE: "3+"
5449
NEXT_PUBLIC_OWNER_EXPERIENCE_LABEL: "years of production DevOps experience"
5550
NEXT_PUBLIC_OWNER_BIO: "Senior DevOps engineer specialising in AWS, Kubernetes, Terraform, and CI/CD automation."
56-
NEXT_PUBLIC_OWNER_LINKEDIN: https://linkedin.com/in/deepaksagar
51+
NEXT_PUBLIC_OWNER_LINKEDIN: https://linkedin.com/in/sagardeepak2002
5752
NEXT_PUBLIC_FREE_REVIEW_HREF: /free-review
5853
NEXT_PUBLIC_FREE_REVIEW_DURATION: "30 min"
59-
NEXT_PUBLIC_CALENDLY_URL: https://calendly.com/deepaksagar/30min
54+
NEXT_PUBLIC_CALENDLY_URL: https://calendly.com/sagardeepak2002/30min
55+
56+
# ── 2. Deploy to Vercel (production — main branch only) ───────────────────
57+
deploy:
58+
name: Deploy to Vercel (Production)
59+
needs: build
60+
runs-on: ubuntu-latest
61+
# Only deploy on push to main, not on pull requests
62+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
63+
64+
steps:
65+
- name: Checkout repository
66+
uses: actions/checkout@v4
67+
68+
- name: Deploy to Vercel
69+
uses: amondnet/vercel-action@v25
70+
with:
71+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
72+
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
73+
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
74+
vercel-args: "--prod"
75+
working-directory: ./

src/app/contact/page.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,31 @@ const fadeUp = (delay = 0) => ({
4444
transition: { duration: 0.45, ease: "easeOut" as const, delay },
4545
});
4646

47+
function getHandleFromUrl(url: string | undefined, provider: "github" | "linkedin") {
48+
if (!url) return "";
49+
try {
50+
const u = new URL(url);
51+
const parts = u.pathname.split("/").filter(Boolean);
52+
const last = parts.length ? parts[parts.length - 1] : u.hostname.replace(/^www\./, "");
53+
if (provider === "github") return `@${last}`;
54+
return last;
55+
} catch (e) {
56+
return url;
57+
}
58+
}
59+
4760
const social = [
4861
{
4962
label: "GitHub",
5063
href: siteConfig.social.github,
5164
icon: Github,
52-
handle: "@cloudforgeops",
65+
handle: getHandleFromUrl(siteConfig.social.github, "github"),
5366
},
5467
{
5568
label: "LinkedIn",
5669
href: siteConfig.social.linkedin,
5770
icon: Linkedin,
58-
handle: "cloudforgeops",
71+
handle: getHandleFromUrl(siteConfig.social.linkedin, "linkedin"),
5972
},
6073
];
6174

src/lib/env.ts

Lines changed: 25 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,46 @@
11
/**
22
* Centralized env variable reader.
33
*
4-
* All keys are prefixed with NEXT_PUBLIC_ so they are available
5-
* on both the server (RSC, metadata, API routes) and the client
6-
* (Client Components, hooks).
4+
* IMPORTANT: Next.js statically replaces process.env.NEXT_PUBLIC_* at bundle
5+
* time using literal key strings. Using process.env[variable] with a dynamic
6+
* key does NOT work in client bundles — the bundler cannot resolve it and
7+
* returns undefined, causing all client-side reads to fall back to defaults.
78
*
8-
* Every value has a typed string fallback so the build never
9-
* crashes when a variable is missing (e.g. in CI or local dev
10-
* without a .env.local file).
11-
*
12-
* To override any value: add it to .env.local (never commit that
13-
* file). See .env.example for the full key list.
9+
* Every key must be accessed as a literal: process.env.NEXT_PUBLIC_SITE_NAME
10+
* Never use bracket notation with a variable for NEXT_PUBLIC_ keys.
1411
*/
1512

16-
function read(key: string, fallback: string): string {
17-
// process.env is statically replaced by the Next.js bundler for
18-
// NEXT_PUBLIC_ prefixed keys — do NOT destructure or alias the
19-
// object; always use the full literal key string.
20-
const value = process.env[key];
21-
return value !== undefined && value.trim() !== "" ? value.trim() : fallback;
13+
function fallback(value: string | undefined, def: string): string {
14+
return value !== undefined && value.trim() !== "" ? value.trim() : def;
2215
}
2316

2417
export const env = {
2518
// ── Brand identity ────────────────────────────────────────────────────────
26-
SITE_NAME: read(
27-
"NEXT_PUBLIC_SITE_NAME",
28-
"CloudForgeOps",
29-
),
30-
SITE_TAGLINE: read(
31-
"NEXT_PUBLIC_SITE_TAGLINE",
32-
"Freelance DevOps & Cloud Engineering",
33-
),
34-
SITE_DESCRIPTION: read(
35-
"NEXT_PUBLIC_SITE_DESCRIPTION",
36-
"I design, build, and operate cloud-native infrastructure. From Kubernetes to CI/CD pipelines, I help startups ship faster and stay reliable.",
37-
),
38-
SITE_URL: read(
39-
"NEXT_PUBLIC_SITE_URL",
40-
"https://cloudforgeops.com",
41-
),
42-
SITE_OG_IMAGE: read(
43-
"NEXT_PUBLIC_SITE_OG_IMAGE",
44-
"/images/og-default.png",
45-
),
19+
SITE_NAME: fallback(process.env.NEXT_PUBLIC_SITE_NAME, "CloudForgeOps"),
20+
SITE_TAGLINE: fallback(process.env.NEXT_PUBLIC_SITE_TAGLINE, "Freelance DevOps & Cloud Engineering"),
21+
SITE_DESCRIPTION: fallback(process.env.NEXT_PUBLIC_SITE_DESCRIPTION, "I design, build, and operate cloud-native infrastructure. From Kubernetes to CI/CD pipelines, I help startups ship faster and stay reliable."),
22+
SITE_URL: fallback(process.env.NEXT_PUBLIC_SITE_URL, "https://cloudforgeops.com"),
23+
SITE_OG_IMAGE: fallback(process.env.NEXT_PUBLIC_SITE_OG_IMAGE, "/images/og-default.png"),
4624

4725
// ── Contact ───────────────────────────────────────────────────────────────
48-
CONTACT_EMAIL: read(
49-
"NEXT_PUBLIC_CONTACT_EMAIL",
50-
"sagardeepak2002@gmail.com",
51-
),
26+
CONTACT_EMAIL: fallback(process.env.NEXT_PUBLIC_CONTACT_EMAIL, "sagardeepak2002@gmail.com"),
5227

5328
// ── Social links ─────────────────────────────────────────────────────────
54-
SOCIAL_GITHUB: read(
55-
"NEXT_PUBLIC_SOCIAL_GITHUB",
56-
"https://github.com/cloudforgeops",
57-
),
58-
SOCIAL_LINKEDIN: read(
59-
"NEXT_PUBLIC_SOCIAL_LINKEDIN",
60-
"https://linkedin.com/company/cloudforgeops",
61-
),
29+
SOCIAL_GITHUB: fallback(process.env.NEXT_PUBLIC_SOCIAL_GITHUB, "https://github.com/sagarDeepakDevOps"),
30+
SOCIAL_LINKEDIN: fallback(process.env.NEXT_PUBLIC_SOCIAL_LINKEDIN, "https://linkedin.com/in/sagardeepak2002"),
6231

6332
// ── Owner / personal identity ─────────────────────────────────────────────
64-
OWNER_NAME: read(
65-
"NEXT_PUBLIC_OWNER_NAME",
66-
"Deepak Sagar",
67-
),
68-
OWNER_TITLE: read(
69-
"NEXT_PUBLIC_OWNER_TITLE",
70-
"Freelance DevOps & Cloud Engineer",
71-
),
72-
OWNER_EXPERIENCE: read(
73-
"NEXT_PUBLIC_OWNER_EXPERIENCE",
74-
"3+",
75-
),
76-
OWNER_EXPERIENCE_LABEL: read(
77-
"NEXT_PUBLIC_OWNER_EXPERIENCE_LABEL",
78-
"Hands-On Cloud & Automation Experience",
79-
),
80-
OWNER_BIO: read(
81-
"NEXT_PUBLIC_OWNER_BIO",
82-
"I work directly with founders and engineering teams to design, automate, and stabilize cloud environments across AWS and Azure.",
83-
),
84-
OWNER_LINKEDIN: read(
85-
"NEXT_PUBLIC_OWNER_LINKEDIN",
86-
"https://linkedin.com/in/sagardeepak2002",
87-
),
33+
OWNER_NAME: fallback(process.env.NEXT_PUBLIC_OWNER_NAME, "Deepak Sagar"),
34+
OWNER_TITLE: fallback(process.env.NEXT_PUBLIC_OWNER_TITLE, "Freelance DevOps & Cloud Engineer"),
35+
OWNER_EXPERIENCE: fallback(process.env.NEXT_PUBLIC_OWNER_EXPERIENCE, "3+"),
36+
OWNER_EXPERIENCE_LABEL: fallback(process.env.NEXT_PUBLIC_OWNER_EXPERIENCE_LABEL, "Hands-On Cloud & Automation Experience"),
37+
OWNER_BIO: fallback(process.env.NEXT_PUBLIC_OWNER_BIO, "I work directly with founders and engineering teams to design, automate, and stabilize cloud environments across AWS and Azure."),
38+
OWNER_LINKEDIN: fallback(process.env.NEXT_PUBLIC_OWNER_LINKEDIN, "https://linkedin.com/in/sagardeepak2002"),
8839

8940
// ── Free Review / Calendly ────────────────────────────────────────────────
90-
FREE_REVIEW_HREF: read(
91-
"NEXT_PUBLIC_FREE_REVIEW_HREF",
92-
"/free-review",
93-
),
94-
FREE_REVIEW_DURATION: read(
95-
"NEXT_PUBLIC_FREE_REVIEW_DURATION",
96-
"30 min",
97-
),
98-
CALENDLY_URL: read(
99-
"NEXT_PUBLIC_CALENDLY_URL",
100-
"https://calendly.com/sagardeepak2002/30min",
101-
),
41+
FREE_REVIEW_HREF: fallback(process.env.NEXT_PUBLIC_FREE_REVIEW_HREF, "/free-review"),
42+
FREE_REVIEW_DURATION: fallback(process.env.NEXT_PUBLIC_FREE_REVIEW_DURATION, "30 min"),
43+
CALENDLY_URL: fallback(process.env.NEXT_PUBLIC_CALENDLY_URL, "https://calendly.com/sagardeepak2002/30min"),
10244
} as const;
10345

10446
export type Env = typeof env;

0 commit comments

Comments
 (0)