mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 20:28:15 +03:00
add shellcheck to CI (#2478)
* add shellcheck to ci * test ci * install bash for shellcheck * set globstar for bash * cleanup shell scripts * do not ignore automated hls tests * rm legacy build script * update shell scripts * cleanup ci * Fix misspell * cleanup ci * fail on curl error in ci
This commit is contained in:
parent
52cf00ff85
commit
c74d5b4f31
12 changed files with 72 additions and 153 deletions
2
.github/workflows/go-lint.yml
vendored
2
.github/workflows/go-lint.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: lint
|
name: Lint
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|
6
.github/workflows/javascript-formatting.yml
vendored
6
.github/workflows/javascript-formatting.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Format+Lint Javascript
|
name: Lint
|
||||||
|
|
||||||
# This action works with pull requests and pushes
|
# This action works with pull requests and pushes
|
||||||
on:
|
on:
|
||||||
|
@ -11,7 +11,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prettier:
|
prettier:
|
||||||
name: Run prettier
|
name: Javascript prettier
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -42,7 +42,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
name: Run linter on changed files
|
name: Javascript linter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
30
.github/workflows/shellcheck.yml
vendored
Normal file
30
.github/workflows/shellcheck.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- webv2
|
||||||
|
paths:
|
||||||
|
- '**.sh'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- webv2
|
||||||
|
paths:
|
||||||
|
- '**.sh'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shellcheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
LANG: C.UTF-8
|
||||||
|
container:
|
||||||
|
image: docker.io/ubuntu:22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install shellcheck
|
||||||
|
run: apt update && apt install -y shellcheck bash && shellcheck --version
|
||||||
|
|
||||||
|
- name: Check shell scripts
|
||||||
|
run: shopt -s globstar && ls **/*.sh && shellcheck --severity=info **/*.sh
|
||||||
|
shell: bash
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,6 +27,7 @@ webroot/preview.gif
|
||||||
webroot/hls
|
webroot/hls
|
||||||
webroot/static/content.md
|
webroot/static/content.md
|
||||||
hls/
|
hls/
|
||||||
|
!test/automated/hls/
|
||||||
dist/
|
dist/
|
||||||
data/
|
data/
|
||||||
transcoder.log
|
transcoder.log
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Human readable names of binary distributions
|
|
||||||
DISTRO=(macOS-64bit linux-64bit linux-32bit linux-arm7 linux-arm64)
|
|
||||||
# Operating systems for the respective distributions
|
|
||||||
OS=(darwin linux linux linux linux)
|
|
||||||
# Architectures for the respective distributions
|
|
||||||
ARCH=(amd64 amd64 386 arm-7 arm64)
|
|
||||||
|
|
||||||
# Version
|
|
||||||
VERSION=$1
|
|
||||||
SHOULD_RELEASE=$2
|
|
||||||
|
|
||||||
# Build info
|
|
||||||
GIT_COMMIT=$(git rev-list -1 HEAD)
|
|
||||||
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
if [[ -z "${VERSION}" ]]; then
|
|
||||||
echo "Version must be specified when running build"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
BUILD_TEMP_DIRECTORY="$(mktemp -d)"
|
|
||||||
cd $BUILD_TEMP_DIRECTORY
|
|
||||||
|
|
||||||
echo "Cloning owncast into $BUILD_TEMP_DIRECTORY..."
|
|
||||||
git clone https://github.com/owncast/owncast 2> /dev/null
|
|
||||||
cd owncast
|
|
||||||
|
|
||||||
echo "Changing to branch: $GIT_BRANCH"
|
|
||||||
git checkout $GIT_BRANCH
|
|
||||||
|
|
||||||
[[ -z "${VERSION}" ]] && VERSION='unknownver' || VERSION="${VERSION}"
|
|
||||||
|
|
||||||
# Change to the root directory of the repository
|
|
||||||
cd $(git rev-parse --show-toplevel)
|
|
||||||
|
|
||||||
echo "Cleaning working directories..."
|
|
||||||
rm -rf ./webroot/hls/* ./hls/* ./webroot/thumbnail.jpg
|
|
||||||
|
|
||||||
echo "Creating version ${VERSION} from commit ${GIT_COMMIT}"
|
|
||||||
|
|
||||||
# Create production build of Tailwind CSS
|
|
||||||
pushd build/javascript >> /dev/null
|
|
||||||
# Install the tailwind & postcss CLIs
|
|
||||||
npm install --quiet --no-progress
|
|
||||||
# Run the tailwind CLI and pipe it to postcss for minification.
|
|
||||||
# Save it to a temp directory that we will reference below.
|
|
||||||
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "${TMPDIR}tailwind.min.css"
|
|
||||||
popd
|
|
||||||
|
|
||||||
mkdir -p dist
|
|
||||||
|
|
||||||
build() {
|
|
||||||
NAME=$1
|
|
||||||
OS=$2
|
|
||||||
ARCH=$3
|
|
||||||
VERSION=$4
|
|
||||||
GIT_COMMIT=$5
|
|
||||||
|
|
||||||
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH} ${GIT_COMMIT}..."
|
|
||||||
|
|
||||||
mkdir -p dist/${NAME}
|
|
||||||
mkdir -p dist/${NAME}/data
|
|
||||||
|
|
||||||
cp -R webroot/ dist/${NAME}/webroot/
|
|
||||||
|
|
||||||
# Copy the production pruned+minified css to the build's directory.
|
|
||||||
cp "${TMPDIR}tailwind.min.css" ./dist/${NAME}/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
|
|
||||||
cp README.md dist/${NAME}
|
|
||||||
|
|
||||||
pushd dist/${NAME} >> /dev/null
|
|
||||||
|
|
||||||
CGO_ENABLED=1 ~/go/bin/xgo -go latest --branch ${GIT_BRANCH} -ldflags "-s -w -X github.com/owncast/owncast/config.GitCommit=${GIT_COMMIT} -X github.com/owncast/owncast/config.BuildVersion=${VERSION} -X github.com/owncast/owncast/config.BuildPlatform=${NAME}" -tags enable_updates -targets "${OS}/${ARCH}" github.com/owncast/owncast
|
|
||||||
mv owncast-*-${ARCH} owncast
|
|
||||||
|
|
||||||
zip -r -q -8 ../owncast-$VERSION-$NAME.zip .
|
|
||||||
popd >> /dev/null
|
|
||||||
|
|
||||||
rm -rf dist/${NAME}/
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in "${!DISTRO[@]}"; do
|
|
||||||
build ${DISTRO[$i]} ${OS[$i]} ${ARCH[$i]} $VERSION $GIT_COMMIT
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Build archives are available in $BUILD_TEMP_DIRECTORY/owncast/dist"
|
|
||||||
ls -alh "$BUILD_TEMP_DIRECTORY/owncast/dist"
|
|
||||||
|
|
||||||
# Use the second argument "release" to create an actual release.
|
|
||||||
if [ "$SHOULD_RELEASE" != "release" ]; then
|
|
||||||
echo "Not uploading a release."
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the tag
|
|
||||||
git tag -a "v${VERSION}" -m "Release build v${VERSION}"
|
|
||||||
|
|
||||||
# On macOS open the Github page for new releases so they can be uploaded
|
|
||||||
if test -f "/usr/bin/open"; then
|
|
||||||
open "https://github.com/owncast/owncast/releases/new"
|
|
||||||
open dist
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Docker build
|
|
||||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
|
|
||||||
DOCKER_IMAGE="owncast-${VERSION}"
|
|
||||||
echo "Building Docker image ${DOCKER_IMAGE}..."
|
|
||||||
|
|
||||||
# Change to the root directory of the repository
|
|
||||||
cd $(git rev-parse --show-toplevel)
|
|
||||||
|
|
||||||
# Docker build
|
|
||||||
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t gabekangas/owncast:$VERSION -t gabekangas/owncast:latest -t owncast .
|
|
||||||
|
|
||||||
# Dockerhub
|
|
||||||
# You must be authenticated via `docker login` with your Dockerhub credentials first.
|
|
||||||
docker push "gabekangas/owncast:${VERSION}"
|
|
|
@ -9,6 +9,6 @@ VERSION="${DATE}-nightly"
|
||||||
echo "Building Docker image ${DOCKER_IMAGE}..."
|
echo "Building Docker image ${DOCKER_IMAGE}..."
|
||||||
|
|
||||||
# Change to the root directory of the repository
|
# Change to the root directory of the repository
|
||||||
cd $(git rev-parse --show-toplevel)
|
cd "$(git rev-parse --show-toplevel)" || exit
|
||||||
|
|
||||||
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"
|
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"
|
||||||
|
|
|
@ -9,7 +9,7 @@ VERSION="${DATE}-${TAG}"
|
||||||
echo "Building Docker image ${DOCKER_IMAGE}..."
|
echo "Building Docker image ${DOCKER_IMAGE}..."
|
||||||
|
|
||||||
# Change to the root directory of the repository
|
# Change to the root directory of the repository
|
||||||
cd $(git rev-parse --show-toplevel)
|
cd "$(git rev-parse --show-toplevel)" || exit
|
||||||
git checkout webv2
|
git checkout webv2
|
||||||
|
|
||||||
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=${TAG} --version="${VERSION}"
|
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag="${TAG}" --version="${VERSION}"
|
||||||
|
|
|
@ -8,21 +8,21 @@ npm install --quiet --no-progress
|
||||||
# Download a specific version of ffmpeg
|
# Download a specific version of ffmpeg
|
||||||
if [ ! -d "ffmpeg" ]; then
|
if [ ! -d "ffmpeg" ]; then
|
||||||
mkdir ffmpeg
|
mkdir ffmpeg
|
||||||
pushd ffmpeg >/dev/null
|
pushd ffmpeg >/dev/null || exit
|
||||||
curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip >/dev/null
|
curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip >/dev/null
|
||||||
unzip -o ffmpeg.zip >/dev/null
|
unzip -o ffmpeg.zip >/dev/null
|
||||||
PATH=$PATH:$(pwd)
|
PATH=$PATH:$(pwd)
|
||||||
popd >/dev/null
|
popd >/dev/null || exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pushd ../../.. >/dev/null
|
pushd ../../.. >/dev/null || exit
|
||||||
|
|
||||||
# Build and run owncast from source
|
# Build and run owncast from source
|
||||||
go build -o owncast main.go
|
go build -o owncast main.go
|
||||||
./owncast -database $TEMP_DB &
|
./owncast -database "$TEMP_DB" &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
popd >/dev/null
|
popd >/dev/null || exit
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Start streaming the test file over RTMP to
|
# Start streaming the test file over RTMP to
|
||||||
|
@ -31,7 +31,7 @@ ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -i ../test.mp4 -vcodec l
|
||||||
FFMPEG_PID=$!
|
FFMPEG_PID=$!
|
||||||
|
|
||||||
function finish {
|
function finish {
|
||||||
rm $TEMP_DB
|
rm "$TEMP_DB"
|
||||||
kill $SERVER_PID $FFMPEG_PID
|
kill $SERVER_PID $FFMPEG_PID
|
||||||
}
|
}
|
||||||
trap finish EXIT
|
trap finish EXIT
|
||||||
|
|
|
@ -26,7 +26,7 @@ if [ ! -d "ffmpeg" ]; then
|
||||||
echo "Downloading ffmpeg..."
|
echo "Downloading ffmpeg..."
|
||||||
mkdir -p /tmp/ffmpeg
|
mkdir -p /tmp/ffmpeg
|
||||||
pushd /tmp/ffmpeg >/dev/null
|
pushd /tmp/ffmpeg >/dev/null
|
||||||
curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip >/dev/null
|
curl -sL --fail https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip
|
||||||
unzip -o ffmpeg.zip >/dev/null
|
unzip -o ffmpeg.zip >/dev/null
|
||||||
PATH=$PATH:$(pwd)
|
PATH=$PATH:$(pwd)
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
|
@ -36,7 +36,7 @@ fi
|
||||||
echo "Building owncast..."
|
echo "Building owncast..."
|
||||||
go build -o owncast main.go
|
go build -o owncast main.go
|
||||||
echo "Running owncast..."
|
echo "Running owncast..."
|
||||||
./owncast -database $TEMP_DB &
|
./owncast -database "$TEMP_DB" &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
pushd test/automated/browser
|
pushd test/automated/browser
|
||||||
|
@ -54,7 +54,7 @@ STREAMING_CLIENT=$!
|
||||||
|
|
||||||
function finish {
|
function finish {
|
||||||
echo "Cleaning up..."
|
echo "Cleaning up..."
|
||||||
rm $TEMP_DB
|
rm "$TEMP_DB"
|
||||||
kill $SERVER_PID $STREAMING_CLIENT
|
kill $SERVER_PID $STREAMING_CLIENT
|
||||||
}
|
}
|
||||||
trap finish EXIT SIGHUP SIGINT SIGTERM SIGQUIT SIGABRT SIGTERM
|
trap finish EXIT SIGHUP SIGINT SIGTERM SIGQUIT SIGABRT SIGTERM
|
||||||
|
|
|
@ -37,12 +37,12 @@ pushd ../../.. >/dev/null
|
||||||
|
|
||||||
# Build and run owncast from source
|
# Build and run owncast from source
|
||||||
go build -o owncast main.go
|
go build -o owncast main.go
|
||||||
./owncast -database $TEMP_DB &
|
./owncast -database "$TEMP_DB" &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
function finish {
|
function finish {
|
||||||
echo "Cleaning up..."
|
echo "Cleaning up..."
|
||||||
rm $TEMP_DB
|
rm "$TEMP_DB"
|
||||||
kill $SERVER_PID $STREAMING_CLIENT
|
kill $SERVER_PID $STREAMING_CLIENT
|
||||||
}
|
}
|
||||||
trap finish EXIT
|
trap finish EXIT
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
# to repeat indefinitely.
|
# to repeat indefinitely.
|
||||||
# Example: ./test/ocTestStream.sh ~/Downloads/*.mp4 rtmp://localhost/live/abc123
|
# Example: ./test/ocTestStream.sh ~/Downloads/*.mp4 rtmp://localhost/live/abc123
|
||||||
|
|
||||||
if ! ([[ $1 ]])
|
if ! [[ $1 ]]
|
||||||
then
|
then
|
||||||
echo "ocTestStream is used for sending pre-recorded content to a RTMP server."
|
echo "ocTestStream is used for sending pre-recorded content to a RTMP server."
|
||||||
echo "Will default to localhost with the stream key of abc123 if one isn't provided."
|
echo "Will default to localhost with the stream key of abc123 if one isn't provided."
|
||||||
|
@ -13,22 +13,23 @@ exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make the destination optional and point to localhost with default key
|
# Make the destination optional and point to localhost with default key
|
||||||
if [[ ${@: -1} == *"rtmp://"* ]]; then
|
if [[ ${*: -1} == *"rtmp://"* ]]; then
|
||||||
echo "RTMP is specified"
|
echo "RTMP server is specified"
|
||||||
DESTINATION_HOST=${@: -1}
|
DESTINATION_HOST=${*: -1}
|
||||||
array=( $@ )
|
FILE_COUNT=$(( ${#} - 1 ))
|
||||||
ARGS_LEN=${#array[@]}
|
|
||||||
CONTENT=${array[@]:0:$ARGS_LEN-1}
|
|
||||||
DESTINATION_HOST=${@: -1}
|
|
||||||
FILE_COUNT=$( expr ${#} - 1 )
|
|
||||||
else
|
else
|
||||||
|
echo "RTMP server is not specified"
|
||||||
DESTINATION_HOST="rtmp://localhost/live/abc123"
|
DESTINATION_HOST="rtmp://localhost/live/abc123"
|
||||||
array=( $@ )
|
FILE_COUNT=${#}
|
||||||
ARGS_LEN=${#array[@]}
|
|
||||||
CONTENT=${array[@]:0:$ARGS_LEN}
|
|
||||||
FILE_COUNT=$( expr ${#} )
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ FILE_COUNT -eq 0 ]]; then
|
||||||
|
echo "ERROR: ocTestStream needs a video file for sending to the RTMP server."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTENT=${*:1:${FILE_COUNT}}
|
||||||
|
|
||||||
# Delete the old list of files if it exists
|
# Delete the old list of files if it exists
|
||||||
if test -f list.txt; then
|
if test -f list.txt; then
|
||||||
rm list.txt
|
rm list.txt
|
||||||
|
@ -44,6 +45,11 @@ function finish {
|
||||||
}
|
}
|
||||||
trap finish EXIT
|
trap finish EXIT
|
||||||
|
|
||||||
echo "Streaming a loop of ${FILE_COUNT} videos to $DESTINATION_HOST. Warning: If these files differ greatly in formats transitioning from one to another may not always work correctly... ctl+c to exit"
|
echo "Streaming a loop of ${FILE_COUNT} videos to $DESTINATION_HOST."
|
||||||
|
if [[ FILE_COUNT -gt 1 ]]; then
|
||||||
|
echo "Warning: If these files differ greatly in formats transitioning from one to another may not always work correctly."
|
||||||
|
fi
|
||||||
|
echo "$CONTENT"
|
||||||
|
echo "...press ctl+c to exit"
|
||||||
|
|
||||||
ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -f concat -safe 0 -i list.txt -vcodec libx264 -profile:v high -g 48 -r 24 -sc_threshold 0 -b:v 1300k -preset veryfast -acodec copy -vf drawtext="fontfile=monofonto.ttf: fontsize=96: box=1: boxcolor=black@0.75: boxborderw=5: fontcolor=white: x=(w-text_w)/2: y=((h-text_h)/2)+((h-text_h)/4): text='%{gmtime\:%H\\\\\:%M\\\\\:%S}'" -f flv $DESTINATION_HOST
|
ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -f concat -safe 0 -i list.txt -vcodec libx264 -profile:v high -g 48 -r 24 -sc_threshold 0 -b:v 1300k -preset veryfast -acodec copy -vf drawtext="fontfile=monofonto.ttf: fontsize=96: box=1: boxcolor=black@0.75: boxborderw=5: fontcolor=white: x=(w-text_w)/2: y=((h-text_h)/2)+((h-text_h)/4): text='%{gmtime\:%H\\\\\:%M\\\\\:%S}'" -f flv "$DESTINATION_HOST"
|
||||||
|
|
|
@ -25,7 +25,7 @@ isolation.
|
||||||
1. Go to the `Canvas` tab of the component you selected and see if there's a Design attached to it.
|
1. Go to the `Canvas` tab of the component you selected and see if there's a Design attached to it.
|
||||||
1. If there is a design, then that's a starting point you can use to start building out the component.
|
1. If there is a design, then that's a starting point you can use to start building out the component.
|
||||||
1. If there isn't, then visit the [Owncast Demo Server](https://watch.owncast.online), the [Owncast Nightly Build](https://nightly.owncast.online), or the proposed [v2 design](https://www.figma.com/file/B6ICOn1J3dyYeoZM5kPM2A/Owncast---Review?node-id=0%3A1) for some ways to start.
|
1. If there isn't, then visit the [Owncast Demo Server](https://watch.owncast.online), the [Owncast Nightly Build](https://nightly.owncast.online), or the proposed [v2 design](https://www.figma.com/file/B6ICOn1J3dyYeoZM5kPM2A/Owncast---Review?node-id=0%3A1) for some ways to start.
|
||||||
1. If no design exists, then you can ask around the Owncast chat for help, for come up with your own ideas!
|
1. If no design exists, then you can ask around the Owncast chat for help, or come up with your own ideas!
|
||||||
1. No designs are stuck in stone, and we're using this as an opportunity to level up the UI of Owncast, so all ideas are welcome.
|
1. No designs are stuck in stone, and we're using this as an opportunity to level up the UI of Owncast, so all ideas are welcome.
|
||||||
|
|
||||||
See the extra how-to guide for components here: [Components How-to](./components/_COMPONENT_HOW_TO.md).
|
See the extra how-to guide for components here: [Components How-to](./components/_COMPONENT_HOW_TO.md).
|
||||||
|
|
Loading…
Reference in a new issue