Skip to content

Commit cfd752f

Browse files
committed
fix: deploy
1 parent e1b02d0 commit cfd752f

File tree

7 files changed

+749
-4
lines changed

7 files changed

+749
-4
lines changed

.github/scripts/linkedin_sync.py

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#!/usr/bin/env python3
2+
"""
3+
LinkedIn Data Sync Script
4+
5+
This script fetches both LinkedIn profile data and recent posts
6+
and saves them to data files for use in the Jekyll site.
7+
8+
Requires the following environment variables:
9+
- LINKEDIN_CLIENT_ID
10+
- LINKEDIN_CLIENT_SECRET
11+
- LINKEDIN_ACCESS_TOKEN
12+
"""
13+
14+
import os
15+
import yaml
16+
import requests
17+
import json
18+
from datetime import datetime
19+
import sys
20+
21+
# LinkedIn API Configuration
22+
CLIENT_ID = os.getenv('LINKEDIN_CLIENT_ID')
23+
CLIENT_SECRET = os.getenv('LINKEDIN_CLIENT_SECRET')
24+
ACCESS_TOKEN = os.getenv('LINKEDIN_ACCESS_TOKEN')
25+
26+
if not all([CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN]):
27+
print("Warning: LinkedIn API credentials are missing. Using sample data instead.")
28+
USE_SAMPLE_DATA = True
29+
else:
30+
USE_SAMPLE_DATA = False
31+
32+
def fetch_linkedin_profile():
33+
"""Fetch LinkedIn profile data"""
34+
35+
if USE_SAMPLE_DATA:
36+
return generate_sample_profile()
37+
38+
# URL for LinkedIn Profile API
39+
url = "https://api.linkedin.com/v2/me"
40+
41+
# Headers for authentication
42+
headers = {
43+
"Authorization": f"Bearer {ACCESS_TOKEN}",
44+
"X-Restli-Protocol-Version": "2.0.0",
45+
"Content-Type": "application/json"
46+
}
47+
48+
# Fields to request
49+
params = {
50+
"projection": "(id,firstName,lastName,headline,profilePicture(displayImage~:playableStreams),vanityName)"
51+
}
52+
53+
try:
54+
response = requests.get(url, headers=headers, params=params)
55+
response.raise_for_status()
56+
57+
profile_data = response.json()
58+
59+
# Format the profile data
60+
formatted_profile = {
61+
"id": profile_data.get("id", ""),
62+
"firstName": profile_data.get("firstName", {}).get("localized", {}).get("en_US", ""),
63+
"lastName": profile_data.get("lastName", {}).get("localized", {}).get("en_US", ""),
64+
"headline": profile_data.get("headline", {}).get("localized", {}).get("en_US", ""),
65+
"vanityName": profile_data.get("vanityName", ""),
66+
"profileUrl": f"https://www.linkedin.com/in/{profile_data.get('vanityName', '')}"
67+
}
68+
69+
# Try to get profile picture if available
70+
try:
71+
picture_elements = profile_data.get("profilePicture", {}).get("displayImage~", {}).get("elements", [])
72+
if picture_elements:
73+
# Get the highest resolution image
74+
formatted_profile["profilePicture"] = picture_elements[-1].get("identifiers", [])[0].get("identifier", "")
75+
except Exception as e:
76+
print(f"Error extracting profile picture: {e}")
77+
78+
return formatted_profile
79+
80+
except Exception as e:
81+
print(f"Error fetching LinkedIn profile: {e}")
82+
return generate_sample_profile()
83+
84+
def fetch_linkedin_cv():
85+
"""Fetch LinkedIn CV data (positions, education, skills)"""
86+
87+
if USE_SAMPLE_DATA:
88+
return generate_sample_cv()
89+
90+
cv_data = {
91+
"positions": [],
92+
"education": [],
93+
"skills": []
94+
}
95+
96+
# Headers for authentication
97+
headers = {
98+
"Authorization": f"Bearer {ACCESS_TOKEN}",
99+
"X-Restli-Protocol-Version": "2.0.0",
100+
"Content-Type": "application/json"
101+
}
102+
103+
# 1. Fetch positions
104+
try:
105+
positions_url = "https://api.linkedin.com/v2/positions"
106+
params = {"q": "members", "members": "urn:li:person:{PERSON_ID}"} # Replace with your LinkedIn ID
107+
108+
response = requests.get(positions_url, headers=headers, params=params)
109+
response.raise_for_status()
110+
111+
positions_data = response.json()
112+
113+
for position in positions_data.get("elements", []):
114+
cv_data["positions"].append({
115+
"title": position.get("title", {}).get("localized", {}).get("en_US", ""),
116+
"company": position.get("name", {}).get("localized", {}).get("en_US", ""),
117+
"startDate": format_date(position.get("startDate", {})),
118+
"endDate": format_date(position.get("endDate", {})) if position.get("endDate") else "Present",
119+
"description": position.get("description", {}).get("localized", {}).get("en_US", "")
120+
})
121+
122+
except Exception as e:
123+
print(f"Error fetching positions: {e}")
124+
125+
# 2. Fetch education (similar approach)
126+
# 3. Fetch skills (similar approach)
127+
128+
# If any section is empty, use sample data
129+
if not cv_data["positions"]:
130+
return generate_sample_cv()
131+
132+
return cv_data
133+
134+
def format_date(date_obj):
135+
"""Format LinkedIn date object (year, month)"""
136+
if not date_obj:
137+
return ""
138+
139+
year = date_obj.get("year", "")
140+
month = date_obj.get("month", "")
141+
142+
if month and year:
143+
return f"{month}/{year}"
144+
elif year:
145+
return str(year)
146+
else:
147+
return ""
148+
149+
def fetch_linkedin_posts():
150+
"""Fetch recent posts from LinkedIn"""
151+
152+
if USE_SAMPLE_DATA:
153+
return generate_sample_posts()
154+
155+
# URL for LinkedIn Posts API (using v2 API)
156+
url = "https://api.linkedin.com/v2/ugcPosts"
157+
158+
# Headers for authentication
159+
headers = {
160+
"Authorization": f"Bearer {ACCESS_TOKEN}",
161+
"X-Restli-Protocol-Version": "2.0.0",
162+
"Content-Type": "application/json"
163+
}
164+
165+
# Parameters for the request
166+
params = {
167+
"q": "authors",
168+
"authors": "urn:li:person:{PERSON_ID}", # Replace with your LinkedIn person ID
169+
"count": 10 # Number of posts to retrieve
170+
}
171+
172+
try:
173+
response = requests.get(url, headers=headers, params=params)
174+
response.raise_for_status()
175+
176+
data = response.json()
177+
178+
# Process and format the posts
179+
posts = []
180+
for item in data.get("elements", []):
181+
# Example structure - adjust based on actual API response
182+
post = {
183+
"id": item.get("id", ""),
184+
"date": item.get("created", {}).get("time", ""),
185+
"title": extract_title(item),
186+
"excerpt": extract_excerpt(item),
187+
"url": f"https://www.linkedin.com/feed/update/{item.get('id', '')}"
188+
}
189+
posts.append(post)
190+
191+
return posts
192+
193+
except Exception as e:
194+
print(f"Error fetching LinkedIn posts: {e}")
195+
return generate_sample_posts()
196+
197+
def extract_title(post_data):
198+
"""Extract a title from the LinkedIn post data"""
199+
# LinkedIn posts don't have explicit titles, so we extract from content
200+
content = post_data.get("specificContent", {}).get("com.linkedin.ugc.ShareContent", {}).get("text", "")
201+
title = content.split('\n')[0][:60] # First line, truncated to 60 chars
202+
203+
if len(title) < len(content) and len(title) == 60:
204+
title += "..."
205+
206+
return title
207+
208+
def extract_excerpt(post_data):
209+
"""Extract an excerpt from the LinkedIn post data"""
210+
content = post_data.get("specificContent", {}).get("com.linkedin.ugc.ShareContent", {}).get("text", "")
211+
# Skip first line if we're using it as title
212+
if '\n' in content:
213+
content = '\n'.join(content.split('\n')[1:])
214+
215+
excerpt = content[:200] # First 200 chars
216+
217+
if len(excerpt) < len(content):
218+
excerpt += "..."
219+
220+
return excerpt
221+
222+
def generate_sample_profile():
223+
"""Generate sample LinkedIn profile data for testing"""
224+
return {
225+
"id": "sample-id",
226+
"firstName": "Awar",
227+
"lastName": "Abdulkarim",
228+
"headline": "Senior Cloud Engineer | Cloud Native Competency Lead",
229+
"vanityName": "notawar",
230+
"profileUrl": "https://www.linkedin.com/in/notawar",
231+
"profilePicture": "https://media.licdn.com/dms/image/sample-profile-pic.jpg"
232+
}
233+
234+
def generate_sample_cv():
235+
"""Generate sample LinkedIn CV data for testing"""
236+
return {
237+
"positions": [
238+
{
239+
"title": "Senior Cloud Engineer",
240+
"company": "Tech Company Inc.",
241+
"startDate": "01/2021",
242+
"endDate": "Present",
243+
"description": "Leading cloud-native initiatives and infrastructure modernization."
244+
},
245+
{
246+
"title": "DevOps Engineer",
247+
"company": "Digital Solutions Ltd",
248+
"startDate": "03/2018",
249+
"endDate": "12/2020",
250+
"description": "Implemented CI/CD pipelines and container orchestration."
251+
}
252+
],
253+
"education": [
254+
{
255+
"school": "University of Technology",
256+
"degree": "Master's Degree, Computer Science",
257+
"startDate": "2016",
258+
"endDate": "2018"
259+
},
260+
{
261+
"school": "Technical College",
262+
"degree": "Bachelor's Degree, Computer Engineering",
263+
"startDate": "2012",
264+
"endDate": "2016"
265+
}
266+
],
267+
"skills": [
268+
"Cloud Architecture",
269+
"Kubernetes",
270+
"Docker",
271+
"CI/CD",
272+
"Infrastructure as Code",
273+
"AWS",
274+
"Azure",
275+
"Python",
276+
"DevOps"
277+
]
278+
}
279+
280+
def generate_sample_posts():
281+
"""Generate sample LinkedIn post data for testing"""
282+
return [
283+
{
284+
"id": "sample1",
285+
"date": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
286+
"title": "Cloud Native Architecture Patterns",
287+
"excerpt": "Recently I've been exploring interesting patterns in cloud-native architectures. Here are 3 key takeaways that can help improve scalability and resilience...",
288+
"url": "https://www.linkedin.com/in/notawar/"
289+
},
290+
{
291+
"id": "sample2",
292+
"date": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
293+
"title": "Kubernetes Tips and Tricks",
294+
"excerpt": "After working with Kubernetes for several years, I've compiled these best practices that can save your team significant debugging time...",
295+
"url": "https://www.linkedin.com/in/notawar/"
296+
},
297+
{
298+
"id": "sample3",
299+
"date": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
300+
"title": "DevOps Culture: Beyond the Tools",
301+
"excerpt": "Tools are important, but the real power of DevOps comes from cultural transformation. Here's how we implemented DevOps principles across our organization...",
302+
"url": "https://www.linkedin.com/in/notawar/"
303+
}
304+
]
305+
306+
def save_to_yml(data, output_path):
307+
"""Save data to YAML file"""
308+
# Ensure directory exists
309+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
310+
311+
# Write data to YAML file
312+
with open(output_path, 'w') as file:
313+
yaml.dump(data, file, default_flow_style=False)
314+
315+
print(f"Successfully saved data to {output_path}")
316+
317+
if __name__ == "__main__":
318+
# Define output paths
319+
profile_file = "_data/linkedin_profile.yml"
320+
cv_file = "_data/linkedin_cv.yml"
321+
posts_file = "_data/linkedin_posts.yml"
322+
323+
# Fetch and save LinkedIn profile data
324+
profile_data = fetch_linkedin_profile()
325+
save_to_yml(profile_data, profile_file)
326+
327+
# Fetch and save LinkedIn CV data
328+
cv_data = fetch_linkedin_cv()
329+
save_to_yml(cv_data, cv_file)
330+
331+
# Fetch and save LinkedIn posts
332+
posts_data = fetch_linkedin_posts()
333+
save_to_yml(posts_data, posts_file)

