Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions .github/workflows/package.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Push Package Images
name: Build and Push Packages

on:
workflow_dispatch:
Expand All @@ -10,60 +10,60 @@ on:
workflow_call:

jobs:
build:
test:
uses: ./.github/workflows/test.yaml

package:
runs-on: ubuntu-latest
needs: [test]
if: always() && needs.test.outputs.packages != '{"package":[]}'
strategy:
matrix: ${{fromJson(needs.setup.outputs.matrix)}}
needs: [setup]
matrix: ${{ fromJson(needs.test.outputs.packages) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Check if all tests passed for this package
id: check
run: |
if jq -e '.${{ matrix.package }} == "false"' <<<'${{ needs.test.outputs.results }}'; then
Comment thread
DanielRuthardt marked this conversation as resolved.
echo "❌ Some tests failed for package ${{ matrix.package }}"
echo "package=false" >> $GITHUB_OUTPUT
else
echo "βœ… All tests passed for package ${{ matrix.package }}"
echo "package=true" >> $GITHUB_OUTPUT
fi

- name: Set up Docker
if: steps.check.outputs.package == 'true'
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
if: steps.check.outputs.package == 'true'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create temporary Dockerfile
if: steps.check.outputs.package == 'true'
run: |
cat >libs/Dockerfile <<'EOF'
FROM scratch
LABEL org.opencontainers.image.source https://github.com/${{ github.repository }}

ARG PACKAGE
ADD ${PACKAGE} /${PACKAGE}/
EOF

- name: Build and push Docker image
if: steps.check.outputs.package == 'true'
uses: docker/build-push-action@v5
with:
context: ./libs
file: ./libs/Dockerfile
build-args: PACKAGE=${{ matrix.item }}
build-args: PACKAGE=${{ matrix.package }}
push: true
tags: ghcr.io/lf-certification/bashp-${{ matrix.item }}:latest

setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: List all directories in libs and create matrix
id: set-matrix
run: |
json=$(
(
for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do
echo -n '"'"${directory#*/}"'",'
done
echo '""'
) | sed -E 's/,?""$//'
)

echo "matrix={\"item\":[$json]}" >> $GITHUB_OUTPUT
shell: bash
tags: ghcr.io/lf-certification/bashp-${{ matrix.package }}:latest
109 changes: 109 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Test Functions

on:
workflow_dispatch:
push:
paths:
- 'libs/**'
workflow_call:
outputs:
packages:
description: All packages
value: ${{ jobs.setup.outputs.packages }}
functions:
description: All functions with tests
value: ${{ jobs.setup.outputs.functions }}
results:
description: Packages with some failed tests
value: ${{ jobs.test.outputs.results }}

jobs:
setup:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.packages.outputs.matrix }}
functions: ${{ steps.functions.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Get all packages
id: packages
shell: bash
run: |
json=$(
(
for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do
echo -n '"'"${directory#*/}"'",'
done
echo '""'
) | sed -E 's/,?""$//'
)
echo "matrix={\"package\":[$json]}" >> $GITHUB_OUTPUT

- name: Get all functions
id: functions
shell: bash
run: |
json=$(
(
for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do
package="${directory#*/}"

for file in $(find "libs/$package" -type f -and \
! -name '\*' -and \
Comment thread
DanielRuthardt marked this conversation as resolved.
Comment thread
DanielRuthardt marked this conversation as resolved.
! -wholename '*/tests/*'); do
directory=$(dirname "$file")
fileName=$(basename "$file")
if [[ ! -d "$directory/tests/$fileName" || ! -f "$directory/tests/$fileName/test" ]]; then
continue
fi

functionName=${file#libs/*}
functionName=${functionName//\//::}

echo -n '{"package":"'"$package"'","function":"'"$functionName"'"},'
done
done
echo '""'
) | sed -E 's/,?""$//'
)

echo "matrix={\"function\":[$json]}" >> $GITHUB_OUTPUT

test:
runs-on: ubuntu-latest
outputs:
results: ${{ toJson(steps.test.outputs) }}
needs: [setup]
if: needs.setup.outputs.functions != '{"function":[]}'
strategy:
matrix: ${{ fromJson(needs.setup.outputs.functions) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Test function
id: test
shell: bash
run: |
export PATH=".:$PATH"
export BASHP_INCLUDE_DIRS="libs"

set +e
output=$(bashpt "${{ matrix.function.function }}" 2>&1)
exitCode=$?
echo "$output"

if [[ $exitCcode -eq 0 ]]; then
echo "βœ… Test passed for ${{ matrix.function.function }}"
elif [[ $exitCcode -eq 127 ]]; then
echo "πŸ” No test defined for ${{ matrix.function.function }} (acceptable)"
else
echo "❌ Test failed for ${{ matrix.function.function }} (exit code: $exitCcode)"
echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT
fi
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ functionalities with ease.
advanced features.
- **Preprocessing**: A preprocessing step that allows for dynamic inclusion of scripts and
packages, supporting multiple package sources and enhancing modularity.
- **Built-in Testing Framework**: Simple, convention-based testing with container isolation and
visual feedback (βœ… β›” βš™οΈ ⚠️). [Learn more β†’](docs/TESTING.md)
- **Git Integration**: Automatically adds included functions to `.gitignore` when pulling from
external sources, preventing unintended commits.

Expand Down Expand Up @@ -87,6 +89,21 @@ This script demonstrates three ways to include and use functions from external l
in your Bash+ scripts. The third option shows how to use nested package paths for better organization
of your code.

### Testing Your Code

Bash+ includes a built-in testing framework that makes it easy to test your functions:

```bash
# Test a specific function
bashpt user::get_uid

# Run tests locally without container isolation
BASHPT_LOCAL=true bashpt user::get_uid
```

Tests are automatically discovered based on naming conventions and run in isolated Docker containers
by default. For more details, see the [Testing Documentation](docs/TESTING.md).

### Package Path Resolution

Bash+ supports nested package paths, allowing for better organization of your code. For example:
Expand Down
63 changes: 63 additions & 0 deletions bashpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
#
# Copyright 2025 The Linux Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -eo pipefail

functionName=${1:?function to be tested must be set}
path=libs/${functionName//::/\/}
testPath=$(dirname "$path")/tests/${functionName##*::}
testFunction=${functionName%::*}::tests::${functionName##*::}::test

if [[ -d "$testPath" && -f "$testPath/test" ]]; then
test=$(bashpc <<<"set -euo pipefail; $testFunction") || {
echo "β›” Failed to prepare test script for '$testFunction'" >&2
exit 2
}

returnCode=0
if [[ "${BASHPT_LOCAL:-false}" == "true" ]]; then
# Run tests locally
bash -c "$test" || returnCode=$?
else
# Run tests in Docker container
arguments=(--rm)

# Only add -ti if we have a TTY (not in CI)
if [ -t 0 ]; then
Comment thread
DanielRuthardt marked this conversation as resolved.
arguments+=(-ti)
fi

image="${BASHPT_IMAGE:-ubuntu:latest}"

# Add user override if specified
if [[ -n "${BASHPT_USER:-}" ]]; then
arguments+=(-u "$BASHPT_USER")
fi

docker run "${arguments[@]}" "$image" bash -c "$test" || returnCode=$?
fi

if [[ $returnCode -eq 0 ]]; then
echo "βœ… All assertions passed: '$testFunction' passed"
exit 0
else
echo "β›” Some assertions failed: '$testFunction' failed" >&2
exit 1
fi
else
echo "🚫 No test $testFunction defined for $functionName, skipping."
exit 127
Comment thread
DanielRuthardt marked this conversation as resolved.
fi
Loading