Skip to content

Commit 2c119e0

Browse files
committed
use PAT owner email
1 parent 88218ae commit 2c119e0

File tree

1 file changed

+97
-13
lines changed

1 file changed

+97
-13
lines changed

src/app/src/utils/providers/github.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,79 @@ import type { GitOptions, GitProviderAPI, GitFile, RawFile, CommitResult, Commit
44
import { StudioFeature } from '../../types'
55
import { DraftStatus } from '../../types/draft'
66

7+
interface GitHubUser {
8+
login: string
9+
email: string | null
10+
name: string | null
11+
}
12+
13+
const NUXT_STUDIO_COAUTHOR = 'Co-authored-by: Nuxt Studio <[email protected]>'
14+
715
export function createGitHubProvider(options: GitOptions): GitProviderAPI {
816
const { owner, repo, token, branch, rootDir, authorName, authorEmail } = options
917
const gitFiles: Record<string, GitFile> = {}
1018

1119
// Support both token formats: "token {token}" for fine grained PATs, "Bearer {token}" for OAuth PATs
12-
const authHeader = token.startsWith('github_pat_') ? `token ${token}` : `Bearer ${token}`
20+
const isPAT = token.startsWith('github_pat_')
21+
const authHeader = isPAT ? `token ${token}` : `Bearer ${token}`
1322

14-
const $api = ofetch.create({
23+
const $repositoryApi = ofetch.create({
1524
baseURL: `https://api.github.com/repos/${owner}/${repo}`,
1625
headers: {
1726
Authorization: authHeader,
1827
Accept: 'application/vnd.github.v3+json',
1928
},
2029
})
2130

31+
const $userApi = ofetch.create({
32+
baseURL: 'https://api.github.com',
33+
headers: {
34+
Authorization: authHeader,
35+
Accept: 'application/vnd.github.v3+json',
36+
},
37+
})
38+
39+
// Cache for authenticated user info (PAT owner)
40+
let cachedPATUser: GitHubUser | null = null
41+
42+
/**
43+
* Fetch the authenticated user associated with the current token
44+
* Used for PAT tokens to get the token owner's info
45+
*/
46+
async function fetchAuthenticatedUser(): Promise<GitHubUser | null> {
47+
if (cachedPATUser) {
48+
return cachedPATUser
49+
}
50+
51+
try {
52+
const user = await $userApi('/user')
53+
54+
// If email is not public, try to fetch from emails endpoint
55+
let email = user.email
56+
if (!email) {
57+
try {
58+
const emails = await $userApi('/user/emails')
59+
const primaryEmail = emails.find((e: { primary: boolean, verified: boolean }) => e.primary && e.verified)
60+
email = primaryEmail?.email || emails.find((e: { verified: boolean }) => e.verified)?.email || null
61+
}
62+
catch {
63+
return null
64+
}
65+
}
66+
67+
cachedPATUser = {
68+
login: user.login,
69+
email,
70+
name: user.name || user.login,
71+
}
72+
73+
return cachedPATUser
74+
}
75+
catch {
76+
return null
77+
}
78+
}
79+
2280
async function fetchFile(path: string, { cached = false }: { cached?: boolean } = {}): Promise<GitFile | null> {
2381
path = joinURL(rootDir, path)
2482
if (cached) {
@@ -29,7 +87,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
2987
}
3088

3189
try {
32-
const ghResponse = await $api(`/contents/${path}?ref=${branch}`)
90+
const ghResponse = await $repositoryApi(`/contents/${path}?ref=${branch}`)
3391
const ghFile: GitFile = {
3492
...ghResponse,
3593
provider: 'github' as const,
@@ -58,7 +116,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
58116
}
59117
}
60118

61-
function commitFiles(files: RawFile[], message: string): Promise<CommitResult | null> {
119+
async function commitFiles(files: RawFile[], message: string): Promise<CommitResult | null> {
62120
if (!token) {
63121
return Promise.resolve(null)
64122
}
@@ -67,24 +125,50 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
67125
.filter(file => file.status !== DraftStatus.Pristine)
68126
.map(file => ({ ...file, path: joinURL(rootDir, file.path) }))
69127

128+
const coAuthors: string[] = [NUXT_STUDIO_COAUTHOR]
129+
130+
let commitAuthorName = authorName
131+
let commitAuthorEmail = authorEmail
132+
133+
// For PAT tokens, use the PAT owner's info for the commit author
134+
// This ensures the commit email is associated with a GitHub account
135+
if (isPAT) {
136+
const patUser = await fetchAuthenticatedUser()
137+
if (patUser?.email) {
138+
// Add the original user (who performed the action) as co-author if different from PAT owner
139+
if (authorEmail && authorEmail !== patUser.email) {
140+
coAuthors.push(`Co-authored-by: ${authorName} <${authorEmail}>`)
141+
}
142+
143+
// Use PAT owner as the commit author
144+
commitAuthorName = patUser.name || patUser.login
145+
commitAuthorEmail = patUser.email
146+
}
147+
}
148+
149+
// Build commit message with co-authors
150+
const fullMessage = coAuthors.length > 0
151+
? `${message}\n\n${coAuthors.join('\n')}`
152+
: message
153+
70154
return commitFilesToGitHub({
71155
owner,
72156
repo,
73157
branch,
74158
files,
75-
message,
76-
authorName,
77-
authorEmail,
159+
message: fullMessage,
160+
authorName: commitAuthorName,
161+
authorEmail: commitAuthorEmail,
78162
})
79163
}
80164

81165
async function commitFilesToGitHub({ owner, repo, branch, files, message, authorName, authorEmail }: CommitFilesOptions) {
82166
// Get latest commit SHA
83-
const refData = await $api(`/git/refs/heads/${branch}`)
167+
const refData = await $repositoryApi(`/git/refs/heads/${branch}`)
84168
const latestCommitSha = refData.object.sha
85169

86170
// Get base tree SHA
87-
const commitData = await $api(`/git/commits/${latestCommitSha}`)
171+
const commitData = await $repositoryApi(`/git/commits/${latestCommitSha}`)
88172
const baseTreeSha = commitData.tree.sha
89173

90174
// Create blobs and prepare tree
@@ -101,7 +185,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
101185
}
102186
else {
103187
// For new/modified files, create blob and use its sha
104-
const blobData = await $api(`/git/blobs`, {
188+
const blobData = await $repositoryApi(`/git/blobs`, {
105189
method: 'POST',
106190
body: JSON.stringify({
107191
content: file.content,
@@ -118,7 +202,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
118202
}
119203

120204
// Create new tree
121-
const treeData = await $api(`/git/trees`, {
205+
const treeData = await $repositoryApi(`/git/trees`, {
122206
method: 'POST',
123207
body: JSON.stringify({
124208
base_tree: baseTreeSha,
@@ -127,7 +211,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
127211
})
128212

129213
// Create new commit
130-
const newCommit = await $api(`/git/commits`, {
214+
const newCommit = await $repositoryApi(`/git/commits`, {
131215
method: 'POST',
132216
body: JSON.stringify({
133217
message,
@@ -142,7 +226,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
142226
})
143227

144228
// Update branch ref
145-
await $api(`/git/refs/heads/${branch}`, {
229+
await $repositoryApi(`/git/refs/heads/${branch}`, {
146230
method: 'PATCH',
147231
body: JSON.stringify({ sha: newCommit.sha }),
148232
})

0 commit comments

Comments
 (0)