Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 58 additions & 1 deletion src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {Parser} from "./parser.js";
import {resolveIncludeLocal, validateIncludeLocal} from "./parser-includes.js";
import globby from "globby";
import terminalLink from "terminal-link";
import * as crypto from "crypto";

const GCL_SHELL_PROMPT_PLACEHOLDER = "<gclShellPromptPlaceholder>";
interface JobOptions {
Expand Down Expand Up @@ -207,7 +208,12 @@ export class Job {
// Find environment matched variables
if (this.environment && expandVariables) {
const expanded = Utils.expandVariables(this._variables);
const envNameBeforeExpansion = this.environment.name;
this.environment.name = Utils.expandText(this.environment.name, expanded);
if (this.environment.name !== envNameBeforeExpansion) {
// Regenerate CI_ENVIRONMENT_SLUG based on env name if it changed after expansion
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this._generateEnvironmentSlug(this.environment.name);
}
this.environment.url = Utils.expandText(this.environment.url, expanded);
}
const envMatchedVariables = Utils.findEnvMatchedVariables(variablesFromFiles, this.fileVariablesDir, this.environment);
Expand Down Expand Up @@ -314,7 +320,7 @@ export class Job {
predefinedVariables["CI_JOB_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/-/jobs/${this.jobId}`; // Changes on rerun.
predefinedVariables["CI_PIPELINE_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/pipelines/${this.pipelineIid}`;
predefinedVariables["CI_ENVIRONMENT_NAME"] = this.environment?.name ?? "";
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this.environment?.name?.replace(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 23).replace(/-$/, "").toLowerCase() ?? "";
predefinedVariables["CI_ENVIRONMENT_SLUG"] = this.environment?.name ? this._generateEnvironmentSlug(this.environment.name) : "";
predefinedVariables["CI_ENVIRONMENT_URL"] = this.environment?.url ?? "";
predefinedVariables["CI_ENVIRONMENT_TIER"] = this.environment?.deployment_tier ?? "";
predefinedVariables["CI_ENVIRONMENT_ACTION"] = this.environment?.action ?? "";
Expand All @@ -328,6 +334,57 @@ export class Job {
return predefinedVariables;
}

/**
* Generates a compliant slug for an environment name.
* See: https://gitlab.com/gitlab-org/gitlab/-/blob/fc31e7ac344e53ebae182ea1dca183bdc0e2ea71/lib/gitlab/slug/environment.rb
*
* The slug:
* - Contains only lowercase letters (a-z), numbers (0-9), and '-'.
* - Begins with a letter.
* - Has a maximum length of 24 characters.
* - Does not end with '-'.
*
* @param name The original environment name.
* @returns A compliant environment slug.
*/
private _generateEnvironmentSlug (name: string): string {
// 1. Lowercase, replace non-alphanumeric with '-', and squeeze repeating '-'
let slug = name
.toLowerCase()
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-");

// 2. Must start with a letter
if (!/^[a-z]/.test(slug)) {
slug = `env-${slug}`;
}

// 3. If it's too long or was modified, shorten and add a hash suffix
if (slug.length > 24 || slug !== name) {
// Truncate to 17 chars (leaving room for '-' + 6-char hash)
slug = slug.slice(0, 17);

// Ensure it ends with a dash before adding the suffix
if (!slug.endsWith("-")) {
slug += "-";
}

// Create the 6-char suffix from a hash of the *original* name
const hexHash = crypto
.createHash("sha256")
.update(name)
.digest("hex");

// Use BigInt for safe conversion from hex -> base36
const suffix = BigInt(`0x${hexHash}`).toString(36).slice(-6);

return slug + suffix;
}

// 4. If it was short and unmodified, just ensure it doesn't end with '-'
return slug.replace(/-$/, "");
}

get jobStatus () {
if (this.preScriptsExitCode == null) return "pending";
if (this.preScriptsExitCode == 0) return "success";
Expand Down
11 changes: 11 additions & 0 deletions tests/test-cases/environment/.gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ deploy-stage-job:
url: http://stage.domain.com
action: stop
deployment_tier: production

deploy-stage-job-with-expansion:
stage: deploy
script:
- echo "Deploy stage something with expansion"
- echo ${CI_ENVIRONMENT_SLUG}
- echo ${CI_ENVIRONMENT_NAME}
variables:
EXPAND_ME: foobar-long-string
environment:
name: stage-domain-${EXPAND_ME}
18 changes: 17 additions & 1 deletion tests/test-cases/environment/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test("environment <deploy-stage-job>", async () => {
}, writeStreams);

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

});

test("environment <deploy-stage-job-with-expansion>", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/environment",
job: ["deploy-stage-job-with-expansion"],
}, writeStreams);

const expected = [
chalk`{blueBright deploy-stage-job-with-expansion} {greenBright >} stage-domain-foob-yafktx`,
chalk`{blueBright deploy-stage-job-with-expansion} {greenBright >} stage-domain-foobar-long-string`,
chalk`{blueBright deploy-stage-job-with-expansion} environment: \{ name: {bold stage-domain-foobar-long-string} \}`,
];
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));

});