Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`,

Your configuration for the \`assignee\` option is \`${stringify(assignee)}\`.`,
}),
EINVALIDGENERICPACKAGELABEL: ({label, asset}) => ({
message: 'Invalid label for generic package asset.',
details: `The label \`${label}\` for a generic package asset is invalid. GitLab generic package filenames must follow these rules:

- Can contain: Letters (A-Z, a-z), Numbers (0-9), and special characters: . _ - + ~ @ /
- Cannot start with: ~ or @
- Cannot end with: ~ or @
- Cannot contain spaces

Your asset configuration: \`${stringify(asset)}\`

Please update the label to comply with [GitLab's package naming restrictions](https://docs.gitlab.com/ee/user/packages/generic_packages/#publish-a-package-file).`,
}),
EINVALIDGITLABURL: () => ({
message: 'The git repository URL is not a valid GitLab URL.',
details: `The **semantic-release** \`repositoryUrl\` option must a valid GitLab URL with the format \`<GitLab_URL>/<projectPath>.git\`.
Expand Down
27 changes: 27 additions & 0 deletions lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ const isStringOrStringArray = (value) =>
const isArrayOf = (validator) => (array) => isArray(array) && array.every((value) => validator(value));
const canBeDisabled = (validator) => (value) => value === false || validator(value);

const isValidGenericPackageLabel = (label) => {
// GitLab generic package filename restrictions
// Can contain: A-Z, a-z, 0-9, . _ - + ~ @ /
// Cannot start with: ~ or @
// Cannot end with: ~ or @
// Cannot contain spaces
if (!label) return true; // label is optional
if (typeof label !== "string") return false;
if (/\s/.test(label)) return false; // no spaces
if (/^[~@]/.test(label)) return false; // cannot start with ~ or @
if (/[~@]$/.test(label)) return false; // cannot end with ~ or @
// Check if it only contains allowed characters
if (!/^[A-Za-z0-9._\-+~@/]+$/.test(label)) return false;
return true;
};

const VALIDATORS = {
assets: isArrayOf(
(asset) =>
Expand Down Expand Up @@ -45,6 +61,17 @@ export default async (pluginConfig, context) => {
.filter(([option, value]) => !isValid(option, value))
.map(([option, value]) => getError(`EINVALID${option.toUpperCase()}`, { [option]: value }));

// Validate generic package labels
if (options.assets && Array.isArray(options.assets)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move this check to the existing VALIDATORS object above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved isValidGenericPackageLabel to the top-level scope above the VALIDATORS object in commit b7e8f14.

options.assets.forEach((asset) => {
if (isPlainObject(asset) && asset.target === "generic_package" && asset.label) {
if (!isValidGenericPackageLabel(asset.label)) {
errors.push(getError("EINVALIDGENERICPACKAGELABEL", { label: asset.label, asset }));
Comment on lines +67 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This can go in line 36

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what should go in line 36. The validation check at lines 67-69 needs to produce specific EINVALIDGENERICPACKAGELABEL error codes for each invalid asset (as expected by the tests), but integrating it into the assets validator at line 36 would only produce a single EINVALIDASSETS error for the entire array. Could you clarify what you'd like me to change?

}
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move this up to the other validation functionality

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the generic package label validation to be immediately after the VALIDATORS-based validation, grouping all validation logic together before the required field checks in commit 8c15736.


if (!projectPath) {
errors.push(getError("EINVALIDGITLABURL"));
}
Expand Down
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 12 additions & 12 deletions test/publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ test.serial("Publish a release with generics", async (t) => {
const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
const encodedVersion = encodeURIComponent(nextRelease.version);
const uploaded = { file: { url: "/uploads/file.css" } };
const generic = { path: "file.css", label: "Style package", target: "generic_package", status: "hidden" };
const generic = { path: "file.css", label: "style-package", target: "generic_package", status: "hidden" };
const assets = [generic];
const encodedLabel = encodeURIComponent(generic.label);
const expectedUrl = `https://gitlab.com/api/v4/projects/${encodedProjectPath}/packages/generic/release/${encodedVersion}/${encodedLabel}`;
Expand All @@ -152,7 +152,7 @@ test.serial("Publish a release with generics", async (t) => {
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down Expand Up @@ -189,7 +189,7 @@ test.serial("Publish a release with generics: with asset.packageName (fixed text
const uploaded = { file: { url: "/uploads/file.css" } };
const generic = {
path: "file.css",
label: "Style package",
label: "style-package",
target: "generic_package",
status: "hidden",
packageName: "microk8s",
Expand All @@ -205,7 +205,7 @@ test.serial("Publish a release with generics: with asset.packageName (fixed text
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down Expand Up @@ -248,7 +248,7 @@ test.serial("Publish a release with generics: with asset.packageName (template)"
const uploaded = { file: { url: "/uploads/file.css" } };
const generic = {
path: "file.css",
label: "Style package",
label: "style-package",
target: "generic_package",
status: "hidden",
packageName: "${nextRelease.channel}",
Expand All @@ -263,7 +263,7 @@ test.serial("Publish a release with generics: with asset.packageName (template)"
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down Expand Up @@ -344,7 +344,7 @@ test.serial("Publish a release with generics and external storage provider (http
const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
const encodedVersion = encodeURIComponent(nextRelease.version);
const uploaded = { file: { url: "http://aws.example.com/bucket/gitlab/file.css" } };
const generic = { path: "file.css", label: "Style package", target: "generic_package", status: "hidden" };
const generic = { path: "file.css", label: "style-package", target: "generic_package", status: "hidden" };
const assets = [generic];
const encodedLabel = encodeURIComponent(generic.label);
const expectedUrl = `https://gitlab.com/api/v4/projects/${encodedProjectPath}/packages/generic/release/${encodedVersion}/${encodedLabel}`;
Expand All @@ -355,7 +355,7 @@ test.serial("Publish a release with generics and external storage provider (http
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down Expand Up @@ -390,7 +390,7 @@ test.serial("Publish a release with generics and external storage provider (http
const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
const encodedVersion = encodeURIComponent(nextRelease.version);
const uploaded = { file: { url: "https://aws.example.com/bucket/gitlab/file.css" } };
const generic = { path: "file.css", label: "Style package", target: "generic_package", status: "hidden" };
const generic = { path: "file.css", label: "style-package", target: "generic_package", status: "hidden" };
const assets = [generic];
const encodedLabel = encodeURIComponent(generic.label);
const expectedUrl = `https://gitlab.com/api/v4/projects/${encodedProjectPath}/packages/generic/release/${encodedVersion}/${encodedLabel}`;
Expand All @@ -401,7 +401,7 @@ test.serial("Publish a release with generics and external storage provider (http
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down Expand Up @@ -436,7 +436,7 @@ test.serial("Publish a release with generics and external storage provider (ftp)
const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
const encodedVersion = encodeURIComponent(nextRelease.version);
const uploaded = { file: { url: "ftp://drive.example.com/gitlab/file.css" } };
const generic = { path: "file.css", label: "Style package", target: "generic_package", status: "hidden" };
const generic = { path: "file.css", label: "style-package", target: "generic_package", status: "hidden" };
const assets = [generic];
const encodedLabel = encodeURIComponent(generic.label);
const expectedUrl = `https://gitlab.com/api/v4/projects/${encodedProjectPath}/packages/generic/release/${encodedVersion}/${encodedLabel}`;
Expand All @@ -447,7 +447,7 @@ test.serial("Publish a release with generics and external storage provider (ftp)
assets: {
links: [
{
name: "Style package",
name: "style-package",
url: expectedUrl,
link_type: "package",
},
Expand Down
88 changes: 88 additions & 0 deletions test/verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,91 @@ test.serial(
t.true(gitlab.isDone());
}
);

test.serial("Throw SemanticReleaseError if generic package asset labels are invalid", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const assets = [
{ path: "file1.css", label: "Style package", target: "generic_package" }, // contains spaces
{ path: "file2.css", label: "~invalid", target: "generic_package" }, // starts with ~
{ path: "file3.css", label: "@invalid", target: "generic_package" }, // starts with @
{ path: "file4.css", label: "invalid~", target: "generic_package" }, // ends with ~
{ path: "file5.css", label: "invalid@", target: "generic_package" }, // ends with @
{ path: "file6.css", label: "invalid$char", target: "generic_package" }, // contains invalid character
];
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

const { errors } = await t.throwsAsync(
verify(
{ assets },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.is(errors.length, 6);
errors.forEach((error) => {
t.is(error.name, "SemanticReleaseError");
t.is(error.code, "EINVALIDGENERICPACKAGELABEL");
});
t.true(gitlab.isDone());
});

test.serial("Does not throw for valid generic package asset labels", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const assets = [
{ path: "file.css", label: "valid-label_123.tar.gz", target: "generic_package" },
{ path: "file2.css", label: "another.valid+label~0", target: "generic_package" },
{ path: "file3.css", label: "path/to/file", target: "generic_package" },
];
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

await t.notThrowsAsync(
verify(
{ assets },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
});

test.serial("Does not throw for generic package assets without labels", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const assets = [{ path: "file.css", target: "generic_package" }];
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

await t.notThrowsAsync(
verify(
{ assets },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
});

test.serial("Does not throw for non-generic package assets with spaces in labels", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const assets = [{ path: "file.css", label: "Valid Label With Spaces" }];
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

await t.notThrowsAsync(
verify(
{ assets },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
});