.github/workflows/jekyll.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
branches: [ main, master ]
66
workflow_dispatch:
7+
schedule:
8+
- cron: '0 8 * * *' # Run daily at 8 AM UTC to fetch new LinkedIn posts
79

810
permissions:
911
contents: write
@@ -17,6 +19,24 @@ jobs:
1719
- name: Checkout
1820
uses: actions/checkout@v4
1921

22+
- name: Setup Python
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: '3.10'
26+
27+
- name: Install Python dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
pip install requests pyyaml
31+
32+
- name: Fetch LinkedIn posts
33+
run: python .github/scripts/linkedin_sync.py
34+
env:
35+
LINKEDIN_CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }}
36+
LINKEDIN_CLIENT_SECRET: ${{ secrets.LINKEDIN_CLIENT_SECRET }}
37+
LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }}
38+
continue-on-error: true # Don't fail the build if LinkedIn fetch fails
39+
2040
- name: Setup Ruby
2141
uses: ruby/[email protected]
2242
with:

_data/navigation.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,11 @@
22
link: /
33
- name: CV
44
link: /cv/
5+
- name: LinkedIn CV
6+
link: /linkedin-cv/
7+
- name: Blog
8+
link: /blog/
9+
- name: About
10+
link: /about/
511
- name: Contact
612
link: /contact/

0 commit comments

Comments
 (0)