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
1 change: 1 addition & 0 deletions .github/workflows/issue-branch-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ jobs:
;;
esac

# shellcheck disable=SC2153 # This is an intentional workflow-level env input.
if [[ ! "$PULL_REQUEST_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
fail_policy "$target_head_sha" "Pull request number must be a positive integer" || true
exit 1
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Quality

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

concurrency:
group: quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Quality gates
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
fetch-depth: 0

- name: Install ShellCheck
run: sudo apt-get install -y shellcheck

- name: Enforce warning ShellCheck profile
run: ./tests/lint-warnings.sh

- name: Enforce repository quality contract
run: ./tests/quality-contract.sh

- name: Enforce shfmt profile
run: |
if [[ -n "${GITHUB_BASE_REF:-}" ]]; then
format_base="origin/$GITHUB_BASE_REF"
else
format_base='HEAD^'
fi
mapfile -t format_files < <(
git diff --name-only "$format_base"...HEAD -- \
'*.sh' '*.bash' 'bin/base-bash' 'scripts/api-manifest' \
'scripts/first-party-cutover' 'scripts/library-bundle' \
'scripts/migrate-v2-symbols' 'scripts/release' 'scripts/vendor' \
'tests/fixtures/basectl-release-stub'
)
((${#format_files[@]} > 0)) || {
echo 'No changed Bash sources require formatting.'
exit 0
}
docker run --rm \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges=true \
--user 65534:65534 \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m,mode=1777 \
--mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \
--workdir /workspace \
docker.io/mvdan/shfmt@sha256:44154be4ec86510d0d98d6eae0cd9d2927bcf8499791fdbee1fa2009b1b32589 \
-d -ln bash -i 4 -sr "${format_files[@]}"

- name: Lint GitHub workflows
run: |
docker run --rm \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges=true \
--user 65534:65534 \
--mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \
--workdir /workspace \
docker.io/rhysd/actionlint@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7 \
-color
54 changes: 52 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
permissions:
contents: read

concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate (${{ matrix.os }})
Expand All @@ -17,8 +21,8 @@ jobs:
fail-fast: false
matrix:
os:
- macos-latest
- ubuntu-latest
- macos-14
- ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
Expand Down Expand Up @@ -99,6 +103,52 @@ jobs:
bash tests/benchmark-contract.sh
'

representative-bash:
name: Compatibility (Bash ${{ matrix.version }})
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- version: 4.4.23
image: docker.io/library/bash@sha256:43ad196f649f1a759d7d5a18c771f2d87290051b4ae053b6eadb0a8b72dbd4eb
- version: 5.0.18
image: docker.io/library/bash@sha256:2ec6d5dd02c732f425fe18c4bd5f02065381ec0083311dffcfb7bad62430e88f
- version: 5.2.37
image: docker.io/library/bash@sha256:534a5f1d11652aadaa9f08838f6637ac11a46a8b4b736a4cbf09c5945e38516f
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Run representative Bash contract
env:
BASH_IMAGE: ${{ matrix.image }}
run: |
docker run --rm \
--platform linux/amd64 \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges=true \
--pids-limit 128 \
--user 65534:65534 \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=32m,mode=1777 \
--env HOME=/tmp \
--env TMPDIR=/tmp \
--mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \
--workdir /workspace \
"$BASH_IMAGE" \
bash -c '
set -e
bash tests/bash-option-contract.sh
bash tests/property-contract.sh
bash tests/artifact-contract.sh
bash tests/benchmark-contract.sh
bash tests/concurrency-contract.sh
'

release-gates:
name: Release gates (matrix and provenance)
runs-on: ubuntu-24.04
Expand Down
26 changes: 21 additions & 5 deletions benchmarks/reference-apps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,37 @@ launcher="$repo_root/bin/base-bash"
framework="$repo_root/lib/bash"
iterations="${BASE_REFERENCE_BENCHMARK_ITERATIONS:-10}"

[[ "$iterations" =~ ^[1-9][0-9]*$ ]] || { printf 'iterations must be positive\n' >&2; exit 2; }
[[ "$iterations" =~ ^[1-9][0-9]*$ ]] || {
printf 'iterations must be positive\n' >&2
exit 2
}

benchmark_timestamp_ns() {
local timestamp

timestamp="$(date +%s%N 2> /dev/null || true)"
if [[ "$timestamp" =~ ^[0-9]+$ ]]; then
printf '%s\n' "$timestamp"
return 0
fi

timestamp="$(date +%s 2> /dev/null)" || return 1
printf '%s000000000\n' "$timestamp"
}

printf 'benchmark_schema=1\n'
printf 'bash=%s\n' "$BASH_VERSION"
printf 'os=%s\n' "$(uname -s)"
printf 'iterations=%s\n' "$iterations"
printf 'framework_commit=%s\n' "$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || printf unknown)"
printf 'framework_commit=%s\n' "$(git -C "$repo_root" rev-parse HEAD 2> /dev/null || printf unknown)"

for app in installer release-helper ops-cli; do
start_ns="$(date +%s%N 2>/dev/null || date +%s000000000)"
start_ns="$(benchmark_timestamp_ns)" || exit 1
for ((index = 0; index < iterations; index++)); do
BASE_BASH_LIBS_DIR="$framework" "$launcher" \
"$repo_root/examples/reference-apps/$app/bin/app" --help >/dev/null || exit $?
"$repo_root/examples/reference-apps/$app/bin/app" --help > /dev/null || exit $?
done
end_ns="$(date +%s%N 2>/dev/null || date +%s000000000)"
end_ns="$(benchmark_timestamp_ns)" || exit 1
elapsed_ns=$((end_ns - start_ns))
printf 'app=%s\tstartup_help_total_ns=%s\tstartup_help_avg_ns=%s\n' \
"$app" "$elapsed_ns" "$((elapsed_ns / iterations))"
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ when updating an existing script.
- [API reference](api-reference.md) and [v2 API contract](v2-api-contract.md)
- [Configuration, lifecycle, and status contracts](v2/architecture.md#application-contract)
- [Testing, vendoring, bundling, and release](v2/architecture.md#delivery)
- [CI and default-branch policy](ci-policy.md)
- [Support and threat model](support-policy.md) · [security reporting](../SECURITY.md)

The quickstart is deliberately pinned to an immutable release ref. It never
Expand Down
50 changes: 50 additions & 0 deletions docs/ci-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# CI and default-branch policy

The `Tests` and `Quality` workflows are release gates, not advisory examples.
They run with `contents: read`, cancel superseded runs, and keep network access
out of the containerized compatibility and lint checks. Actions and container
images are pinned to full immutable commit or digest references; changing one
requires a reviewable dependency update.

The shfmt gate checks every Bash source changed by a pull request (and the
latest commit on `main`). This prevents new formatting debt while allowing the
existing v1-to-v2 codebase to be cleaned incrementally; touching a legacy file
puts its complete contents under the formatter gate.

## Required checks

The `main` branch must require these checks before merge:

- `Tests / Validate (macos-14)`
- `Tests / Validate (ubuntu-24.04)`
- `Tests / Compatibility (Bash 4.4.23)`
- `Tests / Compatibility (Bash 5.0.18)`
- `Tests / Compatibility (Bash 5.2.37)`
- `Tests / Compatibility smoke (Bash 4.2.53)`
- `Tests / Release gates (matrix and provenance)`
- `Quality / Quality gates`
- `Issue Branch Policy / Publish issue branch policy`

Require one approving review, dismiss stale approvals after new commits, and
require branches to be up to date before merging. Administrators should keep
the protection enforced; an emergency merge is an auditable exception, not a
replacement for the required checks.

## Emergency procedure

1. Record the incident, affected commit, approver, and reason in the pull
request and the umbrella issue.
2. Use an administrator-only bypass only for a time-sensitive remediation.
3. Restore branch protection immediately and run the complete workflows on the
resulting `main` commit.
4. Open a follow-up issue for every skipped check or review, with a concrete
owner and due date.

## Platform claims

The hosted matrix covers current macOS, Linux/glibc, exact Bash 4.2.53, and
representative Bash 4.x and 5.x releases. Alpine/musl runs in the networkless
release gate. No maintained GitHub-hosted BSD runner is currently available;
BSD remains explicitly advisory until a maintained runner can execute the same
contract. A release must not describe advisory BSD evidence as a supported
guarantee.
18 changes: 14 additions & 4 deletions docs/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ unreported pass.

| Dimension | Supported contract | Evidence |
| --- | --- | --- |
| Bash | 4.2.53 minimum; representative 4.x; current 5.x | Pinned networkless Bash 4.2 smoke, current runner validation, \`tests/compatibility-matrix.sh\`, deterministic property and artifact contracts |
| macOS | Current GitHub-hosted macOS with Homebrew Bash; system Bash 3.2 is rejected with remediation | macOS validation and unsupported-system-Bash smoke |
| Linux/glibc | Ubuntu runner and pinned Bash 4.2 container | Ubuntu validation and compatibility workflow |
| Bash | 4.2.53 minimum; representative 4.4.23 and 5.0.18/5.2.37; current 5.x | Pinned networkless Bash containers, current runner validation, \`tests/compatibility-matrix.sh\`, deterministic property, artifact, and concurrency contracts |
| macOS | Pinned GitHub-hosted macOS 14 with Homebrew Bash; system Bash 3.2 is rejected with remediation | macOS validation and unsupported-system-Bash smoke |
| Linux/glibc | Pinned Ubuntu 24.04 runner and Bash 4.2/4.4/5.0/5.2 containers | Ubuntu validation and compatibility workflow |
| Linux/musl | Alpine/musl syntax and option-contract probe when the runner provides Docker | \`tests/compatibility-matrix.sh --container alpine\` |
| BSD userland | Best-effort portability checks; no release guarantee until a maintained CI runner is available | Explicitly reported as advisory |
| Locale | UTF-8 and \`C\` locale behavior for parsing, sorting, and diagnostics | Option and parser tests; caller owns locale selection |
| Filesystem | Local POSIX filesystem; symlink and race checks are fail-closed | cleanup, import, bundle, vendor, marker, and artifact-contract tests |
| Network | Core tests are networkless; GitHub/Homebrew integrations are optional and bounded | workflow permissions, Docker \`--network none\`, retry tests |
| Network | Core tests are networkless; GitHub/Homebrew integrations are optional and bounded | read-only workflow permissions, Docker \`--network none\`, retry tests |

## Strict-option combinations

Expand All @@ -39,6 +39,16 @@ argv quoting, empty and glob-like fields, repeatable options, marker edits, and
command-like data. The seed is reported on failure so a downstream report can
replay the exact case without network access or a package manager.

\`tests/concurrency-contract.sh\` runs sixteen independent import and cleanup
workers in parallel and verifies unique managed temporary directories. Signal,
process-tree, and launcher cleanup behavior is covered by the launcher BATS
suite; benchmark evidence is checked by \`tests/benchmark-contract.sh\`.

Workflow formatting and action syntax are required in the separate Quality
workflow. Its shfmt and actionlint images are full-digest pinned and run with
network disabled, read-only mounts, dropped capabilities, and least-privilege
users. See [the CI policy](ci-policy.md) for the branch-protection contract.

## Caller responsibilities

Applications must select a supported Bash, avoid mutating framework-owned
Expand Down
92 changes: 92 additions & 0 deletions tests/concurrency-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash

# Exercise import, temporary-directory, cleanup, and launcher boundaries in
# parallel processes. The contract is deliberately deterministic and
# networkless; it catches shared-state regressions without making a timing
# claim about a particular machine.

concurrency_script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" || exit 1
concurrency_repo_root="$(cd -- "$concurrency_script_dir/.." && pwd -P)" || exit 1
concurrency_tmp="$(mktemp -d "${TMPDIR:-/tmp}/base-bash-concurrency-contract.XXXXXX")" || exit 1
concurrency_status=0

concurrency_cleanup() {
rm -rf -- "$concurrency_tmp"
}
trap concurrency_cleanup EXIT

concurrency_fail() {
printf 'Concurrency contract failed: %s\n' "$*" >&2
concurrency_status=1
}

concurrency_worker() {
local worker_id="$1"
local worker_dir output expected
trap - EXIT
# shellcheck disable=SC2034 # base_init publishes into this caller-owned array by name.
local -a init_args=()

export BASE_BASH_LIBS_DIR="$concurrency_repo_root/lib/bash"
# shellcheck source=../lib/bash/std/lib_std.sh
source "$concurrency_repo_root/lib/bash/std/lib_std.sh" || return 1
base_init init_args --source "$concurrency_script_dir/concurrency-contract.sh" -- || return 1
base_std_make_temp_dir worker_dir "concurrency-$worker_id" || return 1
base_std_import str/lib_str.sh || return 1
output=" worker-$worker_id "
base_str_trim output || return 1
expected="worker-$worker_id"
[[ "$output" == "$expected" ]] || return 1
[[ -d "$worker_dir" ]] || return 1
# Async function subshells do not consistently deliver inherited EXIT
# traps on every supported Bash release. Invoke the shared dispatcher
# explicitly so the cleanup assertion remains portable and deterministic.
__base_bash_libs_std_run_cleanup_hooks__ || return $?
[[ ! -e "$worker_dir" ]] || return 1
printf '%s\n' "$worker_dir"
}

concurrency_workers=()
concurrency_logs=()
concurrency_worker_count=16
for ((concurrency_index = 1; concurrency_index <= concurrency_worker_count; concurrency_index += 1)); do
concurrency_log="$concurrency_tmp/worker-$concurrency_index.log"
concurrency_logs+=("$concurrency_log")
concurrency_worker "$concurrency_index" > "$concurrency_log" 2>&1 &
concurrency_workers+=("$!")
done

for concurrency_index in "${!concurrency_workers[@]}"; do
if ! wait "${concurrency_workers[$concurrency_index]}"; then
concurrency_fail "worker $((concurrency_index + 1)) returned a failure"
sed 's/^/ /' "${concurrency_logs[$concurrency_index]}" >&2
fi
done

concurrency_seen="$concurrency_tmp/seen"
: > "$concurrency_seen" || concurrency_fail 'could not create result ledger'
for concurrency_log in "${concurrency_logs[@]}"; do
concurrency_worker_dir="$(tail -n 1 "$concurrency_log" 2> /dev/null || true)"
[[ -n "$concurrency_worker_dir" ]] || {
concurrency_fail "worker did not publish a managed directory: $concurrency_log"
continue
}
[[ ! -e "$concurrency_worker_dir" ]] ||
concurrency_fail "worker directory was not cleaned up: $concurrency_worker_dir"
printf '%s\n' "$concurrency_worker_dir" >> "$concurrency_seen"
done

concurrency_result_count="$(wc -l < "$concurrency_seen" | tr -d ' ')"
[[ "$concurrency_result_count" == "$concurrency_worker_count" ]] ||
concurrency_fail "expected $concurrency_worker_count worker results, found $concurrency_result_count"

concurrency_unique_count="$(sort -u "$concurrency_seen" | wc -l | tr -d ' ')"
[[ "$concurrency_unique_count" == "$concurrency_worker_count" ]] ||
concurrency_fail 'parallel workers received duplicate temporary directories'

if ((concurrency_status != 0)); then
exit "$concurrency_status"
fi

printf 'Concurrency contract passed: workers=%s unique-temp-dirs=%s.\n' \
"$concurrency_worker_count" "$concurrency_unique_count"
Loading
Loading