Saltar al contenido principal

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.

Crear un repositorio índice y añadir submódulos desde pipeline
scripts/git/New-IndexRepo.ps1
#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 , process y end .

  • Bloque begin — preparación global:

    Se crea e inicializa el repositorio índice. La llamada a GetUnresolvedProviderPathFromPSPath es importante porque la ruta podría no existir todavía. Aquí usamos -ErrorAction Stop porque si no podemos inicializar el índice, el resto del pipeline pierde sentido.

  • Bloque process — ejecución por elemento

    No hay iteración explícita. PowerShell invoca este bloque una vez por cada objeto que llega por pipeline, enlazando RemoteName y LocalName mediante ValueFromPipelineByPropertyName .

    La operación central es git submodule, que registra el submódulo y además clona el repositorio remoto dentro del directorio indicado.

  • Semántica de errores en el pipeline

    En process se emiten errores no terminantes con Write-Error , permitiendo continuar con los siguientes submódulos.

    En cambio, los errores en begin y end son terminantes, ya que invalidan el estado global del pipeline.

  • Bloque end — evidencia final

    Se ejecuta git submodule status para mostrar el estado consolidado. Esto actúa como verificación observable del resultado del pipeline.

Ejecutar el script con submódulos
$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-List

Desde 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).

Cmdlet de pipeline para estado y actualización de submódulos
scripts/git/Invoke-SubmoduleTask.ps1
#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
    }
}
Consumir el cmdlet desde un pipeline
# 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-Table
Desde dibs/. Si se ve bien, puedes ejecutar sin -WhatIf .

Detalles clave

  • Enlace de parámetros por propiedades

    Cada objeto emitido por Get-ChildItem incluye propiedades con nombres compatibles con los parámetros del cmdlet: Name alimenta -Name , y FullName (alias de Path ) alimenta -Path . Este patrón permite encadenar comandos sin construir manualmente argumentos por cada elemento.

  • Qué hace cada comando de Git

    git fetch --all --prune sincroniza referencias remotas y elimina referencias obsoletas ( --prune ) en el repositorio del submódulo. Luego, git submodule update --remote -- <submodule> mueve el submódulo al commit más reciente de la rama remota configurada en .gitmodules .

  • Cómo se calcula el estado

    git status --porcelain=v1 -z emite un formato “estable para máquinas”: si la salida tiene contenido, el repositorio está DirtyOrPending ; si está vacía, se considera Clean . Usar -z reduce ambigüedades en el parseo.

  • Salida compuesta, lista para etapas posteriores

    El cmdlet devuelve un objeto con Submodule , Path , Action , Status , Mode y Reason . Esto permite, por ejemplo, filtrar submódulos sucios, resumir por estado o formatear tablas sin volver a invocar Git.

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.

Publicar el repositorio índice reutilizando Publish-GitRepository
$publishParams = @{
    Path = './dibs-index'
    User = '<TU-USUARIO>'
    Name = 'dibs-index'
}
$publishResult = ./scripts/git/Publish-GitRepository.ps1 @publishParams -WhatIf
$publishResult | Format-List
Desde dibs/. Este flujo prepara el remoto, pero no hace commit ni push. Si el resultado es correcto, puedes ejecutar sin -WhatIf .
Salida esperada (resumen)
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:

Verificación final del índice
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 main

Conclusiones

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 / -Confirm mantiene control operativo antes de afectar entorno local o remoto.

¿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?