Lab. 2: Git Submodules
Metadatos de la lección
- Autoría:
- Ignacio Slater-Muñoz
- Última actualización:
- 27 de marzo de 2026
Cambios recientes:
-
8880728· 27 de marzo de 2026 · ✨ feat(notes): add abstract slots and Python structured-output lesson ( GitLab / GitHub ) -
29b0ae0· 11 de marzo de 2026 · 📝✨ docs(scripting): add Nushell first-script lesson and inline code guidance ( GitLab / GitHub ) -
0d6422b· 2 de marzo de 2026 · 📝 feat(notes): refine Git submodules lesson examples and metadata ( GitLab / GitHub )
Encuentra el código de la lección:
Abstract
Este laboratorio conecta lo aprendido sobre pipeline-awareness y manejo de errores en un caso real: coordinar tareas sobre múltiples repositorios versionados de forma independiente mediante un repositorio índice con submódulos.
Al finalizar, contarás con un flujo reproducible para crear, inspeccionar, actualizar y publicar ese índice en GitLab, reutilizando scripts de laboratorios previos y manteniendo contratos explícitos de entrada, salida y política de fallos.
Por qué usar submódulos en este laboratorio
En esta unidad ya trabajaste con scripts reutilizables y con decisiones explícitas de error en pipelines. Este laboratorio agrega una dificultad habitual en automatización: coordinar tareas sobre varios repositorios versionados de forma independiente sin perder trazabilidad.
Repositorio índice de proyectos
Un submódulo es un repositorio Git referenciado desde otro mediante un commit específico. Un repositorio índice es el repositorio principal que organiza esos submódulos: no implementa la aplicación, sino que fija versiones concretas de varios proyectos externos para asegurar reproducibilidad y coordinar entre piezas autónomas versionadas de forma independiente.
En este laboratorio, el índice organiza proyectos del curso como
astro-website, scripts y scripts-py.
Alcance didáctico de este laboratorio
Los submódulos son un medio, no el fin. El foco está en practicar a orquestar múltiples proyectos con contratos claros: entrada estructurada, salida tipada, política de errores configurable y observabilidad de fallos. No siempre son la mejor solución; en algunos contextos un monorepo es más simple.
Preparar el repositorio índice con submódulos
En este laboratorio vamos a trabajar con un repositorio índice: un repositorio “contenedor” cuya responsabilidad no es implementar una aplicación, sino fijar versiones de varios repositorios del curso en una estructura reproducible.
La idea clave es que el script sea pipeline-aware: en lugar de iterar manualmente sobre una lista de repositorios, recibe objetos por pipeline (cada uno describe un submódulo), y ejecuta la misma acción de manera uniforme.
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[string] $RootPath,
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[string] $GitLabUser,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrWhiteSpace()]
[string] $RemoteName,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrWhiteSpace()]
[string] $LocalName
)
begin {
Set-StrictMode -Version 3.0
$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve
# RootPath puede apuntar a una ruta que aún no existe (por eso se resuelve aquí).
$repoPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($RootPath)
if ($PSCmdlet.ShouldProcess($repoPath, 'Initialize index repository')) {
New-Item -ItemType Directory -Path $repoPath -Force | Out-Null
& $invoker git -C $repoPath init -ErrorAction Stop | Out-Null
}
}
process {
$url = 'https://gitlab.com/{0}/{1}.git' -f $GitLabUser, $RemoteName
$target = Join-Path $repoPath $LocalName
$action = 'Add submodule "{0}" from "{1}"' -f $LocalName, $url
if ($PSCmdlet.ShouldProcess($target, $action)) {
try {
& $invoker git -C $repoPath submodule add $url $LocalName -ErrorAction Stop | Out-Null
}
catch {
$category = [System.Management.Automation.ErrorCategory]::InvalidOperation
$errorParams = @{
Message = 'Failed to add submodule "{0}" at "{1}". {2}' -f @(
$LocalName, $target, $_.Exception.Message)
Category = $category
TargetObject = $target
ErrorId = 'New-IndexRepo.SubmoduleAddFailed'
}
Write-Error @errorParams
}
}
}
end {
& $invoker git -C $repoPath submodule status -ErrorAction Stop
} Detalles clave
Este script sigue la estructura natural de un pipeline en PowerShell:
, begin begin y process process .
end end
- Bloque
— preparación global:beginbeginSe crea e inicializa el repositorio índice. La llamada a
es importante porque la ruta podría no existir todavía. Aquí usamosGetUnresolvedProviderPathFromPSPathGetUnresolvedProviderPathFromPSPathporque si no podemos inicializar el índice, el resto del pipeline pierde sentido.-ErrorAction Stop-ErrorAction Stop - Bloque
— ejecución por elementoprocessprocessNo hay iteración explícita. PowerShell invoca este bloque una vez por cada objeto que llega por pipeline, enlazando
yRemoteNameRemoteNamemedianteLocalNameLocalName.ValueFromPipelineByPropertyNameValueFromPipelineByPropertyNameLa operación central es
, que registra el submódulo y además clona el repositorio remoto dentro del directorio indicado.git submodulegit submodule - Semántica de errores en el pipeline
En
se emiten errores no terminantes conprocessprocess, permitiendo continuar con los siguientes submódulos.Write-ErrorWrite-ErrorEn cambio, los errores en
ybeginbeginson terminantes, ya que invalidan el estado global del pipeline.endend - Bloque
— evidencia finalendendSe ejecuta
para mostrar el estado consolidado. Esto actúa como verificación observable del resultado del pipeline.git submodule statusgit submodule status
$errs = @()
$params = @{
RootPath = "dibs-index"
GitLabUser = '<TU-USUARIO>'
ErrorAction = 'SilentlyContinue'
ErrorVariable = '+errs'
}
$result = @(
[PSCustomObject]@{
RemoteName = 'dibs-astro-website'
LocalName = 'astro-website'
},
[PSCustomObject]@{
RemoteName = 'dibs-scripts'
LocalName = 'scripts'
},
[PSCustomObject]@{
RemoteName = 'dibs-scripts-py'
LocalName = 'scripts-py'
}
) | ./scripts/git/New-IndexRepo.ps1 @params
Write-Host "=== Errors: ===" -ForegroundColor Red
$errs | Format-List -Property TargetObject, Exception
Write-Host "=== Submodule status: ===" -ForegroundColor Green
$result | Format-ListDesde dibs/
Si has seguido las lecciones principales, deberías tener el repositorio
dibs-scripts. Si además realizaste las lecciones opcionales de
Python, también deberías contar con dibs-scripts-py.
El repositorio dibs-astro-website no debería existir
en tu entorno: corresponde al código fuente de las propias lecciones y no forma
parte de los repositorios que has creado. Aquí se utiliza únicamente como
ejemplo de repositorio no disponible.
Pipeline de objetos sobre submódulos
En este caso, cada elemento del pipeline representa un submódulo y cada salida es un objeto con estado, listo para ser consumido por otras etapas (por ejemplo, filtrado, agrupación o reporte).
#Requires -Version 7.5
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[Alias('FullName')]
[string] $Path,
[ValidateSet('Status', 'Fetch')]
[string] $Action = 'Status'
)
begin {
Set-StrictMode -Version 3.0
$invoker = Join-Path $PSScriptRoot '..' 'tools' 'Invoke-Tool.ps1' -Resolve
}
process {
try {
$executionMode = 'Executed'
$statusLabel = 'Unknown'
$skipReason = $null
if ($Action -eq 'Fetch') {
$fetchTarget = '{0} ({1})' -f $Name, $Path
$fetchAction = 'Fetch and prune all remotes'
if (!$PSCmdlet.ShouldProcess($fetchTarget, $fetchAction)) {
$executionMode = 'Skipped'
$skipReason = if ($WhatIfPreference) { 'WhatIf' } else { 'ShouldProcessFalse' }
}
else {
& $invoker git -C $Path fetch --all --prune | Out-Null
$repoRoot = Split-Path $Path -Parent
$updateAction = 'Update submodule to latest configured remote commit'
if (!$PSCmdlet.ShouldProcess($fetchTarget, $updateAction)) {
$executionMode = 'Skipped'
$skipReason = if ($WhatIfPreference) { 'WhatIf' } else { 'ShouldProcessFalse' }
}
else {
& $invoker git -C $repoRoot submodule update --remote -- $Name | Out-Null
}
}
}
$statusResult = & $invoker git -C $Path status --porcelain=v1 -z
$statusOutput = @($statusResult.Output)
$statusPayload = [string]($statusOutput -join '')
$hasPendingChanges = $statusPayload.Length -gt 0
$statusLabel = if ($hasPendingChanges) { 'DirtyOrPending' } else { 'Clean' }
[pscustomobject]@{
Submodule = $Name
Path = $Path
Action = $Action
Status = $statusLabel
Mode = $executionMode
Reason = $skipReason
Error = $null
}
}
catch {
$errorParams = @{
Message = 'Failed to process submodule "{0}" at "{1}". {2}' -f @(
$Name, $Path, $_.Exception.Message)
Category = [System.Management.Automation.ErrorCategory]::InvalidOperation
TargetObject = $Path
ErrorId = 'Invoke-SubmoduleTask.Failed'
}
Write-Error @errorParams
}
}# Ejemplo de consumo: verificar estado de submódulos del índice
Get-ChildItem -Path dibs-index -Exclude .gitmodules |
scripts/git/Invoke-SubmoduleTask.ps1 -Action Status
# Ejemplo de consumo: actualizar submódulos a última versión remota
Get-ChildItem -Path dibs-index -Exclude .gitmodules |
scripts/git/Invoke-SubmoduleTask.ps1 -Action Fetch -WhatIf |
Format-Tabledibs/. Si se ve bien, puedes ejecutar sin -WhatIf -WhatIf . Detalles clave
- Enlace de parámetros por propiedades
Cada objeto emitido por
incluye propiedades con nombres compatibles con los parámetros del cmdlet:Get-ChildItemGet-ChildItemalimentaNameName, y-Name-Name(alias deFullNameFullName) alimentaPathPath. Este patrón permite encadenar comandos sin construir manualmente argumentos por cada elemento.-Path-Path - Qué hace cada comando de Git
sincroniza referencias remotas y elimina referencias obsoletas (git fetch --all --prunegit fetch --all --prune) en el repositorio del submódulo. Luego,--prune--prunemueve el submódulo al commit más reciente de la rama remota configurada engit submodule update --remote -- <submodule>git submodule update --remote -- <submodule>..gitmodules.gitmodules - Cómo se calcula el estado
emite un formato “estable para máquinas”: si la salida tiene contenido, el repositorio estágit status --porcelain=v1 -zgit status --porcelain=v1 -z; si está vacía, se consideraDirtyOrPendingDirtyOrPending. UsarCleanCleanreduce ambigüedades en el parseo.-z-z - Salida compuesta, lista para etapas posteriores
El cmdlet devuelve un objeto con
,SubmoduleSubmodule,PathPath,ActionAction,StatusStatusyModeMode. Esto permite, por ejemplo, filtrar submódulos sucios, resumir por estado o formatear tablas sin volver a invocar Git.ReasonReason
Publicar el índice en GitLab reutilizando Lab 1
Una vez que el índice ya contiene submódulos, el siguiente paso es publicarlo como
repositorio remoto. Aquí no conviene reinventar lógica: puedes reutilizar el script
Publish-GitRepository.ps1 del Lab 1 para crear el proyecto en GitLab y
configurar el remoto local en una sola operación.
$publishParams = @{
Path = './dibs-index'
User = '<TU-USUARIO>'
Name = 'dibs-index'
}
$publishResult = ./scripts/git/Publish-GitRepository.ps1 @publishParams -WhatIf
$publishResult | Format-Listdibs/. Este flujo prepara el remoto, pero no hace commit ni push. Si el resultado es correcto, puedes ejecutar
sin -WhatIf -WhatIf .
Path : ./dibs-index
GitLab : @{Status=Created; Name=dibs-index; Visibility=private; Reason=Repository created.}
Remote : @{Action=Added; RemoteName=origin; RemoteUrl=https://gitlab.com/<TU-USUARIO>/dibs-index.git}Con ese resultado, ya puedes revisar el estado local y decidir explícitamente cuándo publicar contenido:
git -C ./dibs-index status
git -C ./dibs-index remote -v
# Si corresponde:
git -C ./dibs-index add -A
git -C ./dibs-index commit -m "chore: bootstrap index with submodules"
git -C ./dibs-index push -u origin mainConclusiones
Trabajar con submódulos en este laboratorio permitió trasladar los conceptos de pipeline a un escenario de automatización multi-repo: cada submódulo se modela como objeto, cada etapa produce salida estructurada y cada decisión de error se hace explícita según su impacto local o global.
El punto clave no es Git por sí mismo, sino el patrón de diseño: scripts componibles, observables e idempotentes que pueden encadenarse para orquestar sistemas distribuidos sin perder trazabilidad.
Puntos clave
- Modelar submódulos como objetos de pipeline simplifica el procesamiento uniforme por elemento.
- Separar errores terminantes y no terminantes permite mantener continuidad sin ocultar fallos.
- Publicar el índice en GitLab puede resolverse por composición, reutilizando scripts del Lab 1.
-
/-WhatIf-WhatIfmantiene control operativo antes de afectar entorno local o remoto.-Confirm-Confirm
¿Qué nos llevamos?
Si piensas el índice como una interfaz de coordinación entre repositorios, el aprendizaje se vuelve transferible: el mismo enfoque te servirá para bootstrap de entornos, mantenimiento periódico o publicación en lote, siempre priorizando contratos claros y comportamiento determinista.
¿Con ganas de más?
Referencias recomendadas
- “ Git Tools—Submodules ” en Git BookReferencia completa sobre submódulos: qué son, cómo agregarlos, clonarlos e integrarlos en flujos colaborativos. Cubre el modelo de sincronización remota, manejo de cambios locales y estrategias de merge/rebase. Esencial para entender cómo Git fija versiones específicas de repositorios externos y mantener reproducibilidad en proyectos multi-repo.