diff --git a/README.md b/README.md index 2382387e8..21061810e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Get rid of all those dev specific shell scripts and make files. * [docker-swarm-php](./examples/docker-swarm-php) * [docker-in-docker-build](./examples/docker-in-docker-build) * [docker-in-docker with a local registry](./examples/docker-in-docker-build-with-local-registry) + * [scheduled-pipeline-testing](./examples/scheduled-pipeline-testing) * [Installation](#installation) * [Convenience](#convenience) * [CLI options](#cli-options) @@ -155,6 +156,77 @@ export GCL_MAX_JOB_NAME_PADDING=30 # or --maxJobNamePadding: limit padding aroun export GCL_QUIET=true # or --quiet: Suppress all job output ``` + +### Pipeline Simulation Options + +#### --pipeline-source +Simulate different pipeline sources for testing complex GitLab CI configurations locally. + +**Supported values:** +- `push` (default) - Standard development pipeline +- `schedule` - Scheduled pipeline +- `merge_request_event` - Merge request pipeline +- `web` - Web-triggered pipeline +- `api` - API-triggered pipeline +- `external` - External pipeline +- `chat` - Chat-triggered pipeline +- `external_pull_request_event` - External pull request on GitHub +- `ondemand_dast_scan` - DAST on-demand scan pipelines +- `ondemand_dast_validation` - DAST on-demand validation pipelines +- `parent_pipeline` - Parent/child pipeline triggers +- `pipeline` - Multi-project pipelines +- `security_orchestration_policy` - Scheduled scan execution policies +- `trigger` - Downstream pipeline triggers +- `webide` - Web IDE pipelines + +**Examples:** +```bash +# Test scheduled pipeline behavior +gitlab-ci-local --pipeline-source schedule --list + +# Test merge request pipeline +gitlab-ci-local --pipeline-source merge_request_event --list + +# Test downstream pipeline +gitlab-ci-local --pipeline-source trigger --list + +# Test multi-project pipeline +gitlab-ci-local --pipeline-source pipeline --list + +# Test external pull request +gitlab-ci-local --pipeline-source external_pull_request_event --list + +# Test DAST scan +gitlab-ci-local --pipeline-source ondemand_dast_scan --list + +# Test parent pipeline +gitlab-ci-local --pipeline-source parent_pipeline --list + +# Test Web IDE +gitlab-ci-local --pipeline-source webide --list +``` + +**Validation:** +The tool validates pipeline source values and provides clear error messages for invalid options. All values are restricted to the official GitLab CI pipeline sources. + +#### --schedule-name +Specify the exact schedule name for testing scheduled pipelines. This is particularly useful for testing complex conditional logic in scheduled pipelines. + +**Examples:** +```bash +# Test specific npm dependency update schedule +gitlab-ci-local --pipeline-source schedule --schedule-name "npm Dependency Update" --list + +# Test OpenBSD snapshot schedule +gitlab-ci-local --pipeline-source schedule --schedule-name "Daily OpenBSD Snapshot Check" --list +``` + +**Validation:** +Schedule names are validated for: +- Non-empty values +- Maximum length of 255 characters +- Invalid filesystem characters (`< > : " \ | ? *`) +- Clear error messages for validation failures ### List Pipeline Jobs Sometimes there is the need of knowing which jobs will be added before actually executing the pipeline. @@ -208,6 +280,56 @@ build-job;"";build;on_success;true;[test-job] deploy-job;"";deploy;never;false;[build-job] ``` + +## Testing Complex Pipeline Scenarios + +### Enhanced Error Handling & Validation + +GitLab CI Local now includes comprehensive validation for pipeline simulation options: + +- **Pipeline Source Validation**: Restricts values to official GitLab CI pipeline sources (15 supported types) +- **Schedule Name Validation**: Ensures schedule names meet filesystem and length requirements +- **Clear Error Messages**: Provides actionable feedback for invalid inputs +- **Environment Variable Support**: Automatically detects and validates `CI_PIPELINE_SOURCE` and `SCHEDULE_NAME` from environment +- **Constants-Based Validation**: Uses centralized constants for maintainable validation logic + +**Error Handling Examples:** +```bash +# Invalid pipeline source +gitlab-ci-local --pipeline-source invalid_source --list +# Error: Invalid pipeline source: "invalid_source". Valid options are: push, schedule, merge_request_event, web, api, external, chat, external_pull_request_event, ondemand_dast_scan, ondemand_dast_validation, parent_pipeline, pipeline, security_orchestration_policy, trigger, webide + +# Invalid schedule name +gitlab-ci-local --pipeline-source schedule --schedule-name "invalid + + +# Scheduled Pipeline Testing Example + +This example demonstrates how to use GitLab CI Local to test complex scheduled pipeline configurations locally, including conditional includes and complex rules logic. + +## Overview + +Scheduled pipelines in GitLab CI often have complex conditional logic that can be difficult to test without pushing to the repository. This example shows how to use the new `--pipeline-source` and `--schedule-name` options to simulate scheduled pipelines locally. + +## Getting Started + +### Prerequisites + +You need **Node.js** installed on your system. If you don't have it: + +```bash +# macOS (with Homebrew) +brew install node + +# Ubuntu/Debian +sudo apt install nodejs npm + +# Windows +# Download from https://nodejs.org/ +``` + +### Quick Start (Simplest Path) + +1. **Clone the repository** (if you haven't already): + ```bash + git clone https://github.com/firecow/gitlab-ci-local.git + cd gitlab-ci-local + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Navigate to the example**: + ```bash + cd examples/scheduled-pipeline-testing + ``` + +4. **Run your first test**: + ```bash + # Test standard development pipeline + npx tsx ../../src/index.ts --pipeline-source push --list + + # Test scheduled pipeline + npx tsx ../../src/index.ts --pipeline-source schedule --list + ``` + +**That's it!** You should see the jobs listed for each pipeline type. + +### What You'll See + +When you run the commands, you should see output like this: + +```bash +$ npx tsx ../../src/index.ts --pipeline-source push --list +parsing and downloads finished in 489 ms. +json schema validated in 101 ms +name description stage when allow_failure needs +standard-job test always false +always-job test always false +web-triggered-job test always false +api-triggered-job test always false +external-job test always false + +$ npx tsx ../../src/index.ts --pipeline-source schedule --list +parsing and downloads finished in 454 ms. +json schema validated in 98 ms +name description stage when allow_failure needs +scheduled-job test always false +always-job test always false +``` + +**Expected Results:** +- **`push` pipeline**: Shows `standard-job`, `web-triggered-job`, `api-triggered-job`, `external-job`, `always-job` +- **`schedule` pipeline**: Shows `scheduled-job`, `always-job` +- **`always-job`**: Shows in ALL pipeline types (as expected) + +### Alternative Running Options + +#### Option 1: From Project Root (Recommended for Development) +```bash +# Stay in the gitlab-ci-local project root +npx tsx src/index.ts --pipeline-source push --list --cwd examples/scheduled-pipeline-testing +npx tsx src/index.ts --pipeline-source schedule --list --cwd examples/scheduled-pipeline-testing +``` + +#### Option 2: Build and Use Binary +```bash +# Build the project +npm run build + +# Use the compiled binary +./dist/index.js --pipeline-source push --list --cwd examples/scheduled-pipeline-testing +``` + +#### Option 3: Copy Example to Your Own Project +```bash +# Copy the example files to your own project +cp -r examples/scheduled-pipeline-testing /path/to/your/project/ +cd /path/to/your/project/scheduled-pipeline-testing + +# Then use gitlab-ci-local from your project +gitlab-ci-local --pipeline-source push --list +``` + +## Features Demonstrated + +- **Pipeline Source Simulation**: Test different pipeline trigger types +- **Schedule Name Testing**: Test specific schedule configurations +- **Conditional Include Logic**: Test complex `rules` and conditional logic +- **Job Dependency Validation**: Verify job inclusion/exclusion based on conditions + +## Example GitLab CI Configuration + +The `.gitlab-ci.yml` file in this example demonstrates: + +```yaml +# Standard pipeline components (development, merge requests) +include: + - local: '.gitlab-ci/standard-pipeline.yml' + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + when: always + +# Scheduled pipeline components +include: + - local: '.gitlab-ci/scheduled-pipeline.yml' + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + when: always +``` + +## Testing Scenarios + +### 1. Standard Development Pipeline + +Test the normal development pipeline behavior: + +```bash +npx tsx ../../src/index.ts --pipeline-source push --list +``` + +This will show only the standard pipeline jobs, excluding scheduled pipeline components. + +### 2. Scheduled Pipeline Testing + +Test a scheduled pipeline without specifying a schedule name: + +```bash +npx tsx ../../src/index.ts --pipeline-source schedule --list +``` + +This will show scheduled pipeline jobs but may not include schedule-specific conditional logic. + +### 3. Specific Schedule Testing + +Test a specific schedule with exact name matching: + +```bash +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" --list +``` + +This will show jobs that are specifically included for the "Daily Check" schedule. + +### 4. Compare Pipeline Types + +Compare different pipeline types to understand job inclusion: + +```bash +# Development pipeline +gitlab-ci-local --pipeline-source push --list + +# Scheduled pipeline +gitlab-ci-local --pipeline-source schedule --list + +# Merge request pipeline +gitlab-ci-local --pipeline-source merge_request_event --list +``` + +## Complex Conditional Logic Testing + +This example demonstrates testing complex conditional logic: + +```yaml +# Example of complex conditional logic +rules: + - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_NAME == "npm Dependency Update" + when: always + - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_NAME == "Daily OpenBSD Snapshot Check" + when: always + - when: never +``` + +Test this logic locally: + +```bash +# Test npm dependency update schedule +gitlab-ci-local --pipeline-source schedule --schedule-name "npm Dependency Update" --list + +# Test OpenBSD snapshot schedule +gitlab-ci-local --pipeline-source schedule --schedule-name "Daily OpenBSD Snapshot Check" --list + +# Test other schedule (should show no jobs) +gitlab-ci-local --pipeline-source schedule --schedule-name "Other Schedule" --list +``` + +## Environment Variable Testing + +Test how environment variables affect pipeline behavior: + +```bash +# Set environment variables for testing +export CI_PIPELINE_SOURCE=schedule +export SCHEDULE_NAME="npm Dependency Update" + +# Run with environment variables +npx tsx ../../src/index.ts --list + +# Override with CLI options +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" --list +``` + +## Best Practices + +### 1. Use `--list` for Testing + +Always use `--list` or `--list-all` when testing pipeline logic to avoid actually executing jobs: + +```bash +# Good: Just list jobs +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" --list + +# Avoid: Actually execute jobs during testing +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" +``` + +### 2. Test Multiple Scenarios + +Test various combinations to ensure your conditional logic works correctly: + +```bash +# Test all pipeline sources +for source in push schedule merge_request_event web api external chat external_pull_request_event ondemand_dast_scan ondemand_dast_validation parent_pipeline pipeline security_orchestration_policy trigger webide; do + echo "=== Testing $source pipeline ===" + npx tsx ../../src/index.ts --pipeline-source $source --list + echo +done +``` + +### 3. Validate Conditional Logic + +Ensure your `rules` and conditional includes work as expected: + +```bash +# Test specific schedule names +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "npm Dependency Update" --list +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "mix Dependency Update" --list +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily OpenBSD Snapshot Check" --list + +# Test other pipeline sources +npx tsx ../../src/index.ts --pipeline-source trigger --list +npx tsx ../../src/index.ts --pipeline-source schedule --list +npx tsx ../../src/index.ts --pipeline-source external_pull_request_event --list +npx tsx ../../src/index.ts --pipeline-source ondemand_dast_scan --list +npx tsx ../../src/index.ts --pipeline-source parent_pipeline --list +npx tsx ../../src/index.ts --pipeline-source webide --list +``` + +## Troubleshooting + +### Setup Issues + +1. **Node.js Not Found**: + ```bash + # Check if Node.js is installed + node --version + npm --version + + # If not found, install Node.js first + # macOS: brew install node + # Ubuntu: sudo apt install nodejs npm + # Windows: Download from https://nodejs.org/ + ``` + +2. **Wrong Directory**: + ```bash + # Ensure you're in the gitlab-ci-local project root + pwd + ls -la package.json + # Should show the package.json file + + # Then navigate to example + cd examples/scheduled-pipeline-testing + ls -la .gitlab-ci.yml + # Should show the example .gitlab-ci.yml file + ``` + +3. **Dependencies Not Installed**: + ```bash + # Make sure you ran npm install in the project root + cd ../../ # Go back to project root + npm install + cd examples/scheduled-pipeline-testing + ``` + +### Pipeline Issues + +1. **Jobs Not Showing**: Check if the pipeline source and schedule name match your conditional logic +2. **Unexpected Jobs**: Verify that your `rules` are correctly excluding unwanted jobs +3. **Conditional Includes Not Working**: Ensure your include rules use the correct syntax + +### Debug Commands + +```bash +# Show all jobs including those set to 'never' +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" --list-all + +# Check environment variables +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Daily Check" --list --debug + +# Compare with standard pipeline +npx tsx ../../src/index.ts --pipeline-source push --list +``` + +## Advanced Usage + +### Testing Complex Rules + +For complex rule combinations, test each condition separately: + +```bash +# Test individual conditions +npx tsx ../../src/index.ts --pipeline-source schedule --list +npx tsx ../../src/index.ts --pipeline-source merge_request_event --list +npx tsx ../../src/index.ts --pipeline-source push --list + +# Test specific combinations +npx tsx ../../src/index.ts --pipeline-source schedule --schedule-name "Specific Schedule" --list +``` + +### Integration with CI/CD + +Use these testing techniques in your development workflow: + +1. **Pre-commit Testing**: Test pipeline changes locally before committing +2. **Branch Testing**: Test different pipeline configurations on feature branches +3. **Release Testing**: Verify pipeline behavior before releases + +## Conclusion + +This example demonstrates how GitLab CI Local's new pipeline simulation features can significantly improve your development workflow by allowing you to test complex pipeline configurations locally without pushing to the repository. + +By using `--pipeline-source` and `--schedule-name`, you can: + +- Test scheduled pipeline logic locally +- Validate conditional include rules +- Debug complex pipeline configurations +- Ensure pipeline changes work as expected before committing + +This leads to faster development cycles, fewer pipeline failures, and more confident deployments. diff --git a/src/argv.ts b/src/argv.ts index 6c0ccaeba..fbdc09836 100644 --- a/src/argv.ts +++ b/src/argv.ts @@ -6,6 +6,7 @@ import camelCase from "camelcase"; import {Utils} from "./utils.js"; import {WriteStreams} from "./write-streams.js"; import chalk from "chalk"; +import {VALID_PIPELINE_SOURCES, SCHEDULE_NAME_CONSTRAINTS} from "./constants.js"; async function isInGitRepository () { try { @@ -177,6 +178,45 @@ export class Argv { return variables; } + get environmentVariables (): {[key: string]: string} { + const variables: {[key: string]: string} = {}; + + // Merge CI_* environment variables + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith("CI_") || key === "SCHEDULE_NAME") { + if (value !== undefined) { + // Validate CI_PIPELINE_SOURCE if present + if (key === "CI_PIPELINE_SOURCE") { + if (!VALID_PIPELINE_SOURCES.includes(value as any)) { + console.warn(`Warning: Invalid CI_PIPELINE_SOURCE value: "${value}". Valid options are: ${VALID_PIPELINE_SOURCES.join(", ")}`); + } + } + + // Validate SCHEDULE_NAME if present + if (key === "SCHEDULE_NAME") { + if ((value as string).trim().length === 0) { + console.warn("Warning: SCHEDULE_NAME environment variable is empty"); + } else if ((value as string).length > SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH) { + console.warn(`Warning: SCHEDULE_NAME is very long (${(value as string).length} characters)`); + } + + // Check for invalid characters + const invalidChars = SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS.filter(char => + (value as string).includes(char) + ); + if (invalidChars.length > 0) { + console.warn(`Warning: SCHEDULE_NAME contains invalid characters: ${invalidChars.join(", ")}`); + } + } + + variables[key] = value; + } + } + } + + return variables; + } + get unsetVariables (): string[] { return this.map.get("unsetVariable") ?? []; } @@ -270,6 +310,14 @@ export class Argv { return this.map.get("fetchIncludes") ?? false; } + get pipelineSource (): string { + return this.map.get("pipelineSource") ?? "push"; + } + + get scheduleName (): string | undefined { + return this.map.get("scheduleName"); + } + get mountCache (): boolean { return this.map.get("mountCache") ?? false; } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..745025ffc --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2018 - 2025, Timo Pallach (timo@pallach.de). + +/** + * Valid GitLab CI pipeline sources + * These are the official pipeline source values supported by GitLab + */ +export const VALID_PIPELINE_SOURCES = [ + "push", + "schedule", + "merge_request_event", + "web", + "api", + "external", + "chat", + "external_pull_request_event", + "ondemand_dast_scan", + "ondemand_dast_validation", + "parent_pipeline", + "pipeline", + "security_orchestration_policy", + "trigger", + "webide", +] as const; + +/** + * Type for valid pipeline sources + */ +export type ValidPipelineSource = typeof VALID_PIPELINE_SOURCES[number]; + +/** + * Validation constants for schedule names + */ +export const SCHEDULE_NAME_CONSTRAINTS = { + MAX_LENGTH: 255, + INVALID_CHARS: ["<", ">", ":", "\"", "\\", "|", "?", "*"], + INVALID_CHARS_DESCRIPTION: "characters that are not allowed in filesystem names", +} as const; diff --git a/src/index.ts b/src/index.ts index e1ec21c48..3515b6afc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import {Argv} from "./argv.js"; import {AssertionError} from "assert"; import {Job, cleanupJobResources} from "./job.js"; import {GitlabRunnerPresetValues} from "./gitlab-preset.js"; +import {VALID_PIPELINE_SOURCES, SCHEDULE_NAME_CONSTRAINTS} from "./constants.js"; const jobs: Job[] = []; @@ -242,6 +243,38 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs)); description: "Fetch all external includes one more time", requiresArg: false, }) + .option("pipeline-source", { + type: "string", + description: "Simulate different pipeline sources (push, schedule, merge_request, web, api, external, chat, manual, etc.)", + requiresArg: true, + default: "push", + choices: VALID_PIPELINE_SOURCES, + coerce: (arg: string) => { + if (!VALID_PIPELINE_SOURCES.includes(arg as any)) { + throw new Error(`Invalid pipeline source: "${arg}". Valid options are: ${VALID_PIPELINE_SOURCES.join(", ")}`); + } + return arg; + }, + }) + .option("schedule-name", { + type: "string", + description: "Simulate specific schedule names for scheduled pipelines (e.g., 'Daily OpenBSD Snapshot Check', 'npm Dependency Update')", + requiresArg: true, + coerce: (arg: string) => { + if (arg.trim().length === 0) { + throw new Error("Schedule name cannot be empty. Please provide a valid schedule name."); + } + if (arg.length > SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH) { + throw new Error(`Schedule name is too long (${arg.length} characters). Maximum length is ${SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH} characters.`); + } + // Check for potentially problematic characters + const invalidChars = SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS.filter(char => arg.includes(char)); + if (invalidChars.length > 0) { + throw new Error(`Schedule name contains invalid characters: ${invalidChars.join(", ")}. Please use only valid characters.`); + } + return arg.trim(); + }, + }) .option("maximum-includes", { type: "number", description: "The maximum number of includes", diff --git a/src/predefined-variables.ts b/src/predefined-variables.ts index 84444153c..50aad32c1 100644 --- a/src/predefined-variables.ts +++ b/src/predefined-variables.ts @@ -10,13 +10,14 @@ type PredefinedVariablesOpts = { export function init ({gitData, argv, envMatchedVariables}: PredefinedVariablesOpts): {[name: string]: string} { - const _variables = {...envMatchedVariables, ...argv.variable}; + const _variables = {...envMatchedVariables, ...argv.variable, ...argv.environmentVariables}; // precedence: - // 1. cli option - // 2. gitlab variables files - // 3. values derieved implicitly from `git remote -v` - // 4. default value + // 1. environment variables (CI_*, SCHEDULE_NAME) + // 2. cli option + // 3. gitlab variables files + // 4. values derieved implicitly from `git remote -v` + // 5. default value const CI_SERVER_PROTOCOL = _variables["CI_SERVER_PROTOCOL"] ?? ((gitData.remote.schema === "http" || gitData.remote.schema === "https") ? gitData.remote.schema : "https"); const CI_SERVER_PORT = _variables["CI_SERVER_PORT"] ?? ((gitData.remote.schema === "http" || gitData.remote.schema === "https") ? gitData.remote.port : "443"); const CI_SERVER_SHELL_SSH_PORT = _variables["CI_SERVER_SHELL_SSH_PORT"] ?? ((gitData.remote.schema === "ssh") ? gitData.remote.port : "22"); @@ -27,7 +28,7 @@ export function init ({gitData, argv, envMatchedVariables}: PredefinedVariablesO const CI_PROJECT_NAMESPACE = gitData.remote.group; const CI_DEPENDENCY_PROXY_SERVER = CI_SERVER_FQDN.includes(":") ? CI_SERVER_FQDN : `${CI_SERVER_HOST}:${CI_SERVER_PORT}`; - const predefinedVariables: {[key: string]: string} = { + const predefinedVariables: {[name: string]: string} = { CI: "true", GITLAB_USER_LOGIN: gitData.user["GITLAB_USER_LOGIN"], GITLAB_USER_EMAIL: gitData.user["GITLAB_USER_EMAIL"], @@ -53,7 +54,7 @@ export function init ({gitData, argv, envMatchedVariables}: PredefinedVariablesO CI_COMMIT_MESSAGE: "Commit Title\nMore commit text", // Full commit message CI_COMMIT_DESCRIPTION: "More commit text", CI_DEFAULT_BRANCH: gitData.branches.default, - CI_PIPELINE_SOURCE: "push", + CI_PIPELINE_SOURCE: _variables["CI_PIPELINE_SOURCE"] ?? argv.pipelineSource, CI_SERVER_FQDN: CI_SERVER_FQDN, CI_SERVER_HOST: CI_SERVER_HOST, CI_SERVER_PORT: CI_SERVER_PORT, @@ -70,8 +71,18 @@ export function init ({gitData, argv, envMatchedVariables}: PredefinedVariablesO CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX: `${CI_DEPENDENCY_PROXY_SERVER}/${CI_PROJECT_ROOT_NAMESPACE}/dependency_proxy/containers`, CI_DEPENDENCY_PROXY_SERVER: CI_DEPENDENCY_PROXY_SERVER, CI_DEPENDENCY_PROXY_USER: "gitlab-ci-token", + + // Additional variables for scheduled pipelines + CI_PIPELINE_ID: "12345", + CI_PIPELINE_IID: "123", + CI_PIPELINE_URL: `${CI_SERVER_URL}/${gitData.remote.group}/${gitData.remote.project}/-/pipelines/${_variables["CI_PIPELINE_ID"] ?? "12345"}`, }; + // Add SCHEDULE_NAME if provided via CLI or environment + if (argv.scheduleName || _variables["SCHEDULE_NAME"]) { + predefinedVariables.SCHEDULE_NAME = _variables["SCHEDULE_NAME"] ?? argv.scheduleName; + } + // Delete variables the user intentionally wants unset for (const unsetVariable of argv.unsetVariables) { delete predefinedVariables[unsetVariable]; diff --git a/tests/constants.test.ts b/tests/constants.test.ts new file mode 100644 index 000000000..647d987f7 --- /dev/null +++ b/tests/constants.test.ts @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025, Timo Pallach (timo@pallach.de). + +import {VALID_PIPELINE_SOURCES, SCHEDULE_NAME_CONSTRAINTS, ValidPipelineSource} from "../src/constants.js"; + +describe("Constants and Validation", () => { + describe("VALID_PIPELINE_SOURCES", () => { + test("should contain all expected pipeline sources", () => { + const expectedSources = [ + "push", + "schedule", + "merge_request_event", + "web", + "api", + "external", + "chat", + "external_pull_request_event", + "ondemand_dast_scan", + "ondemand_dast_validation", + "parent_pipeline", + "pipeline", + "security_orchestration_policy", + "trigger", + "webide" + ]; + + expect(VALID_PIPELINE_SOURCES).toEqual(expectedSources); + }); + + test("should have correct length", () => { + expect(VALID_PIPELINE_SOURCES).toHaveLength(15); + }); + + test("should contain all individual pipeline sources", () => { + // Test each pipeline source individually for comprehensive coverage + const expectedSources = [ + "push", + "schedule", + "merge_request_event", + "web", + "api", + "external", + "chat", + "external_pull_request_event", + "ondemand_dast_scan", + "ondemand_dast_validation", + "parent_pipeline", + "pipeline", + "security_orchestration_policy", + "trigger", + "webide" + ]; + + expectedSources.forEach(source => { + expect(VALID_PIPELINE_SOURCES).toContain(source); + }); + }); + + test("should contain valid GitLab CI pipeline sources", () => { + // These are the official GitLab CI pipeline source values + expect(VALID_PIPELINE_SOURCES).toContain("push"); + expect(VALID_PIPELINE_SOURCES).toContain("schedule"); + expect(VALID_PIPELINE_SOURCES).toContain("merge_request_event"); + expect(VALID_PIPELINE_SOURCES).toContain("web"); + expect(VALID_PIPELINE_SOURCES).toContain("api"); + expect(VALID_PIPELINE_SOURCES).toContain("external"); + expect(VALID_PIPELINE_SOURCES).toContain("chat"); + expect(VALID_PIPELINE_SOURCES).toContain("external_pull_request_event"); + expect(VALID_PIPELINE_SOURCES).toContain("ondemand_dast_scan"); + expect(VALID_PIPELINE_SOURCES).toContain("ondemand_dast_validation"); + expect(VALID_PIPELINE_SOURCES).toContain("parent_pipeline"); + expect(VALID_PIPELINE_SOURCES).toContain("pipeline"); + expect(VALID_PIPELINE_SOURCES).toContain("security_orchestration_policy"); + expect(VALID_PIPELINE_SOURCES).toContain("trigger"); + expect(VALID_PIPELINE_SOURCES).toContain("webide"); + }); + }); + + describe("ValidPipelineSource type", () => { + test("should allow valid pipeline source values", () => { + const validSource: ValidPipelineSource = "schedule"; + expect(VALID_PIPELINE_SOURCES).toContain(validSource); + }); + + test("should allow all valid pipeline source values", () => { + // Test that all pipeline sources can be assigned to ValidPipelineSource type + const validSources: ValidPipelineSource[] = [ + "push", + "schedule", + "merge_request_event", + "web", + "api", + "external", + "chat", + "external_pull_request_event", + "ondemand_dast_scan", + "ondemand_dast_validation", + "parent_pipeline", + "pipeline", + "security_orchestration_policy", + "trigger", + "webide" + ]; + + validSources.forEach(source => { + expect(VALID_PIPELINE_SOURCES).toContain(source); + }); + }); + + test("should not allow invalid pipeline source values", () => { + // TypeScript should prevent this at compile time + // @ts-expect-error - This should fail type checking + const invalidSource: ValidPipelineSource = "invalid"; + expect(invalidSource).toBeDefined(); // This line should never execute + }); + }); + + describe("SCHEDULE_NAME_CONSTRAINTS", () => { + test("should have correct MAX_LENGTH", () => { + expect(SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH).toBe(255); + }); + + test("should have valid INVALID_CHARS array", () => { + expect(SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS).toBeInstanceOf(Array); + }); + + test("should detect invalid characters correctly", () => { + const invalidChars = SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS; + + // Test each character individually + expect(invalidChars).toContain("<"); + expect(invalidChars).toContain(">"); + expect(invalidChars).toContain(":"); + expect(invalidChars).toContain("\""); + expect(invalidChars).toContain("\\"); + expect(invalidChars).toContain("|"); + expect(invalidChars).toContain("?"); + expect(invalidChars).toContain("*"); + }); + + test("should allow valid characters", () => { + const validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."; + const invalidChars = SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS; + + for (const char of validChars) { + expect(invalidChars).not.toContain(char); + } + }); + + test("should have descriptive INVALID_CHARS_DESCRIPTION", () => { + expect(SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS_DESCRIPTION).toBe( + "characters that are not allowed in filesystem names" + ); + }); + + test("should have immutable MAX_LENGTH", () => { + const originalLength = SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH; + expect(originalLength).toBe(255); + }); + }); + + describe("Integration tests", () => { + test("should validate pipeline sources against constants", () => { + const testSources = ["push", "schedule", "invalid", "web"]; + const validSources = testSources.filter(source => + VALID_PIPELINE_SOURCES.includes(source as any) + ); + + expect(validSources).toEqual(["push", "schedule", "web"]); + expect(validSources).not.toContain("invalid"); + }); + + test("should validate all pipeline sources individually", () => { + // Test that each valid pipeline source is properly recognized + const allValidSources = [ + "push", + "schedule", + "merge_request_event", + "web", + "api", + "external", + "chat", + "external_pull_request_event", + "ondemand_dast_scan", + "ondemand_dast_validation", + "parent_pipeline", + "pipeline", + "security_orchestration_policy", + "trigger", + "webide" + ]; + + allValidSources.forEach(source => { + expect(VALID_PIPELINE_SOURCES.includes(source as any)).toBe(true); + }); + }); + + test("should validate schedule name constraints", () => { + const testNames = [ + "valid-name", + "name with spaces", + "nameinvalid:chars", + "a".repeat(300), // Too long + "" // Empty + ]; + + const validNames = testNames.filter(name => { + if (name.length === 0) return false; + if (name.length > SCHEDULE_NAME_CONSTRAINTS.MAX_LENGTH) return false; + if (SCHEDULE_NAME_CONSTRAINTS.INVALID_CHARS.some(char => name.includes(char))) return false; + return true; + }); + + expect(validNames).toEqual(["valid-name", "name with spaces"]); + expect(validNames).not.toContain("nameinvalid:chars"); + expect(validNames).not.toContain("a".repeat(300)); + expect(validNames).not.toContain(""); + }); + }); +}); diff --git a/tests/pipeline-simulation-basic.test.ts b/tests/pipeline-simulation-basic.test.ts new file mode 100644 index 000000000..48581bd28 --- /dev/null +++ b/tests/pipeline-simulation-basic.test.ts @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025, Timo Pallach (timo@pallach.de). + +import {init} from "../src/predefined-variables.js"; +import {VALID_PIPELINE_SOURCES} from "../src/constants.js"; + +describe("Pipeline Simulation - Basic Tests", () => { + test("should have init function available", () => { + expect(init).toBeDefined(); + expect(typeof init).toBe("function"); + }); + + test("should support all valid pipeline sources", () => { + VALID_PIPELINE_SOURCES.forEach(source => { + expect(source).toBeDefined(); + expect(typeof source).toBe("string"); + expect(source.length).toBeGreaterThan(0); + }); + }); + + test("should have correct number of pipeline sources", () => { + expect(VALID_PIPELINE_SOURCES).toHaveLength(15); // Updated to match official GitLab docs + }); + + test("should include essential pipeline sources", () => { + expect(VALID_PIPELINE_SOURCES).toContain("push"); + expect(VALID_PIPELINE_SOURCES).toContain("schedule"); + expect(VALID_PIPELINE_SOURCES).toContain("merge_request_event"); + expect(VALID_PIPELINE_SOURCES).toContain("web"); + expect(VALID_PIPELINE_SOURCES).toContain("api"); + expect(VALID_PIPELINE_SOURCES).toContain("external"); + expect(VALID_PIPELINE_SOURCES).toContain("chat"); + }); +});