Added Release Pipeline
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"openwrt_release": "25.12.1",
|
||||||
|
"version": { "major": 0, "minor": 4 },
|
||||||
|
"targets": [
|
||||||
|
{ "architecture": "x64", "target": "x86", "subtarget": "64" },
|
||||||
|
{ "architecture": "x86", "target": "x86", "subtarget": "generic" },
|
||||||
|
{ "architecture": "arm64", "target": "armsr", "subtarget": "armv8" },
|
||||||
|
{ "architecture": "arm32", "target": "armsr", "subtarget": "armv7" },
|
||||||
|
{ "architecture": "mpc85xx", "target": "mpc85xx", "subtarget": "p1020" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Build and publish release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: openunifi-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
code: read
|
||||||
|
releases: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
metadata:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.release.outputs.matrix }}
|
||||||
|
version: ${{ steps.release.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install metadata dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install --yes jq
|
||||||
|
- name: Read release configuration
|
||||||
|
id: release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
major="$(jq -er '.version.major | select(type == "number" and floor == . and . >= 0)' .gitea/release-targets.json)"
|
||||||
|
minor="$(jq -er '.version.minor | select(type == "number" and floor == . and . >= 0)' .gitea/release-targets.json)"
|
||||||
|
tag_prefix="v${major}.${minor}."
|
||||||
|
latest_patch=-1
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
candidate="${tag#"$tag_prefix"}"
|
||||||
|
if [[ $candidate =~ ^(0|[1-9][0-9]*)$ ]] && (( candidate > latest_patch )); then
|
||||||
|
latest_patch=$candidate
|
||||||
|
fi
|
||||||
|
done < <(git tag --list "${tag_prefix}*")
|
||||||
|
patch=$((latest_patch + 1))
|
||||||
|
version="${major}.${minor}.${patch}"
|
||||||
|
matrix="$(jq -c '{include: .targets}' .gitea/release-targets.json)"
|
||||||
|
jq -e '.targets | length > 0' .gitea/release-targets.json >/dev/null
|
||||||
|
printf 'version=%s\n' "$version" >>"$GITHUB_OUTPUT"
|
||||||
|
printf 'matrix=%s\n' "$matrix" >>"$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
create-release:
|
||||||
|
needs: metadata
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release-id: ${{ steps.create.outputs.release-id }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install release dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install --yes curl jq
|
||||||
|
- name: Create draft release
|
||||||
|
id: create
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
VERSION: ${{ needs.metadata.outputs.version }}
|
||||||
|
run: |
|
||||||
|
release_id="$(scripts/ci/gitea-release.sh create)"
|
||||||
|
printf 'release-id=%s\n' "$release_id" >>"$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [metadata, create-release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install SDK build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install --yes build-essential clang flex bison g++ gawk gettext git libncurses-dev libssl-dev python3 rsync unzip zlib1g-dev file wget patch time curl jq zstd
|
||||||
|
- name: Build ${{ matrix.architecture }} package
|
||||||
|
env:
|
||||||
|
ARCHITECTURE: ${{ matrix.architecture }}
|
||||||
|
VERSION: ${{ needs.metadata.outputs.version }}
|
||||||
|
run: scripts/ci/build-release.sh .gitea/release-targets.json "$ARCHITECTURE" "$VERSION" dist
|
||||||
|
- name: Upload package to draft release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
RELEASE_ID: ${{ needs.create-release.outputs.release-id }}
|
||||||
|
ASSET: dist/openUniFi-${{ matrix.architecture }}-${{ needs.metadata.outputs.version }}.apk
|
||||||
|
run: scripts/ci/gitea-release.sh upload
|
||||||
|
|
||||||
|
publish-release:
|
||||||
|
needs: [metadata, create-release, build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Publish completed release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
RELEASE_ID: ${{ needs.create-release.outputs.release-id }}
|
||||||
|
run: scripts/ci/gitea-release.sh publish
|
||||||
@@ -46,6 +46,32 @@ make -f Makefile.standalone
|
|||||||
Contributors and AI agents should read [`AGENTS.md`](AGENTS.md) for the concise
|
Contributors and AI agents should read [`AGENTS.md`](AGENTS.md) for the concise
|
||||||
architecture map, invariants, and validation checklist.
|
architecture map, invariants, and validation checklist.
|
||||||
|
|
||||||
|
## Automated releases
|
||||||
|
|
||||||
|
Every push to `main` runs `.gitea/workflows/release.yaml`. It builds with pinned
|
||||||
|
OpenWrt SDKs and publishes `openUniFi-<architecture>-<version>.apk` for x64,
|
||||||
|
x86, arm64, arm32, and mpc85xx. The workflow uses Gitea's built-in
|
||||||
|
`GITEA_TOKEN`; repository Actions settings must allow it `write` access to
|
||||||
|
releases. A failed target leaves the release as a draft rather than publishing
|
||||||
|
an incomplete set.
|
||||||
|
|
||||||
|
Targets and the pinned OpenWrt release live in `.gitea/release-targets.json`.
|
||||||
|
Add an object with a unique `architecture` label and a valid OpenWrt
|
||||||
|
`target`/`subtarget` pair to extend the matrix. The generic ARM entries select
|
||||||
|
ARMv7 and ARMv8 ABIs; use a device-specific OpenWrt target when necessary.
|
||||||
|
|
||||||
|
Releases use the next available `MAJOR.MINOR.PATCH` tag. `MAJOR` and `MINOR`
|
||||||
|
live in the release config; the workflow finds the highest existing
|
||||||
|
`vMAJOR.MINOR.PATCH` tag and increments its patch, starting at `0` when a new
|
||||||
|
major or minor line is introduced. For example, changing the config from `0.4`
|
||||||
|
to `0.5` makes the next release `0.5.0`. Release runs are serialized so two
|
||||||
|
merges cannot select the same patch version.
|
||||||
|
|
||||||
|
Alternatives are Conventional Commits with a semantic-release tool (best when
|
||||||
|
not every merge must release), or a manually maintained `VERSION` file bumped
|
||||||
|
in every pull request (simple, but a forgotten bump blocks the release). Change
|
||||||
|
`major` for incompatible changes and `minor` for compatible features.
|
||||||
|
|
||||||
## Changing Compiler Settings
|
## Changing Compiler Settings
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -ne 4 ]]; then
|
||||||
|
echo "usage: $0 CONFIG ARCHITECTURE VERSION OUTPUT_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
config=$1
|
||||||
|
architecture=$2
|
||||||
|
version=$3
|
||||||
|
output_dir=$4
|
||||||
|
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||||
|
|
||||||
|
[[ $architecture =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "invalid architecture name: $architecture" >&2; exit 2; }
|
||||||
|
[[ $version =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || { echo "invalid semantic version: $version" >&2; exit 2; }
|
||||||
|
|
||||||
|
matches=$(jq --arg architecture "$architecture" '[.targets[] | select(.architecture == $architecture)] | length' "$config")
|
||||||
|
[[ $matches -eq 1 ]] || { echo "architecture must occur exactly once in $config: $architecture" >&2; exit 2; }
|
||||||
|
openwrt_release=$(jq -er '.openwrt_release' "$config")
|
||||||
|
target=$(jq -er --arg architecture "$architecture" '.targets[] | select(.architecture == $architecture) | .target' "$config")
|
||||||
|
subtarget=$(jq -er --arg architecture "$architecture" '.targets[] | select(.architecture == $architecture) | .subtarget' "$config")
|
||||||
|
|
||||||
|
download_base="https://downloads.openwrt.org/releases/${openwrt_release}/targets/${target}/${subtarget}"
|
||||||
|
sdk_archive=$(curl -fsSL "${download_base}/" | grep -o "openwrt-sdk-${openwrt_release}-[^\"]*\.tar\.zst" | head -n 1)
|
||||||
|
[[ -n $sdk_archive ]] || { echo "no SDK found at $download_base" >&2; exit 1; }
|
||||||
|
|
||||||
|
work_dir="${RUNNER_TEMP:-/tmp}/openunifi-${architecture}-${version}"
|
||||||
|
rm -rf -- "$work_dir"
|
||||||
|
mkdir -p -- "$work_dir" "$repo_root/$output_dir"
|
||||||
|
curl -fL --retry 3 --output "$work_dir/$sdk_archive" "$download_base/$sdk_archive"
|
||||||
|
expected_sha=$(curl -fsSL "$download_base/sha256sums" | awk -v archive="$sdk_archive" '{ name=$2; sub(/^\*/, "", name); if (name == archive) { print $1; exit } }')
|
||||||
|
[[ -n $expected_sha ]] || { echo "no checksum found for $sdk_archive" >&2; exit 1; }
|
||||||
|
printf '%s %s\n' "$expected_sha" "$work_dir/$sdk_archive" | sha256sum --check
|
||||||
|
|
||||||
|
tar --zstd -xf "$work_dir/$sdk_archive" -C "$work_dir"
|
||||||
|
sdk_dir=$(find "$work_dir" -mindepth 1 -maxdepth 1 -type d -name 'openwrt-sdk-*' -print -quit)
|
||||||
|
[[ -n $sdk_dir ]] || { echo "SDK directory was not extracted" >&2; exit 1; }
|
||||||
|
|
||||||
|
cd "$sdk_dir"
|
||||||
|
./scripts/feeds update base packages
|
||||||
|
./scripts/feeds install mbedtls uci usteer
|
||||||
|
mkdir -p package/openuf
|
||||||
|
(cd "$repo_root" && tar --exclude=.git --exclude="$output_dir" -cf - .) | (cd package/openuf && tar -xf -)
|
||||||
|
sed -i -E "s/^(PKG_VERSION[[:space:]]*:?=[[:space:]]*).*/\\1${version}/" package/openuf/Makefile
|
||||||
|
|
||||||
|
# Release SDKs remember the package set used to create the SDK. Neutralize
|
||||||
|
# those baked-in defaults so this job builds openuf and its dependency closure,
|
||||||
|
# rather than every package and kernel module available for the target.
|
||||||
|
sed -i -E '/^config PACKAGE_/,/^$/ s/^([[:space:]]*)default [ym]$/\1default n/' Config-build.in
|
||||||
|
for symbol in TARGET_MULTI_PROFILE TARGET_ALL_PROFILES TARGET_PER_DEVICE_ROOTFS ALL_NONSHARED ALL_KMODS ALL BUILDBOT; do
|
||||||
|
sed -i -E "/^config ${symbol}$/,/^$/ s/^([[:space:]]*)default y$/\\1default n/" Config.in Config-build.in
|
||||||
|
done
|
||||||
|
printf '%s\n' \
|
||||||
|
'# CONFIG_ALL is not set' \
|
||||||
|
'# CONFIG_ALL_KMODS is not set' \
|
||||||
|
'# CONFIG_ALL_NONSHARED is not set' \
|
||||||
|
'# CONFIG_TARGET_MULTI_PROFILE is not set' \
|
||||||
|
'# CONFIG_TARGET_ALL_PROFILES is not set' \
|
||||||
|
'CONFIG_PACKAGE_openuf=m' >.config
|
||||||
|
make defconfig
|
||||||
|
make -j"$(nproc)" package/openuf/compile
|
||||||
|
|
||||||
|
mapfile -t packages < <(find bin/packages -type f -name "openuf-${version}-*.apk")
|
||||||
|
if [[ ${#packages[@]} -ne 1 ]]; then
|
||||||
|
echo "expected one openuf package, found ${#packages[@]}" >&2
|
||||||
|
printf '%s\n' "${packages[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
asset="$repo_root/$output_dir/openUniFi-${architecture}-${version}.apk"
|
||||||
|
cp -- "${packages[0]}" "$asset"
|
||||||
|
sha256sum "$asset"
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
action=${1:-}
|
||||||
|
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
|
||||||
|
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
|
||||||
|
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
||||||
|
api="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
auth_header="Authorization: token ${GITEA_TOKEN}"
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
create)
|
||||||
|
: "${VERSION:?VERSION is required}"
|
||||||
|
tag="v${VERSION}"
|
||||||
|
if response=$(curl -fsS -H "$auth_header" "$api/releases/tags/$tag" 2>/dev/null); then
|
||||||
|
jq -er '.id' <<<"$response"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
openwrt_release=$(jq -er '.openwrt_release' .gitea/release-targets.json)
|
||||||
|
payload=$(jq -n --arg tag "$tag" --arg version "$VERSION" --arg sha "${GITHUB_SHA:-main}" --arg openwrt "$openwrt_release" '{tag_name: $tag, target_commitish: $sha, name: ("openUniFi " + $version), body: ("Automated multi-architecture build for OpenWrt " + $openwrt + "."), draft: true, prerelease: false}')
|
||||||
|
curl -fsS -X POST -H "$auth_header" -H 'Content-Type: application/json' --data "$payload" "$api/releases" | jq -er '.id'
|
||||||
|
;;
|
||||||
|
upload)
|
||||||
|
: "${RELEASE_ID:?RELEASE_ID is required}"
|
||||||
|
: "${ASSET:?ASSET is required}"
|
||||||
|
[[ -f $ASSET ]] || { echo "asset not found: $ASSET" >&2; exit 1; }
|
||||||
|
asset_name=$(basename -- "$ASSET")
|
||||||
|
old_asset_id=$(curl -fsS -H "$auth_header" "$api/releases/$RELEASE_ID/assets" | jq -r --arg name "$asset_name" '.[] | select(.name == $name) | .id' | head -n 1)
|
||||||
|
if [[ -n $old_asset_id ]]; then
|
||||||
|
curl -fsS -X DELETE -H "$auth_header" "$api/releases/$RELEASE_ID/assets/$old_asset_id"
|
||||||
|
fi
|
||||||
|
curl -fsS -X POST -H "$auth_header" -F "attachment=@${ASSET}" "$api/releases/$RELEASE_ID/assets?name=$asset_name" >/dev/null
|
||||||
|
;;
|
||||||
|
publish)
|
||||||
|
: "${RELEASE_ID:?RELEASE_ID is required}"
|
||||||
|
curl -fsS -X PATCH -H "$auth_header" -H 'Content-Type: application/json' --data '{"draft":false}' "$api/releases/$RELEASE_ID" >/dev/null
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 {create|upload|publish}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user