mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-10 19:10:50 +02:00
Merge pull request #10853 from night1rider/cross-library-testing
Add cross-library compile-testing for wolfSSL products
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
name: cross-library (reusable)
|
||||
|
||||
# Reusable engine for compile-testing a downstream wolfSSL product against the
|
||||
# wolfSSL in this checkout. It builds wolfSSL once from this checkout (the PR
|
||||
# merge commit) with the flags a product needs, then compiles the product
|
||||
# against that install at both the product's default-branch HEAD and its highest
|
||||
# release tag (compile-only, no `make check`).
|
||||
#
|
||||
# The wolfSSL build runs once and is shared: the build-wolfssl job installs
|
||||
# wolfSSL and uploads it as an artifact, and the compile matrix (head, latest)
|
||||
# downloads that artifact instead of rebuilding. This halves the wolfSSL builds
|
||||
# per product from two to one.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
product:
|
||||
description: 'Short product label (used for the job name)'
|
||||
required: true
|
||||
type: string
|
||||
repo:
|
||||
description: 'Product source: "owner/repo" shorthand or a full git URL'
|
||||
required: true
|
||||
type: string
|
||||
wolfssl_configure:
|
||||
description: 'Configure flags for the local wolfSSL build'
|
||||
required: true
|
||||
type: string
|
||||
product_configure:
|
||||
description: "The product's own ./configure flags"
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
script:
|
||||
description: 'Build script name under .github/workflows/cross-library/scripts/'
|
||||
required: true
|
||||
type: string
|
||||
apt_packages:
|
||||
description: 'Extra apt packages needed to build the product'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
container:
|
||||
description: 'Container image to build in (e.g. ubuntu:24.04, debian:13)'
|
||||
required: false
|
||||
default: 'ubuntu:24.04'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
# Build wolfSSL once and publish the install dir. Both compile legs (head,
|
||||
# latest) consume it, so this runs a single time per product instead of once
|
||||
# per leg.
|
||||
build-wolfssl:
|
||||
name: Build wolfSSL (${{ inputs.product }})
|
||||
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ${{ inputs.container }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
# Minimal-image containers ship without git/toolchain; install them
|
||||
# before checkout. Product-specific extras come from apt_packages.
|
||||
- name: Install build tools
|
||||
run: |
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool pkg-config \
|
||||
git ca-certificates ${{ inputs.apt_packages }}
|
||||
|
||||
# Building only needs the commit under test, not history. The break check
|
||||
# that needs history runs in the compile job, not here.
|
||||
- name: Checkout wolfSSL
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
|
||||
# Container job: the bind-mounted workspace is owned by a different uid,
|
||||
# so mark it safe or git refuses to operate on it ("dubious ownership").
|
||||
- name: Mark workspace safe for git
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
# Build wolfSSL and install to a local dir. The configure flags go through
|
||||
# an env var (not inline ${{ }}) so a quoted CFLAGS/C_EXTRA_FLAGS group
|
||||
# survives GitHub's expansion intact; the string is trusted caller input
|
||||
# that build-wolfssl.sh eval's.
|
||||
- name: Build and install wolfSSL
|
||||
env:
|
||||
WOLFSSL_CONFIGURE: ${{ inputs.wolfssl_configure }}
|
||||
run: |
|
||||
.github/workflows/cross-library/scripts/build-wolfssl.sh \
|
||||
"$GITHUB_WORKSPACE" \
|
||||
"$GITHUB_WORKSPACE/wolfssl-install" \
|
||||
"$WOLFSSL_CONFIGURE"
|
||||
|
||||
# Pack as a tar so libtool symlinks, exec bits, and the absolute-path
|
||||
# .la/.pc files survive the artifact round-trip. The compile job unpacks to
|
||||
# the same $GITHUB_WORKSPACE path, so those baked-in paths stay valid.
|
||||
- name: Pack the wolfSSL install
|
||||
run: tar -C "$GITHUB_WORKSPACE" -czf wolfssl-install.tar.gz wolfssl-install
|
||||
|
||||
- name: Upload the wolfSSL install
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wolfssl-install-${{ inputs.product }}
|
||||
path: wolfssl-install.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
# Compile the product against the shared wolfSSL, once per ref_mode:
|
||||
# head -> HEAD of the product's default branch (master/main, auto-detected)
|
||||
# latest -> the product's highest version tag (polled at run time)
|
||||
compile:
|
||||
name: Compile ${{ inputs.product }} (${{ matrix.ref_mode }})
|
||||
needs: build-wolfssl
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ${{ inputs.container }}
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ref_mode: [ head, latest ]
|
||||
steps:
|
||||
# Minimal-image containers ship without git/toolchain; install them
|
||||
# before checkout. Product-specific extras come from apt_packages.
|
||||
- name: Install build tools
|
||||
run: |
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool pkg-config \
|
||||
git ca-certificates ${{ inputs.apt_packages }}
|
||||
|
||||
# This job does not build wolfSSL, but the latest leg still checks out
|
||||
# wolfSSL history because check-break.sh scans commit messages here. The
|
||||
# head leg never waives a break, so a depth-1 checkout is enough for it.
|
||||
- name: Checkout wolfSSL
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
|
||||
# This is a container job: the workspace is a host bind-mount owned by a
|
||||
# different uid than the (root) container user, so git refuses to operate
|
||||
# on it ("detected dubious ownership") unless it is marked safe. actions/
|
||||
# checkout marks it safe only under its own *temporary* HOME, which our
|
||||
# run: steps (HOME=/github/home) do not inherit, so check-break.sh's
|
||||
# `git tag`/`git log` would fail and, with stderr hidden, look like "no
|
||||
# break declared". Mark it safe under this HOME for all following steps.
|
||||
- name: Mark workspace safe for git
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
# The `latest` leg is the only one that reads wolfSSL history: it needs the
|
||||
# commit messages check-break.sh scans (the last two release cycles) plus
|
||||
# the two newest release tags, and no file contents, so --filter=tree:0
|
||||
# fetches commits+tags only and turns a 912 MiB full clone into ~1 MiB on
|
||||
# top of the source. The exclude boundary is the THIRD-newest release, not
|
||||
# the scan base, because --shallow-exclude severs the ref it names and
|
||||
# severing the scan base would drop it from `git tag --merged HEAD` and
|
||||
# silently halve the window. Tags come in by explicit refspec (never
|
||||
# --tags, which re-pulls every tag's history), and note a server without
|
||||
# uploadpack.allowFilter silently ignores the filter and clones in full.
|
||||
- name: Deepen wolfSSL history for the break check
|
||||
if: ${{ matrix.ref_mode == 'latest' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Release-tag selection MUST agree with check-break.sh's
|
||||
# `--list 'v*-stable' --sort=-v:refname`. `sort -Vr` matches git's
|
||||
# -v:refname on this tag set (verified incl. double-digit minors,
|
||||
# e.g. v5.9.10 > v5.9.2). The assertion below catches any drift.
|
||||
mapfile -t T < <(git ls-remote --tags --refs origin 'v*-stable' \
|
||||
| sed 's#.*refs/tags/##' | sort -Vr)
|
||||
echo "Newest release tags: ${T[*]:0:3}"
|
||||
|
||||
if [ "${#T[@]}" -lt 3 ]; then
|
||||
echo "Fewer than three release tags; falling back to full history."
|
||||
git fetch --unshallow --filter=tree:0 --tags origin
|
||||
else
|
||||
# Fetch by ref (not raw SHA) so this does not depend on the server
|
||||
# allowing reachable-SHA1-in-want.
|
||||
git fetch --no-tags --filter=tree:0 --shallow-exclude="${T[2]}" origin \
|
||||
"$GITHUB_REF" \
|
||||
"refs/tags/${T[0]}:refs/tags/${T[0]}" \
|
||||
"refs/tags/${T[1]}:refs/tags/${T[1]}"
|
||||
fi
|
||||
|
||||
# Fail LOUD if this step and check-break.sh disagree on the scan base.
|
||||
# check-break.sh:61 falls back to the NEWEST tag when the second line
|
||||
# is missing, which would quietly halve the window rather than error.
|
||||
rel="$(git tag --merged HEAD --sort=-v:refname --list 'v*-stable')"
|
||||
base="$(printf '%s\n' "$rel" | sed -n '2p')"
|
||||
echo "Deepened to $(git rev-list --count HEAD) commits; scan base='${base}'"
|
||||
if [ -z "$base" ]; then
|
||||
echo "::error::Deepen produced no usable scan base; check-break.sh would scan the wrong window."
|
||||
printf 'Release tags merged into HEAD: %s\n' "${rel:-<none>}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download the wolfSSL install
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wolfssl-install-${{ inputs.product }}
|
||||
|
||||
# Unpack to the same path wolfSSL was built at, keeping the .la/.pc
|
||||
# absolute paths valid.
|
||||
- name: Unpack the wolfSSL install
|
||||
run: tar -C "$GITHUB_WORKSPACE" -xzf wolfssl-install.tar.gz
|
||||
|
||||
# Resolve the concrete ref to compile: the highest tag (latest) or the
|
||||
# default branch (head). Used both for the build and the break check.
|
||||
- name: Resolve product ref
|
||||
id: ref
|
||||
env:
|
||||
REPO: ${{ inputs.repo }}
|
||||
run: |
|
||||
ref="$(.github/workflows/cross-library/scripts/resolve-ref.sh "$REPO" "${{ matrix.ref_mode }}")"
|
||||
echo "Resolved ${{ matrix.ref_mode }} ref for $REPO: $ref"
|
||||
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Compile-only (never `make check`). A compile failure is allowed ONLY if
|
||||
# a wolfSSL commit since the last release tag declared it with a
|
||||
# `breaks-<product>=<ref>` token (see check-break.sh); otherwise the job
|
||||
# fails, forcing intentional breaks to be recorded in a commit.
|
||||
# PRODUCT_CONFIGURE is intentionally unquoted so multiple flags split.
|
||||
- name: Compile ${{ inputs.product }} against wolfSSL
|
||||
env:
|
||||
REPO: ${{ inputs.repo }}
|
||||
PRODUCT_CONFIGURE: ${{ inputs.product_configure }}
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
REF: ${{ steps.ref.outputs.ref }}
|
||||
MODE: ${{ matrix.ref_mode }}
|
||||
run: |
|
||||
S=.github/workflows/cross-library/scripts
|
||||
set +e
|
||||
# shellcheck disable=SC2086 # $PRODUCT_CONFIGURE must word-split into flags
|
||||
"$S/${{ inputs.script }}" -t "$REF" \
|
||||
"$GITHUB_WORKSPACE/wolfssl-install" "$REPO" $PRODUCT_CONFIGURE
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
# The breaks-<product>= mechanism applies ONLY to the latest release
|
||||
# tag. A head/master break is never waivable, never consults the
|
||||
# ledger, and must be fixed.
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
if [ "$MODE" = "latest" ] && "$S/check-break.sh" "$PRODUCT" "$REF" >/tmp/brk 2>/dev/null; then
|
||||
echo "::warning::$PRODUCT ($REF) compiled OK but a break is still declared, remove the stale breaks-$PRODUCT=$REF token:"
|
||||
cat /tmp/brk
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "===== $PRODUCT ($REF) failed to compile against this wolfSSL ====="
|
||||
|
||||
if [ "$MODE" = "latest" ]; then
|
||||
# Released, immutable tag: allowed only if the exact tag is declared.
|
||||
if "$S/check-break.sh" "$PRODUCT" "$REF"; then
|
||||
echo "::warning::$PRODUCT $REF failed to compile, but this break is DECLARED (see above). Treating as a known/tracked break."
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::$PRODUCT $REF (latest release) no longer compiles against this wolfSSL and no break is declared."
|
||||
echo "That tag is a released version and cannot be changed. If wolfSSL is intentionally dropping"
|
||||
echo "compatibility with it, record the break so CI tracks it and this job passes. Add this exact"
|
||||
echo "token to a commit message in this PR (the release must be named explicitly):"
|
||||
echo " breaks-$PRODUCT=$REF"
|
||||
echo "Otherwise, rework the change so $PRODUCT $REF still builds."
|
||||
else
|
||||
# Product master/HEAD: NEVER waivable, no breaks- declaration exists.
|
||||
echo "::error::$PRODUCT $REF (default branch) no longer compiles against this wolfSSL."
|
||||
echo "A master/HEAD break cannot be waived; there is no breaks- declaration for it."
|
||||
echo "$PRODUCT $REF must stay compatible with wolfSSL master. Fix it by either:"
|
||||
echo " * reworking this PR so $PRODUCT $REF builds against wolfSSL master again, or"
|
||||
echo " * putting up a matching fix on $PRODUCT's $REF branch, then re-running this job."
|
||||
fi
|
||||
exit 1
|
||||
@@ -0,0 +1,124 @@
|
||||
# Cross-library compile testing
|
||||
|
||||
Compile tests the wolfSSL product family (wolfSSH, wolfCLU, wolfTPM, wolfMQTT,
|
||||
wolfPKCS11, wolfProvider) against **this** wolfSSL, so a wolfSSL change that
|
||||
would stop a downstream product from *compiling* is caught in CI.
|
||||
|
||||
**Compile only. There is no runtime testing here** (the product scripts run
|
||||
`make`, never `make check`). Each product is built twice: at the HEAD of its
|
||||
default branch and at its latest tagged release.
|
||||
|
||||
> Note: the workflow `.yml` files (the reusable engine `cross-library.yml` and
|
||||
> the per-product `cross-<product>.yml` callers) live in `.github/workflows/`
|
||||
> itself, because GitHub only discovers workflows directly in that directory,
|
||||
> not in subfolders. Everything else (these scripts) lives here.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.github/workflows/
|
||||
cross-library.yml # reusable engine (on: workflow_call)
|
||||
cross-<product>.yml # one thin caller per product (matrix: head, latest)
|
||||
cross-library/
|
||||
README.md # this file
|
||||
scripts/ # all the machinery
|
||||
```
|
||||
|
||||
## How a run works (per product, per ref)
|
||||
|
||||
The engine (`cross-library.yml`) runs one job in a clean container
|
||||
(`ubuntu:24.04` by default; a caller may pass `debian:13`):
|
||||
|
||||
1. **Install build tools** with `apt-get` (`+ apt_packages` from the caller).
|
||||
2. **Checkout wolfSSL** (full history + tags, for the break check below).
|
||||
3. **`build-wolfssl.sh`** builds this checkout's wolfSSL with the product's
|
||||
required `wolfssl_configure` flags and installs it to a local dir.
|
||||
4. **`resolve-ref.sh`** resolves the ref to build: the highest version tag
|
||||
(`ref_mode: latest`) or the default branch (`ref_mode: head`).
|
||||
5. **`<product>.sh`** clones the product at that ref and compiles it against the
|
||||
installed wolfSSL (`--with-wolfssl=<install dir>`).
|
||||
6. If the compile fails, **`check-break.sh`** decides whether it was a
|
||||
*declared* break (allowed, tracked) or an *undeclared* one (job fails). See
|
||||
below.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Role |
|
||||
|---|---|
|
||||
| `build-wolfssl.sh <src> <install> <configure>` | Build and install wolfSSL from `<src>`. Configure flags are one `eval`-ed string so a quoted `CFLAGS=`/`C_EXTRA_FLAGS=` group survives. |
|
||||
| `common.sh` | Shared helpers: `resolve_repo_url`, `default_branch` (master/main auto-detect), `latest_tag`, and `cross_build_autotools` (clone, configure `--with-wolfssl` plus `-I`/`-L`/rpath/`PKG_CONFIG_PATH`, then `make`). |
|
||||
| `resolve-ref.sh <repo> <mode>` | Echo the ref for `head` (default branch) or `latest` (highest tag). |
|
||||
| `latest-tag.sh <repo>` | Poll the highest version tag (`git ls-remote --sort=-v:refname`, robust to mixed tag styles). |
|
||||
| `check-break.sh <product> <ref>` | Break declaration check (see below). |
|
||||
| `<product>.sh [-t <ref>] <install> <repo> [product_configure...]` | Per-product build. Most just call `cross_build_autotools`; `wolfprovider.sh` also passes `--with-openssl`. |
|
||||
|
||||
## Break declarations
|
||||
|
||||
Testing a product's last *release* against wolfSSL HEAD can legitimately fail
|
||||
when wolfSSL intentionally changes an API. This applies **only to the `latest`
|
||||
(release-tag) leg**. To keep such a break honest and auditable, it must be
|
||||
**declared in a wolfSSL commit message** with a token naming the **exact release
|
||||
tag**:
|
||||
|
||||
```
|
||||
breaks-<product>=<tag> e.g. breaks-wolfssh=v1.5.0-stable
|
||||
```
|
||||
|
||||
There is **no `latest`/`head`/`*` shorthand**. The broken release must be named
|
||||
explicitly, so every newly-broken release needs its own fresh, reviewable
|
||||
declaration (an old token simply stops matching once the tag moves on).
|
||||
|
||||
**A `head` (master) break is never declarable.** If a product's default branch
|
||||
stops compiling against wolfSSL master, there is no token to wave it through.
|
||||
The PR must be reworked, or a fix put up on the product's master. A token whose
|
||||
value is `head`, `master`, `main`, or `*` is ignored outright.
|
||||
|
||||
`check-break.sh` scans wolfSSL commits for a matching token (case-insensitive)
|
||||
over a window of the **last two wolfSSL release cycles**, from the
|
||||
*second-newest* `v*-stable` release tag to HEAD. Two cycles (not one) so that
|
||||
when wolfSSL cuts a new release, a break declared in the prior cycle keeps being
|
||||
honored for one more cycle, giving the downstream product time to ship a fixed
|
||||
release before PRs go red again for the same known issue. (Release tags are
|
||||
picked by version order, not commit ancestry, so wolfSSL's many non-release tags
|
||||
like `*-CHKIN` are ignored.) Then the engine:
|
||||
|
||||
| Compile | Declared? | Result |
|
||||
|---|---|---|
|
||||
| passes | no | green |
|
||||
| passes | yes | green, plus a warning to remove the now-stale token |
|
||||
| fails | yes | green, plus a warning "known/tracked break" (shows the commit) |
|
||||
| fails | no | red. Fix it, or add a `breaks-<product>=<tag>` token |
|
||||
|
||||
Because the token names the exact tag, it automatically stops matching once the
|
||||
product releases a newer tag, forcing a fresh, explicit declaration if the new
|
||||
release is still broken.
|
||||
|
||||
The failure message depends on which leg broke:
|
||||
|
||||
- **`latest` (a released tag)**: the tag is immutable, so the job explains the
|
||||
`breaks-<product>=<tag>` mechanism and asks you to either declare the break
|
||||
(to track it and go green) or rework the change so the release still builds.
|
||||
- **`head` (the product's default branch)**: this is expected to track wolfSSL
|
||||
and is **never waivable**. The job fails and asks you to **rework the PR or put
|
||||
up a fix on the product's master branch**. There is no break declaration for a
|
||||
head failure.
|
||||
|
||||
## Adding a product
|
||||
|
||||
1. Copy an existing `cross-<product>.yml` caller and set `product`, `repo`,
|
||||
`wolfssl_configure` (the wolfSSL flags that product documents), optional
|
||||
`product_configure`, `script`, and optional `apt_packages`.
|
||||
2. Add a `scripts/<product>.sh`. If it is a standard autotools project, it is
|
||||
just:
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
```
|
||||
Non-autotools products (see `wolfprovider.sh`) can `_prepare "$@"` and then
|
||||
run their own build steps.
|
||||
|
||||
Get each product's required `wolfssl_configure` from that product's own
|
||||
README or CI, not by guessing. The flags matter (e.g. wolfPKCS11 and
|
||||
wolfProvider need specific `C_EXTRA_FLAGS`/`CFLAGS` defines).
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the wolfSSL sources in <src_dir> with the given configure flags and
|
||||
# install to <install_dir>. In CI <src_dir> is this checkout (the PR merge
|
||||
# commit). The resulting <install_dir> is handed to a product build script so
|
||||
# it can locate wolfSSL via --with-wolfssl=<install_dir>.
|
||||
#
|
||||
# Compile + install only; wolfSSL's own tests are not run here.
|
||||
#
|
||||
# The configure flags are one string (not separate args) and are `eval`-ed so a
|
||||
# quoted CFLAGS/C_EXTRA_FLAGS group survives intact, e.g.
|
||||
# --enable-all CFLAGS="-DWC_RSA_DIRECT -DHAVE_AES_ECB"
|
||||
# The string comes from our own caller workflows (trusted input).
|
||||
#
|
||||
# Usage: build-wolfssl.sh <src_dir> <install_dir> <configure_string>
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
src="$1"
|
||||
prefix="$2"
|
||||
configure="${3:-}"
|
||||
|
||||
cd "$src"
|
||||
./autogen.sh
|
||||
eval "./configure --prefix='$prefix' $configure"
|
||||
make "-j$(nproc)"
|
||||
make install
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Decide whether an intentional wolfSSL break of a downstream product has been
|
||||
# DECLARED in a commit message, so the cross-library job can tell an accountable
|
||||
# break from an accidental one.
|
||||
#
|
||||
# ONLY a break of a product's latest RELEASE TAG can be declared, i.e. a
|
||||
# released, immutable version that wolfSSL is intentionally dropping
|
||||
# compatibility with. The declaring commit names that exact tag:
|
||||
#
|
||||
# breaks-<product>=<tag> e.g. breaks-wolfssh=v1.5.0-stable
|
||||
#
|
||||
# There is deliberately no shorthand (no latest/head/*), and there is NO way to
|
||||
# declare a break of a product's master/HEAD: if wolfSSL master breaks a
|
||||
# product's master, the PR must be reworked or the product's master fixed, and
|
||||
# it is never waived. (The engine only consults this script for the latest-tag leg.)
|
||||
#
|
||||
# The scan window is "the release tag BEFORE the last one .. HEAD", i.e. the
|
||||
# last TWO wolfSSL release cycles. Two (not one) so that when wolfSSL cuts a new
|
||||
# release, a break declared in the prior cycle keeps being recognized for one
|
||||
# more cycle, giving the downstream product time to ship a fixed release before
|
||||
# PRs go red again for the known issue. Matching is case-insensitive.
|
||||
#
|
||||
# Exit 0 (prints the declaring commits) if the (product, ref) break is declared;
|
||||
# exit 1 otherwise. Requires full history + tags in the checkout
|
||||
# (actions/checkout fetch-depth: 0, fetch-tags: true).
|
||||
#
|
||||
# Usage: check-break.sh <product> <ref>
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
product="$1"
|
||||
ref="$2"
|
||||
|
||||
lc() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
ref_lc="$(lc "$ref")"
|
||||
|
||||
# Fail LOUD, never fail open. This is a gate: if git cannot read the repository
|
||||
# (e.g. a container-job "dubious ownership" refusal, or a shallow/absent
|
||||
# checkout), every git call below returns nothing and the scan would silently
|
||||
# report "no break declared", turning an environment failure into a wrong
|
||||
# compatibility verdict. Refuse to run rather than answer from a broken repo.
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "check-break: ERROR: cannot read git history at '$(pwd)':" >&2
|
||||
git rev-parse --is-inside-work-tree 1>&2 || true # re-run to surface git's own message
|
||||
echo "check-break: not a usable git checkout (dubious ownership in a container job, or missing history/tags)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Window: the release tag BEFORE the last one .. HEAD (last two release cycles,
|
||||
# for the one-release grace period described above). We pick release tags by
|
||||
# version order, not commit ancestry: wolfSSL carries many non-release tags
|
||||
# (e.g. *-CHKIN, wolfEntropy*) that `git describe` would otherwise land on.
|
||||
# RELEASE_GLOB matches wolfSSL's release tags (vX.Y.Z-stable).
|
||||
RELEASE_GLOB='v*-stable'
|
||||
rel="$(git tag --merged HEAD --sort=-v:refname --list "$RELEASE_GLOB" 2>/dev/null)"
|
||||
# Line 2 = the release before the newest (our scan base). Fall back to the
|
||||
# newest (line 1) if only one exists, or full history if there are no release
|
||||
# tags (e.g. an unexpectedly shallow clone).
|
||||
base="$(printf '%s\n' "$rel" | sed -n '2p')"
|
||||
[ -n "$base" ] || base="$(printf '%s\n' "$rel" | sed -n '1p')"
|
||||
range="HEAD"
|
||||
[ -n "$base" ] && range="${base}..HEAD"
|
||||
|
||||
# Diagnostics (to stderr, so they always show in the CI log). This is what makes
|
||||
# a "no break is declared" failure debuggable: it prints the scan window and the
|
||||
# actual commit messages/tokens check-break.sh is looking at. git stderr is NOT
|
||||
# suppressed here (unlike the scan below) so a real git failure, e.g. "dubious
|
||||
# ownership" of the workspace in a container job, surfaces instead of silently
|
||||
# looking like "no token found".
|
||||
{
|
||||
echo "check-break: product='${product}' ref='${ref}'"
|
||||
echo "check-break: release tags merged into HEAD (newest first):"
|
||||
printf '%s\n' "$rel" | sed 's/^/ /'
|
||||
echo "check-break: scan base='${base:-<root>}' range='${range}'"
|
||||
echo "check-break: commit messages in range (hash subject):"
|
||||
git log "$range" --no-merges --format=' %h %s' 2>&1 | sed -n '1,300p'
|
||||
echo "check-break: breaks-${product}= tokens found in range:"
|
||||
git log "$range" --no-merges --format='%B' 2>&1 \
|
||||
| grep -ioaE "breaks-${product}=[^[:space:]]+" | sed 's/^/ /' \
|
||||
|| echo " (none)"
|
||||
} >&2
|
||||
|
||||
declared=0
|
||||
while IFS= read -r val; do
|
||||
[ -z "$val" ] && continue
|
||||
v="$(lc "$val")"
|
||||
# A break target may ONLY be an exact release tag. Branch names and
|
||||
# wildcards (head, master, main, latest, all, *) are never valid break
|
||||
# targets and are ignored outright, so master/HEAD breaks are not waivable.
|
||||
case "$v" in
|
||||
head|master|main|latest|all|*'*'*) continue ;;
|
||||
esac
|
||||
[ "$v" = "$ref_lc" ] && declared=1
|
||||
done < <(git log "$range" --no-merges --format='%B' 2>/dev/null \
|
||||
| grep -ioaE "breaks-${product}=[^[:space:]]+" \
|
||||
| sed 's/^[^=]*=//')
|
||||
|
||||
if [ "$declared" -eq 1 ]; then
|
||||
echo "Declared break: '${product}' at '${ref}' is covered by a breaks-${product}= token (since ${base:-<root>}):"
|
||||
git log "$range" --no-merges -i --grep="breaks-${product}=" \
|
||||
--format=' %h %s (%an)' 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Shared helpers for the wolfSSL cross-library compile checks.
|
||||
#
|
||||
# Each cross-library script builds a downstream wolfSSL product against a
|
||||
# locally-built, already-installed wolfSSL and confirms it COMPILES. There is
|
||||
# no runtime testing here (no `make check`): the goal is only to catch changes
|
||||
# in wolfSSL that would stop a product from compiling.
|
||||
#
|
||||
# Product-script interface (see wolfssh.sh etc.):
|
||||
# <product>.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
# -t <ref> build that git ref (tag or branch)
|
||||
# (no -t) build HEAD of the repo's default branch (master or main,
|
||||
# auto-detected, no guessing)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# resolve_repo_url <repo>
|
||||
# Accept either "owner/repo" shorthand or a full git URL / scp-style remote and
|
||||
# echo a clonable URL.
|
||||
resolve_repo_url() {
|
||||
local repo="$1"
|
||||
case "$repo" in
|
||||
*://*|git@*) printf '%s\n' "$repo" ;;
|
||||
*) printf 'https://github.com/%s.git\n' "$repo" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# default_branch <url>
|
||||
# Echo the remote's default branch (e.g. master or main). Falls back to master.
|
||||
default_branch() {
|
||||
local url="$1" br
|
||||
br="$(git ls-remote --symref "$url" HEAD 2>/dev/null \
|
||||
| sed -n 's@^ref:[[:space:]]*refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$@\1@p')"
|
||||
printf '%s\n' "${br:-master}"
|
||||
}
|
||||
|
||||
# latest_tag <url>
|
||||
# Echo the highest version tag on the remote, or nothing if it has no tags.
|
||||
# Uses git's version sort (--sort=-v:refname), which is robust to a repo mixing
|
||||
# tag styles (e.g. wolfTPM has both "v4.0.0" and a legacy "v.1.8" that GNU
|
||||
# `sort -V` mis-orders to the top). sed picks line 1 without cutting git's pipe,
|
||||
# so `set -o pipefail` stays happy.
|
||||
latest_tag() {
|
||||
local url="$1"
|
||||
git ls-remote --tags --refs --sort='-v:refname' "$url" \
|
||||
| sed -n '1s@.*refs/tags/@@p'
|
||||
}
|
||||
|
||||
# Globals populated by _prepare, consumed by the build functions below.
|
||||
CL_INSTALL=""
|
||||
CL_REPO=""
|
||||
CL_SRC=""
|
||||
CL_REF=""
|
||||
CL_CONFIGURE=()
|
||||
|
||||
# _prepare [-t <ref>] <wolfssl_install_dir> <repo> [configure...]
|
||||
# Parse args, resolve the ref (tag via -t, else default branch), shallow-clone
|
||||
# the product, and leave CWD inside the cloned tree.
|
||||
_prepare() {
|
||||
local tag="" opt OPTIND=1
|
||||
while getopts ":t:" opt; do
|
||||
case "$opt" in
|
||||
t) tag="$OPTARG" ;;
|
||||
*) echo "usage: $0 [-t <ref>] <wolfssl_install_dir> <repo> [configure...]" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
CL_INSTALL="$1"; shift
|
||||
CL_REPO="$1"; shift
|
||||
CL_CONFIGURE=("$@")
|
||||
|
||||
local url
|
||||
url="$(resolve_repo_url "$CL_REPO")"
|
||||
CL_SRC="$(basename "$CL_REPO" .git)"
|
||||
|
||||
if [ -n "$tag" ]; then
|
||||
CL_REF="$tag"
|
||||
else
|
||||
CL_REF="$(default_branch "$url")"
|
||||
fi
|
||||
|
||||
echo "==> Building ${CL_REPO} @ ${CL_REF} against wolfSSL in ${CL_INSTALL}"
|
||||
set -x
|
||||
git clone --depth 1 --branch "$CL_REF" "$url" "$CL_SRC"
|
||||
cd "$CL_SRC"
|
||||
set +x
|
||||
}
|
||||
|
||||
# cross_build_autotools [-t <ref>] <wolfssl_install_dir> <repo> [configure...]
|
||||
# Standard autotools product: configure --with-wolfssl and compile (no check).
|
||||
cross_build_autotools() {
|
||||
_prepare "$@"
|
||||
set -x
|
||||
if [ -x ./autogen.sh ]; then
|
||||
./autogen.sh
|
||||
fi
|
||||
# Point the compiler/linker at the installed wolfSSL. --with-wolfssl alone
|
||||
# is not enough for every product: some configure link-tests need -L to
|
||||
# find -lwolfssl, and some example/app compiles need -I to find headers
|
||||
# (e.g. wolfssl/options.h). rpath lets the built binaries run without
|
||||
# LD_LIBRARY_PATH, and PKG_CONFIG_PATH covers products that probe pkg-config.
|
||||
export PKG_CONFIG_PATH="${CL_INSTALL}/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
./configure --with-wolfssl="$CL_INSTALL" "${CL_CONFIGURE[@]}" \
|
||||
CPPFLAGS="-I${CL_INSTALL}/include" \
|
||||
LDFLAGS="-L${CL_INSTALL}/lib -Wl,-rpath,${CL_INSTALL}/lib"
|
||||
make "-j$(nproc)"
|
||||
set +x
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Poll a product repo for its highest version tag (version-sorted) and echo it.
|
||||
# Used by the workflow to resolve the "latest release" matrix leg, which is then
|
||||
# passed to the product script as `-t <tag>`.
|
||||
#
|
||||
# Usage: latest-tag.sh <repo> (repo: "owner/repo" or a full git URL)
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
|
||||
url="$(resolve_repo_url "$1")"
|
||||
tag="$(latest_tag "$url")"
|
||||
if [ -z "$tag" ]; then
|
||||
echo "no tags found for $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$tag"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Echo the git ref a product should be built at:
|
||||
# mode=latest -> the highest version tag (polled)
|
||||
# mode=head -> the default branch (master/main, auto-detected)
|
||||
# Usage: resolve-ref.sh <repo> <mode>
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
|
||||
repo="$1"; mode="$2"
|
||||
url="$(resolve_repo_url "$repo")"
|
||||
|
||||
case "$mode" in
|
||||
latest)
|
||||
t="$(latest_tag "$url")"
|
||||
[ -n "$t" ] || { echo "no tags found for $repo" >&2; exit 1; }
|
||||
printf '%s\n' "$t" ;;
|
||||
head)
|
||||
default_branch "$url" ;;
|
||||
*)
|
||||
echo "resolve-ref: unknown mode '$mode' (expected 'head' or 'latest')" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolfclu against a locally-built wolfSSL.
|
||||
# Usage: wolfclu.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolfmqtt against a locally-built wolfSSL.
|
||||
# Usage: wolfmqtt.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolfpkcs11 against a locally-built wolfSSL.
|
||||
# Usage: wolfpkcs11.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolfProvider against a locally-built wolfSSL.
|
||||
#
|
||||
# wolfProvider is an OpenSSL 3.x provider, so unlike the other products it also
|
||||
# needs OpenSSL dev headers/libs (installed via the caller's apt_packages, e.g.
|
||||
# libssl-dev) and an extra --with-openssl. NOTE: the exact flag set may need
|
||||
# tuning per release; adjust the caller's wolfssl_configure / product_configure.
|
||||
#
|
||||
# Usage: wolfprovider.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
|
||||
_prepare "$@"
|
||||
set -x
|
||||
if [ -x ./autogen.sh ]; then
|
||||
./autogen.sh
|
||||
fi
|
||||
export PKG_CONFIG_PATH="${CL_INSTALL}/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
./configure --with-wolfssl="$CL_INSTALL" --with-openssl=/usr "${CL_CONFIGURE[@]}" \
|
||||
CPPFLAGS="-I${CL_INSTALL}/include" \
|
||||
LDFLAGS="-L${CL_INSTALL}/lib -Wl,-rpath,${CL_INSTALL}/lib"
|
||||
make "-j$(nproc)"
|
||||
set +x
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolfssh against a locally-built wolfSSL.
|
||||
# Usage: wolfssh.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile-check wolftpm against a locally-built wolfSSL.
|
||||
# Usage: wolftpm.sh [-t <ref>] <wolfssl_install_dir> <repo> [product_configure...]
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./common.sh
|
||||
. "$DIR/common.sh"
|
||||
cross_build_autotools "$@"
|
||||
@@ -0,0 +1,24 @@
|
||||
name: wolfCLU cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolfclu
|
||||
repo: wolfSSL/wolfCLU
|
||||
wolfssl_configure: --enable-wolfclu
|
||||
product_configure: ''
|
||||
script: wolfclu.sh
|
||||
@@ -0,0 +1,25 @@
|
||||
name: wolfMQTT cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolfmqtt
|
||||
repo: wolfSSL/wolfMQTT
|
||||
# wolfMQTT needs --enable-base64encode; --enable-all covers it.
|
||||
wolfssl_configure: --enable-all
|
||||
product_configure: ''
|
||||
script: wolfmqtt.sh
|
||||
@@ -0,0 +1,28 @@
|
||||
name: wolfPKCS11 cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolfpkcs11
|
||||
repo: wolfSSL/wolfPKCS11
|
||||
# wolfPKCS11's documented wolfSSL build (see its README).
|
||||
wolfssl_configure: >-
|
||||
--enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased
|
||||
--enable-scrypt
|
||||
C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP"
|
||||
product_configure: ''
|
||||
script: wolfpkcs11.sh
|
||||
@@ -0,0 +1,29 @@
|
||||
name: wolfProvider cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolfprovider
|
||||
repo: wolfSSL/wolfProvider
|
||||
# wolfProvider's documented wolfSSL build (see its scripts/utils-wolfssl.sh).
|
||||
wolfssl_configure: >-
|
||||
--enable-all-crypto --with-eccminsz=192 --with-max-ecc-bits=1024
|
||||
--enable-opensslcoexist --enable-sha
|
||||
CFLAGS="-DWC_RSA_NO_PADDING -DWOLFSSL_PUBLIC_MP -DHAVE_PUBLIC_FFDHE -DHAVE_FFDHE_6144 -DHAVE_FFDHE_8192 -DWOLFSSL_PSS_LONG_SALT -DWOLFSSL_PSS_SALT_LEN_DISCOVER -DRSA_MIN_SIZE=1024 -DWOLFSSL_OLD_OID_SUM"
|
||||
product_configure: ''
|
||||
script: wolfprovider.sh
|
||||
apt_packages: libssl-dev
|
||||
@@ -0,0 +1,25 @@
|
||||
name: wolfSSH cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolfssh
|
||||
repo: wolfSSL/wolfssh
|
||||
# wolfSSH's own CI builds wolfSSL with --enable-all.
|
||||
wolfssl_configure: --enable-all
|
||||
product_configure: --enable-all
|
||||
script: wolfssh.sh
|
||||
@@ -0,0 +1,24 @@
|
||||
name: wolfTPM cross-library
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'release/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ '*' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
cross-library:
|
||||
uses: ./.github/workflows/cross-library.yml
|
||||
with:
|
||||
product: wolftpm
|
||||
repo: wolfSSL/wolfTPM
|
||||
wolfssl_configure: --enable-wolftpm --enable-pkcallbacks --enable-keygen
|
||||
product_configure: ''
|
||||
script: wolftpm.sh
|
||||
Reference in New Issue
Block a user