mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-05-18 05:05:35 +02:00
feat: add GitLab pipeline integration
This commit is contained in:
107
.gitlab-ci.yml
Normal file
107
.gitlab-ci.yml
Normal file
@@ -0,0 +1,107 @@
|
||||
# GitLab CI/CD Pipeline for wrkflw
|
||||
# This pipeline will build and test the Rust project
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- release
|
||||
|
||||
variables:
|
||||
CARGO_HOME: ${CI_PROJECT_DIR}/.cargo
|
||||
RUST_VERSION: stable
|
||||
|
||||
# Cache dependencies between jobs
|
||||
cache:
|
||||
paths:
|
||||
- .cargo/
|
||||
- target/
|
||||
|
||||
# Lint job - runs rustfmt and clippy
|
||||
lint:
|
||||
stage: lint
|
||||
image: rust:${RUST_VERSION}
|
||||
script:
|
||||
- rustup component add rustfmt clippy
|
||||
- cargo fmt -- --check
|
||||
- cargo clippy -- -D warnings
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: always
|
||||
|
||||
# Build job - builds the application
|
||||
build:
|
||||
stage: build
|
||||
image: rust:${RUST_VERSION}
|
||||
script:
|
||||
- cargo build --verbose
|
||||
artifacts:
|
||||
paths:
|
||||
- target/debug/wrkflw
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: always
|
||||
|
||||
# Test job - runs unit and integration tests
|
||||
test:
|
||||
stage: test
|
||||
image: rust:${RUST_VERSION}
|
||||
script:
|
||||
- cargo test --verbose
|
||||
needs:
|
||||
- build
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: always
|
||||
|
||||
# Release job - creates a release build
|
||||
release:
|
||||
stage: release
|
||||
image: rust:${RUST_VERSION}
|
||||
script:
|
||||
- cargo build --release --verbose
|
||||
artifacts:
|
||||
paths:
|
||||
- target/release/wrkflw
|
||||
expire_in: 1 month
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "web" && $BUILD_RELEASE == "true"
|
||||
when: always
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
# Custom job for documentation
|
||||
docs:
|
||||
stage: release
|
||||
image: rust:${RUST_VERSION}
|
||||
script:
|
||||
- cargo doc --no-deps
|
||||
artifacts:
|
||||
paths:
|
||||
- target/doc/
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "web" && $BUILD_DOCS == "true"
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- when: never
|
||||
83
GITLAB_USAGE.md
Normal file
83
GITLAB_USAGE.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Using wrkflw with GitLab Pipelines
|
||||
|
||||
This guide explains how to use the `wrkflw` tool to trigger GitLab CI/CD pipelines.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A GitLab repository with a `.gitlab-ci.yml` file
|
||||
2. A GitLab personal access token with API access
|
||||
3. `wrkflw` installed on your system
|
||||
|
||||
## Setting Up
|
||||
|
||||
1. Create a GitLab personal access token:
|
||||
- Go to GitLab > User Settings > Access Tokens
|
||||
- Create a token with `api` scope
|
||||
- Copy the token value
|
||||
|
||||
2. Set the token as an environment variable:
|
||||
```bash
|
||||
export GITLAB_TOKEN=your_token_here
|
||||
```
|
||||
|
||||
## Triggering a Pipeline
|
||||
|
||||
You can trigger a GitLab pipeline using the `trigger-gitlab` command:
|
||||
|
||||
```bash
|
||||
# Trigger using the default branch
|
||||
wrkflw trigger-gitlab
|
||||
|
||||
# Trigger on a specific branch
|
||||
wrkflw trigger-gitlab --branch feature-branch
|
||||
|
||||
# Trigger with custom variables
|
||||
wrkflw trigger-gitlab --variable BUILD_RELEASE=true
|
||||
```
|
||||
|
||||
### Example: Triggering a Release Build
|
||||
|
||||
To trigger the release build job in our sample pipeline:
|
||||
|
||||
```bash
|
||||
wrkflw trigger-gitlab --variable BUILD_RELEASE=true
|
||||
```
|
||||
|
||||
This will set the `BUILD_RELEASE` variable to `true`, which activates the release job in our sample pipeline.
|
||||
|
||||
### Example: Building Documentation
|
||||
|
||||
To trigger the documentation build job:
|
||||
|
||||
```bash
|
||||
wrkflw trigger-gitlab --variable BUILD_DOCS=true
|
||||
```
|
||||
|
||||
## Controlling Job Execution with Variables
|
||||
|
||||
Our sample GitLab pipeline is configured to make certain jobs conditional based on variables. You can use the `--variable` flag to control which jobs run:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `BUILD_RELEASE` | Set to `true` to run the release job |
|
||||
| `BUILD_DOCS` | Set to `true` to build documentation |
|
||||
|
||||
## Checking Pipeline Status
|
||||
|
||||
After triggering a pipeline, you can check its status directly on GitLab:
|
||||
|
||||
1. Navigate to your GitLab repository
|
||||
2. Go to CI/CD > Pipelines
|
||||
3. Find your recently triggered pipeline
|
||||
|
||||
The `wrkflw` command will also provide a direct URL to the pipeline after triggering.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Verify your GitLab token is set correctly
|
||||
2. Check that you're in a repository with a valid GitLab remote URL
|
||||
3. Ensure your `.gitlab-ci.yml` file is valid
|
||||
4. Check that your GitLab token has API access permissions
|
||||
5. Review GitLab's CI/CD pipeline logs for detailed error information
|
||||
79
examples/trigger_gitlab.sh
Executable file
79
examples/trigger_gitlab.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# Example script to trigger GitLab pipelines using wrkflw
|
||||
|
||||
# Check if GITLAB_TOKEN is set
|
||||
if [ -z "${GITLAB_TOKEN}" ]; then
|
||||
echo "Error: GITLAB_TOKEN environment variable is not set."
|
||||
echo "Please set it with: export GITLAB_TOKEN=your_token_here"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure we're in a Git repository
|
||||
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
echo "Error: Not in a Git repository."
|
||||
echo "Please run this script from within a Git repository with a GitLab remote."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for .gitlab-ci.yml file
|
||||
if [ ! -f .gitlab-ci.yml ]; then
|
||||
echo "Warning: No .gitlab-ci.yml file found in the current directory."
|
||||
echo "The pipeline trigger might fail if there is no pipeline configuration."
|
||||
fi
|
||||
|
||||
# Function to display help
|
||||
show_help() {
|
||||
echo "GitLab Pipeline Trigger Examples"
|
||||
echo "--------------------------------"
|
||||
echo "Usage: $0 [example-number]"
|
||||
echo ""
|
||||
echo "Available examples:"
|
||||
echo " 1: Trigger default pipeline on the current branch"
|
||||
echo " 2: Trigger pipeline on main branch"
|
||||
echo " 3: Trigger release build"
|
||||
echo " 4: Trigger documentation build"
|
||||
echo " 5: Trigger pipeline with multiple variables"
|
||||
echo ""
|
||||
echo "For custom commands, modify this script or run wrkflw directly:"
|
||||
echo " wrkflw trigger-gitlab [options]"
|
||||
}
|
||||
|
||||
# No arguments, show help
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle examples
|
||||
case "$1" in
|
||||
"1")
|
||||
echo "Triggering default pipeline on the current branch..."
|
||||
wrkflw trigger-gitlab
|
||||
;;
|
||||
|
||||
"2")
|
||||
echo "Triggering pipeline on main branch..."
|
||||
wrkflw trigger-gitlab --branch main
|
||||
;;
|
||||
|
||||
"3")
|
||||
echo "Triggering release build..."
|
||||
wrkflw trigger-gitlab --variable BUILD_RELEASE=true
|
||||
;;
|
||||
|
||||
"4")
|
||||
echo "Triggering documentation build..."
|
||||
wrkflw trigger-gitlab --variable BUILD_DOCS=true
|
||||
;;
|
||||
|
||||
"5")
|
||||
echo "Triggering pipeline with multiple variables..."
|
||||
wrkflw trigger-gitlab --variable BUILD_RELEASE=true --variable BUILD_DOCS=true
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown example: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
276
src/gitlab.rs
Normal file
276
src/gitlab.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use reqwest::header;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GitlabError {
|
||||
#[error("HTTP error: {0}")]
|
||||
RequestError(#[from] reqwest::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to parse Git repository URL: {0}")]
|
||||
GitParseError(String),
|
||||
|
||||
#[error("GitLab token not found. Please set GITLAB_TOKEN environment variable")]
|
||||
TokenNotFound,
|
||||
|
||||
#[error("API error: {status} - {message}")]
|
||||
ApiError { status: u16, message: String },
|
||||
}
|
||||
|
||||
/// Information about a GitLab repository
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RepoInfo {
|
||||
pub namespace: String,
|
||||
pub project: String,
|
||||
pub default_branch: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref GITLAB_REPO_REGEX: Regex =
|
||||
Regex::new(r"(?:https://gitlab\.com/|git@gitlab\.com:)([^/]+)/([^/.]+)(?:\.git)?")
|
||||
.expect("Failed to compile GitLab repo regex - this is a critical error");
|
||||
}
|
||||
|
||||
/// Extract repository information from the current git repository for GitLab
|
||||
pub fn get_repo_info() -> Result<RepoInfo, GitlabError> {
|
||||
let output = Command::new("git")
|
||||
.args(["remote", "get-url", "origin"])
|
||||
.output()
|
||||
.map_err(|e| GitlabError::GitParseError(format!("Failed to execute git command: {}", e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GitlabError::GitParseError(
|
||||
"Failed to get git origin URL. Are you in a git repository?".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
if let Some(captures) = GITLAB_REPO_REGEX.captures(&url) {
|
||||
let namespace = captures
|
||||
.get(1)
|
||||
.ok_or_else(|| {
|
||||
GitlabError::GitParseError(
|
||||
"Unable to extract namespace from GitLab URL".to_string(),
|
||||
)
|
||||
})?
|
||||
.as_str()
|
||||
.to_string();
|
||||
|
||||
let project = captures
|
||||
.get(2)
|
||||
.ok_or_else(|| {
|
||||
GitlabError::GitParseError(
|
||||
"Unable to extract project name from GitLab URL".to_string(),
|
||||
)
|
||||
})?
|
||||
.as_str()
|
||||
.to_string();
|
||||
|
||||
// Get the default branch
|
||||
let branch_output = Command::new("git")
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
GitlabError::GitParseError(format!("Failed to execute git command: {}", e))
|
||||
})?;
|
||||
|
||||
if !branch_output.status.success() {
|
||||
return Err(GitlabError::GitParseError(
|
||||
"Failed to get current branch".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let default_branch = String::from_utf8_lossy(&branch_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(RepoInfo {
|
||||
namespace,
|
||||
project,
|
||||
default_branch,
|
||||
})
|
||||
} else {
|
||||
Err(GitlabError::GitParseError(format!(
|
||||
"URL '{}' is not a valid GitLab repository URL",
|
||||
url
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of available pipeline files in the repository
|
||||
pub async fn list_pipelines(_repo_info: &RepoInfo) -> Result<Vec<String>, GitlabError> {
|
||||
// GitLab CI/CD pipelines are defined in .gitlab-ci.yml files
|
||||
let pipeline_file = Path::new(".gitlab-ci.yml");
|
||||
|
||||
if !pipeline_file.exists() {
|
||||
return Err(GitlabError::IoError(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"GitLab CI/CD pipeline file not found (.gitlab-ci.yml)",
|
||||
)));
|
||||
}
|
||||
|
||||
// In GitLab, there's typically a single pipeline file with multiple jobs
|
||||
// Return a list with just that file name
|
||||
Ok(vec!["gitlab-ci".to_string()])
|
||||
}
|
||||
|
||||
/// Trigger a pipeline on GitLab
|
||||
pub async fn trigger_pipeline(
|
||||
branch: Option<&str>,
|
||||
variables: Option<HashMap<String, String>>,
|
||||
) -> Result<(), GitlabError> {
|
||||
// Get GitLab token from environment
|
||||
let token = std::env::var("GITLAB_TOKEN").map_err(|_| GitlabError::TokenNotFound)?;
|
||||
|
||||
// Trim the token to remove any leading or trailing whitespace
|
||||
let trimmed_token = token.trim();
|
||||
|
||||
// Get repository information
|
||||
let repo_info = get_repo_info()?;
|
||||
println!(
|
||||
"GitLab Repository: {}/{}",
|
||||
repo_info.namespace, repo_info.project
|
||||
);
|
||||
|
||||
// Prepare the request payload
|
||||
let branch_ref = branch.unwrap_or(&repo_info.default_branch);
|
||||
println!("Using branch: {}", branch_ref);
|
||||
|
||||
// Create simplified payload
|
||||
let mut payload = serde_json::json!({
|
||||
"ref": branch_ref
|
||||
});
|
||||
|
||||
// Add variables if provided
|
||||
if let Some(vars_map) = variables {
|
||||
// GitLab expects variables in a specific format
|
||||
let formatted_vars: Vec<serde_json::Value> = vars_map
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
serde_json::json!({
|
||||
"key": key,
|
||||
"value": value
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
payload["variables"] = serde_json::json!(formatted_vars);
|
||||
println!("With variables: {:?}", vars_map);
|
||||
}
|
||||
|
||||
// URL encode the namespace and project for use in URL
|
||||
let encoded_namespace = urlencoding::encode(&repo_info.namespace);
|
||||
let encoded_project = urlencoding::encode(&repo_info.project);
|
||||
|
||||
// Send the pipeline trigger request
|
||||
let url = format!(
|
||||
"https://gitlab.com/api/v4/projects/{encoded_namespace}%2F{encoded_project}/pipeline",
|
||||
encoded_namespace = encoded_namespace,
|
||||
encoded_project = encoded_project,
|
||||
);
|
||||
|
||||
println!("Triggering pipeline at URL: {}", url);
|
||||
|
||||
// Create a reqwest client
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Send the request using reqwest
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("PRIVATE-TOKEN", trimmed_token)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(GitlabError::RequestError)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().as_u16();
|
||||
let error_message = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| format!("Unknown error (HTTP {})", status));
|
||||
|
||||
// Add more detailed error information
|
||||
let error_details = if status == 404 {
|
||||
"Project not found or token doesn't have access to it. This could be due to:\n\
|
||||
1. The project doesn't exist\n\
|
||||
2. The GitLab token doesn't have sufficient permissions\n\
|
||||
Please check:\n\
|
||||
- The repository URL is correct\n\
|
||||
- Your GitLab token has the correct scope (api access)\n\
|
||||
- Your token has access to the project"
|
||||
} else if status == 401 {
|
||||
"Unauthorized. Your GitLab token may be invalid or expired."
|
||||
} else {
|
||||
&error_message
|
||||
};
|
||||
|
||||
return Err(GitlabError::ApiError {
|
||||
status,
|
||||
message: error_details.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Parse response to get pipeline ID
|
||||
let pipeline_info: serde_json::Value = response.json().await?;
|
||||
let pipeline_id = pipeline_info["id"].as_i64().unwrap_or(0);
|
||||
let pipeline_url = format!(
|
||||
"https://gitlab.com/{}/{}/pipelines/{}",
|
||||
repo_info.namespace, repo_info.project, pipeline_id
|
||||
);
|
||||
|
||||
println!("Pipeline triggered successfully!");
|
||||
println!("View pipeline at: {}", pipeline_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_gitlab_url_https() {
|
||||
let url = "https://gitlab.com/mygroup/myproject.git";
|
||||
assert!(GITLAB_REPO_REGEX.is_match(url));
|
||||
|
||||
let captures = GITLAB_REPO_REGEX.captures(url).unwrap();
|
||||
assert_eq!(captures.get(1).unwrap().as_str(), "mygroup");
|
||||
assert_eq!(captures.get(2).unwrap().as_str(), "myproject");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gitlab_url_ssh() {
|
||||
let url = "git@gitlab.com:mygroup/myproject.git";
|
||||
assert!(GITLAB_REPO_REGEX.is_match(url));
|
||||
|
||||
let captures = GITLAB_REPO_REGEX.captures(url).unwrap();
|
||||
assert_eq!(captures.get(1).unwrap().as_str(), "mygroup");
|
||||
assert_eq!(captures.get(2).unwrap().as_str(), "myproject");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gitlab_url_no_git_extension() {
|
||||
let url = "https://gitlab.com/mygroup/myproject";
|
||||
assert!(GITLAB_REPO_REGEX.is_match(url));
|
||||
|
||||
let captures = GITLAB_REPO_REGEX.captures(url).unwrap();
|
||||
assert_eq!(captures.get(1).unwrap().as_str(), "mygroup");
|
||||
assert_eq!(captures.get(2).unwrap().as_str(), "myproject");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_url() {
|
||||
let url = "https://github.com/myuser/myrepo.git";
|
||||
assert!(!GITLAB_REPO_REGEX.is_match(url));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user