Laboratorio 1: GitLab — crear repos y remotos

Abstract

Este laboratorio condensa varias lecciones en un flujo real: retomamos validaciones, salida tipada y herramientas de línea de comandos, y las combinamos en un proceso que puedes ejecutar de principio a fin sin pasos repetitivos.

El hilo conductor es aprender a diseñar scripts como herramientas: componibles, predecibles y fáciles de inspeccionar. Verás contratos explícitos, salida estructurada e idempotencia con -WhatIf / -Confirm ; al final tendrás un "flujo de automatización" de PowerShell listo para automatizar tareas similares en otros proyectos.

Por qué GitLab y qué aprenderemos

En este curso trabajaremos con GitLab. Es probable que ya tengas familiaridad con GitHub, pero GitLab propone un modelo distinto: integra repositorios, CI/CD y registro de paquetes en un mismo espacio. Esta visión integrada resulta especialmente útil para practicar el flujo completo de creación y publicación de bibliotecas de software sin depender de múltiples servicios externos.

A lo largo de las unidades veremos cómo usar GitLab para crear, versionar y publicar bibliotecas. Más adelante aprovecharemos su Package Registry como mecanismo de distribución, aunque el foco del curso estará en la orquestación local (scripts, Gradle) y en comprender el proceso de construcción y publicación, sin llegar a implementar integración continua en GitLab.

Qué ganamos

  • CI/CD integrado: GitLab incorpora su sistema de integración continua (GitLab CI/CD) como parte nativa del proyecto. El modelo es similar a GitHub Actions, pero con menos dependencia de servicios externos. Aunque en este curso no lo utilizaremos, es útil como referencia para flujos completos de publicación.
  • Registro de paquetes en el mismo entorno: permite publicar y consumir artefactos (bibliotecas, contenedores, etc.) sin salir de la plataforma. En GitHub existe GitHub Packages, pero como un servicio más desacoplado.
  • Opciones autogestionadas: muchas organizaciones utilizan GitLab en servidores propios, lo que facilita cumplir requisitos internos de seguridad. GitHub también ofrece una variante empresarial (pagada), pero con un modelo diferente de distribución.
  • Modelo de grupos y permisos detallado: GitLab organiza repositorios en grupos y subgrupos, lo que facilita estructurar proyectos relacionados. GitHub ofrece organizaciones, pero con un modelo distinto de jerarquía.
  • CLI oficial mediante glab : permite crear y gestionar proyectos desde la terminal. GitHub cuenta con gh , con un alcance comparable.

En qué topamos...

  • Menor visibilidad pública: GitHub sigue siendo el principal punto de encuentro para proyectos abiertos, por lo que bibliotecas alojadas en GitLab suelen tener menos exposición inicial. 1
  • Configuración más detallada en CI/CD: GitLab ofrece un alto nivel de control, lo que puede resultar más verboso al inicio si vienes desde GitHub. No es un problema en este curso, pero sí un factor a considerar en proyectos futuros.
  • Restricciones en la nube gratuita: GitLab Cloud incluye límites de almacenamiento y de minutos de CI que pueden sentirse más estrictos que los de GitHub en su capa gratuita.
  • Sin equivalente directo a GitHub Classroom: GitLab no incluye un sistema integrado para gestionar entregas de estudiantes. Se pueden construir alternativas, pero requieren más trabajo manual.

En esta primera parte aprenderemos a:

  • Crear repositorios remotos automáticamente.
  • Inicializar proyectos locales y enlazarlos con su remoto.
  • Automatizar ambos pasos con un script para agilizar el flujo.

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.

Probar si una carpeta es un repositorio Git

Antes de crear un remoto o asignar origin, conviene verificar si la carpeta indicada ya es un repositorio Git.

Este script actúa como comprobación preliminar idempotente: no modifica la carpeta; únicamente inspecciona su estado usando Git de forma segura. Así evitamos intentar leer o configurar remotos donde todavía no existe .git/, y podemos decidir condicionalmente qué otros scripts ejecutar.

