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.
- 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
: la CLI oficial de GitLab permite crear, clonar o administrar repositorios sin usar la interfaz web. GitHub cuenta con su propia CLI (glabglab) con fines similares.ghgh
- 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:
glab auth loginTodos los scripts son idempotentes: puedes ejecutarlos varias veces sin generar duplicados ni inconsistencias.
...
#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 , de forma
idempotente: primero verifica su existencia y, si ya existe, devuelve un resultado estable sin fallar.
Además, respeta glab glab /-WhatIf -WhatIf gracias a SupportsShouldProcess, y normaliza el nombre para cumplir con las restricciones del hosting.
-Confirm -Confirm
#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?
-
evita efectos de cultura (e.g. “I/İ” en turco) y las expresiones regulares sustituyen espacios por guiones y filtran a.ToLowerInvariant().ToLowerInvariant()[a-z0-9-]. -
consulta el repo; si devuelve& $invoker glab repo view $normalized& $invoker glab repo view $normalizedExitCode = 0asumimos que existe y devolvemos un objeto con.Created = $falseCreated = $false -
ejecuta& $invoker glab $args& $invoker glab $argsconglab repo createglab repo createy visibilidad según--defaultBranch main--defaultBranch main. El bloque está protegido con-Public-Public/trytryy arroja un AggregateException con contexto si falla.catchcatch
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
para operar en la carpeta del repo sin cambiar el directorio actual, y SupportsShouldProcess para
habilitar
git -C git -C y -WhatIf -WhatIf .
-Confirm -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:
asegura que apuntemos a un remoto válido (HTTPS o SSH) antes de ejecutar Git.[ValidatePattern('^(https://|git@|ssh://)')][ValidatePattern('^(https://|git@|ssh://)')] - Verificar repo:
confirma que la ruta es un repositorio Git; si falla, se lanza una excepción con contexto.& $invoker git -C $repoPath rev-parse --is-inside-work-tree& $invoker git -C $repoPath rev-parse --is-inside-work-tree - Leer remoto actual: el bloque
/trytryasignacatchcatchsi el remoto no existe con$null$null.$currentUrl = try { ... } catch { $null }$currentUrl = try { ... } catch { $null } - Comparación:
evita cambios innecesarios por diferencias de mayúsculas/minúsculas en la URL.[string]::Equals(..., 'OrdinalIgnoreCase')[string]::Equals(..., 'OrdinalIgnoreCase') - 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 -WhatIf .
-Confirm -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:recoge las banderas$forward$forward/-WhatIf-WhatIfy las reenvía a los scripts internos con-Confirm-Confirm, manteniendo la simulación/confirmación en toda la orquestación.@forward@forward -
repoSlug: a partir dese genera un slug seguro para el remoto: minúsculas, espacios a guiones y filtrado a$RepositoryName$RepositoryName[a-z0-9-]. Si hay, se antepone (por ejemplo:$Prefix$Prefixdibs-scripts). - Remoto + local + origin: el script primero asegura el remoto en GitLab, luego inicializa el repositorio local (si hace falta), y finalmente establece
origina la URL SSHgit@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.
dibs/scripts $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-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.
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 Nota
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 ramamain 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
ProviderPathal invocar binarios externos, y usamosgit -Cpara no depender del directorio actual. - Control seguro:
-WhatIf/-Confirmpermiten 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, primercommit/pushy 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
- 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