Laboratorio 1: GitLab — crear repos y remotos

Abstract

Por qué GitLab y qué aprenderemos

En este curso utilizaremos GitLab, una plataforma similar a GitHub pero con un enfoque más completo en integración continua (CI/CD), gestión de dependencias y publicación de paquetes. Es ampliamente usada en entornos profesionales, especialmente cuando las organizaciones mantienen su propia infraestructura de repositorios.

Con GitLab practicaremos el flujo completo de trabajo: crear, clonar, organizar, versionar y publicar bibliotecas de software. Más adelante, usaremos su Package Registry para distribuir nuestras bibliotecas dentro de pipelines automatizados.

Ventajas
  • CI/CD integrado: GitLab incluye un sistema de pipelines nativo (GitLab CI) sin depender de servicios externos, ideal para automatizar pruebas, compilaciones y despliegues. GitHub ofrece una solución similar con GitHub Actions, pero con una configuración separada en YAML y límites distintos.
  • Registro de paquetes unificado: permite publicar y consumir bibliotecas o imágenes de contenedores en el mismo entorno, simplificando el flujo build → test → publish. En GitHub, esta función está separada en GitHub Packages.
  • Instalación autogestionada: muchas organizaciones lo implementan en servidores propios, facilitando el cumplimiento de políticas de seguridad y confidencialidad. GitHub ofrece una alternativa con Enterprise Server, pero bajo licenciamiento específico.
  • Gestión avanzada de roles y grupos: ofrece jerarquías y permisos más detallados, especialmente útiles en equipos grandes o con múltiples proyectos relacionados.
  • Automatización por línea de comandos mediante glab : la CLI oficial de GitLab permite crear, clonar o administrar repositorios sin usar la interfaz web. GitHub cuenta con su propia CLI ( gh ) con fines similares.
Limitaciones
  • Menor comunidad pública: GitHub sigue siendo el principal espacio de colaboración abierta, por lo que los proyectos en GitLab pueden tener menor visibilidad y participación al inicio. 1
  • Curva de aprendizaje más alta: la interfaz y las opciones de CI/CD ofrecen mayor control, pero también más complejidad para quienes comienzan.
  • Límites más estrictos en la nube: la versión gratuita de GitLab Cloud tiene menor espacio de almacenamiento y minutos de CI que GitHub.
  • Sin equivalente directo a GitHub Classroom: GitLab no incluye una herramienta integrada para gestionar tareas o repositorios de estudiantes. Aunque pueden configurarse soluciones alternativas con grupos y scripts, GitHub Classroom sigue siendo más conveniente en contextos educativos.

Hoy aprenderemos a:

  • Crear repositorios remotos automáticamente.
  • Inicializar proyectos locales y enlazarlos con su remoto.
  • Automatizar ambos pasos en un solo script.

Scripts del laboratorio

Requisitos previos

Antes de comenzar, asegúrate de haber iniciado sesión en GitLab desde la línea de comandos con:

Desde la terminal
glab auth login
Iniciar sesión en GitLab CLI

Todos los scripts son idempotentes: puedes ejecutarlos varias veces sin generar duplicados ni inconsistencias.

...

...
path/to/script.ps1
#Requires -Version 7.5
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string] $Path
)

$invoker = Join-Path $PSScriptRoot '..' 'core' 'Invoke-Tool.ps1' -Resolve

$repoPath = $PSCmdlet.GetResolvedProviderPathFromPSPath($Path)

try {
    Write-Verbose "Checking if '$repoPath' is inside a Git work tree"
    & $invoker git -C $repoPath rev-parse --is-inside-work-tree | Out-Null
    $true
}
catch {
    Write-Verbose "'$repoPath' is not inside a Git work tree"
    $false
}
...

¿Qué acabamos de hacer?

Crear repo remoto en GitLab

Este script crea un repositorio remoto en GitLab usando glab , de forma idempotente: primero verifica su existencia y, si ya existe, devuelve un resultado estable sin fallar. Además, respeta -WhatIf / -Confirm gracias a SupportsShouldProcess, y normaliza el nombre para cumplir con las restricciones del hosting.

Creación idempotente de repositorios en GitLab
scripts/git/New-GitLabRepository.ps1
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Name,

    [switch] $Public
)