Contrato

Dada una ruta válida, siempre devuelve un objeto que indica si es o no un repositorio Git.

La lógica usa git -C <ruta> rev-parse --is-inside-work-tree para comprobar sin cambiar el directorio actual. Captura excepciones y devuelve un resultado estable, sin romper la orquestación de scripts mayores.

Test-GitRepository.ps1
scripts/git/Test-GitRepository.ps1
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string] $Path
)

Set-StrictMode -Version 3.0

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

$result = [PSCustomObject]@{
    Status = 'Pending'
    Error  = $null
}

try {
    $repoPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath
    & $invoker git -C $repoPath rev-parse --is-inside-work-tree | Out-Null
    $result.Status = 'IsRepository'
}
catch {
    $result.Status = 'NotRepository'
    $result.Error = $_
}

$result
Devuelve objeto con Status: IsRepository o NotRepository.

Detalles clave

  • Chequeo nativo: git -C <ruta> rev-parse --is-inside-work-tree verifica el estado sin cambiar el directorio actual. Desglose del comando:
    • git : el ejecutable de Git que invoca los subcomandos.
    • -C <ruta> : indica a Git que ejecute el subcomando desde la carpeta especificada sin cambiar el directorio del proceso actual.
    • rev-parse : subcomando de Git usado para obtener/parsear información interna y valores de configuración.
    • --is-inside-work-tree : opción que hace que rev-parse devuelva "true" (por stdout) y ExitCode 0 cuando la ruta está dentro de un work tree; si no, el ExitCode será distinto de 0.
  • Resolución física: Resolve-Path -LiteralPath obtiene ProviderPath para evitar ambigüedades y fallar temprano si la ruta es inválida.
  • Manejo de errores: cualquier excepción asigna Status = 'NotRepository' y preserva el detalle en Error para inspección posterior sin detener otros scripts.
  • Salida estructurada: retornar un objeto facilita componer condiciones ( if ($result.Status -eq 'IsRepository') ) y serializar resultados.

Normalizar nombres para GitLab

GitLab exige nombres de repositorio en minúsculas, sin espacios ni caracteres especiales (solo [a-z0-9-]). Este pequeño filtro convierte cualquier entrada —por ejemplo, "R.E.M."— en un slug válido como "rem", y falla explícitamente en strings que no pueda convertir como "菊池ひみこ". Lo usamos antes de crear el remoto para evitar errores de sintaxis y mantener nombres legibles y predecibles.

Contrato

Toma un nombre arbitrario y devuelve un identificador válido para GitLab, o falla de forma explícita si no puede hacerlo. La normalización no intenta «forzar» un nombre válido: si el resultado no es significativo, el script falla.
ConvertTo-ValidGitLabName.ps1
scripts/git/ConvertTo-ValidGitLabName.ps1
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $Name
)

Set-StrictMode -Version 3.0

$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-])."
    )
}

$normalized
Retorna el nombre normalizado o lanza excepción si el resultado está vacío.

Detalles clave

  • Minúsculas invariantes: .ToLowerInvariant() convierte sin depender de la cultura local (evita casos como "I/İ" en turco).
  • Limpieza de caracteres: -replace '\s+', '-' cambia espacios por guiones; -replace '[^a-z0-9-]', '' elimina todo lo que no sea alfanumérico o guion.

Este tipo de normalización es común al integrar herramientas externas (APIs, registries, CI/CD), donde los nombres visibles para las personas deben mapearse a identificadores técnicos válidos.

Crear repo remoto en GitLab

Este script crea un repositorio remoto de forma idempotente: primero consulta si existe y, solo si no existe, intenta crearlo. Devuelve un objeto con Name , Visibility y Status { Created; Reason } .

Contrato

Si el repo existe, no hace cambios; si no existe, intenta crearlo (respetando -WhatIf / -Confirm ) y siempre retorna un objeto describiendo el resultado.

