Migrate pipelines - Best Effort
This commit is contained in:
parent
4e7cf06abf
commit
111f06ddf6
6 changed files with 620 additions and 5 deletions
295
internal/converter/pipeline.go
Normal file
295
internal/converter/pipeline.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// AzurePipeline represents the structure of azure-pipelines.yml
|
||||
type AzurePipeline struct {
|
||||
Trigger interface{} `yaml:"trigger"`
|
||||
PR interface{} `yaml:"pr"`
|
||||
Pool interface{} `yaml:"pool"`
|
||||
Variables map[string]interface{} `yaml:"variables"`
|
||||
Stages []AzureStage `yaml:"stages"`
|
||||
Jobs []AzureJob `yaml:"jobs"`
|
||||
Steps []AzureStep `yaml:"steps"`
|
||||
}
|
||||
|
||||
type AzureStage struct {
|
||||
Stage string `yaml:"stage"`
|
||||
Jobs []AzureJob `yaml:"jobs"`
|
||||
}
|
||||
|
||||
type AzureJob struct {
|
||||
Job string `yaml:"job"`
|
||||
Pool interface{} `yaml:"pool"`
|
||||
Steps []AzureStep `yaml:"steps"`
|
||||
}
|
||||
|
||||
type AzureStep struct {
|
||||
Task string `yaml:"task,omitempty"`
|
||||
Script string `yaml:"script,omitempty"`
|
||||
Powershell string `yaml:"powershell,omitempty"`
|
||||
Bash string `yaml:"bash,omitempty"`
|
||||
DisplayName string `yaml:"displayName,omitempty"`
|
||||
Inputs map[string]string `yaml:"inputs,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
}
|
||||
|
||||
// ForgejoWorkflow represents GitHub Actions compatible workflow
|
||||
type ForgejoWorkflow struct {
|
||||
Name string `yaml:"name"`
|
||||
On interface{} `yaml:"on"`
|
||||
Jobs map[string]ForgejoJob `yaml:"jobs"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
}
|
||||
|
||||
type ForgejoJob struct {
|
||||
RunsOn string `yaml:"runs-on"`
|
||||
Steps []ForgejoStep `yaml:"steps"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
Needs []string `yaml:"needs,omitempty"`
|
||||
Timeout int `yaml:"timeout-minutes,omitempty"`
|
||||
}
|
||||
|
||||
type ForgejoStep struct {
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Run string `yaml:"run,omitempty"`
|
||||
With map[string]string `yaml:"with,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
}
|
||||
|
||||
// Converter handles the transformation
|
||||
type Converter struct {
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
func New() *Converter {
|
||||
return &Converter{
|
||||
Warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converter) ConvertAzureToForgejo(azureYAML string, repoName string) (string, error) {
|
||||
var azure AzurePipeline
|
||||
if err := yaml.Unmarshal([]byte(azureYAML), &azure); err != nil {
|
||||
return "", fmt.Errorf("failed to parse azure-pipelines.yml: %w", err)
|
||||
}
|
||||
|
||||
forgejo := ForgejoWorkflow{
|
||||
Name: fmt.Sprintf("Migrated Azure Pipeline - %s", repoName),
|
||||
Jobs: make(map[string]ForgejoJob),
|
||||
Env: make(map[string]string),
|
||||
}
|
||||
|
||||
// Convert trigger
|
||||
forgejo.On = c.convertTrigger(azure.Trigger)
|
||||
|
||||
// Convert variables
|
||||
for k, v := range azure.Variables {
|
||||
if str, ok := v.(string); ok {
|
||||
forgejo.Env[k] = str
|
||||
} else if m, ok := v.(map[string]interface{}); ok {
|
||||
if val, exists := m["value"]; exists {
|
||||
forgejo.Env[k] = fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different structures: steps at root, jobs at root, or stages
|
||||
if len(azure.Steps) > 0 {
|
||||
// Simple pipeline with just steps
|
||||
job := c.convertStepsToJob(azure.Steps, azure.Pool)
|
||||
forgejo.Jobs["build"] = job
|
||||
} else if len(azure.Jobs) > 0 {
|
||||
// Multiple jobs
|
||||
for _, job := range azure.Jobs {
|
||||
fjJob := c.convertStepsToJob(job.Steps, job.Pool)
|
||||
fjJob.Env = job.Steps[0].Env // Simplified
|
||||
forgejo.Jobs[job.Job] = fjJob
|
||||
}
|
||||
} else if len(azure.Stages) > 0 {
|
||||
// Stages become job dependencies
|
||||
prevJob := ""
|
||||
for _, stage := range azure.Stages {
|
||||
for _, job := range stage.Jobs {
|
||||
fjJob := c.convertStepsToJob(job.Steps, job.Pool)
|
||||
if prevJob != "" {
|
||||
fjJob.Needs = []string{prevJob}
|
||||
}
|
||||
jobName := fmt.Sprintf("%s_%s", stage.Stage, job.Job)
|
||||
forgejo.Jobs[jobName] = fjJob
|
||||
prevJob = jobName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize
|
||||
out, err := yaml.Marshal(forgejo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add header comment
|
||||
header := fmt.Sprintf("# Migrated from Azure DevOps Pipeline\n# Repository: %s\n# WARNING: Review this file carefully. Azure-specific tasks require manual conversion.\n\n", repoName)
|
||||
|
||||
return header + string(out), nil
|
||||
}
|
||||
|
||||
func (c *Converter) convertTrigger(trigger interface{}) interface{} {
|
||||
// Simple conversion: Azure trigger -> GitHub on.push
|
||||
if trigger == nil {
|
||||
return map[string]interface{}{
|
||||
"push": map[string]interface{}{
|
||||
"branches": []string{"main", "master"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If trigger is a list (branches), convert to push.branches
|
||||
if branches, ok := trigger.([]interface{}); ok {
|
||||
return map[string]interface{}{
|
||||
"push": map[string]interface{}{
|
||||
"branches": branches,
|
||||
},
|
||||
"pull_request": map[string]interface{}{
|
||||
"branches": branches,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return trigger
|
||||
}
|
||||
|
||||
func (c *Converter) convertStepsToJob(steps []AzureStep, pool interface{}) ForgejoJob {
|
||||
job := ForgejoJob{
|
||||
RunsOn: c.convertPool(pool),
|
||||
Steps: make([]ForgejoStep, 0),
|
||||
}
|
||||
|
||||
// Add checkout step
|
||||
job.Steps = append(job.Steps, ForgejoStep{
|
||||
Uses: "actions/checkout@v3",
|
||||
Name: "Checkout",
|
||||
})
|
||||
|
||||
for _, step := range steps {
|
||||
fjStep := ForgejoStep{}
|
||||
|
||||
if step.Script != "" {
|
||||
fjStep.Run = step.Script
|
||||
fjStep.Name = step.DisplayName
|
||||
} else if step.Bash != "" {
|
||||
fjStep.Run = step.Bash
|
||||
fjStep.Name = step.DisplayName
|
||||
fjStep.Run = "bash -c '" + strings.ReplaceAll(step.Bash, "'", "'\\''") + "'"
|
||||
} else if step.Powershell != "" {
|
||||
fjStep.Run = step.Powershell
|
||||
fjStep.Name = step.DisplayName
|
||||
c.Warnings = append(c.Warnings, fmt.Sprintf("PowerShell script detected: %s", step.DisplayName))
|
||||
} else if step.Task != "" {
|
||||
// Convert known Azure tasks to actions
|
||||
fjStep = c.convertAzureTask(step)
|
||||
if fjStep.Uses == "" && fjStep.Run == "" {
|
||||
fjStep.Run = fmt.Sprintf("# TODO: Convert Azure Task '%s' manually", step.Task)
|
||||
fjStep.Name = fmt.Sprintf("[TODO] %s", step.DisplayName)
|
||||
c.Warnings = append(c.Warnings, fmt.Sprintf("Cannot automatically convert task: %s", step.Task))
|
||||
}
|
||||
}
|
||||
|
||||
if fjStep.Name == "" {
|
||||
fjStep.Name = step.DisplayName
|
||||
}
|
||||
|
||||
job.Steps = append(job.Steps, fjStep)
|
||||
}
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
func (c *Converter) convertPool(pool interface{}) string {
|
||||
if pool == nil {
|
||||
return "ubuntu-latest"
|
||||
}
|
||||
|
||||
// Pool can be a string or a map with vmImage
|
||||
if str, ok := pool.(string); ok {
|
||||
return c.mapAgent(str)
|
||||
}
|
||||
|
||||
if m, ok := pool.(map[string]interface{}); ok {
|
||||
if vm, exists := m["vmImage"]; exists {
|
||||
return c.mapAgent(vm.(string))
|
||||
}
|
||||
if name, exists := m["name"]; exists {
|
||||
// Self-hosted pool
|
||||
return fmt.Sprintf("self-hosted,%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
return "ubuntu-latest"
|
||||
}
|
||||
|
||||
func (c *Converter) mapAgent(azureImage string) string {
|
||||
// Map Azure VM images to Forgejo/Gitea runner labels
|
||||
mappings := map[string]string{
|
||||
"ubuntu-latest": "ubuntu-latest",
|
||||
"ubuntu-20.04": "ubuntu-20.04",
|
||||
"ubuntu-22.04": "ubuntu-22.04",
|
||||
"windows-latest": "windows-latest",
|
||||
"macOS-latest": "macos-latest",
|
||||
"macOS-11": "macos-11",
|
||||
}
|
||||
|
||||
if mapped, ok := mappings[azureImage]; ok {
|
||||
return mapped
|
||||
}
|
||||
|
||||
c.Warnings = append(c.Warnings, fmt.Sprintf("Unknown VM image '%s', defaulting to ubuntu-latest", azureImage))
|
||||
return "ubuntu-latest"
|
||||
}
|
||||
|
||||
func (c *Converter) convertAzureTask(step AzureStep) ForgejoStep {
|
||||
// Map common Azure tasks to GitHub Actions
|
||||
switch step.Task {
|
||||
case "AzureCLI@2", "AzureCLI@1":
|
||||
return ForgejoStep{
|
||||
Name: step.DisplayName,
|
||||
Run: "# Azure CLI step - configure azure/login action manually\n# az login ...\n" + step.Inputs["script"],
|
||||
}
|
||||
case "Docker@2":
|
||||
return ForgejoStep{
|
||||
Uses: "docker/build-push-action@v4",
|
||||
Name: step.DisplayName,
|
||||
With: map[string]string{
|
||||
"push": step.Inputs["push"],
|
||||
"tags": step.Inputs["containerRegistry"] + "/" + step.Inputs["repository"],
|
||||
},
|
||||
}
|
||||
case "PublishTestResults@2":
|
||||
return ForgejoStep{
|
||||
Uses: "actions/upload-artifact@v3",
|
||||
Name: step.DisplayName,
|
||||
With: map[string]string{
|
||||
"name": "test-results",
|
||||
"path": step.Inputs["testResultsFiles"],
|
||||
},
|
||||
}
|
||||
case "CopyFiles@2":
|
||||
return ForgejoStep{
|
||||
Run: fmt.Sprintf("cp -r %s %s", step.Inputs["SourceFolder"], step.Inputs["TargetFolder"]),
|
||||
Name: step.DisplayName,
|
||||
}
|
||||
case "ArchiveFiles@2":
|
||||
return ForgejoStep{
|
||||
Run: fmt.Sprintf("zip -r %s %s", step.Inputs["archiveFile"], step.Inputs["rootFolderOrFile"]),
|
||||
Name: step.DisplayName,
|
||||
}
|
||||
default:
|
||||
return ForgejoStep{}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue