296 lines
8.1 KiB
Go
296 lines
8.1 KiB
Go
|
|
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{}
|
||
|
|
}
|
||
|
|
}
|