$normalized = $Name.ToLowerInvariant() -replace '\s+', '-' -replace '[^a-z0-9-]', ''
if ([string]::IsNullOrWhiteSpace($normalized)) {
    throw [System.ArgumentException]::new(
        "Name '$Name' is not valid after normalizing to '$normalized' (only [a-z0-9-])."
    )
}

$visibility = if ($Public) { 'public' } else { 'private' }

# Invocador utilitario para ejecutar binarios externos y capturar ExitCode/Output.
$invoker = Join-Path $PSScriptRoot '..' 'core' 'Invoke-Tool.ps1' -Resolve

try {
    # Idempotencia: salir en éxito si ya existe.
    & $invoker glab repo view $normalized | Out-Null
    Write-Verbose "Repository '$normalized' already exists. Not creating it again."
    return [pscustomobject]@{
        Name       = $normalized
        Visibility = $visibility
        Created    = $false
        Output     = @()
        Message    = 'Repository already exists'
    }
}
catch {
    Write-Verbose "Repo '$normalized' not found. Will attempt to create it."
}

if ($PSCmdlet.ShouldProcess($normalized, "Create GitLab repository ($visibility)")) {
    try {
        $args = @('repo', 'create', $normalized, '--defaultBranch', 'main')
        if ($Public) { $args += '--public' } else { $args += '--private' }

        $result = & $invoker glab $args
        $out = $result.Output

        Write-Verbose ("Created '{0}' as {1}.{2}{3}" -f $normalized, $visibility, 
            [Environment]::NewLine, ($out -join [Environment]::NewLine))

        [pscustomobject]@{
            Name       = $normalized
            Visibility = $visibility
            Created    = $true
            Output     = $out
            Message    = 'Repository created'
        }
    }
    catch {
        throw [System.AggregateException]::new(
            "Failed to create repository '$Name' ('$normalized').", $_.Exception)
    }
}

¿Qué acabamos de hacer?

  • .ToLowerInvariant() evita efectos de cultura (e.g. “I/İ” en turco) y las expresiones regulares sustituyen espacios por guiones y filtran a [a-z0-9-].
  • & $invoker glab repo view $normalized consulta el repo; si devuelve ExitCode = 0 asumimos que existe y devolvemos un objeto con Created = $false .
  • & $invoker glab $args ejecuta glab repo create con --defaultBranch main y visibilidad según -Public . El bloque está protegido con try / catch y arroja un AggregateException con contexto si falla.

Asignar remoto en Git

Este script configura un remoto de Git de forma idempotente: si el remoto ya existe con la misma URL, no hace nada; si existe con otra URL, la actualiza; si no existe, lo crea. Usamos git -C para operar en la carpeta del repo sin cambiar el directorio actual, y SupportsShouldProcess para habilitar -WhatIf y -Confirm .

#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidatePattern('^(https://|git@|ssh://)')]
    [string] $RemoteUrl,

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $Path = '.',

    [ValidatePattern('^[\w.-]+$')]
    [string] $RemoteName = 'origin'
)

$invoker = Join-Path $PSScriptRoot '..' 'core' 'Invoke-Tool.ps1' -Resolve

$repoPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)

$result = @{
    RepoPath = $repoPath
    Remote   = $RemoteName
    Url      = $RemoteUrl
    Existed  = $false
    Changed  = $false
    Action   = 'none'
    Output   = @()
    Message  = ''
}

$inside = $false
try {
    & $invoker git -C $repoPath rev-parse --is-inside-work-tree | Out-Null
    $inside = $true
}
catch { $inside = $false }