Aquí Reason funciona como un campo de diagnóstico: prioriza registrar “qué pasó” sobre imponer un único tipo. Por eso puede ser texto, la salida de glab o un ErrorRecord si algo falla.

New-GitLabRepository.ps1
scripts/git/New-GitLabRepository.ps1
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $Name,

    [switch] $Public
)

Set-StrictMode -Version 3.0

$normalized = & (Join-Path $PSScriptRoot 'ConvertTo-ValidGitLabName.ps1') -Name $Name
$visibility = if ($Public.IsPresent) { 'public' } else { 'private' }
$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve

$repository = [PSCustomObject]@{
    Name       = $normalized
    Visibility = $visibility
    Status     = [PSCustomObject]@{
        Created = $false
        Reason  = ''
    }
}

try {
    & $invoker glab repo view $repository.Name | Out-Null
    $repository.Status.Reason = '{0} already exists on GitLab.' -f $repository.Name
}
catch {
    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
            $repository.Status.Created = $true
            $repository.Status.Reason = $result
        }
        catch {
            $repository.Status.Reason = $_
        }
    }
    else {
        $repository.Status.Reason = 'Creation cancelled by user.'
    }
}

$repository
Devuelve objeto con Status.Created y Status.Reason (texto, salida de glab o ErrorRecord), más Name/Visibility.

Detalles clave

  • Idempotencia: primero intenta glab repo view ; si existe, solo informa; si no, procede a crear.
  • glab repo view: ejecuta glab repo view $repository.Name solo para comprobar si el proyecto ya existe en GitLab. Si el comando termina correctamente, asumimos que el repo está creado y solo rellenamos Status.Reason ; si lanza error, caemos en el catch y pasamos a la lógica de creación. Un fallo aquí puede significar «no existe» , pero también permisos o conectividad; por eso el script preserva el detalle en Status.Reason .
  • glab repo create: construye el array $args como repo create $normalized --defaultBranch main , añadiendo --public o --private según el parámetro -Public . --defaultBranch main fija el nombre de la rama principal y los flags --public / --private controlan la visibilidad del proyecto en GitLab.
  • Salida rica: Status.Reason puede ser texto, la salida de glab o un ErrorRecord; útil para inspección y logs.

Asignar remoto GitLab a un repo existente

Este script conecta un repositorio local ya inicializado con un remoto en GitLab: valida que la ruta sea un repo Git, genera la URL a partir del usuario y el nombre normalizado, y luego crea o actualiza el remoto (por defecto origin) de forma idempotente. Devuelve un objeto con la acción realizada y la razón, útil para inspección y logs.

Contrato

Si la ruta no es un repo Git, falla explícitamente; si lo es, configura el remoto y siempre retorna un objeto describiendo la acción (Added/Updated/Skipped/Failed).

Nota

Configurar el remoto no autentica ni hace push; para publicar luego necesitarás credenciales (HTTPS con token o SSH).

Supuesto

Esta versión asume GitLab.com (https://gitlab.com/...); en GitLab autogestionado la URL base cambia.
Set-GitLabRemote.ps1
scripts/git/Set-GitLabRemote.ps1
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string] $Path,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $User,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $Name,

    [ValidateNotNullOrWhiteSpace()]
    [string] $Remote = 'origin'
)

Set-StrictMode -Version 3.0

$testRepo = Join-Path $PSScriptRoot 'Test-GitRepository.ps1' -Resolve
$convertName = Join-Path $PSScriptRoot 'ConvertTo-ValidGitLabName.ps1' -Resolve
$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve

if ((& $testRepo -Path $Path).Status -ne 'IsRepository') {
    throw [System.InvalidOperationException]::new(
        "Path '$Path' is not a valid Git repository."
    )
}

$repoPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath
$remoteUrl = 'https://gitlab.com/{0}/{1}.git' -f $User, (& $convertName -Name $Name)

