From 44e2941aa2c754d1311b58d92cc0539adef9fc6a Mon Sep 17 00:00:00 2001 From: Bue Petersen <1579826+buep@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:05:20 +0200 Subject: [PATCH] fix!: avoid git submodule init hook merges This bugfix ensure that the devbox base-config plugin do not try to merge git submodules during the devbox base plugin init hook. An earlier feature in introduced in PR #50 that would check for uninitialized git submodules also tries to merge content. The merge was done to avoid it beeing "shadowed" by incoming changes, but it showed to be too intrusive as we don't alwasy want to merge. The fix changes behavior of the init hook in the plugin to not merge anything. However the original intention of the git submodule status check to guardrail against uninitialized git submodules persist and is improved to handle all cases now instead, one submodule at the time. Recursively as earlier also. Because of the bug, tests are introduced to show desired behavior and ensure it works as expected. Since the plugin didn't have any tests either, the tests serves as a simple start for testing and proof-of-concept. Minor changes also included: * Some markdown lint and formatting fixes in project README. * devbox used for the project itself also, to ensure BATS installed and `devbox run test` is easily available. * Github Action to run tests. * Test and release section in the project README. BREAKING CHANGE: In case you depended on automatic merge of Git Submodules in the devbox base-config plugin's init hook, this have now been disabled and only uninitialized Git submodules are initialized. --- .envrc | 9 + .github/workflows/test.yml | 31 +++ .gitmodules | 6 + README.md | 42 +++- .../config/ensure-submodules-populated.sh | 49 ++++ devbox-plugins/base-config/plugin.json | 10 +- devbox-plugins/base-config/tests/README.md | 9 + .../tests/ensure-submodules-populated.bats | 233 ++++++++++++++++++ .../tests/git-submodule-helpers.bash | 114 +++++++++ devbox-plugins/base-config/tests/helpers.bash | 3 + .../base-config/tests/libs/bats-assert | 1 + .../base-config/tests/libs/bats-support | 1 + devbox.json | 13 + devbox.lock | 57 +++++ 14 files changed, 567 insertions(+), 11 deletions(-) create mode 100644 .envrc create mode 100644 .github/workflows/test.yml create mode 100644 .gitmodules create mode 100644 devbox-plugins/base-config/config/ensure-submodules-populated.sh create mode 100644 devbox-plugins/base-config/tests/README.md create mode 100644 devbox-plugins/base-config/tests/ensure-submodules-populated.bats create mode 100644 devbox-plugins/base-config/tests/git-submodule-helpers.bash create mode 100644 devbox-plugins/base-config/tests/helpers.bash create mode 160000 devbox-plugins/base-config/tests/libs/bats-assert create mode 160000 devbox-plugins/base-config/tests/libs/bats-support create mode 100644 devbox.json create mode 100644 devbox.lock diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..2f05af9 --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..631c949 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +# This workflow runs the BATS test suite on every pull request and every push to master. +# Tests are co-located with the plugin they cover: +# devbox-plugins/base-config/tests/ +# +# bats-support and bats-assert are registered as git submodules pinned to specific +# tag SHAs and are populated here via actions/checkout submodules: recursive. +on: + pull_request: + push: + branches: + - master + +name: test + +jobs: + test-plugin-base-config: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + with: + # Populates bats-support and bats-assert lib submodules + submodules: recursive + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.15.0 + + - name: Run base-config plugin tests + run: devbox run test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce51da7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "devbox-plugins/base-config/tests/libs/bats-support"] + path = devbox-plugins/base-config/tests/libs/bats-support + url = https://github.com/bats-core/bats-support +[submodule "devbox-plugins/base-config/tests/libs/bats-assert"] + path = devbox-plugins/base-config/tests/libs/bats-assert + url = https://github.com/bats-core/bats-assert diff --git a/README.md b/README.md index 61a0259..3c9a9aa 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ Example: | Plugin | Description | | --- | --- | -| [base-config](#base-config) | Base configuration for a DevOps project - our initial proof-of-concept with everything. | +| [base-config](#base-config) | Base configuration for a DevOps project - our initial proof-of-concept with everything, that do not yet take "archetypes" into account and such. | -### Base config +### base-config -This plugin has the baseline of tools needed for various DevOps projects. +This plugin has the baseline of tools needed for our own various DevOps projects. This includes: @@ -38,6 +38,7 @@ This includes: * Pre-commit and linting hooks, and CI Github actions for it. * tenv, tflint and terraform-docs for working with terraform or tofu * Taskfile or reusable tasks +* _ensure-submodules-populated:_ Init hook ensure git submodules are initialized with content, to guard rail against other tools that do not automatically detect or warn about missing contents and acts as nothing is wrong. Only populates submodules marked `-` by `git submodule status` (uninitialized). Submodules with ` ` (up-to-date), `+` (different SHA / local commits), or `U` (merge conflict) prefixes are **never touched**. Skip projects without git submodules as fast as possible, by checking `.gitmodules` first (fast exit), then `git rev-parse`. Assumptions: @@ -73,7 +74,7 @@ Using 'tenv' you need to install either, the selection of the correct one, isn't #### Scripts -The base config plugin includes some scripts to help bootstrap a new project. +The `base-config` plugin includes some devbox scripts to help bootstrap a new project. Available scripts: @@ -98,9 +99,10 @@ The following credentials are **automatically provisioned** via Terraform in the | Secret | `APP_OC_GITHUB_ACTIONS_PEM` | The GitHub App private key (PEM format) | **How it works:** -- Repositories are configured in `github-infrastructure` Terraform with `add_app_oc_github_actions = true` -- Terraform automatically provisions these credentials to the repository's Actions secrets/variables -- The GitHub App itself is created once; credentials are then distributed to repos via Terraform + +* Repositories are configured in `github-infrastructure` Terraform with `add_app_oc_github_actions = true` +* Terraform automatically provisions these credentials to the repository's Actions secrets/variables +* The GitHub App itself is created once; credentials are then distributed to repos via Terraform If a repository is missing these credentials, it needs to be configured in the `github-infrastructure` repository's Terraform configuration. @@ -110,7 +112,7 @@ The devbox script common `bootstrap-taskfile` will create a taskfile in the root ## Roadmap -* If needed we could split the base config into optional plugins eg. a python plugin, a terraform plugin etc. +* If needed we could split the `base-config` plugin into optional "archetypes" kind of plugins eg. a python plugin, a terraform plugin etc. * Make bootstrap scripts idempotent (seen from the consuming projects point of view) or consider the bootstrap vs configuration concept described below. * Notice the devbox have a nice feature of writing a project readme. We don't use it yet, but could create a 2nd readme in our projects and under our project specific tooling and usage sections we always have in our project just link to this 2nd readme that is automatically generated and be kept updated automatically as well. Only problem if the autogenerated readme contain user specific paths, but it might be possible to clean up with `sed` or just wait for devbox to improve. @@ -131,3 +133,27 @@ As a first effort of trying to keep projects using the taskfile and devbox plugi Additions are typically not breaking, but moving configuration to other files or removing configuration might be breaking and thus should be cleaned up. We supply devbox run commands for migrations, e.g. `devbox run migration-1x-to-2x` for migrating from version 1.x to 2.x. The command or helper scripts document the details. + +## Tests + +As part of a bugfix, a proof-of-concept for testing the devbox plugin functionality was created. The tests verifies functionality related to the changes, and is planned to serve as inspiration for further tests. + +We have chosen to use [BATS](https://bats-core.readthedocs.io/en/stable/) ([source code](https://github.com/bats-core)) for testing, in the `base-config` plugin as it seems a reasonable fit testing shell related functionality as a devbox plugin, and especially the shell scripts used. + +For now the tests are located in the `tests` folder of the plugin. There is currently only one devbox plugin in this repository, no effort of re-use across multiple plugins with BATS helper functions have been done, nor is any wrapper functionality created in the root of the repository to collect all future tests (devbox can easily be used though). + +See details about specific tests in the READMEs in the `tests` folders. First one in [`base-config` plugin testing](devbox-plugins/base-config/tests/README.md). + +### BATS test framework guidelines + +* Use Git submodules to include BATS helper libraries like their [`bats-assert`](https://github.com/bats-core/bats-assert), so we can security review the code and ensure it is locked dependency we fully manage. +* BATS are installed through devbox in the is project. +* Use a devbox run script `test` to run the tests, easily usable in CI actions and locally with `devbox run test`. + +## Releases + +See [CHANGELOG.md](CHANGELOG.md) for releases, migration and compatibility between versions. + +Release process is described here: [https://docs.devops.oc.dk/hww/release-process/](https://docs.devops.oc.dk/hww/release-process/) + +_Sorry for the internal links in a public repository, but you basically just need to know it follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) and we use [release-please](https://github.com/googleapis/release-please) to automate the release process_. diff --git a/devbox-plugins/base-config/config/ensure-submodules-populated.sh b/devbox-plugins/base-config/config/ensure-submodules-populated.sh new file mode 100644 index 0000000..bfd0ba6 --- /dev/null +++ b/devbox-plugins/base-config/config/ensure-submodules-populated.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Ensures all git submodules have content (are populated), without modifying +# submodules that are already initialized -- even if they are at a different +# SHA or have local changes. +# +# Only submodules marked with '-' by `git submodule status` (uninitialized / +# empty) are populated. Submodules marked ' ' (up to date), '+' (different +# SHA / local commits) or 'U' (merge conflict) are left completely untouched. +# +# This is intentionally non-intrusive: it is safe to run on every devbox shell +# enter without risking loss of in-progress work inside a submodule. + +set -euo pipefail + + +# Guard order matters: checks `.gitmodules` first (fast exit), then `git rev-parse` +# to see if the repository is a git repository as all. +# Gracefully skip when there is nothing to do +if [ ! -f .gitmodules ]; then + echo "ensure-submodules-populated: git submodules not detected, skipping" + exit 0 +fi + +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "ensure-submodules-populated: not inside a git repository, skipping" + exit 0 +fi + +# Collect the paths of uninitialized submodules only. +# `git submodule status` output format: [] +# prefix '-' means the submodule has never been initialized (directory is empty). +mapfile -t uninitialized < <(git submodule status | awk '$0 ~ /^-/ { print $2 }') + +if [ "${#uninitialized[@]}" -eq 0 ]; then + echo "ensure-submodules-populated: all git submodules are already initialized, skipping" + exit 0 +fi + +echo "ensure-submodules-populated: found ${#uninitialized[@]} uninitialized submodule(s), populating..." + +for path in "${uninitialized[@]}"; do + echo " populating submodule: ${path}" + # --init : register the submodule if not yet in .git/config + # --recursive : also populate nested submodules inside this one + # -- : explicit end-of-options to safely handle unusual paths + git submodule update --init --recursive -- "${path}" +done + +echo "ensure-submodules-populated: done" diff --git a/devbox-plugins/base-config/plugin.json b/devbox-plugins/base-config/plugin.json index 893bfb1..22db482 100644 --- a/devbox-plugins/base-config/plugin.json +++ b/devbox-plugins/base-config/plugin.json @@ -70,7 +70,8 @@ "{{ .Virtenv }}/devbox-requirements.txt": "config/devbox-requirements.txt", "{{ .Virtenv }}/migrate-1x-to-2x-helper.sh": "config/migrate-1x-to-2x-helper.sh", "{{ .Virtenv }}/alias-terraform-tofu-init.sh": "config/alias-terraform-tofu-init.sh", - "{{ .Virtenv }}/init-submodules.sh": "config/init-submodules.sh" + "{{ .Virtenv }}/init-submodules.sh": "config/init-submodules.sh", + "{{ .Virtenv }}/ensure-submodules-populated.sh": "config/ensure-submodules-populated.sh" }, "shell": { "init_hook": [ @@ -103,8 +104,11 @@ "if crudini --get .gitignore \"\" \".aliases\";then true; else echo -e '\n\n# Aliases dir with script for devbox\n.aliases' >> .gitignore; fi", // run the different alias scripts: "bash {{ .Virtenv }}/alias-terraform-tofu-init.sh", - // ensure git submodules are initialized and updated so we don't have empty submodule dirs (exits clean if no submodules are present) - also merge any local commits to the update as they would else be detached - "git submodule update --init --recursive --merge" + // Ensure all git submodules have content (are populated). Only uninitialized (empty) submodules are + // populated; submodules that already have content -- including ones with local commits or uncommitted + // changes -- are left completely untouched. This prevents silent failures where tools like Ansible + // silently operate on an empty submodule directory. + "bash {{ .Virtenv }}/ensure-submodules-populated.sh" ], "scripts": { "bootstrap-envrc-private": [ diff --git a/devbox-plugins/base-config/tests/README.md b/devbox-plugins/base-config/tests/README.md new file mode 100644 index 0000000..ab9cf48 --- /dev/null +++ b/devbox-plugins/base-config/tests/README.md @@ -0,0 +1,9 @@ +# devbox base-config plugin tests + +This is just a proof-of-concept, as mentioned the [repository root README](../../../README.md). + +Overview of tests, possible further test specific details and implementation details in subsections below. + +| Test name | Description | +| --- | --- | +| `ensure-submodules-populated.bats` | Verifies the shell script that is called in the plugin init hook, to detect uninitialized git submodules of a project and initializes the with content, to guard rail against other tools that do not automatically detect or warn about missing contents and acts as nothing is wrong (_Looking angry a you Ansible!_). | diff --git a/devbox-plugins/base-config/tests/ensure-submodules-populated.bats b/devbox-plugins/base-config/tests/ensure-submodules-populated.bats new file mode 100644 index 0000000..aa3fc33 --- /dev/null +++ b/devbox-plugins/base-config/tests/ensure-submodules-populated.bats @@ -0,0 +1,233 @@ +#!/usr/bin/env bats +# FIXME docs note! Resolve paths relative to this test file so the suite can be run from any cwd +# +# Tests for the script relative to this Bats test file: +SCRIPT_UNDER_TEST="$BATS_TEST_DIRNAME/../config/ensure-submodules-populated.sh" +# +# +# Strategy: each test builds a real local git repo tree (gitrepo + bare +# "remotes") in a throwaway temp directory so no network access is needed and +# no developer state is touched. +# +# +# +# Only need load these once for all the tests in here +# bats-support / bats-assert libraries are loaded from pinned submodules +load "$BATS_TEST_DIRNAME/libs/bats-support/load.bash" +load "$BATS_TEST_DIRNAME/libs/bats-assert/load.bash" +# our own helper functions - generic as well as some specific +load "$BATS_TEST_DIRNAME/helpers.bash" +load "$BATS_TEST_DIRNAME/git-submodule-helpers.bash" + +# --------------------------------------------------------------------------- +# Lifecycle +# --------------------------------------------------------------------------- + +setup() { + # Isolate git config to a throwaway file so developer's global config is not + # affected and file:// submodule clones are allowed (blocked by default since git 2.38) + export GIT_CONFIG_GLOBAL="$BATS_TEST_TMPDIR/.gitconfig" + git config --global protocol.file.allow always +} + +# Teardown not used, because we use temporary BATS generated dirs that are automatically cleaned. +# Only clean cruft left outside those dirs. +#teardown() { +# +#} + +# --------------------------------------------------------------------------- +# Test 1: no git submodules → exits cleanly with no output +# --------------------------------------------------------------------------- +@test "no git submodules exits cleanly with no output" { + local gitrepo="$BATS_TEST_TMPDIR/repo" + create_gitrepo "$gitrepo" + + cd "$gitrepo" + run bash "${SCRIPT_UNDER_TEST}" + + assert_success + # All cases should include a small foot-print of the script was running: + assert_output "ensure-submodules-populated: git submodules not detected, skipping" +} + +# --------------------------------------------------------------------------- +# Test 2: not a git repo → exits cleanly, prints skipping message +# --------------------------------------------------------------------------- +@test "not a git repo exits cleanly with skipping message" { + local plain_dir="$BATS_TEST_TMPDIR/plain" + mkdir -p "$plain_dir" + # Script checks .gitmodules first for faster skipping + # so create a stub so it reaches the later checks in the script + touch "$plain_dir/.gitmodules" + + cd "$plain_dir" + run bash "${SCRIPT_UNDER_TEST}" + + assert_success + # All cases should include a small foot-print of the script was running: + assert_output --partial "ensure-submodules-populated: not inside a git repository, skipping" +} + +# --------------------------------------------------------------------------- +# Test 3: all submodules already populated → no changes, no output +# --------------------------------------------------------------------------- +@test "all submodules already populated produces no output and changes nothing" { + local remote_a="$BATS_TEST_TMPDIR/remote-a.git" + local remote_b="$BATS_TEST_TMPDIR/remote-b.git" + local gitrepo="$BATS_TEST_TMPDIR/repo" + + create_bare_remote "$remote_a" + create_bare_remote "$remote_b" + create_gitrepo "$gitrepo" + add_submodule_to_gitrepo "$gitrepo" "$remote_a" "sub-a" + add_submodule_to_gitrepo "$gitrepo" "$remote_b" "sub-b" + + local sha_a_before sha_b_before + sha_a_before=$(submodule_sha "$gitrepo" "sub-a") + sha_b_before=$(submodule_sha "$gitrepo" "sub-b") + + cd "$gitrepo" + run bash "${SCRIPT_UNDER_TEST}" + + assert_success + # All cases should include a small foot-print of the script was running: + assert_output "ensure-submodules-populated: all git submodules are already initialized, skipping" + + # SHAs must be unchanged + assert_equal "$(submodule_sha "$gitrepo" "sub-a")" "$sha_a_before" + assert_equal "$(submodule_sha "$gitrepo" "sub-b")" "$sha_b_before" +} + +# --------------------------------------------------------------------------- +# Test 4: one uninitialized submodule ('-' prefix) → gets populated +# --------------------------------------------------------------------------- +@test "uninitialized submodule gets populated" { + local child_remote="$BATS_TEST_TMPDIR/child-remote.git" + local parent_remote="$BATS_TEST_TMPDIR/parent-remote.git" + local gitrepo="$BATS_TEST_TMPDIR/repo" + + create_bare_remote "$child_remote" + create_bare_remote_with_child_submodule "$parent_remote" "$child_remote" + + # To avoid fumbling with de-init of submodules etc. we mimic the behavior + # we want to guard-rail against: cloning a repository with submodules + # but without initializing and update, or cloning recursive. + # We first need to push the changes to remote, then throw away + # the repository clone and clone again. + + run git clone "$parent_remote" "$gitrepo" + cd "$gitrepo" + + # Confirm the directory is empty / uninitialized before running + run git -C "$gitrepo" submodule status + assert_output --regexp '^-[0-9a-f]{5,40} child$' + + run bash "${SCRIPT_UNDER_TEST}" + assert_success + # All cases should include a small foot-print of the script was running: + assert_output --partial "ensure-submodules-populated: found 1 uninitialized submodule(s), populating..." + assert_output --partial "populating submodule: child" + assert_output --partial "ensure-submodules-populated: done" + + run git -C "$gitrepo" submodule status + assert_output --regexp '^ [0-9a-f]{5,40} child.*' + + # Submodule directory must now be non-empty (contains .git file) + # NOTICE 'child' is from internal details of the method 'create_bare_remote_with_child_submodule' + assert [ -e "$gitrepo/child/.git" ] +} + +# --------------------------------------------------------------------------- +# Test 5: submodule at different SHA ('+' prefix) → not touched +# --------------------------------------------------------------------------- +@test "submodule at different SHA is not touched" { + local remote="$BATS_TEST_TMPDIR/remote.git" + local gitrepo="$BATS_TEST_TMPDIR/repo" + + create_bare_remote "$remote" + create_gitrepo "$gitrepo" + add_submodule_to_gitrepo "$gitrepo" "$remote" "sub" + + mark_submodule_at_different_sha "$gitrepo" "sub" + + local sha_before + sha_before=$(submodule_sha "$gitrepo" "sub") + + # Confirm '+' prefix is present before running + run git -C "$gitrepo" submodule status + assert_output --partial "+" + + cd "$gitrepo" + run bash "${SCRIPT_UNDER_TEST}" + assert_success + # All cases should include a small foot-print of the script was running: + assert_output --partial "ensure-submodules-populated: all git submodules are already initialized, skipping" + + # SHA must be unchanged – script must not have touched this submodule + assert_equal "$(submodule_sha "$gitrepo" "sub")" "$sha_before" +} + +# --------------------------------------------------------------------------- +# Test 6: mixed – uninitialized submodule gets populated, '+' submodule left alone +# --------------------------------------------------------------------------- +@test "mixed: uninit submodule populated, different-SHA submodule unchanged" { + local remote_a="$BATS_TEST_TMPDIR/remote-a.git" + local remote_b="$BATS_TEST_TMPDIR/remote-b.git" + local gitrepo="$BATS_TEST_TMPDIR/repo" + + create_bare_remote "$remote_a" + create_bare_remote "$remote_b" + create_gitrepo "$gitrepo" + add_submodule_to_gitrepo "$gitrepo" "$remote_a" "sub-a" + add_submodule_to_gitrepo "$gitrepo" "$remote_b" "sub-b" + + # sub-a → uninitialised ('-') + mark_submodule_uninitialized "$gitrepo" "sub-a" + # sub-b → local commit ahead ('+') + mark_submodule_at_different_sha "$gitrepo" "sub-b" + + local sha_b_before + sha_b_before=$(submodule_sha "$gitrepo" "sub-b") + + cd "$gitrepo" + run bash "${SCRIPT_UNDER_TEST}" + assert_success + # All cases should include a small foot-print of the script was running: + assert_output --partial "ensure-submodules-populated: found 1 uninitialized submodule(s), populating..." + assert_output --partial "populating submodule: sub-a" + assert_output --partial "ensure-submodules-populated: done" + + # sub-a must now be populated + assert [ -e "$gitrepo/sub-a/.git" ] + + # sub-b SHA must be unchanged + assert_equal "$(submodule_sha "$gitrepo" "sub-b")" "$sha_b_before" +} + +# --------------------------------------------------------------------------- +# Test 7: nested submodules – parent uninitialized → both parent and nested +# child are populated (proves --recursive behaviour) +# --------------------------------------------------------------------------- +@test "nested submodules: parent and child both populated when parent is uninitialized" { + local child_remote="$BATS_TEST_TMPDIR/child-remote.git" + local parent_remote="$BATS_TEST_TMPDIR/parent-remote.git" + local gitrepo="$BATS_TEST_TMPDIR/repo" + + create_bare_remote "$child_remote" + create_bare_remote_with_child_submodule "$parent_remote" "$child_remote" + create_gitrepo "$gitrepo" + add_submodule_to_gitrepo "$gitrepo" "$parent_remote" "parent" + + mark_submodule_uninitialized "$gitrepo" "parent" + + cd "$gitrepo" + run bash "${SCRIPT_UNDER_TEST}" + assert_success + + # Parent must be populated + assert [ -e "$gitrepo/parent/.git" ] + + # Nested child inside parent must also be populated + assert [ -e "$gitrepo/parent/child/.git" ] +} diff --git a/devbox-plugins/base-config/tests/git-submodule-helpers.bash b/devbox-plugins/base-config/tests/git-submodule-helpers.bash new file mode 100644 index 0000000..c1a16cf --- /dev/null +++ b/devbox-plugins/base-config/tests/git-submodule-helpers.bash @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Git submodule BATS helper functions for ensure-submodules-populated.sh tests. +# +# NOTICE refers to BATS special variables +# https://bats-core.readthedocs.io/en/stable/writing-tests.html#run-test-other-commands +# +# Provides utilities to build isolated local git repo trees (gitrepo + +# bare "remotes") in throwaway temp directories. No network access is required. + +# Ensure some error handling. +# Most importantly is the -u option to catch misuse of the script if the BATS special variables are not set for some reason. +set -euo pipefail + +# Git identity used for all test commits to avoid depending on developer's global config. +# Usable in any test that needs to make git commits. +GIT_IDENTITY=(-c user.email="test@test" -c user.name="test for devops-tools with bats") + +# --------------------------------------------------------------------------- +# create_bare_remote +# +# Creates a local bare git repo at with a single empty initial +# commit. Used as the "remote" URL for submodule registrations so no network +# access is needed. +# --------------------------------------------------------------------------- +create_bare_remote() { + local dest="$1" + local tmp="$dest.tmp" + mkdir "$tmp" + git -C "$tmp" init -b main + git -C "$tmp" "${GIT_IDENTITY[@]}" commit --allow-empty -m "initial" + git clone --bare "$tmp" "$dest" + rm -rf "$tmp" +} + +# --------------------------------------------------------------------------- +# create_bare_remote_with_child_submodule +# +# Creates a local bare git repo at that itself contains a submodule +# pointing at . Used to set up the nested-submodule test. +# --------------------------------------------------------------------------- +create_bare_remote_with_child_submodule() { + local dest="$1" + local child_remote="$2" + local tmp="$dest.tmp" + mkdir "$tmp" + git -C "$tmp" init -b main + git -C "$tmp" "${GIT_IDENTITY[@]}" submodule add "$child_remote" child + git -C "$tmp" "${GIT_IDENTITY[@]}" commit -m "add child submodule" + git clone --bare "$tmp" "$dest" + rm -rf "$tmp" +} + +# --------------------------------------------------------------------------- +# create_gitrepo +# +# Initialises an empty git repo at with an initial commit. +# Submodules are added separately via add_submodule_to_gitrepo. +# --------------------------------------------------------------------------- +create_gitrepo() { + local dir="$1" + mkdir "$dir" + git -C "$dir" init -b main + git -C "$dir" "${GIT_IDENTITY[@]}" commit --allow-empty -m "initial" +} + +# --------------------------------------------------------------------------- +# add_submodule_to_gitrepo +# +# Adds a submodule at inside the gitrepo, pointing at +# . Commits the change so the gitrepo records the submodule SHA. +# --------------------------------------------------------------------------- +add_submodule_to_gitrepo() { + local gitrepo="$1" + local remote="$2" + local subpath="$3" + git -C "$gitrepo" "${GIT_IDENTITY[@]}" submodule add "$remote" "$subpath" + git -C "$gitrepo" "${GIT_IDENTITY[@]}" commit -m "add submodule $subpath" +} + +# --------------------------------------------------------------------------- +# mark_submodule_uninitialized +# +# Deinits the submodule: removes the working-tree content and the .git/config +# entry. `git submodule status` will show '-' prefix for this path. +# --------------------------------------------------------------------------- +mark_submodule_uninitialized() { + local gitrepo="$1" + local subpath="$2" + git -C "$gitrepo" submodule deinit "$subpath" +} + +# --------------------------------------------------------------------------- +# mark_submodule_at_different_sha +# +# Makes a new commit inside the already-checked-out submodule directory. +# The gitrepo still records the old SHA, so `git submodule status` +# shows '+' prefix (checked out at a SHA different from what is recorded). +# --------------------------------------------------------------------------- +mark_submodule_at_different_sha() { + local gitrepo="$1" + local subpath="$2" + git -C "$gitrepo/$subpath" "${GIT_IDENTITY[@]}" commit --allow-empty -m "local commit in submodule" +} + +# --------------------------------------------------------------------------- +# submodule_sha +# +# Returns the current HEAD SHA of the checked-out submodule. +# --------------------------------------------------------------------------- +submodule_sha() { + local gitrepo="$1" + local subpath="$2" + git -C "$gitrepo/$subpath" rev-parse HEAD +} diff --git a/devbox-plugins/base-config/tests/helpers.bash b/devbox-plugins/base-config/tests/helpers.bash new file mode 100644 index 0000000..3ba3f7d --- /dev/null +++ b/devbox-plugins/base-config/tests/helpers.bash @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Generic BATS helper functions, reusable across all test suites. + diff --git a/devbox-plugins/base-config/tests/libs/bats-assert b/devbox-plugins/base-config/tests/libs/bats-assert new file mode 160000 index 0000000..f1e9280 --- /dev/null +++ b/devbox-plugins/base-config/tests/libs/bats-assert @@ -0,0 +1 @@ +Subproject commit f1e9280eaae8f86cbe278a687e6ba755bc802c1a diff --git a/devbox-plugins/base-config/tests/libs/bats-support b/devbox-plugins/base-config/tests/libs/bats-support new file mode 160000 index 0000000..24a72e1 --- /dev/null +++ b/devbox-plugins/base-config/tests/libs/bats-support @@ -0,0 +1 @@ +Subproject commit 24a72e14349690bcbf7c151b9d2d1cdd32d36eb1 diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..2c1258e --- /dev/null +++ b/devbox.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetpack-io/devbox/main/schemas/devbox.schema.json", + "packages": [ + // bats: Bash Automated Testing System, used to test the devbox plugin shell scripts + // https://github.com/bats-core/bats-core + "bats@1.12.0" + ], + "shell": { + "scripts": { + "test": "bats --print-output-on-failure devbox-plugins/base-config/tests/ensure-submodules-populated.bats" + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..7294427 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,57 @@ +{ + "lockfile_version": "1", + "packages": { + "bats@1.12.0": { + "last_modified": "2026-03-21T07:29:51Z", + "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#bats", + "source": "devbox-search", + "version": "1.12.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/cf6x64q554kxbd4hrdz5wa7fpksqchl8-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/cf6x64q554kxbd4hrdz5wa7fpksqchl8-bats-1.12.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/bs1bpak7wqbbns9zm0dy8abprw40gyqi-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/bs1bpak7wqbbns9zm0dy8abprw40gyqi-bats-1.12.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8n192nfnqffnxqh9m4agr0gppa64ir5x-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/8n192nfnqffnxqh9m4agr0gppa64ir5x-bats-1.12.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6lnqfzv0xh0nv5gm4a74kj5apa9czfbf-bats-1.12.0", + "default": true + } + ], + "store_path": "/nix/store/6lnqfzv0xh0nv5gm4a74kj5apa9czfbf-bats-1.12.0" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-03-30T07:26:21Z", + "resolved": "github:NixOS/nixpkgs/15c6719d8c604779cf59e03c245ea61d3d7ab69b?lastModified=1774855581&narHash=sha256-YkreHeMgTCYvJ5fESV0YyqQK49bHGe2B51tH6claUh4%3D" + } + } +}