if (!$inside -and $WhatIfPreference) {
    $result.Action = 'skipped'
    $result.Message = 'WhatIf: would set remote after repository initialization'
}
elseif (!$inside) {
    throw [System.AggregateException]::new(
        "The path '$repoPath' is not a Git repository (or does not exist).",
        $_.Exception
    )
}
else {
    $currentUrl = try {
        $res = & $invoker git -C $repoPath remote get-url $RemoteName
        $res.Output.Trim()
    }
    catch { $null }

    $result.Existed = [bool]$currentUrl

    if ($null -ne $currentUrl) {
        if ([string]::Equals($currentUrl, $RemoteUrl, 'OrdinalIgnoreCase')) {
            $result.Action = 'none'
            $result.Message = 'Remote already configured'
        }
        elseif ($PSCmdlet.ShouldProcess("$RemoteName$RemoteUrl", 'git remote set-url')) {
            $set = & $invoker git -C $repoPath remote set-url $RemoteName $RemoteUrl
            $result.Changed = $true
            $result.Action = 'set-url'
            $result.Output = $set.Output
            $result.Message = "Remote URL updated (was: $currentUrl)"
        }
        else {
            $result.Action = 'skipped'
            $result.Message = 'Operation skipped'
        }
    }
    elseif ($PSCmdlet.ShouldProcess("$RemoteName$RemoteUrl", 'git remote add')) {
        $add = & $invoker git -C $repoPath remote add $RemoteName $RemoteUrl
        $result.Changed = $true
        $result.Action = 'add'
        $result.Output = $add.Output
        $result.Message = 'Remote added'
    }
    else {
        $result.Action = 'skipped'
        $result.Message = 'Operation skipped'
    }
}

[pscustomobject]$result

¿Qué acabamos de hacer?

  • Validación de URL: [ValidatePattern('^(https://|git@|ssh://)')] asegura que apuntemos a un remoto válido (HTTPS o SSH) antes de ejecutar Git.
  • Verificar repo: & $invoker git -C $repoPath rev-parse --is-inside-work-tree confirma que la ruta es un repositorio Git; si falla, se lanza una excepción con contexto.
  • Leer remoto actual: el bloque try / catch asigna $null si el remoto no existe con $currentUrl = try { ... } catch { $null } .
  • Comparación: [string]::Equals(..., 'OrdinalIgnoreCase') evita cambios innecesarios por diferencias de mayúsculas/minúsculas en la URL.
  • Acciones: si no hay cambios → none; si difiere → set-url; si no existe → add. Gracias a ShouldProcess, puedes simular con -WhatIf.

Crear proyecto completo (remoto + local + remoto origin)

Este script combina los pasos anteriores: crea el repositorio remoto, inicializa el repositorio local y asigna el remoto origin. Es idempotente de extremo a extremo y respeta -WhatIf / -Confirm .

#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidatePattern('^[a-zA-Z0-9._-]+$')]
    [string] $Username,

    [Parameter(Mandatory)]
    [ValidatePattern('^[a-zA-Z0-9._-]+$')]
    [string] $RepositoryName,

    [ValidatePattern('^[a-zA-Z0-9._-]*$')]
    [string] $Prefix = '',

    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string] $LiteralPath = '.',

    [switch] $Public
)

$result = @{
    NewRepository        = $null
    InitializeRepository = $null
    SetRemote            = $null
}

$newRepository = Join-Path $PSScriptRoot 'New-GitLabRepository.ps1' -Resolve
$initializeRepository = Join-Path $PSScriptRoot 'Initialize-Repository.ps1' -Resolve
$setRemote = Join-Path $PSScriptRoot 'Add-Remote.ps1' -Resolve

$forward = @{}
foreach ($k in 'WhatIf', 'Confirm') {
    if ($PSBoundParameters.ContainsKey($k)) { $forward[$k] = $PSBoundParameters[$k] }
}

$normalized = (
    $RepositoryName.ToLowerInvariant() -replace '\s+', '-' -replace '[^a-z0-9-]', '')

$repoSlug = if ([string]::IsNullOrEmpty($Prefix)) {
    $normalized
}
else {
    ($Prefix.Trim('-') + '-' + $normalized.Trim('-')).Trim('-')
}

$result.NewRepository = & $newRepository @forward -Name $repoSlug -Public:$Public
$remoteUrl = "git@gitlab.com:$Username/$repoSlug.git"
$result.InitializeRepository = & $initializeRepository @forward -LiteralPath $LiteralPath
$result.SetRemote = (
    & $setRemote @forward -Path $LiteralPath -RemoteName 'origin' -RemoteUrl $remoteUrl)

[pscustomobject]$result

¿Qué acabamos de hacer?

  • Propagación de WhatIf/Confirm: $forward recoge las banderas -WhatIf / -Confirm y las reenvía a los scripts internos con @forward , manteniendo la simulación/confirmación en toda la orquestación.
  • repoSlug: a partir de $RepositoryName se genera un slug seguro para el remoto: minúsculas, espacios a guiones y filtrado a [a-z0-9-]. Si hay $Prefix , se antepone (por ejemplo: dibs-scripts).
  • Remoto + local + origin: el script primero asegura el remoto en GitLab, luego inicializa el repositorio local (si hace falta), y finalmente establece origin a la URL SSH git@gitlab.com:<user>/<slug>.git.
  • Salida agregada: se retorna un objeto con tres propiedades (NewRepository, InitializeRepository, SetRemote) para inspeccionar el resultado de cada paso por separado.
