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
20 changes: 20 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,23 @@ jobs:
--workdir /workspace \
docker.io/rhysd/actionlint@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7 \
-color

quality-contract:
name: Quality contract
if: ${{ always() }}
needs: [quality]
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Require the quality lane to succeed
env:
QUALITY_RESULT: ${{ needs.quality.result }}
run: |
set -euo pipefail
if [[ "$QUALITY_RESULT" != success ]]; then
printf 'Quality contract is blocked by a non-success quality lane: %s\n' \
"$QUALITY_RESULT" >&2
exit 1
fi
printf 'Quality contract passed: the quality lane succeeded.\n'
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,33 @@ jobs:
framework_commit="$(git rev-parse --verify 'HEAD^{commit}')"
.downstream/base-bash-libs-demo/tests/candidate-smoke.sh \
"$GITHUB_WORKSPACE" "$(< VERSION)" "$framework_commit"

product-validation:
name: Product validation
if: ${{ always() }}
needs: [validate, bash-42-logging, representative-bash, release-gates, downstream-demo]
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Require every product lane to succeed
env:
VALIDATE_RESULT: ${{ needs.validate.result }}
BASH_42_RESULT: ${{ needs.bash-42-logging.result }}
REPRESENTATIVE_BASH_RESULT: ${{ needs.representative-bash.result }}
RELEASE_GATES_RESULT: ${{ needs.release-gates.result }}
DOWNSTREAM_DEMO_RESULT: ${{ needs.downstream-demo.result }}
run: |
set -euo pipefail
failures=()
[[ "$VALIDATE_RESULT" == success ]] || failures+=("Validate")
[[ "$BASH_42_RESULT" == success ]] || failures+=("Compatibility smoke (Bash 4.2.53)")
[[ "$REPRESENTATIVE_BASH_RESULT" == success ]] || failures+=("Compatibility (Bash matrix)")
[[ "$RELEASE_GATES_RESULT" == success ]] || failures+=("Release gates (matrix and provenance)")
[[ "$DOWNSTREAM_DEMO_RESULT" == success ]] || failures+=("Downstream demo (Beacon)")
if (( ${#failures[@]} > 0 )); then
printf 'Product validation is blocked by non-success lanes: %s\n' \
"${failures[*]}" >&2
exit 1
fi
printf 'Product validation passed: all required product lanes succeeded.\n'
58 changes: 53 additions & 5 deletions docs/ci-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ 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.

Each workflow ends with a stable, fail-closed aggregate context:

- `Product validation` depends on Linux and macOS repository validation, the
Bash 4.2.53 minimum-runtime smoke, the representative Bash compatibility
matrix, release/provenance gates, and the pinned Beacon downstream smoke.
- `Quality contract` depends on the ShellCheck, repository-contract, shfmt, and
actionlint quality lane.

Both aggregates run with `always()` so a cancelled, failed, or unexpectedly
skipped dependency cannot leave a green required context. They explicitly
require every dependency result to equal `success`. The individual lanes remain
visible for diagnosis, while the aggregate contexts are the merge-policy
surface.

These aggregates run in the same `pull_request` workflows they protect; they
are not a trusted `pull_request_target` boundary. That is an intentional
solo-maintainer trade-off for this repository, and changes to the aggregate
logic still require review through the repository's existing workflow.

The workflows emitted by `base-bash init` follow the same policy: every
third-party action is pinned to a full commit SHA and carries a human-readable
release comment. Generated consumer workflows can therefore be reviewed and
Expand All @@ -16,7 +35,7 @@ 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.

## Default-branch baseline
## Default-branch baseline (current)

`base-bash-libs` follows the same modest default-branch baseline as Base:

Expand All @@ -26,12 +45,41 @@ puts its complete contents under the formatter gate.
`base/issue-branch-policy` status, and prevents deletion and non-fast-forward
updates;
- administrators remain subject to branch protection; and
- no approval count or individual Tests/Quality job is a default merge
requirement.
- no approval count is a default merge requirement.

## Planned required aggregate contexts

After the aggregate jobs land on `main`, the effective ruleset should require
these exact GitHub Actions contexts in addition to `base/issue-branch-policy`:

| Context | Actions integration | Coverage |
| --- | ---: | --- |
| `Product validation` | `15368` | Supported-platform, minimum-runtime, compatibility, release-contract, and Beacon evidence |
| `Quality contract` | `15368` | ShellCheck, repository quality, shfmt, and actionlint evidence |

The repository owner must add those contexts through the normal reviewed
ruleset/configuration workflow, then read back both the effective ruleset and
classic branch protection. The readback must confirm the exact context names,
integration ID `15368`, strictness, review/thread settings, and any existing
stronger controls. `base/issue-branch-policy` remains required; project metadata
intake remains outside these product gates. Until that administrative readback
is complete, the aggregate checks are present and fail closed but are not yet
merge-blocking.

The repeatable readback commands are:

```bash
gh api repos/basefoundry/base-bash-libs/rules/branches/main
gh api repos/basefoundry/base-bash-libs/branches/main/protection
```

Inspect the returned rules and required-status contexts rather than relying on
the workflow files alone.

This is a merge-policy choice, not a validation waiver. The `Tests` and
`Quality` workflows still run on pull requests and `main`, and they remain
release gates. Run the complete local validation and release readiness checks
`Quality` workflows still run on pull requests and `main`, and their aggregate
contexts are the merge-blocking release gates once the ruleset readback is
complete. Run the complete local validation and release readiness checks
before publishing a release, even when a pull request can merge after the
issue-branch policy succeeds.

Expand Down
80 changes: 80 additions & 0 deletions tests/ci-policy-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" || exit 1
tests_workflow="$repo_root/.github/workflows/tests.yml"
quality_workflow="$repo_root/.github/workflows/quality.yml"

contract_fail() {
printf 'CI policy contract failed: %s\n' "$*" >&2
exit 1
}

for file in "$tests_workflow" "$quality_workflow"; do
[[ -f "$file" ]] || contract_fail "missing workflow: $file"
done

grep -F ' product-validation:' "$tests_workflow" > /dev/null ||
contract_fail 'product validation aggregate job is missing'
grep -F ' name: Product validation' "$tests_workflow" > /dev/null ||
contract_fail 'product validation context name is unstable or missing'
grep -F ' if: ${{ always() }}' "$tests_workflow" > /dev/null ||
contract_fail 'product validation must run after skipped or failed dependencies'
grep -F ' needs: [validate, bash-42-logging, representative-bash, release-gates, downstream-demo]' \
"$tests_workflow" > /dev/null ||
contract_fail 'product validation must cover every product lane'
for result_name in VALIDATE_RESULT BASH_42_RESULT REPRESENTATIVE_BASH_RESULT RELEASE_GATES_RESULT DOWNSTREAM_DEMO_RESULT; do
grep -F " $result_name:" "$tests_workflow" > /dev/null ||
contract_fail "product result is not wired: $result_name"
grep -F "[[ \"\$$result_name\" == success ]]" "$tests_workflow" > /dev/null ||
contract_fail "product result is not required to succeed: $result_name"
done

product_needs_line="$(
awk '
/^ product-validation:/ { in_product = 1; next }
in_product && /^ [A-Za-z0-9_-]+:/ { exit }
in_product && /needs:/ { print; exit }
' "$tests_workflow"
)"
[[ -n "$product_needs_line" ]] ||
contract_fail 'product validation needs list is missing'
while IFS= read -r job_id; do
[[ "$job_id" == product-validation ]] && continue
awk -v expected_job="$job_id" '
{
for (i = 1; i <= NF; i++) {
token = $i
gsub(/[\[\],]/, "", token)
if (token == expected_job) {
found = 1
}
}
}
END { exit !found }
' <<< "$product_needs_line" ||
contract_fail "product validation does not cover job: $job_id"
done < <(
awk '
/^jobs:/ { in_jobs = 1; next }
in_jobs && /^ [A-Za-z0-9_-]+:/ {
job_id = $1
sub(/:$/, "", job_id)
print job_id
}
' "$tests_workflow"
)

grep -F ' quality-contract:' "$quality_workflow" > /dev/null ||
contract_fail 'quality aggregate job is missing'
grep -F ' name: Quality contract' "$quality_workflow" > /dev/null ||
contract_fail 'quality context name is unstable or missing'
grep -F ' if: ${{ always() }}' "$quality_workflow" > /dev/null ||
contract_fail 'quality aggregate must run after a skipped or failed dependency'
grep -F ' needs: [quality]' "$quality_workflow" > /dev/null ||
contract_fail 'quality aggregate must depend on the quality lane'
grep -F ' QUALITY_RESULT: ${{ needs.quality.result }}' "$quality_workflow" > /dev/null ||
contract_fail 'quality result is not wired into the aggregate'
grep -F 'if [[ "$QUALITY_RESULT" != success ]]' "$quality_workflow" > /dev/null ||
contract_fail 'quality aggregate must require success, not merely completion'

printf 'CI policy contract passed: product and quality aggregates are explicit and fail closed.\n'
1 change: 1 addition & 0 deletions tests/lint-warnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ lint_files=(
tests/reference-release-contract.sh
tests/concurrency-contract.sh
tests/quality-contract.sh
tests/ci-policy-contract.sh
tests/shfmt-contract.sh
examples/std-usage.sh
examples/cookbook-cleanup-temp.sh
Expand Down
4 changes: 4 additions & 0 deletions tests/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ required_files=(
tests/integration-release-contract.sh
tests/concurrency-contract.sh
tests/quality-contract.sh
tests/ci-policy-contract.sh
tests/shfmt-contract.sh
tests/changelog-guard.sh
tests/release-invariants.sh
Expand Down Expand Up @@ -145,6 +146,7 @@ check_no_strict_mode() {
tests/reference-release-contract.sh
tests/concurrency-contract.sh
tests/quality-contract.sh
tests/ci-policy-contract.sh
tests/shfmt-contract.sh
tests/changelog-guard.sh
tests/release-invariants.sh
Expand Down Expand Up @@ -432,6 +434,7 @@ run_stage "ShellCheck error profile" shellcheck --severity=error \
tests/reference-release-contract.sh \
tests/concurrency-contract.sh \
tests/quality-contract.sh \
tests/ci-policy-contract.sh \
tests/shfmt-contract.sh \
tests/release-invariants.sh \
tests/release-invariants.bats \
Expand Down Expand Up @@ -506,6 +509,7 @@ run_stage "integration release contract" tests/integration-release-contract.sh |
run_stage "reference release contract" tests/reference-release-contract.sh || exit $?
run_stage "concurrency contract" tests/concurrency-contract.sh || exit $?
run_stage "quality workflow contract" tests/quality-contract.sh || exit $?
run_stage "CI policy contract" tests/ci-policy-contract.sh || exit $?
run_stage "support matrix" tests/compatibility-matrix.sh || exit $?
run_stage "release invariants" tests/release-invariants.sh || exit $?
run_stage "examples/std-usage.sh" examples/std-usage.sh > /dev/null || exit $?
Expand Down
Loading