Skip to content

Commit bf2b3ba

Browse files
committed
feat: add GitHub Direct Commit support
- Add github_commit authentication type for direct file commits - Implement GithubRepository module for GitHub API integration - Add UI components for branch, filepath, and commit message configuration - Update README documentation with github_commit auth option - Supports automatic branch creation if not exists - Handles both file creation and updates via GitHub API
1 parent 8ea92fb commit bf2b3ba

File tree

5 files changed

+331
-8
lines changed

5 files changed

+331
-8
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
> [!WARNING]
2-
> **This plugin is only partially supported!**
2+
> **This plugin is only partially supported!**
33
> This means: I don't have capacity to add any features or really improve stuff. Bugfixes may be added if time allows.
44
> I am also happy to discuss and merge PRs.
5-
>
6-
> I personally have changed my opinion a long time ago to favor the source fo truth for design tokens to be in a json file. Figma should be a consumer so you only import tokens into figma. This is why I don't use plugins like this one anymore in my work.
7-
>
5+
>
6+
> I personally have changed my opinion a long time ago to favor the source fo truth for design tokens to be in a json file. Figma should be a consumer so you only import tokens into figma. This is why I don't use plugins like this one anymore in my work.
7+
>
88
> [Read more about the a better setup](https://medium.com/user-experience-design-1/the-ultimate-design-token-setup-cdf50dc841c8#:~:text=you%20are%20making.-,Source%20of%20truth,-There%20is%20some)
99
1010

@@ -37,7 +37,7 @@ The **Design Tokens** plugin for Figma allows you to export design tokens into a
3737
- [Opacity token](#opacity)
3838
- [Available properties](#available-properties)
3939
- [Settings](#settings)
40-
- [File Export Settings](#file-export-settings)
40+
- [File Export Settings](#file-export-settings)
4141
- [Push tokens to Github / Server](#push-to-server)
4242
- [Contribution](#contribution)
4343

@@ -353,7 +353,7 @@ You can define any additional prefix via this option, e.g `*`. This can be helpf
353353

354354
### Reference mode in variables
355355

356-
You can configure whether to include these mode names in the output JSON or not.
356+
You can configure whether to include these mode names in the output JSON or not.
357357
By default, the mode names are not included in both the token names and the token values. You can turn on this behavior in the plug-in settings:
358358

359359
- If you wish to include the mode name in the token names, but not in the token values, you can activate the "Add mode to token value".
@@ -449,7 +449,7 @@ The body of the request will look like the following:
449449

450450
```ts
451451
"event_type": "update-tokens", // or any string you defined
452-
"client_payload": {
452+
"client_payload": {
453453
"tokens": "{...}", // the stringified json object holding all your design tokens
454454
"filename": "Design Tokens", // the Figma file name from which the tokens were exported
455455
"commitMessage": "Your commit message"
@@ -496,7 +496,8 @@ https://api.github.com/repos/lukasoppermann/design-token-transformer/dispatches
496496

497497
This defines the authentication method used with the access token. The current choices are:
498498

499-
- `token` (used for github)
499+
- `token` (used for github workflow triggers)
500+
- `github_commit` (used for direct file commits to GitHub)
500501
- `gitlab_token` (used for Gitlab requests)
501502
- `bearer` token
502503
- `basic` auth

src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default {
2626
token: 'token',
2727
gitlabToken: 'gitlab_token',
2828
gitlabCommit: 'gitlab_commit',
29+
githubCommit: 'github_commit',
2930
basic: 'Basic',
3031
bearer: 'Bearer'
3132
}

src/ui/components/UrlExportSettings.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ export const UrlExportSettings = () => {
209209
label: '(Github) token',
210210
value: config.key.authType.token
211211
},
212+
{
213+
label: '(Github) Direct Commit',
214+
value: config.key.authType.githubCommit
215+
},
212216
{
213217
label: '(Gitlab) token',
214218
value: config.key.authType.gitlabToken
@@ -270,6 +274,32 @@ export const UrlExportSettings = () => {
270274
</>
271275
)}
272276

277+
{config.key.authType.githubCommit === settings.authType && (
278+
<>
279+
<h3>
280+
Branch
281+
<Info
282+
width={150}
283+
label='The branch where the file will be committed. Only used when Github Direct Commit is selected for "Auth type"'
284+
/>
285+
</h3>
286+
<Row fill>
287+
<Input
288+
type="text"
289+
required
290+
pattern="\S+"
291+
placeholder="main"
292+
value={settings.reference}
293+
onChange={(value) =>
294+
updateSettings((draft) => {
295+
draft.reference = value
296+
})
297+
}
298+
/>
299+
</Row>
300+
</>
301+
)}
302+
273303
{config.key.authType.gitlabCommit === settings.authType && (
274304
<>
275305
<h3>

src/ui/modules/githubRepository.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { utf8ToBase64 } from '@utils/base64'
2+
import {
3+
urlExportRequestBody,
4+
urlExportSettings
5+
} from '@typings/urlExportData'
6+
7+
export class GithubRepository {
8+
owner: string
9+
repo: string
10+
token: string
11+
12+
constructor(props: { owner: string; repo: string; token: string }) {
13+
this.owner = props.owner
14+
this.repo = props.repo
15+
this.token = props.token
16+
}
17+
18+
async upload(
19+
{ client_payload: clientPayload }: urlExportRequestBody,
20+
{ reference: branch }: urlExportSettings,
21+
responseHandler: {
22+
onError: () => void;
23+
onLoaded: (request: XMLHttpRequest) => void;
24+
}
25+
) {
26+
const encodedContent = utf8ToBase64(clientPayload.tokens)
27+
const filepath = clientPayload.filename
28+
29+
try {
30+
// Use reference field (branch name) from settings, consistent with GitLab implementation
31+
const targetBranch = branch
32+
33+
// Check if branch exists
34+
const branchExists = await this._checkBranchExists(targetBranch)
35+
36+
if (!branchExists) {
37+
// Branch doesn't exist - create it from default branch
38+
const defaultBranch = await this._getDefaultBranch()
39+
const defaultBranchSHA = await this._getBranchSHA(defaultBranch)
40+
await this._createBranch(targetBranch, defaultBranchSHA)
41+
}
42+
43+
// Check if file exists to get its SHA (required for updates)
44+
const fileSHA = await this._getFileSHA(filepath, targetBranch)
45+
46+
// Upload the file
47+
const uploadRequest = new XMLHttpRequest()
48+
uploadRequest.onerror = (_err) => responseHandler.onError()
49+
uploadRequest.onload = (event) => responseHandler.onLoaded(event.target as XMLHttpRequest)
50+
51+
this._uploadFile({
52+
request: uploadRequest,
53+
content: encodedContent,
54+
commitMessage: clientPayload.commitMessage || `Update design tokens at ${Date.now()}`,
55+
filepath: filepath,
56+
branch: targetBranch,
57+
fileSHA: fileSHA
58+
})
59+
} catch (error) {
60+
if (error && error.request && error.code === 401) {
61+
responseHandler.onLoaded(error.request)
62+
} else {
63+
responseHandler.onError()
64+
}
65+
}
66+
}
67+
68+
private _getDefaultBranch(): Promise<string> {
69+
return new Promise<string>((resolve, reject) => {
70+
const request = new XMLHttpRequest()
71+
request.open(
72+
'GET',
73+
`https://api.github.com/repos/${this.owner}/${this.repo}`
74+
)
75+
this._setRequestHeader(request)
76+
77+
request.onreadystatechange = (_ev: ProgressEvent) => {
78+
if (request.readyState !== XMLHttpRequest.DONE) {
79+
return
80+
}
81+
82+
const statusCode = request.status
83+
if (statusCode === 200) {
84+
const response = JSON.parse(request.responseText)
85+
resolve(response.default_branch || 'main')
86+
return
87+
}
88+
89+
reject({
90+
code: statusCode,
91+
message: request.response,
92+
request: request
93+
})
94+
}
95+
96+
request.send()
97+
})
98+
}
99+
100+
private _checkBranchExists(branch: string): Promise<boolean> {
101+
return new Promise<boolean>((resolve, reject) => {
102+
const request = new XMLHttpRequest()
103+
request.open(
104+
'GET',
105+
`https://api.github.com/repos/${this.owner}/${this.repo}/git/ref/heads/${branch}`
106+
)
107+
this._setRequestHeader(request)
108+
109+
request.onreadystatechange = (_ev: ProgressEvent) => {
110+
if (request.readyState !== XMLHttpRequest.DONE) {
111+
return
112+
}
113+
114+
const statusCode = request.status
115+
if (statusCode === 200) {
116+
resolve(true)
117+
return
118+
}
119+
120+
if (statusCode === 404) {
121+
resolve(false)
122+
return
123+
}
124+
125+
reject({
126+
code: statusCode,
127+
message: request.response,
128+
request: request
129+
})
130+
}
131+
132+
request.send()
133+
})
134+
}
135+
136+
private _getBranchSHA(branch: string): Promise<string> {
137+
return new Promise<string>((resolve, reject) => {
138+
const request = new XMLHttpRequest()
139+
request.open(
140+
'GET',
141+
`https://api.github.com/repos/${this.owner}/${this.repo}/git/ref/heads/${branch}`
142+
)
143+
this._setRequestHeader(request)
144+
145+
request.onreadystatechange = (_ev: ProgressEvent) => {
146+
if (request.readyState !== XMLHttpRequest.DONE) {
147+
return
148+
}
149+
150+
const statusCode = request.status
151+
if (statusCode === 200) {
152+
const response = JSON.parse(request.responseText)
153+
resolve(response.object.sha)
154+
return
155+
}
156+
157+
reject({
158+
code: statusCode,
159+
message: request.response,
160+
request: request
161+
})
162+
}
163+
164+
request.send()
165+
})
166+
}
167+
168+
private _createBranch(branchName: string, sha: string): Promise<void> {
169+
return new Promise<void>((resolve, reject) => {
170+
const request = new XMLHttpRequest()
171+
request.open(
172+
'POST',
173+
`https://api.github.com/repos/${this.owner}/${this.repo}/git/refs`
174+
)
175+
this._setRequestHeader(request)
176+
177+
request.onreadystatechange = (_ev: ProgressEvent) => {
178+
if (request.readyState !== XMLHttpRequest.DONE) {
179+
return
180+
}
181+
182+
const statusCode = request.status
183+
if (statusCode === 201 || statusCode === 422) {
184+
resolve()
185+
return
186+
}
187+
188+
reject({
189+
code: statusCode,
190+
message: request.response,
191+
request: request
192+
})
193+
}
194+
195+
request.send(JSON.stringify({
196+
ref: `refs/heads/${branchName}`,
197+
sha: sha
198+
}))
199+
})
200+
}
201+
202+
private _getFileSHA(
203+
filepath: string,
204+
branch: string
205+
): Promise<string | null> {
206+
return new Promise<string | null>((resolve, reject) => {
207+
const request = new XMLHttpRequest()
208+
request.open(
209+
'GET',
210+
`https://api.github.com/repos/${this.owner}/${this.repo}/contents/${filepath}?ref=${branch}`
211+
)
212+
this._setRequestHeader(request)
213+
214+
request.onreadystatechange = (_ev: ProgressEvent) => {
215+
if (request.readyState !== XMLHttpRequest.DONE) {
216+
return
217+
}
218+
219+
const statusCode = request.status
220+
if (statusCode === 200) {
221+
const response = JSON.parse(request.responseText)
222+
resolve(response.sha)
223+
return
224+
}
225+
226+
if (statusCode === 404) {
227+
resolve(null)
228+
return
229+
}
230+
231+
reject({
232+
code: statusCode,
233+
message: request.response,
234+
request: request
235+
})
236+
}
237+
238+
request.send()
239+
})
240+
}
241+
242+
private _uploadFile(args: {
243+
request: XMLHttpRequest;
244+
filepath: string;
245+
content: string;
246+
commitMessage: string;
247+
branch: string;
248+
fileSHA: string | null;
249+
}) {
250+
const { request, branch, content, commitMessage, filepath, fileSHA } = args
251+
252+
const body: any = {
253+
message: commitMessage,
254+
content: content,
255+
branch: branch
256+
}
257+
258+
// Include SHA if file exists (required for updates)
259+
if (fileSHA) {
260+
body.sha = fileSHA
261+
}
262+
263+
request.open(
264+
'PUT',
265+
`https://api.github.com/repos/${this.owner}/${this.repo}/contents/${filepath}`
266+
)
267+
this._setRequestHeader(request)
268+
269+
request.send(JSON.stringify(body))
270+
}
271+
272+
private _setRequestHeader(request: XMLHttpRequest) {
273+
request.setRequestHeader('Authorization', `Bearer ${this.token}`)
274+
request.setRequestHeader('Content-Type', 'application/json')
275+
request.setRequestHeader('Accept', 'application/vnd.github+json')
276+
}
277+
}

0 commit comments

Comments
 (0)