Skip to content

Commit 86661f2

Browse files
authored
feat(linkedin): add reusable workflow for posting to LinkedIn (#5)
* feat(linkedin): add reusable workflow for posting to LinkedIn Adds linkedin-post.yml reusable workflow that: - Handles OAuth token refresh when expired - Posts text or article shares to LinkedIn - Uses LinkedIn's REST Posts API (v202401) Requires secrets: LINKEDIN_ACCESS_TOKEN, LINKEDIN_REFRESH_TOKEN, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET * refactor(linkedin): simplify to use access token only (no refresh)
1 parent 7a7461e commit 86661f2

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Post to LinkedIn
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
post_text:
7+
description: 'The text content of the post'
8+
required: true
9+
type: string
10+
article_url:
11+
description: 'Optional URL to share as an article'
12+
required: false
13+
type: string
14+
article_title:
15+
description: 'Title for the article (used with article_url)'
16+
required: false
17+
type: string
18+
article_description:
19+
description: 'Description for the article (used with article_url)'
20+
required: false
21+
type: string
22+
secrets:
23+
LINKEDIN_ACCESS_TOKEN:
24+
required: true
25+
LINKEDIN_CLIENT_ID:
26+
required: true
27+
28+
jobs:
29+
post:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Post to LinkedIn
33+
env:
34+
ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }}
35+
CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }}
36+
POST_TEXT: ${{ inputs.post_text }}
37+
ARTICLE_URL: ${{ inputs.article_url }}
38+
ARTICLE_TITLE: ${{ inputs.article_title }}
39+
ARTICLE_DESC: ${{ inputs.article_description }}
40+
run: |
41+
# Verify token and get user info
42+
echo "Verifying LinkedIn access token..."
43+
USER_RESPONSE=$(curl -s -w "\n%{http_code}" \
44+
-H "Authorization: Bearer $ACCESS_TOKEN" \
45+
"https://api.linkedin.com/v2/userinfo")
46+
47+
HTTP_CODE=$(echo "$USER_RESPONSE" | tail -1)
48+
USER_DATA=$(echo "$USER_RESPONSE" | sed '$d')
49+
50+
if [ "$HTTP_CODE" == "401" ]; then
51+
echo "::error::LinkedIn access token has expired (tokens last 60 days)."
52+
echo ""
53+
echo "To re-authorize, visit:"
54+
echo "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=$CLIENT_ID&redirect_uri=https://localhost&scope=openid%20profile%20w_member_social"
55+
echo ""
56+
echo "Then exchange the code for a new token and update the LINKEDIN_ACCESS_TOKEN secret."
57+
exit 1
58+
fi
59+
60+
# Extract person URN from userinfo
61+
PERSON_ID=$(echo "$USER_DATA" | jq -r '.sub // empty')
62+
63+
if [ -z "$PERSON_ID" ]; then
64+
echo "Error: Could not get LinkedIn user ID"
65+
echo "Response: $USER_DATA"
66+
exit 1
67+
fi
68+
69+
PERSON_URN="urn:li:person:$PERSON_ID"
70+
echo "Authenticated as: $PERSON_URN"
71+
72+
# Build the post payload
73+
echo "Creating LinkedIn post..."
74+
75+
if [ -n "$ARTICLE_URL" ]; then
76+
# Post with article share
77+
POST_PAYLOAD=$(jq -n \
78+
--arg author "$PERSON_URN" \
79+
--arg text "$POST_TEXT" \
80+
--arg url "$ARTICLE_URL" \
81+
--arg title "$ARTICLE_TITLE" \
82+
--arg desc "$ARTICLE_DESC" \
83+
'{
84+
"author": $author,
85+
"commentary": $text,
86+
"visibility": "PUBLIC",
87+
"distribution": {
88+
"feedDistribution": "MAIN_FEED",
89+
"targetEntities": [],
90+
"thirdPartyDistributionChannels": []
91+
},
92+
"content": {
93+
"article": {
94+
"source": $url,
95+
"title": $title,
96+
"description": $desc
97+
}
98+
},
99+
"lifecycleState": "PUBLISHED",
100+
"isReshareDisabledByAuthor": false
101+
}')
102+
else
103+
# Text-only post
104+
POST_PAYLOAD=$(jq -n \
105+
--arg author "$PERSON_URN" \
106+
--arg text "$POST_TEXT" \
107+
'{
108+
"author": $author,
109+
"commentary": $text,
110+
"visibility": "PUBLIC",
111+
"distribution": {
112+
"feedDistribution": "MAIN_FEED",
113+
"targetEntities": [],
114+
"thirdPartyDistributionChannels": []
115+
},
116+
"lifecycleState": "PUBLISHED",
117+
"isReshareDisabledByAuthor": false
118+
}')
119+
fi
120+
121+
# Create the post using LinkedIn's Posts API
122+
POST_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
123+
"https://api.linkedin.com/rest/posts" \
124+
-H "Authorization: Bearer $ACCESS_TOKEN" \
125+
-H "Content-Type: application/json" \
126+
-H "LinkedIn-Version: 202401" \
127+
-H "X-Restli-Protocol-Version: 2.0.0" \
128+
-d "$POST_PAYLOAD")
129+
130+
HTTP_CODE=$(echo "$POST_RESPONSE" | tail -1)
131+
RESPONSE_BODY=$(echo "$POST_RESPONSE" | sed '$d')
132+
133+
if [ "$HTTP_CODE" == "201" ] || [ "$HTTP_CODE" == "200" ]; then
134+
POST_ID=$(echo "$RESPONSE_BODY" | jq -r '.id // empty')
135+
if [ -n "$POST_ID" ]; then
136+
echo "Posted to LinkedIn successfully!"
137+
echo "Post ID: $POST_ID"
138+
else
139+
echo "Posted to LinkedIn successfully!"
140+
fi
141+
else
142+
echo "Error: Failed to create LinkedIn post"
143+
echo "HTTP Code: $HTTP_CODE"
144+
echo "Response: $RESPONSE_BODY"
145+
exit 1
146+
fi
147+
shell: bash

0 commit comments

Comments
 (0)