$result = [PSCustomObject]@{
    Path       = $repoPath
    RemoteName = $Remote
    RemoteUrl  = $remoteUrl
    Action     = 'None'
    Reason     = ''
}

$target = '{0} remote {1} -> {2}' -f $repoPath, $Remote, $remoteUrl
if (!$PSCmdlet.ShouldProcess($target, 'Set Git remote')) {
    $result.Action = 'Skipped'
    $result.Reason = 'Operation cancelled by user.'
}
else {
    try {
        try {
            & $invoker git -C $repoPath remote get-url $Remote | Out-Null
            & $invoker git -C $repoPath remote set-url $Remote $remoteUrl | Out-Null
            $result.Action = 'Updated'
            $result.Reason = "Remote '$Remote' URL updated."
        }
        catch {
            & $invoker git -C $repoPath remote add $Remote $remoteUrl | Out-Null
            $result.Action = 'Added'
            $result.Reason = "Remote '$Remote' created."
        }
    }
    catch {
        $result.Action = 'Failed'
        $result.Reason = $_
    }
}

$result
Configura el remoto de un repositorio local hacia GitLab (por defecto origin) y devuelve qué acción se ejecutó (Added, Updated, Skipped, Failed).

Detalles clave

  • Verificación del repo: usa Test-GitRepository.ps1 para asegurarse de que $Path apunta a un repositorio Git válido antes de tocar remotos; si no, lanza una InvalidOperationException con un mensaje claro.
  • Nombre normalizado: delega en ConvertTo-ValidGitLabName.ps1 la generación del slug del proyecto y construye la URL como https://gitlab.com/<user>/<slug>.git.
  • Idempotencia del remoto: primero intenta git -C $repoPath remote get-url $Remote ; si el remoto existe, ejecuta git -C $repoPath remote set-url $Remote $remoteUrl , y si falla (por ejemplo, porque el remoto no existe) cae en el catch interno y usa git -C $repoPath remote add $Remote $remoteUrl .
  • Confirmación opcional: soporta -WhatIf/-Confirm para simular cambios o pedir confirmación antes de modificar remotos.

Orquestar creación de repo y remoto

Para cerrar el flujo, este script actúa como orquestador: crea (si hace falta) el repositorio remoto en GitLab y a continuación configura el remoto en el repositorio local apuntando a ese proyecto. Reutiliza los scripts anteriores, respeta -WhatIf / -Confirm y devuelve un objeto con el detalle de cada paso.

Contrato

Intenta garantizar que (1) el repo remoto exista y (2) el remoto local apunte a él; el resultado siempre indica qué ocurrió en cada etapa.

Nota

Aquí «publish» significa preparar la publicación (repo remoto + remoto local). Este script no hace commit ni push (esta decisión es intencionada).
Publish-GitRepository.ps1
scripts/git/Publish-GitRepository.ps1
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string] $Path,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $User,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $Name,

    [switch] $Public,

    [ValidateNotNullOrWhiteSpace()]
    [string] $Remote = 'origin'
)

Set-StrictMode -Version 3.0

$createGitLabRepo = Join-Path $PSScriptRoot 'New-GitLabRepository.ps1' -Resolve
$setGitRemote = Join-Path $PSScriptRoot 'Set-GitRemote.ps1' -Resolve

$forwardBoundParams = @{}
if ($PSBoundParameters.ContainsKey('WhatIf')) {
    $forwardBoundParams['WhatIf'] = $PSBoundParameters['WhatIf']
}
if ($PSBoundParameters.ContainsKey('Confirm')) {
    $forwardBoundParams['Confirm'] = $PSBoundParameters['Confirm']
}

$gitLab = & $createGitLabRepo -Name $Name -Public:$Public @forwardBoundParams

$remoteParams = @{}
$remoteParams += $forwardBoundParams
$remoteParams += @{ Path = $Path; User = $User; Name = $Name; Remote = $Remote }

$remoteResult = & $setGitRemote @remoteParams

