Skip to content

Commit 8b5f63d

Browse files
authored
Improve environment slug handling (#1673)
* - Implement environment slug generation - Implement environment slug handling when environt name is expanded * fix linting
1 parent 679e8af commit 8b5f63d

File tree

3 files changed

+86
-2
lines changed

3 files changed

+86
-2
lines changed

src/job.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {Parser} from "./parser.js";
1818
import {resolveIncludeLocal, validateIncludeLocal} from "./parser-includes.js";
1919
import globby from "globby";
2020
import terminalLink from "terminal-link";
21+
import * as crypto from "crypto";
2122

2223
const GCL_SHELL_PROMPT_PLACEHOLDER = "<gclShellPromptPlaceholder>";
2324
interface JobOptions {
@@ -207,7 +208,12 @@ export class Job {
207208
// Find environment matched variables
208209
if (this.environment && expandVariables) {
209210
const expanded = Utils.expandVariables(this._variables);
211+
const envNameBeforeExpansion = this.environment.name;
210212
this.environment.name = Utils.expandText(this.environment.name, expanded);
213+
if (this.environment.name !== envNameBeforeExpansion) {
214+
// Regenerate CI_ENVIRONMENT_SLUG based on env name if it changed after expansion
215+
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this._generateEnvironmentSlug(this.environment.name);
216+
}
211217
this.environment.url = Utils.expandText(this.environment.url, expanded);
212218
}
213219
const envMatchedVariables = Utils.findEnvMatchedVariables(variablesFromFiles, this.fileVariablesDir, this.environment);
@@ -314,7 +320,7 @@ export class Job {
314320
predefinedVariables["CI_JOB_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/-/jobs/${this.jobId}`; // Changes on rerun.
315321
predefinedVariables["CI_PIPELINE_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/pipelines/${this.pipelineIid}`;
316322
predefinedVariables["CI_ENVIRONMENT_NAME"] = this.environment?.name ?? "";
317-
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this.environment?.name?.replace(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 23).replace(/-$/, "").toLowerCase() ?? "";
323+
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this.environment?.name ? this._generateEnvironmentSlug(this.environment.name) : "";
318324
predefinedVariables["CI_ENVIRONMENT_URL"] = this.environment?.url ?? "";
319325
predefinedVariables["CI_ENVIRONMENT_TIER"] = this.environment?.deployment_tier ?? "";
320326
predefinedVariables["CI_ENVIRONMENT_ACTION"] = this.environment?.action ?? "";
@@ -328,6 +334,57 @@ export class Job {
328334
return predefinedVariables;
329335
}
330336

337+
/**
338+
* Generates a compliant slug for an environment name.
339+
* See: https://gitlab.com/gitlab-org/gitlab/-/blob/fc31e7ac344e53ebae182ea1dca183bdc0e2ea71/lib/gitlab/slug/environment.rb
340+
*
341+
* The slug:
342+
* - Contains only lowercase letters (a-z), numbers (0-9), and '-'.
343+
* - Begins with a letter.
344+
* - Has a maximum length of 24 characters.
345+
* - Does not end with '-'.
346+
*
347+
* @param name The original environment name.
348+
* @returns A compliant environment slug.
349+
*/
350+
private _generateEnvironmentSlug (name: string): string {
351+
// 1. Lowercase, replace non-alphanumeric with '-', and squeeze repeating '-'
352+
let slug = name
353+
.toLowerCase()
354+
.replace(/[^a-z0-9]/g, "-")
355+
.replace(/-+/g, "-");
356+
357+
// 2. Must start with a letter
358+
if (!/^[a-z]/.test(slug)) {
359+
slug = `env-${slug}`;
360+
}
361+
362+
// 3. If it's too long or was modified, shorten and add a hash suffix
363+
if (slug.length > 24 || slug !== name) {
364+
// Truncate to 17 chars (leaving room for '-' + 6-char hash)
365+
slug = slug.slice(0, 17);
366+
367+
// Ensure it ends with a dash before adding the suffix
368+
if (!slug.endsWith("-")) {
369+
slug += "-";
370+
}
371+
372+
// Create the 6-char suffix from a hash of the *original* name
373+
const hexHash = crypto
374+
.createHash("sha256")
375+
.update(name)
376+
.digest("hex");
377+
378+
// Use BigInt for safe conversion from hex -> base36
379+
const suffix = BigInt(`0x${hexHash}`).toString(36).slice(-6);
380+
381+
return slug + suffix;
382+
}
383+
384+
// 4. If it was short and unmodified, just ensure it doesn't end with '-'
385+
return slug.replace(/-$/, "");
386+
}
387+
331388
get jobStatus () {
332389
if (this.preScriptsExitCode == null) return "pending";
333390
if (this.preScriptsExitCode == 0) return "success";

tests/test-cases/environment/.gitlab-ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,14 @@ deploy-stage-job:
2222
url: http://stage.domain.com
2323
action: stop
2424
deployment_tier: production
25+
26+
deploy-stage-job-with-expansion:
27+
stage: deploy
28+
script:
29+
- echo "Deploy stage something with expansion"
30+
- echo ${CI_ENVIRONMENT_SLUG}
31+
- echo ${CI_ENVIRONMENT_NAME}
32+
variables:
33+
EXPAND_ME: foobar-long-string
34+
environment:
35+
name: stage-domain-${EXPAND_ME}

tests/test-cases/environment/integration.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test("environment <deploy-stage-job>", async () => {
3030
}, writeStreams);
3131

3232
const expected = [
33-
chalk`{blueBright deploy-stage-job} {greenBright >} stage-domain`,
33+
chalk`{blueBright deploy-stage-job} {greenBright >} stage-domain-ip33nk`,
3434
chalk`{blueBright deploy-stage-job} {greenBright >} Stage Domain`,
3535
chalk`{blueBright deploy-stage-job} {greenBright >} http://stage.domain.com`,
3636
chalk`{blueBright deploy-stage-job} {greenBright >} stop`,
@@ -40,3 +40,19 @@ test("environment <deploy-stage-job>", async () => {
4040
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
4141

4242
});
43+
44+
test("environment <deploy-stage-job-with-expansion>", async () => {
45+
const writeStreams = new WriteStreamsMock();
46+
await handler({
47+
cwd: "tests/test-cases/environment",
48+
job: ["deploy-stage-job-with-expansion"],
49+
}, writeStreams);
50+
51+
const expected = [
52+
chalk`{blueBright deploy-stage-job-with-expansion} {greenBright >} stage-domain-foob-yafktx`,
53+
chalk`{blueBright deploy-stage-job-with-expansion} {greenBright >} stage-domain-foobar-long-string`,
54+
chalk`{blueBright deploy-stage-job-with-expansion} environment: \{ name: {bold stage-domain-foobar-long-string} \}`,
55+
];
56+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
57+
58+
});

0 commit comments

Comments
 (0)