Skip to content

Commit 8912b20

Browse files
committed
Write our own GitHub release asset uploader
The old action doesn't realize that the release already exists and so it makes a new tag, then I have to download the assets and reupload them so that the release author doesn't say github-actions. Easier to write our own script for this. It's also safer in some ways since now a misconfiguration won't start deleting assets or mark as released early since that code just doesn't exist to run in the first place.
1 parent 4b35aba commit 8912b20

File tree

2 files changed

+189
-12
lines changed

2 files changed

+189
-12
lines changed

.github/workflows/release.yml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ jobs:
4444
dist/*.tar.gz
4545
dist/*.AppImage
4646
- name: Upload artifacts to tag
47-
uses: xresloader/upload-to-github-release@2bcae85344d41e21f7fc4c47fa2ed68223afdb49
48-
with:
49-
file: dist/*.deb;dist/*.tar.gz;dist/*.AppImage
50-
draft: true
47+
run: |
48+
node scripts/upload-to-github-releases.js TurboWarp desktop "dist/*.deb"
49+
node scripts/upload-to-github-releases.js TurboWarp desktop "dist/*.tar.gz"
50+
node scripts/upload-to-github-releases.js TurboWarp desktop "dist/*.AppImage"
51+
env:
52+
GH_TOKEN: "${{ github.token }}"
5153

5254
release-mac:
5355
runs-on: macos-latest
@@ -97,10 +99,10 @@ jobs:
9799
name: mac
98100
path: dist/*.dmg
99101
- name: Upload artifacts to tag
100-
uses: xresloader/upload-to-github-release@2bcae85344d41e21f7fc4c47fa2ed68223afdb49
101-
with:
102-
file: dist/*.dmg
103-
draft: true
102+
run: |
103+
node scripts/upload-to-github-releases.js TurboWarp desktop "dist/*.dmg"
104+
env:
105+
GH_TOKEN: "${{ github.token }}"
104106

105107
release-windows:
106108
# GitHub's Windows runners have a C: drive and a D: drive.
@@ -190,7 +192,8 @@ jobs:
190192
path: |
191193
D:\repo\dist\TurboWarp-Setup*x64.exe
192194
- name: Upload signed artifacts to tag
193-
uses: xresloader/upload-to-github-release@2bcae85344d41e21f7fc4c47fa2ed68223afdb49
194-
with:
195-
file: D:\repo\dist\*.exe
196-
draft: true
195+
working-directory: D:\repo
196+
run: |
197+
node scripts/upload-to-github-releases.js TurboWarp desktop "dist/*.exe"
198+
env:
199+
GH_TOKEN: "${{ github.token }}"
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const fs = require('node:fs');
2+
const fsPromises = require('node:fs/promises');
3+
const pathUtil = require('node:path');
4+
const childProcess = require('node:child_process');
5+
6+
/** @type {string} */
7+
const GH_TOKEN = process.env.GH_TOKEN;
8+
if (!GH_TOKEN) {
9+
throw new Error('GH_TOKEN environment variable not set.');
10+
}
11+
12+
const githubHeaders = {
13+
'Accept': 'application/vnd.github+json',
14+
'Authorization': `Bearer ${GH_TOKEN}`,
15+
'X-GitHub-Api-Version': '2022-11-28'
16+
};
17+
18+
/**
19+
* @param {number} ms
20+
* @returns {Promise<void>}
21+
*/
22+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
23+
24+
/**
25+
* @returns {string} Name of tag for current commit
26+
*/
27+
const getMostRecentTag = () => {
28+
const gitProcess = childProcess.spawnSync('git', [
29+
'describe',
30+
'--tags',
31+
'--abbrev=0'
32+
]);
33+
34+
if (gitProcess.error) {
35+
throw gitProcess.error;
36+
}
37+
38+
if (gitProcess.status !== 0) {
39+
throw new Error(`Git returned status ${gitProcess.status} while getting tag`);
40+
}
41+
42+
return gitProcess.stdout.toString().trim();
43+
};
44+
45+
/**
46+
* @typedef Release
47+
* @property {number} id
48+
* @property {string} tag_name
49+
*/
50+
51+
/**
52+
* @param {string} owner
53+
* @param {string} repo
54+
* @returns {Promise<Array<Release>>}
55+
*/
56+
const getRecentReleases = async (owner, repo) => {
57+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=10`, {
58+
headers: githubHeaders
59+
});
60+
61+
if (!res.ok) {
62+
throw new Error(`HTTP ${res.status} fetching releases`);
63+
}
64+
return res.json();
65+
};
66+
67+
/**
68+
* @param {string} owner
69+
* @param {string} repo
70+
* @param {string} tagName
71+
* @returns {Promise<string>} GitHub release ID
72+
*/
73+
const getReleaseForTag = async (owner, repo, tagName) => {
74+
for (let attempt = 0; attempt < 10; attempt++) {
75+
const releases = await getRecentReleases(owner, repo);
76+
77+
for (const release of releases) {
78+
if (release.tag_name === tagName) {
79+
return release.id;
80+
}
81+
}
82+
83+
console.log(`No release found for tag ${tagName}. Checking again in a minute...`);
84+
await sleep(1000 * 60);
85+
}
86+
87+
throw new Error('Could not find release');
88+
};
89+
90+
/**
91+
* @param {string} file File name or path
92+
* @returns {string}
93+
*/
94+
const getContentType = (file) => {
95+
file = file.toLowerCase();
96+
if (file.endsWith('.exe')) return 'application/vnd.microsoft.portable-executable';
97+
if (file.endsWith('.dmg')) return 'application/x-apple-diskimage';
98+
if (file.endsWith('.deb')) return 'application/vnd.debian.binary-package';
99+
if (file.endsWith('.appimage')) return 'application/vnd.appimage';
100+
if (file.endsWith('.tar.gz')) return 'application/gzip';
101+
return 'application/octet-stream';
102+
};
103+
104+
/**
105+
* @typedef Asset
106+
* @property {number} id
107+
*/
108+
109+
/**
110+
* @param {string} owner
111+
* @param {string} repo
112+
* @param {number} releaseId
113+
* @param {string} file Absolute path. Will be uploaded with same file name.
114+
* @returns {Promise<Asset>}
115+
*/
116+
const uploadReleaseAsset = async (owner, repo, releaseId, file) => {
117+
const fileName = pathUtil.basename(file);
118+
const fileType = getContentType(fileName);
119+
const fileData = await fsPromises.readFile(file);
120+
121+
const res = await fetch(`https://uploads.github.com/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`, {
122+
method: 'POST',
123+
headers: {
124+
...githubHeaders,
125+
'Content-Length': fileData.byteLength,
126+
'Content-Type': fileType
127+
},
128+
body: fileData
129+
});
130+
131+
if (!res.ok) {
132+
throw new Error(`HTTP ${res.status} uploading asset to release`);
133+
}
134+
return res.json();
135+
};
136+
137+
const run = async () => {
138+
if (process.argv.length !== 5) {
139+
throw new Error('Usage: node scripts/upload-to-github-releases.js <owner> <repo> <glob>');
140+
}
141+
142+
const owner = process.argv[2];
143+
const repo = process.argv[3];
144+
console.log(`Repo: ${owner}/${repo}`);
145+
146+
const glob = process.argv[4];
147+
console.log(`Glob: ${glob}`);
148+
149+
const filesToUpload = await Array.fromAsync(fsPromises.glob(glob));
150+
console.log(`Files to upload: ${filesToUpload.join(', ')}`);
151+
152+
const tagName = getMostRecentTag();
153+
console.log(`Tag: ${tagName}`);
154+
155+
const releaseId = await getReleaseForTag(owner, repo, tagName);
156+
console.log(`GitHub release: ${releaseId}`);
157+
158+
for (const file of filesToUpload) {
159+
console.log(`Uploading ${file}`);
160+
const asset = await uploadReleaseAsset(owner, repo, releaseId, file);
161+
console.log(`Uploaded asset: ${asset.id}`);
162+
}
163+
164+
console.log('Done.');
165+
};
166+
167+
run()
168+
.then(() => {
169+
170+
})
171+
.catch((err) => {
172+
console.error(err);
173+
process.exit(1);
174+
});

0 commit comments

Comments
 (0)