[PSCustomObject]@{
    Path   = $Path
    GitLab = $gitLab
    Remote = $remoteResult
}
Orquesta la creación del repositorio remoto en GitLab y la configuración del remoto local, devolviendo un objeto con el resultado de ambos pasos.

Detalles clave

  • Propaga -WhatIf / -Confirm : construye $forwardBoundParams a partir de $PSBoundParameters y lo pasa a ambos scripts, asegurando que los ensayos y confirmaciones se respeten de extremo a extremo.
  • Salida estructurada: retorna un objeto con Path , más los resultados completos de GitLab y Remote . Esto permite inspeccionar qué ocurrió en cada etapa o registrar el resultado en logs sin necesidad de parsear texto.

Ejemplo de uso

Desde dibs/scripts
git init # Si aún no es un repo Git
$params = @{
    User = 'tu-usuario' # Reemplaza con tu nombre de usuario de GitLab
    Path = '.'
    Name = (Get-Item -Path '.').Name
}
$result = ./git/Publish-GitRepository.ps1 @params -WhatIf

Revisa $result para verificar que todo está en orden, luego ejecuta sin -WhatIf para crear el repo y asignar el remoto. Usa -Confirm para aprobar cada paso si prefieres mayor control.

Si quieres confirmar el lado remoto, puedes inspeccionarlo con glab repo view <nombre> .

Verificar

Inspeccionar resultado
# Revisar resultado
$result.GitLab.Status    # Created y Reason
$result.Remote.Action    # Added/Updated/Skipped/Failed

# Confirmar con Git
git remote -v

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.

Esta decisión separa la automatización de infraestructura (crear repos, remotos, validar estado) de las decisiones sobre el contenido del código (qué archivos versionar y cuándo publicarlos).

El primer commit suele incluir solo la estructura inicial, configuración y documentación básica del proyecto. Además, es recomendable agregar un .gitignore antes de ese primer commit.

Asegúrate de estar en la rama correcta (main) antes de hacer push.

Desde dibs/scripts
# Revisa estado y agrega .gitignore
git status
# Visita https://www.toptal.com/developers/gitignore/ para generar .gitignore

# Primer commit y push
git add .
git commit -m "init: estructura y configuración inicial"
git push origin main
Publicar cambios manualmente

Importante

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

Conectamos validaciones, salida tipada y CLI en un flujo completo. Pasamos de comprobar si una carpeta es repo, a normalizar nombres, crear el proyecto remoto con glab y fijar el remoto de forma idempotente.

La salida estructurada y el soporte de -WhatIf / -Confirm hacen visibles las decisiones del flujo y permiten ensayar antes de afectar el entorno. Esa observabilidad —contratos claros + resultados inspeccionables— reduce fricción sin perder control.

Puntos clave

  • Flujo completo automatizado: desde glab repo view para detectar existentes hasta glab repo create y la configuración del remoto con git remote , todo se ensambla sin pasos repetitivos.
  • Normalización y validaciones: transformar nombres a [a-z0-9-] y verificar rutas antes de operar evita errores de sintaxis o rutas mal apuntadas.
  • Idempotencia y seguridad: los scripts no sobreescriben sin revisar, exponen objetos con estado (Status/Reason) y soportan -WhatIf / -Confirm para probar sin riesgos.
  • Salida estructurada: devolver objetos facilita registrar, auditar y encadenar resultados en orquestaciones mayores sin parsear texto.

¿Qué nos llevamos?

El objetivo no es “GitLab”, sino el patrón: automatizar infraestructura y validaciones manteniendo humana la decisión del contenido. Idempotencia + confirmación + salida estructurada es una combinación transferible.

Con estos bloques listos, el siguiente paso del curso es el pipeline de PowerShell: encadenar scripts que devuelven objetos (no texto) y construir orquestadores que puedas reutilizar para bootstrap de proyectos, tareas de build o publicación de artefactos.

¿Con ganas de más?

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