Skip to content

Commit 7ae554b

Browse files
fix(publisher-github): don't sanitize asset names before upload (#3485)
Co-authored-by: Erick Zhao <[email protected]>
1 parent a5c60d5 commit 7ae554b

File tree

4 files changed

+72
-27
lines changed

4 files changed

+72
-27
lines changed

packages/publisher/github/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
"@electron-forge/shared-types": "7.3.0",
2626
"@octokit/core": "^3.2.4",
2727
"@octokit/plugin-retry": "^3.0.9",
28+
"@octokit/request-error": "^2.0.5",
2829
"@octokit/rest": "^18.0.11",
2930
"@octokit/types": "^6.1.2",
31+
"chalk": "^4.0.0",
3032
"debug": "^4.3.1",
3133
"fs-extra": "^10.0.0",
34+
"log-symbols": "^4.0.0",
3235
"mime-types": "^2.1.25"
3336
},
3437
"publishConfig": {

packages/publisher/github/src/PublisherGithub.ts

+38-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import path from 'node:path';
2+
13
import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base';
24
import { ForgeMakeResult } from '@electron-forge/shared-types';
5+
import { RequestError } from '@octokit/request-error';
36
import { GetResponseDataTypeFromEndpointMethod } from '@octokit/types';
7+
import chalk from 'chalk';
48
import fs from 'fs-extra';
9+
import logSymbols from 'log-symbols';
510
import mime from 'mime-types';
611

712
import { PublisherGitHubConfig } from './Config';
@@ -97,9 +102,10 @@ export default class PublisherGithub extends PublisherBase<PublisherGitHubConfig
97102
uploaded += 1;
98103
updateUploadStatus();
99104
};
100-
const artifactName = GitHub.sanitizeName(artifactPath);
105+
const artifactName = path.basename(artifactPath);
106+
const sanitizedArtifactName = GitHub.sanitizeName(artifactName);
101107
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
102-
const asset = release!.assets.find((item: OctokitReleaseAsset) => item.name === artifactName);
108+
const asset = release!.assets.find((item: OctokitReleaseAsset) => item.name === sanitizedArtifactName);
103109
if (asset !== undefined) {
104110
if (config.force === true) {
105111
await github.getGitHub().repos.deleteReleaseAsset({
@@ -111,21 +117,36 @@ export default class PublisherGithub extends PublisherBase<PublisherGitHubConfig
111117
return done();
112118
}
113119
}
114-
await github.getGitHub().repos.uploadReleaseAsset({
115-
owner: config.repository.owner,
116-
repo: config.repository.name,
117-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
118-
release_id: release!.id,
119-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120-
url: release!.upload_url,
121-
// https://github.com/octokit/rest.js/issues/1645
122-
data: (await fs.readFile(artifactPath)) as unknown as string,
123-
headers: {
124-
'content-type': mime.lookup(artifactPath) || 'application/octet-stream',
125-
'content-length': (await fs.stat(artifactPath)).size,
126-
},
127-
name: artifactName,
128-
});
120+
try {
121+
const { data: uploadedAsset } = await github.getGitHub().repos.uploadReleaseAsset({
122+
owner: config.repository.owner,
123+
repo: config.repository.name,
124+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125+
release_id: release!.id,
126+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127+
url: release!.upload_url,
128+
// https://github.com/octokit/rest.js/issues/1645
129+
data: (await fs.readFile(artifactPath)) as unknown as string,
130+
headers: {
131+
'content-type': mime.lookup(artifactPath) || 'application/octet-stream',
132+
'content-length': (await fs.stat(artifactPath)).size,
133+
},
134+
name: artifactName,
135+
});
136+
if (uploadedAsset.name !== sanitizedArtifactName) {
137+
// There's definitely a bug with GitHub.sanitizeName
138+
console.warn(logSymbols.warning, chalk.yellow(`Expected artifact's name to be '${sanitizedArtifactName}' - got '${uploadedAsset.name}'`));
139+
}
140+
} catch (err) {
141+
// If an asset with that name already exists, it's either a bug with GitHub.sanitizeName
142+
// where it did not sanitize the artifact name in the same way as GitHub did, or there
143+
// was simply a race condition with uploading artifacts with the same name
144+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
145+
if (err instanceof RequestError && err.status === 422 && (err.response?.data as any)?.errors?.[0].code === 'already_exists') {
146+
console.error(`Asset with name '${artifactName}' already exists - there may be a bug with Forge's GitHub.sanitizeName util`);
147+
}
148+
throw err;
149+
}
129150
return done();
130151
})
131152
);

packages/publisher/github/src/util/github.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,22 @@ export default class GitHub {
4848
return github;
4949
}
5050

51-
// Based on https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset and
52-
// https://stackoverflow.com/questions/59081778/rules-for-special-characters-in-github-repository-name
51+
// Based on https://github.com/cli/cli/blob/b07f955c23fb54c400b169d39255569e240b324e/pkg/cmd/release/upload/upload.go#L131-L153
5352
static sanitizeName(name: string): string {
54-
return path
55-
.basename(name)
56-
.replace(/\.+/g, '.')
57-
.replace(/^\./g, '')
58-
.replace(/\.$/g, '')
59-
.replace(/[^\w.-]+/g, '-');
53+
return (
54+
path
55+
.basename(name)
56+
// Remove diacritics (e.g. é -> e)
57+
.normalize('NFD')
58+
.replace(/\p{Diacritic}/gu, '')
59+
// Replace special characters with dot
60+
.replace(/[^\w_.@+-]+/g, '.')
61+
// Replace multiple dots with a single dot
62+
.replace(/\.+/g, '.')
63+
// Remove leading dot if present
64+
.replace(/^\./g, '')
65+
// Remove trailing dot if present
66+
.replace(/\.$/g, '')
67+
);
6068
}
6169
}

packages/publisher/github/test/github_spec.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,21 @@ describe('GitHub', () => {
115115
expect(GitHub.sanitizeName('path/to/foo..bar')).to.equal('foo.bar');
116116
});
117117

118-
it('should replace non-alphanumeric, non-hyphen characters with hyphens', () => {
119-
expect(GitHub.sanitizeName('path/to/foo%$bar baz.')).to.equal('foo-bar-baz');
118+
it('should replace non-alphanumeric, non-hyphen characters with periods', () => {
119+
expect(GitHub.sanitizeName('path/to/foo%$bar baz.')).to.equal('foo.bar.baz');
120+
});
121+
122+
it('should preserve special symbols', () => {
123+
expect(GitHub.sanitizeName('path/to/@foo+bar_')).to.equal('@foo+bar_');
124+
});
125+
126+
it('should preserve hyphens', () => {
127+
const name = 'electron-fiddle-0.99.0-full.nupkg';
128+
expect(GitHub.sanitizeName(`path/to/${name}`)).to.equal(name);
129+
});
130+
131+
it('should remove diacritics', () => {
132+
expect(GitHub.sanitizeName('électron')).to.equal('electron');
120133
});
121134
});
122135
});

0 commit comments

Comments
 (0)