$params = @{
    Username       = 'tu-usuario'   # <- reemplaza con tu usuario real de GitLab
    RepositoryName = 'scripts'
    Prefix         = 'dibs'
    LiteralPath    = '.'            # carpeta actual como repo local
    Public         = $true
    Verbose        = $true
    Confirm        = $true
}

$result = .\git\New-GitProject.ps1 @params

# Ver resultados individuales:
$result.NewRepository        | Format-List
$result.InitializeRepository | Format-List
$result.SetRemote            | Format-List
Con -WhatIf el script describe lo que haría sin efectuar cambios; con -Confirm solicita confirmación paso a paso.

Ejercicio propuesto

Modifica el script Initialize-Project.ps1 para que llame a New-GitProject.ps1 y cree el repositorio remoto en GitLab, inicialice el repo local (si procede) y configure el remoto origin. Mantén el comportamiento idempotente y respeta -WhatIf/-Confirm.

Siguientes pasos

Los scripts no hacen commit ni push para evitar cambios accidentales. La idea es que revises el estado del repo y confirmes conscientemente qué subirás. Además, conviene agregar un .gitignore antes de tu primer commit.

Desde dibs/scripts
# 1) Revisa el estado y los archivos creados
git status

# 2) Crea un .gitignore recomendado para tu stack
# Visita: https://www.toptal.com/developers/gitignore/
# Copia el contenido y guárdalo como .gitignore en la raíz del proyecto

# 3) Haz tu primer commit (cuando estés conforme)
git add .
git commit -m "init: estructura y configuración inicial"

# 4) Publica en el remoto (rama main)
git push origin main
Publicar cambios manualmente

Nota

Si tu proyecto genera artefactos (por ejemplo, bin/, obj/, dist/, node_modules/, *.env), asegúrate de ignorarlos en .gitignore para evitar subir archivos innecesarios o sensibles.

Conclusiones

En este laboratorio establecimos una base sólida para trabajar con GitLab desde la línea de comandos: normalizamos nombres de repos, creamos el remoto de forma idempotente con glab, inicializamos el repositorio local con rama main y configuramos origin sin duplicar estado. Orquestamos todo con un solo script que respeta -WhatIf/-Confirm, y cuidamos la compatibilidad de rutas usando ProviderPath cuando interactuamos con herramientas externas. El resultado es un flujo repetible, seguro y fácil de compartir con el equipo.

Puntos clave

    • Idempotencia: los comandos pueden ejecutarse varias veces sin efectos secundarios (crear/no crear, agregar/actualizar).
    • Determinismo y compatibilidad: preferimos ProviderPath al invocar binarios externos, y usamos git -C para no depender del directorio actual.
    • Control seguro: -WhatIf/-Confirm permiten ensayar y confirmar cambios; los scripts devuelven objetos para inspeccionar resultados.
    • Composición: piezas pequeñas (crear remoto, inicializar, asignar origin) que se combinan en un comando único.
    • Próximos pasos: añadir .gitignore, primer commit/push y preparar la base para CI/CD.

¿Qué nos llevamos?

Automatizar no es solo ahorrar tiempo: es diseñar confianza operacional. Al convertir acciones frágiles en comandos idempotentes —probables con -WhatIf y confirmables paso a paso— ganamos reproducibilidad y tranquilidad para iterar. Empieza por lo esencial, compón tareas pequeñas y prioriza claridad sobre “magia”. Este hábito se vuelve ventaja compuesta: menos sorpresas, más foco en el diseño de la biblioteca y un equipo que comparte el mismo lenguaje de trabajo.

Notas

  1. Una estrategia común para mitigar este problema es mantener un mirror del repositorio en GitHub (Git permite definir varios remotos). Sin embargo, esto puede complicar la gestión de issues o solicitudes de cambio (merge requests en GitLab, equivalentes a pull requests en GitHub), ya que habría que coordinar dos fuentes distintas de colaboración. Volver