feat(bootstrap): install the published release build instead of compiling on the host
deploy/bootstrap.sh now resolves the newest published GitHub release, downloads
the binary CI built for that tag, and builds a thin image around it. Compiling
on the target host becomes the fallback and the opt-in, not the default.
The panel is not a separate artifact. The Dockerfile copies panel/dist into
internal/panel/static before the go build, so the control plane — panel and
backend — ships as ONE file. The release channel therefore downloads exactly
one asset, felis-linux-<arch>, and needs no registry, no Go toolchain and no
checkout on the host.
The Minecraft game stack (limbo, lobby, the Velocity plugin) is still always
built locally. game_stack_source now keys on HAVE_PREBUILT_BINARY — the same
flag build_image uses — so on any prebuilt path it unpacks the tar embedded in
that binary instead of trusting a checkout an earlier install left behind.
Trusting the checkout would build the plugin from an old commit against a
freshly downloaded control plane: a silent version skew across the plugin/API
boundary.
Channels:
(default) newest published release, downloaded
FELIS_VERSION_BOOTSTRAP=dev clone main and compile
FELIS_REF=<ref> pins the tree, forces the source path
The download is best-effort. A tag whose assets are not uploaded yet, an
architecture with no published asset, or an asset that fails validation each
warn and fall back to compiling THE SAME TAG from source — never a different
commit.
The ref is resolved right after install_base, the first point curl exists and
well before docker and k3s, so a missing FELIS_GITHUB_TOKEN or an unpublished
release costs the operator seconds instead of a k3s install they then have to
unwind. It is skipped on exactly the paths that never consume the result: the
TUI, which rebuilds the binary it is already running, and FELIS_SKIP_FETCH,
which builds whatever is staged. Resolving anyway would set FELIS_VERSION to
the newest tag and stamp a staged tree as that release.
The asset is staged next to HOST_BIN rather than in TMPDIR. Validation EXECUTES
it, and /tmp is noexec on CIS-hardened images, where the exec dies 126, the
check reads it as a bad asset, and every such host silently falls back to the
full on-host compile this path exists to avoid. It also keeps a private-repo
artifact out of a world-readable 1777 directory.
git_auth, which supplies the token to git for a private-repo clone, passes an EMPTY
credential.helper before the inline one. credential.helper is multi-valued: a bare
`-c credential.helper=...` APPENDS to whatever the host has configured rather than
replacing it, and an empty value is git's documented list reset. Without it, on a host
with a persistent helper (Git for Windows ships `manager` at SYSTEM scope) two things
go wrong. Git runs `credential approve` automatically after a successful clone and
feeds every helper in the list, so a `store` helper writes the PAT to
~/.git-credentials in cleartext — the token outlives the install, in a file bootstrap
never created and never cleans up. And because the inline helper is LAST, a
pre-existing helper answers `fill` first, so a stale cached credential can win and the
clone authenticates as the wrong account — surfacing as exactly the 404-on-private-repo
the surrounding code works hard to explain. Reproduced both against a real clone, and
confirmed the reset closes both.
internal/panel parses the new stamp. The dev channel now emits "<tag>+g<sha>", which
matched neither describeSuffix ("-N-g<sha>") nor releaseTag, so a dev build fell through
to the default case and the version badge rendered the entire stamp as the release with
no commit. A devSuffix case handles it; the git-describe case stays for hand-rolled
`-ldflags "-X main.version=$(git describe)"` builds. Table test covers both forms plus
the release, dirty and unstamped cases.
CRD application no longer branches on the install path: it is always
`felis bootstrap-assets crd`. That output is byte-identical to deploy/crd/ —
bootstrap_asset.go embeds that very file — and needs no checkout, so one source
replaces a branch whose two arms had to be kept in agreement by hand.
Dockerfile gains a FELIS_VERSION build arg wired into -X main.version, declared
after `go mod download` so a version bump does not invalidate that layer. Both
build stages are pinned to $BUILDPLATFORM so a multi-platform buildx run never
emulates them: the panel's output is architecture-independent and the Go stage
cross-compiles via TARGETARCH. The final stage stays on the target platform and
is COPY-only, which BuildKit performs without QEMU.
.github/workflows/release.yml publishes on a vX.Y.Z tag: vet, tests, then one
buildx run producing both architectures through the repo Dockerfile. Not a bare
`go build` — internal/panel/static holds a tracked placeholder index.html so the
//go:embed compiles without node, which means a direct build succeeds and
quietly ships a release whose panel is that placeholder.
The stamp is asserted end to end, because it fails silently: an unstamped binary
reports "dev", which the updater refuses to compare, disabling update reporting
for every install built from that release. The arm64 artifact is checked by ELF
machine type rather than by running it — runners have binfmt registered, so
executing an amd64 binary misnamed arm64 would succeed.
Prerelease tags are flagged explicitly. The trigger glob is v*, gh does not read
semver out of a tag name, and an RC published as a full release becomes
/releases/latest — the single endpoint the default channel installs from and
`felis update` polls.
No SHA256SUMS. A checksum fetched over the same TLS session, with the same
credential, from the same host as the binary adds no trust root; signing is the
real answer and is a separate decision.
Not verified: the download -> validate -> image -> k3s path has never run on a
host against a real published release, because no tag exists yet. The shell
logic around it is verified out of tree; the network and exec behaviour is not.
This commit is contained in:
7 files changed
+605
-39
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# Publishes a Felis release when a vX.Y.Z tag is pushed.
|
||||
#
|
||||
# Two things depend on this job. /repos/{repo}/releases/latest must ANSWER — that endpoint is
|
||||
# what `felis update` polls (internal/updater/github.go) and what deploy/bootstrap.sh's default
|
||||
# "release" channel resolves its ref from. And the binaries below are what that channel then
|
||||
# INSTALLS: bootstrap downloads felis-linux-<arch> instead of compiling on the target host, so
|
||||
# these are the shipped artifact, not a convenience.
|
||||
#
|
||||
# The asset NAME is a contract with deploy/bootstrap.sh (download_release_binary builds
|
||||
# "felis-linux-${arch}"). It is deliberately a plain literal on both sides: a GitHub Actions
|
||||
# YAML and a go:embed'ed shell script have no honest way to share a constant, and the failure
|
||||
# mode is benign — bootstrap warns and falls back to a source build of the same tag.
|
||||
#
|
||||
# The binary is built through the repo Dockerfile rather than a plain `go build`.
|
||||
# internal/panel/static holds a tracked PLACEHOLDER index.html so the //go:embed
|
||||
# compiles in a checkout without node; a direct `go build` therefore succeeds and
|
||||
# quietly ships a release whose panel is that placeholder. The Dockerfile runs the npm
|
||||
# build first, and is the same recipe bootstrap uses, so there is one way to build felis
|
||||
# rather than two that can drift.
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write # gh release create/upload
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# A tag that ships red is worse than a tag that fails to ship.
|
||||
- run: go vet ./...
|
||||
- run: go test ./...
|
||||
|
||||
# Both architectures, because bootstrap's default release channel DOWNLOADS these
|
||||
# rather than compiling on the target host — an arm64 host with no asset silently
|
||||
# falls back to a slow source build. Neither stage is emulated: the Dockerfile pins
|
||||
# both build stages to $BUILDPLATFORM and the Go stage cross-compiles via TARGETARCH,
|
||||
# so the second architecture costs about a minute.
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build the stamped binaries
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
--build-arg FELIS_VERSION="${GITHUB_REF_NAME}" \
|
||||
--output type=local,dest=out .
|
||||
mv out/linux_amd64/usr/local/bin/felis ./felis-linux-amd64
|
||||
mv out/linux_arm64/usr/local/bin/felis ./felis-linux-arm64
|
||||
chmod +x ./felis-linux-amd64 ./felis-linux-arm64
|
||||
|
||||
# The stamp is the whole point and it fails silently: an unstamped binary reports
|
||||
# "dev", which `felis update` refuses to compare, disabling update reporting for
|
||||
# every install built from it. Assert it end to end instead of trusting the ARG
|
||||
# reached the linker.
|
||||
- name: Verify the version stamp
|
||||
run: |
|
||||
got="$(./felis-linux-amd64 version | head -1)"
|
||||
echo "reported: ${got}"
|
||||
[ "$got" = "felis ${GITHUB_REF_NAME}" ] \
|
||||
|| { echo "expected 'felis ${GITHUB_REF_NAME}' — the -X main.version stamp did not reach the binary"; exit 1; }
|
||||
# The arm64 binary is checked by ELF machine type, NOT by running it. Runners have
|
||||
# binfmt/QEMU registered, so `./felis-linux-arm64 version` would happily succeed on
|
||||
# an amd64 binary misnamed arm64 — which is exactly the failure the Dockerfile's
|
||||
# ${TARGETARCH:-$(go env GOARCH)} fallback produces if buildx did not take. Both
|
||||
# binaries come out of one RUN with one -ldflags string, so the stamp is checked once.
|
||||
file ./felis-linux-arm64
|
||||
file ./felis-linux-arm64 | grep -q 'ARM aarch64' \
|
||||
|| { echo "felis-linux-arm64 is not an arm64 ELF — TARGETARCH did not reach the go build"; exit 1; }
|
||||
|
||||
# --verify-tag refuses to invent a release for a tag that is not pushed. The upload
|
||||
# fallback makes a re-run converge rather than failing on an existing release.
|
||||
#
|
||||
# The prerelease flag has to be passed explicitly: the trigger glob is v*, so v1.2.3-rc1
|
||||
# lands here too, and gh does not read semver out of the tag name. Published as a full
|
||||
# release an RC becomes /releases/latest — the single endpoint bootstrap's default
|
||||
# channel installs from and `felis update` polls — so every fresh install would get the
|
||||
# RC binary and every deployed felis-api would error on the felis component until a
|
||||
# stable tag was cut. Flagged, GitHub keeps latest pointing at the last stable release.
|
||||
- name: Publish the release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
flags=""
|
||||
case "$GITHUB_REF_NAME" in *-*) flags="--prerelease" ;; esac
|
||||
gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes $flags \
|
||||
./felis-linux-amd64 ./felis-linux-arm64 \
|
||||
|| gh release upload "$GITHUB_REF_NAME" \
|
||||
./felis-linux-amd64 ./felis-linux-arm64 --clobber
|
||||
+33
-1
@@ -186,11 +186,43 @@ Useful overrides:
|
||||
|
||||
```bash
|
||||
export FELIS_REPO_URL=<your fork url>
|
||||
export FELIS_REF=<your branch>
|
||||
export FELIS_REF=<your branch> # pins the build; overrides the channel below
|
||||
export FELIS_IMAGE=felis:dev
|
||||
export FELIS_ROOT_DOMAIN=<node-ip>.nip.io
|
||||
```
|
||||
|
||||
By default the installer builds the newest **published GitHub release**. While this
|
||||
repository is private that lookup — and the clone itself — needs a token, and building
|
||||
the development tip needs an opt-in:
|
||||
|
||||
```bash
|
||||
export FELIS_GITHUB_TOKEN=<token with read access to the repo>
|
||||
export FELIS_VERSION_BOOTSTRAP=dev # build main instead of the newest release
|
||||
```
|
||||
|
||||
`dev` is also the escape hatch before the first `vX.Y.Z` tag exists: with no published
|
||||
release the default channel has nothing to resolve and stops with that instruction.
|
||||
|
||||
The two channels differ in more than the version they pick. `release` **downloads** the
|
||||
`felis-linux-<arch>` binary that CI published for that tag and builds only a thin image
|
||||
around it, so the host needs neither a Go toolchain nor a checkout — the panel rides along
|
||||
inside that same binary (`internal/panel` embeds it). `dev` clones and compiles. Either way
|
||||
the Minecraft game stack (limbo, lobby, the Velocity plugin) is always built locally.
|
||||
|
||||
The download is best-effort by design: if the tag's assets are not uploaded yet — the release
|
||||
workflow runs vet, tests and a full image build first — or this architecture has no published
|
||||
asset, the installer warns and compiles **the same tag** from source. It never silently
|
||||
switches you to a different commit. Setting `FELIS_REF` also forces the source path, since
|
||||
naming a ref asks for that tree rather than a published artifact.
|
||||
|
||||
The channel decides the version stamp linked into the binary (`felis version`), which is
|
||||
what `felis update` compares against upstream — release builds stamp the tag, dev builds
|
||||
stamp `<latest-tag>+g<short-sha>`, and a pinned `FELIS_REF` stamps `v0.0.0+g<short-sha>`
|
||||
because skipping channel resolution also skips the tag lookup. An unstamped build reports
|
||||
`dev` and update reporting
|
||||
is disabled for it, so build through `bootstrap.sh` (or the Dockerfile's `FELIS_VERSION`
|
||||
build arg) rather than a bare `go build` when testing that path.
|
||||
|
||||
The setup flow wraps the host bootstrap, then continues to Owner account setup
|
||||
and optional Cloudflare edge setup in the same command. The raw
|
||||
`deploy/bootstrap.sh` script remains available for low-level host provisioning
|
||||
|
||||
+24
-3
@@ -15,14 +15,20 @@
|
||||
# deploy/bootstrap.sh also extracts this same binary onto the host (docker cp)
|
||||
# so `felis migrate up` and the `felis setup` TUI run with the identical build.
|
||||
|
||||
FROM node:22-bookworm AS panel
|
||||
# Both BUILD stages are pinned to the BUILDPLATFORM so a multi-platform buildx run never
|
||||
# emulates them: the panel's output is plain JS and identical on every architecture, and the
|
||||
# Go stage cross-compiles natively via TARGETARCH below. Under QEMU an `npm ci` alone costs
|
||||
# minutes. The FINAL stage is deliberately NOT pinned — it must stay on the target platform
|
||||
# or the published arm64 image would carry amd64 layers. It contains only COPY, which
|
||||
# BuildKit performs itself, so it needs no QEMU either; adding a RUN there would.
|
||||
FROM --platform=$BUILDPLATFORM node:22-bookworm AS panel
|
||||
WORKDIR /panel
|
||||
COPY panel/package*.json ./
|
||||
RUN npm ci
|
||||
COPY panel/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.26 AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26 AS build
|
||||
WORKDIR /src
|
||||
ARG TARGETOS=linux
|
||||
ARG TARGETARCH
|
||||
@@ -31,8 +37,23 @@ COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=panel /panel/dist ./internal/panel/static
|
||||
# Declared HERE, not beside TARGETOS above: changing it invalidates every layer that
|
||||
# follows, and the go mod download layer must survive a version bump.
|
||||
#
|
||||
# The stamp is what makes `felis version` and `felis update` mean anything — unstamped,
|
||||
# main.version stays "dev" and the updater refuses to compare rather than treating it as
|
||||
# 0.0.0. deploy/bootstrap.sh computes the value per channel; see the FELIS_VERSION_BOOTSTRAP
|
||||
# block there for why the dev channel uses "+" build metadata and not `git describe`.
|
||||
#
|
||||
# The ${TARGETARCH:-...} fallback is a trap now that this stage is pinned to BUILDPLATFORM:
|
||||
# `go env GOARCH` reports the BUILDER's architecture, so an empty TARGETARCH (a legacy
|
||||
# `docker build`, or a setup-buildx step that quietly did not take) produces a working amd64
|
||||
# binary that gets published under the arm64 name. .github/workflows/release.yml asserts the
|
||||
# ELF machine type with file(1) for exactly this reason — an exec-based check cannot catch it,
|
||||
# because CI runners have binfmt/QEMU registered and will happily run the wrong one.
|
||||
ARG FELIS_VERSION=dev
|
||||
RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="${TARGETARCH:-$(go env GOARCH)}" \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/felis ./cmd/felis
|
||||
go build -trimpath -ldflags="-s -w -X main.version=${FELIS_VERSION}" -o /out/felis ./cmd/felis
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
ENV PATH=/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
+13
-5
@@ -9,13 +9,21 @@ import (
|
||||
|
||||
// version is the build stamp injected at link time via
|
||||
//
|
||||
// -ldflags "-X main.version=<git describe>"
|
||||
// -ldflags "-X main.version=v1.2.3" (release channel: the tag verbatim)
|
||||
// -ldflags "-X main.version=v1.2.3+g1a2b3c4" (dev channel: tag + build metadata)
|
||||
//
|
||||
// deploy/bootstrap.sh computes it from the checked-out source with
|
||||
// `git describe --tags --always --dirty`: the release channel builds the newest
|
||||
// vX.Y.Z tag (a clean name like v1.0.0-earlyAccess), the dev channel builds main
|
||||
// HEAD (a tag+distance+gSHA string). It stays "dev" for an un-stamped local
|
||||
// deploy/bootstrap.sh computes it per install channel (FELIS_VERSION_BOOTSTRAP):
|
||||
// the release channel stamps the resolved tag verbatim (v1.2.3), the dev channel
|
||||
// stamps "<latest-tag>+g<short-sha>". It stays "dev" for an un-stamped local
|
||||
// `go build`, where ReadBuildInfo below still surfaces the vcs revision.
|
||||
//
|
||||
// NOT `git describe`, for two reasons that both bite. Its "<tag>-<n>-g<sha>" form
|
||||
// puts the distance in the PRERELEASE field, which sorts BELOW the bare tag, so a
|
||||
// dev build ahead of v1.2.3 would compare as older than v1.2.3 and `felis update`
|
||||
// would propose "upgrading" onto the release it already contains — hence "+", which
|
||||
// is build metadata and ignored for ordering. And bootstrap's primary clone is
|
||||
// --depth 1, which carries no tags, so describe would fall back to a bare SHA that
|
||||
// updates.Parse rejects outright.
|
||||
var version = "dev"
|
||||
|
||||
// cmdVersion prints the build stamp. It takes no flags and never touches the
|
||||
|
||||
+383
-17
@@ -32,7 +32,15 @@
|
||||
# only; set a private-network IP to serve an off-host proxy)
|
||||
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
|
||||
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
||||
# FELIS_REF branch/tag/sha (raw script mode only)
|
||||
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
||||
# release DOWNLOADS the prebuilt felis binary published for the newest
|
||||
# tag (panel included — it is go:embed'ed into that same binary) and
|
||||
# builds only a thin image around it; dev clones and compiles. If the
|
||||
# asset is missing or this architecture has none, release warns and falls
|
||||
# back to compiling the SAME tag. The game stack is always built here.
|
||||
# FELIS_GITHUB_TOKEN GitHub token; REQUIRED while the repo is private
|
||||
# FELIS_REF branch/tag/sha — pins the build, overrides the channel, and forces a
|
||||
# source build (naming a ref asks for that tree, not a published asset)
|
||||
# FELIS_IMAGE local image tag (default: felis:demo — never :latest)
|
||||
# FELIS_ROOT_DOMAIN deployment root domain (default: <node-ip>.nip.io)
|
||||
# FELIS_PANEL_NODEPORT local HTTPS panel/API NodePort (default: 30443)
|
||||
@@ -45,7 +53,38 @@ set -Eeuo pipefail
|
||||
# Configuration & constants
|
||||
# ---------------------------------------------------------------------------
|
||||
FELIS_REPO_URL="${FELIS_REPO_URL:-https://github.com/MliroLirrorsIngenuity/Felis.git}"
|
||||
FELIS_REF="${FELIS_REF:-main}"
|
||||
# Which version to install. "release" builds the newest published GitHub release;
|
||||
# "dev" builds the tip of main. Release is the default because an installer that
|
||||
# tracks a moving branch by default hands every new host a different, untested
|
||||
# commit — the version an operator reports in a bug is then meaningless.
|
||||
FELIS_VERSION_BOOTSTRAP="${FELIS_VERSION_BOOTSTRAP:-release}"
|
||||
# Empty by default and resolved from the channel below. Setting it explicitly pins the
|
||||
# build to that ref and skips resolution entirely: naming a ref IS asking for a
|
||||
# development build, so it takes the dev-FORM stamp regardless of the channel. Skipping
|
||||
# resolution also skips the tag lookup, so the base stays v0.0.0 and the stamp is
|
||||
# v0.0.0+g<sha> rather than <latest-tag>+g<sha> — deliberate, so pinning a ref costs no
|
||||
# network call the operator did not ask for.
|
||||
FELIS_REF="${FELIS_REF:-}"
|
||||
# Recorded HERE because resolve_install_ref overwrites FELIS_REF on both channels — after it
|
||||
# runs, "did the operator pin a ref?" is unanswerable. Naming a ref asks for THAT tree to be
|
||||
# built, so it takes the source path even on the release channel.
|
||||
FELIS_REF_PINNED=""
|
||||
if [ -n "$FELIS_REF" ]; then FELIS_REF_PINNED=1; fi
|
||||
# Set once a prebuilt felis binary is installed at HOST_BIN, by either the TUI hand-off or a
|
||||
# release download. It is what the image build, the CRD apply and the game stack key off:
|
||||
# all three only need "is there a binary and no checkout", never "which route got us here".
|
||||
HAVE_PREBUILT_BINARY=""
|
||||
# Optional GitHub credential, needed while this repository is private: GitHub answers
|
||||
# 404 (not 403) for a repo the caller cannot see, so without it both the release lookup
|
||||
# and the clone fail as "not found". Exported because git's credential helper below runs
|
||||
# as a child process and reads it from the environment — which is also why it is never
|
||||
# interpolated into the clone URL. A token in the URL is written verbatim into
|
||||
# .git/config and survives the install; an environment variable does not.
|
||||
FELIS_GITHUB_TOKEN="${FELIS_GITHUB_TOKEN:-}"
|
||||
export FELIS_GITHUB_TOKEN
|
||||
# Set by resolve_install_ref/stamp_version and linked into the binary as main.version.
|
||||
FELIS_VERSION=""
|
||||
FELIS_VERSION_BASE=""
|
||||
FELIS_IMAGE="${FELIS_IMAGE:-felis:demo}"
|
||||
FELIS_EGRESS_MODE="${FELIS_EGRESS_MODE:-nodeport}"
|
||||
FELIS_PANEL_NODEPORT="${FELIS_PANEL_NODEPORT:-30443}"
|
||||
@@ -640,22 +679,308 @@ install_k3s() {
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Source/binary + image build + containerd import
|
||||
# ---------------------------------------------------------------------------
|
||||
# repo_slug prints the "owner/name" of FELIS_REPO_URL, for the REST API.
|
||||
repo_slug() {
|
||||
printf '%s\n' "$FELIS_REPO_URL" | sed -e 's#^.*github\.com[:/]##' -e 's#\.git$##'
|
||||
}
|
||||
|
||||
# github_api GETs a REST path and prints the body.
|
||||
#
|
||||
# The token goes in through `curl --config -` rather than `-H "Authorization: ..."`
|
||||
# because argv is world-readable via /proc while this runs. Same reason git_auth uses a
|
||||
# credential helper instead of a URL: a bootstrap that leaks its own credential to any
|
||||
# local user has not really installed anything privately.
|
||||
github_api() {
|
||||
local url="https://api.github.com/$1"
|
||||
local ua="felis-bootstrap (+${FELIS_REPO_URL})"
|
||||
if [ -n "$FELIS_GITHUB_TOKEN" ]; then
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$FELIS_GITHUB_TOKEN" \
|
||||
| curl -fsSL --retry 5 --retry-delay 2 --config - \
|
||||
-A "$ua" -H "Accept: application/vnd.github+json" "$url"
|
||||
else
|
||||
curl -fsSL --retry 5 --retry-delay 2 \
|
||||
-A "$ua" -H "Accept: application/vnd.github+json" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# github_latest_tag prints the tag of the newest published stable release, or fails.
|
||||
# Same endpoint internal/updater/github.go polls, so `felis update` and the installer
|
||||
# can never disagree about what "latest" means.
|
||||
github_latest_tag() {
|
||||
local json tag
|
||||
# Fetch first, filter second — the SIGPIPE reason documented on resolve_game_jars.
|
||||
json="$(github_api "repos/$(repo_slug)/releases/latest")" || return 1
|
||||
tag="$(printf '%s' "$json" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' || true)"
|
||||
tag="${tag%%$'\n'*}"
|
||||
tag="${tag#*:}" # "v1.2.3"
|
||||
tag="${tag#*\"}" # v1.2.3"
|
||||
tag="${tag%\"}" # v1.2.3
|
||||
[ -n "$tag" ] || return 1
|
||||
printf '%s\n' "$tag"
|
||||
}
|
||||
|
||||
# felis_asset_arch prints the release-asset suffix for this host, or fails. Unlike the
|
||||
# cloudflared/JRE/Go mappers just below, this one does NOT die on an unmapped architecture:
|
||||
# those have no local alternative, whereas a missing prebuilt binary only means we compile
|
||||
# the same tag on this host — slower, byte-for-byte the same result.
|
||||
felis_asset_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) printf 'amd64\n' ;;
|
||||
aarch64|arm64) printf 'arm64\n' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# github_asset_id prints the numeric id of the asset named $2 on release tag $1.
|
||||
#
|
||||
# Two details here are load-bearing and both are wrong in the obvious version. The newline
|
||||
# flatten comes FIRST: api.github.com pretty-prints, so "name" and "url" land on different
|
||||
# lines and splitting on "{" alone matches nothing at all. And the id is read out of the
|
||||
# asset's own url, never from a bare "id" key — the payload carries a release id, an author
|
||||
# id, one id per asset AND one per nested uploader, so matching "id" silently resolves the
|
||||
# wrong file. Anchoring on the CLOSING quote keeps felis-linux-amd64 from also matching a
|
||||
# sibling like felis-linux-amd64.sha256.
|
||||
#
|
||||
# The segment this greps ends at the nested "uploader" object, which is fine because "url"
|
||||
# precedes it. Anything published after uploader (size, digest, browser_download_url) is NOT
|
||||
# reachable this way — fetch releases/assets/<id> with the JSON Accept if that is ever needed.
|
||||
github_asset_id() {
|
||||
local tag="$1" name="$2" json id
|
||||
# Fetch first, filter second — the SIGPIPE reason documented on resolve_game_jars.
|
||||
json="$(github_api "repos/$(repo_slug)/releases/tags/${tag}")" || return 1
|
||||
id="$(printf '%s' "$json" | tr -d '\n' | tr '{' '\n' \
|
||||
| grep "\"name\":[[:space:]]*\"${name}\"" \
|
||||
| grep -o 'releases/assets/[0-9]\{1,\}' | head -1)" || true
|
||||
id="${id##*/}"
|
||||
[ -n "$id" ] || return 1
|
||||
printf '%s\n' "$id"
|
||||
}
|
||||
|
||||
# download_release_asset fetches ONE asset of a published release into $3.
|
||||
#
|
||||
# It RETURNS non-zero rather than dying: every caller falls back to building the same tag
|
||||
# from source, so a tag whose release workflow has not finished uploading yet — a real window,
|
||||
# since that job runs vet, tests and a full image build first — still installs.
|
||||
#
|
||||
# This cannot ride the github_api helper above. That helper sends
|
||||
# Accept: application/vnd.github+json, and with it this endpoint returns the asset's METADATA
|
||||
# as JSON under HTTP 200: a "successful" download of a text blob that passes every check and
|
||||
# only surfaces much later, inside docker build, as an exec format error.
|
||||
#
|
||||
# Plain -L, never --location-trusted. GitHub 302s to a DIFFERENT host whose signed URL carries
|
||||
# its own credentials in the query string; curl drops Authorization across that hop, and the
|
||||
# drop is REQUIRED — forwarding the token makes the storage backend reject the request with
|
||||
# 400 while leaking the credential to a third host for nothing.
|
||||
#
|
||||
# -f is not cosmetic: without it curl writes GitHub's error JSON into the output file and
|
||||
# still exits 0.
|
||||
download_release_asset() {
|
||||
local tag="$1" name="$2" dest="$3" id ua url rc=0
|
||||
ua="felis-bootstrap (+${FELIS_REPO_URL})"
|
||||
id="$(github_asset_id "$tag" "$name")" || return 1
|
||||
url="https://api.github.com/repos/$(repo_slug)/releases/assets/${id}"
|
||||
log "downloading ${name} from release ${tag}"
|
||||
if [ -n "$FELIS_GITHUB_TOKEN" ]; then
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$FELIS_GITHUB_TOKEN" \
|
||||
| curl -fsSL --retry 5 --retry-delay 2 --config - \
|
||||
-A "$ua" -H "Accept: application/octet-stream" -o "$dest" "$url" || rc=$?
|
||||
else
|
||||
curl -fsSL --retry 5 --retry-delay 2 \
|
||||
-A "$ua" -H "Accept: application/octet-stream" -o "$dest" "$url" || rc=$?
|
||||
fi
|
||||
# curl -f leaves a PARTIAL file behind when a transfer dies mid-stream, so a failed
|
||||
# download must not hand the caller something it could mistake for a complete one.
|
||||
if [ "$rc" -ne 0 ] || [ ! -s "$dest" ]; then
|
||||
rm -f "$dest"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# use_release_binary reports whether this run should install a prebuilt binary instead of
|
||||
# compiling one. Only the plain release channel qualifies: FELIS_SKIP_FETCH means "build
|
||||
# exactly what I staged" and a pinned FELIS_REF means "build that tree", both of which are
|
||||
# explicit requests for a source build, and the dev channel has no release to download.
|
||||
use_release_binary() {
|
||||
[ -z "${FELIS_SKIP_FETCH:-}" ] || return 1
|
||||
[ -z "$FELIS_REF_PINNED" ] || return 1
|
||||
[ "$FELIS_VERSION_BOOTSTRAP" = "release" ]
|
||||
}
|
||||
|
||||
# download_release_binary installs the prebuilt felis binary for $FELIS_REF onto the host.
|
||||
# It returns non-zero to ask the caller to build that same tag from source instead; it never
|
||||
# dies, because no reachable failure here is worth aborting an install over.
|
||||
#
|
||||
# One asset covers the panel too: internal/panel/panel.go go:embeds internal/panel/static, and
|
||||
# release.yml builds through the repo Dockerfile so that tree holds the real npm output rather
|
||||
# than the tracked placeholder. There is nothing else to fetch.
|
||||
download_release_binary() {
|
||||
local arch asset tmp got
|
||||
if ! arch="$(felis_asset_arch)"; then
|
||||
warn "no prebuilt felis binary for architecture $(uname -m); building ${FELIS_REF} from source on this host instead"
|
||||
return 1
|
||||
fi
|
||||
asset="felis-linux-${arch}"
|
||||
|
||||
# Convergence check, and the cheapest one available: no API call, no download, and it asks
|
||||
# the exact question that matters. Reruns are the common case for this installer.
|
||||
if [ -x "$HOST_BIN" ] && [ "$("$HOST_BIN" version 2>/dev/null | head -n 1)" = "felis ${FELIS_REF}" ]; then
|
||||
HAVE_PREBUILT_BINARY=1
|
||||
ok "host binary is already ${FELIS_REF}; skipping the download"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Staged next to HOST_BIN rather than in TMPDIR, because the validation below EXECUTES it
|
||||
# and /tmp is mounted noexec on CIS-hardened images. There the exec dies 126, the check
|
||||
# reads it as a bad asset, and every such host silently falls back to the full on-host
|
||||
# compile this path exists to avoid. /usr/local/bin has to be exec for felis to run at
|
||||
# all, so validating there tests the binary instead of the mount — and it keeps the
|
||||
# private-repo artifact out of a world-readable 1777 directory on the way in.
|
||||
mkdir -p "$(dirname "$HOST_BIN")"
|
||||
tmp="$(mktemp "$(dirname "$HOST_BIN")/.felis-download.XXXXXX")"
|
||||
remember_temp "$tmp"
|
||||
if ! download_release_asset "$FELIS_REF" "$asset" "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
# Deliberately NOT "set FELIS_VERSION_BOOTSTRAP=dev": that channel builds main, a
|
||||
# different commit than the tag the operator asked for. Falling back keeps the tag and
|
||||
# changes only how it is obtained, so no operator action is needed at all.
|
||||
warn "release ${FELIS_REF} publishes no usable ${asset}; building ${FELIS_REF} from source on this host instead"
|
||||
return 1
|
||||
fi
|
||||
chmod 0755 "$tmp"
|
||||
|
||||
# Run it once. This single exec subsumes three checks that would otherwise each need their
|
||||
# own code: a truncated download segfaults (curl reports success for an EOF-delimited body,
|
||||
# so nothing earlier catches it), a wrong-architecture asset fails to exec, and an unstamped
|
||||
# or mis-stamped build reports the wrong string here — rather than silently disabling
|
||||
# `felis update` for the entire life of the install.
|
||||
got="$("$tmp" version 2>/dev/null | head -n 1 || true)"
|
||||
if [ "$got" != "felis ${FELIS_REF}" ]; then
|
||||
rm -f "$tmp"
|
||||
warn "downloaded ${asset} reports '${got:-nothing}' rather than 'felis ${FELIS_REF}'; building ${FELIS_REF} from source on this host instead"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# install(1) onto a freshly created destination, matching install_embedded_binary. A rename
|
||||
# would carry the source SELinux label instead of type-transitioning to bin_t — see
|
||||
# build_nano_binary for the 203/EXEC this shape avoids. Same-directory staging does not
|
||||
# change that: install(1) still creates the destination and copies.
|
||||
rm -f "$HOST_BIN"
|
||||
install -m 0755 "$tmp" "$HOST_BIN"
|
||||
rm -f "$tmp"
|
||||
command -v restorecon >/dev/null 2>&1 && restorecon "$HOST_BIN" >/dev/null 2>&1 || true
|
||||
|
||||
HAVE_PREBUILT_BINARY=1
|
||||
ok "installed ${asset} ${FELIS_REF} at ${HOST_BIN}"
|
||||
}
|
||||
|
||||
# git_auth runs git with the token supplied by an inline credential helper. The helper
|
||||
# is a shell snippet that READS the exported variable when git asks; the token is never
|
||||
# in argv and never reaches .git/config, so it does not outlive the process.
|
||||
#
|
||||
# The EMPTY credential.helper in front is load-bearing, not a typo. credential.helper is a
|
||||
# MULTI-valued config: a bare `-c credential.helper=...` APPENDS to whatever the host has
|
||||
# configured, it does not replace it. On a host with `credential.helper=store` git then runs
|
||||
# both — ours answers the prompt, and store writes the token to ~/.git-credentials on the
|
||||
# approve that follows a successful auth, so it outlives the install after all. An empty
|
||||
# value is git's documented list reset. Verified on Fedora: with store configured the single
|
||||
# -c form persists the token to disk, the reset form writes nothing.
|
||||
git_auth() {
|
||||
if [ -n "$FELIS_GITHUB_TOKEN" ]; then
|
||||
git -c 'credential.helper=' \
|
||||
-c 'credential.helper=!f() { printf "username=x-access-token\npassword=%s\n" "$FELIS_GITHUB_TOKEN"; }; f' "$@"
|
||||
else
|
||||
git "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# resolve_install_ref decides WHICH commit to build and, on the release channel, what to
|
||||
# stamp it as. Runs before fetch_source because it chooses that fetch's ref.
|
||||
resolve_install_ref() {
|
||||
# Idempotent: main calls it early to fail fast on a missing token, and fetch_source
|
||||
# calls it again because the nano path reaches fetch_source by its own route.
|
||||
[ -n "${REF_RESOLVED:-}" ] && return 0
|
||||
REF_RESOLVED=1
|
||||
if [ -n "$FELIS_REF" ]; then
|
||||
ok "building the pinned ref ${FELIS_REF}"
|
||||
return 0
|
||||
fi
|
||||
case "$FELIS_VERSION_BOOTSTRAP" in
|
||||
release)
|
||||
log "resolving the newest published Felis release"
|
||||
FELIS_REF="$(github_latest_tag)" || die "could not resolve the newest Felis release.
|
||||
If the repository is private, set FELIS_GITHUB_TOKEN to a token with read access to it.
|
||||
If no release has been published yet, set FELIS_VERSION_BOOTSTRAP=dev to build main instead."
|
||||
# A release IS its tag, so the stamp is final here and stamp_version leaves it be.
|
||||
FELIS_VERSION="$FELIS_REF"
|
||||
ok "release channel: ${FELIS_REF}"
|
||||
;;
|
||||
dev)
|
||||
FELIS_REF="main"
|
||||
# Best effort: the tag only names what this build is AHEAD of, and a repo with no
|
||||
# release yet is exactly when someone reaches for the dev channel. v0.0.0 keeps the
|
||||
# stamp parseable so `felis update` still compares rather than refusing.
|
||||
FELIS_VERSION_BASE="$(github_latest_tag 2>/dev/null || true)"
|
||||
[ -n "$FELIS_VERSION_BASE" ] || FELIS_VERSION_BASE="v0.0.0"
|
||||
ok "dev channel: main (ahead of ${FELIS_VERSION_BASE})"
|
||||
;;
|
||||
*)
|
||||
die "FELIS_VERSION_BOOTSTRAP must be 'release' or 'dev', got '${FELIS_VERSION_BOOTSTRAP}'"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# stamp_version finalises the dev/pinned stamp once a checkout exists to read the SHA
|
||||
# from. Release builds are already stamped and return untouched.
|
||||
#
|
||||
# The form is "<tag>+g<sha>", NOT `git describe`'s "<tag>-<n>-g<sha>", and the difference
|
||||
# is load-bearing. updates.Parse reads a "-" tail as a PRERELEASE, which sorts BELOW the
|
||||
# plain tag: a dev build 14 commits past v1.2.3 would compare as older than v1.2.3, and
|
||||
# `felis update` would propose "upgrading" onto the release it already contains. After
|
||||
# "+" the tail is build metadata, ignored for ordering, so the build reads as current
|
||||
# against v1.2.3 and as behind against v1.3.0 — both correct.
|
||||
#
|
||||
# `git describe` is also unreliable here: the primary clone is --depth 1 and carries no
|
||||
# tags, so describe falls back to a bare SHA, which updates.Parse rejects outright (it
|
||||
# fails closed on a non-numeric core). fetch_source does retry with a full clone when the
|
||||
# shallow one fails, which WOULD carry tags — that is exactly the point: the stamp must not
|
||||
# depend on which arm happened to win. rev-parse needs no history at all.
|
||||
stamp_version() {
|
||||
local sha
|
||||
[ -n "$FELIS_VERSION" ] && return 0
|
||||
sha="$(git -C "$SRC_DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||||
[ -n "$sha" ] || sha="unknown"
|
||||
[ -n "$FELIS_VERSION_BASE" ] || FELIS_VERSION_BASE="v0.0.0"
|
||||
FELIS_VERSION="${FELIS_VERSION_BASE}+g${sha}"
|
||||
ok "build stamp: ${FELIS_VERSION}"
|
||||
}
|
||||
|
||||
fetch_source() {
|
||||
if [ -n "${FELIS_SKIP_FETCH:-}" ]; then
|
||||
[ -d "$SRC_DIR" ] || die "FELIS_SKIP_FETCH set but ${SRC_DIR} does not exist"
|
||||
ok "skipping fetch; using pre-staged source at ${SRC_DIR}"
|
||||
# Whatever is staged is what gets built, so the channel has no say here and no
|
||||
# release lookup is made. stamp_version reads the staged checkout's own SHA.
|
||||
stamp_version
|
||||
return 0
|
||||
fi
|
||||
resolve_install_ref
|
||||
if [ -d "${SRC_DIR}/.git" ]; then
|
||||
log "updating source in ${SRC_DIR}"
|
||||
git -C "$SRC_DIR" fetch --depth 1 origin "$FELIS_REF"
|
||||
git_auth -C "$SRC_DIR" fetch --depth 1 origin "$FELIS_REF"
|
||||
git -C "$SRC_DIR" checkout -f FETCH_HEAD
|
||||
else
|
||||
log "cloning ${FELIS_REPO_URL} (${FELIS_REF})"
|
||||
mkdir -p "$(dirname "$SRC_DIR")"
|
||||
git clone --depth 1 --branch "$FELIS_REF" "$FELIS_REPO_URL" "$SRC_DIR" 2>/dev/null \
|
||||
|| git clone "$FELIS_REPO_URL" "$SRC_DIR"
|
||||
# The fallback checks the ref out explicitly. It used to be a bare full clone, which
|
||||
# silently landed on the default branch: harmless when FELIS_REF was always "main",
|
||||
# but the release channel now asks for a tag, and a build stamped v1.2.3 that
|
||||
# actually contains main is worse than a failed install.
|
||||
git_auth clone --depth 1 --branch "$FELIS_REF" "$FELIS_REPO_URL" "$SRC_DIR" 2>/dev/null \
|
||||
|| { git_auth clone "$FELIS_REPO_URL" "$SRC_DIR" \
|
||||
&& git_auth -C "$SRC_DIR" checkout -f "$FELIS_REF"; } \
|
||||
|| die "could not check out ${FELIS_REF} from ${FELIS_REPO_URL}"
|
||||
fi
|
||||
stamp_version
|
||||
ok "source ready at ${SRC_DIR}"
|
||||
}
|
||||
|
||||
@@ -672,6 +997,7 @@ install_embedded_binary() {
|
||||
else
|
||||
ok "host binary already installed at ${HOST_BIN}"
|
||||
fi
|
||||
HAVE_PREBUILT_BINARY=1
|
||||
}
|
||||
|
||||
build_image_from_binary() {
|
||||
@@ -699,8 +1025,12 @@ verify_image_starts() {
|
||||
}
|
||||
|
||||
build_image_from_source() {
|
||||
log "building ${FELIS_IMAGE} (this compiles the Go binary; first run is slow)"
|
||||
docker build -t "$FELIS_IMAGE" "$SRC_DIR"
|
||||
log "building ${FELIS_IMAGE} ${FELIS_VERSION:+(${FELIS_VERSION}) }(this compiles the Go binary; first run is slow)"
|
||||
# Without the stamp main.version stays "dev", and `felis update` refuses to compare a
|
||||
# "dev" build against upstream rather than treating it as 0.0.0. So an unstamped image
|
||||
# is not a cosmetic problem: it silently disables update reporting for the install.
|
||||
docker build -t "$FELIS_IMAGE" \
|
||||
--build-arg FELIS_VERSION="${FELIS_VERSION:-dev}" "$SRC_DIR"
|
||||
|
||||
log "extracting the felis binary onto the host (${HOST_BIN})"
|
||||
local cid
|
||||
@@ -713,7 +1043,10 @@ build_image_from_source() {
|
||||
|
||||
build_image() {
|
||||
systemctl start docker
|
||||
if bootstrap_from_tui; then
|
||||
# Keyed on the binary, not on the route that produced it: the TUI hand-off and a release
|
||||
# download both land on exactly the same state (a felis binary at HOST_BIN, no checkout),
|
||||
# and a release download that fell back to source has cleared this so the source build runs.
|
||||
if [ -n "$HAVE_PREBUILT_BINARY" ]; then
|
||||
build_image_from_binary
|
||||
else
|
||||
build_image_from_source
|
||||
@@ -756,8 +1089,15 @@ remove_k3s_image() {
|
||||
# game_stack_source sets GAME_STACK_DIR to a docker build context holding
|
||||
# deploy/{limbo,lobby} and plugins/. The TUI path pipes this script in over stdin and
|
||||
# has no checkout on disk, so there the sources come out of the felis binary itself.
|
||||
#
|
||||
# Keyed on HAVE_PREBUILT_BINARY, the same flag build_image uses, because the question is
|
||||
# identical: a prebuilt binary means fetch_source never ran, so SRC_DIR is whatever an
|
||||
# EARLIER install left behind. Trusting it there would build felis-paper and the Velocity
|
||||
# plugin from the old commit while the control plane is the freshly downloaded release —
|
||||
# a silent version skew across the plugin/API boundary. The embedded tar always matches
|
||||
# the binary it came out of, so it is the correct source on every prebuilt path.
|
||||
game_stack_source() {
|
||||
if ! bootstrap_from_tui && [ -f "${SRC_DIR}/deploy/limbo/Dockerfile" ]; then
|
||||
if [ -z "$HAVE_PREBUILT_BINARY" ] && [ -f "${SRC_DIR}/deploy/limbo/Dockerfile" ]; then
|
||||
GAME_STACK_DIR="$SRC_DIR"
|
||||
ok "game-stack sources: ${SRC_DIR}"
|
||||
return 0
|
||||
@@ -1373,12 +1713,11 @@ deploy_bundle() {
|
||||
kube -n "$CONTROL_NS" get deployment felis-api >/dev/null 2>&1 && had_api=1
|
||||
kube -n "$CONTROL_NS" get deployment felis-operator >/dev/null 2>&1 && had_operator=1
|
||||
|
||||
# Always the embedded copy. It is byte-identical to deploy/crd/ (bootstrap_asset.go embeds
|
||||
# that very file), it needs no checkout — which the release-download path does not have —
|
||||
# and one source beats a branch whose two arms have to be kept in agreement by hand.
|
||||
log "applying MinecraftServer CRD"
|
||||
if bootstrap_from_tui; then
|
||||
"$HOST_BIN" bootstrap-assets crd | kube apply -f -
|
||||
else
|
||||
kube apply -f "${SRC_DIR}/deploy/crd/"
|
||||
fi
|
||||
"$HOST_BIN" bootstrap-assets crd | kube apply -f -
|
||||
|
||||
log "ensuring namespaces"
|
||||
local ns
|
||||
@@ -1574,9 +1913,12 @@ install_go_toolchain() {
|
||||
build_nano_binary() {
|
||||
local staged="${SRC_DIR}/.felis-nano-build"
|
||||
|
||||
log "building felis from source (${SRC_DIR})"
|
||||
log "building felis from source (${SRC_DIR})${FELIS_VERSION:+ — ${FELIS_VERSION}}"
|
||||
# Stamped for the same reason the image build is: a nano host runs `felis version` and
|
||||
# `felis update` too, and this path used to pass no ldflags at all.
|
||||
( cd "$SRC_DIR" \
|
||||
&& PATH="${GOROOT_DIR}/bin:${PATH}" CGO_ENABLED=0 go build -trimpath -o "$staged" ./cmd/felis ) \
|
||||
&& PATH="${GOROOT_DIR}/bin:${PATH}" CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags "-X main.version=${FELIS_VERSION:-dev}" -o "$staged" ./cmd/felis ) \
|
||||
|| die "go build ./cmd/felis failed"
|
||||
|
||||
# The whole point of this path is the nano subcommand; a binary without it would
|
||||
@@ -1603,7 +1945,16 @@ acquire_nano_binary() {
|
||||
install_embedded_binary
|
||||
return 0
|
||||
fi
|
||||
# Raw curl|bash: no binary yet. Build it straight from source with a pinned Go
|
||||
# The release channel takes the same prebuilt binary the control plane does. This is the
|
||||
# biggest win on this path: a host that only wants the auth multiplexer stops needing a Go
|
||||
# toolchain and a checkout at all.
|
||||
if use_release_binary; then
|
||||
resolve_install_ref
|
||||
if download_release_binary; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
# Raw curl|bash with no usable release: build it straight from source with a pinned Go
|
||||
# toolchain — nano needs one static binary, not an image, so dragging in docker
|
||||
# (as the full control-plane path does) buys nothing and costs a daemon that must
|
||||
# start. It does not start on EL10: the docker-ce el10 rpms install but dockerd
|
||||
@@ -1743,13 +2094,28 @@ main() {
|
||||
detect_node_ip
|
||||
ensure_swap
|
||||
install_base
|
||||
# Right after install_base because it is the first point curl exists, and well before
|
||||
# docker and k3s: a missing FELIS_GITHUB_TOKEN or an unpublished release should cost
|
||||
# the operator seconds, not a k3s install they then have to unwind. This is purely
|
||||
# fail-fast — fetch_source resolves again itself — so it must skip on exactly the paths
|
||||
# that never consume the result, or it invents a network dependency and a version they
|
||||
# do not have: the TUI rebuilds the binary it is already running, and FELIS_SKIP_FETCH
|
||||
# builds whatever is staged, which stamp_version reads the SHA off. Resolving anyway
|
||||
# would set FELIS_VERSION to the newest tag and stamp a staged tree as that release.
|
||||
bootstrap_from_tui || [ -n "${FELIS_SKIP_FETCH:-}" ] || resolve_install_ref
|
||||
install_cloudflared
|
||||
load_or_make_secrets
|
||||
ensure_panel_tls_cert
|
||||
install_docker
|
||||
install_k3s
|
||||
# Three ways to end up with a felis binary, in preference order. The release download is
|
||||
# the only one that skips compiling: it is the CI artifact for this exact tag, panel
|
||||
# included. Both other arms leave HAVE_PREBUILT_BINARY unset where a source build is what
|
||||
# actually happens, which is what routes build_image below.
|
||||
if bootstrap_from_tui; then
|
||||
install_embedded_binary
|
||||
elif use_release_binary && download_release_binary; then
|
||||
:
|
||||
else
|
||||
fetch_source
|
||||
fi
|
||||
|
||||
+27
-13
@@ -26,13 +26,13 @@ type runtimeConfig struct {
|
||||
}
|
||||
|
||||
// buildInfo is the resolved build stamp the panel renders in its version badge.
|
||||
// It is derived once, server-side, from the binary's `main.version` (a
|
||||
// `git describe --tags --always --dirty` string) so the panel needs no brittle
|
||||
// It is derived once, server-side, from the binary's `main.version` — the stamp
|
||||
// deploy/bootstrap.sh links in per install channel — so the panel needs no brittle
|
||||
// string parsing — it just renders `Release`, appending `+Commit` when `Dev`.
|
||||
type buildInfo struct {
|
||||
// Version is the raw resolved stamp (e.g. "v1.0.0-earlyAccess-3-g1a2b3c4").
|
||||
// Version is the raw resolved stamp (e.g. "v1.0.0-earlyAccess+g1a2b3c4").
|
||||
Version string `json:"version"`
|
||||
// Release is the "big version" — the newest tag with any git-describe suffix
|
||||
// Release is the "big version" — the newest tag with any commit suffix
|
||||
// stripped (e.g. "v1.0.0-earlyAccess"). It is what the release channel shows.
|
||||
Release string `json:"release"`
|
||||
// Commit is the short commit the dev channel was built from (e.g. "1a2b3c4"),
|
||||
@@ -43,11 +43,20 @@ type buildInfo struct {
|
||||
Dev bool `json:"dev"`
|
||||
}
|
||||
|
||||
// devSuffix matches the trailing "+g<sha>" that deploy/bootstrap.sh's dev channel
|
||||
// appends to the newest tag — the shape a dev build actually carries. The "+" is
|
||||
// deliberate and load-bearing: it is semver BUILD METADATA, ignored for ordering,
|
||||
// so a dev build ahead of v1.2.3 still compares as newer than v1.2.3. The
|
||||
// git-describe form below puts that distance in the PRERELEASE field instead,
|
||||
// which sorts BELOW the bare tag.
|
||||
var devSuffix = regexp.MustCompile(`\+g([0-9a-f]+)$`)
|
||||
|
||||
// describeSuffix matches the trailing "-<commits>-g<sha>" that `git describe`
|
||||
// appends to the newest tag once HEAD is past it — the shape the dev channel
|
||||
// (main HEAD) produces. The release channel builds the exact tag, so its stamp
|
||||
// carries no such suffix. Match is anchored at end so a tag whose prerelease part
|
||||
// itself contains hyphens (v1.0.0-earlyAccess) keeps that part in Release.
|
||||
// appends to the newest tag once HEAD is past it. bootstrap.sh no longer produces
|
||||
// this form, but a hand-rolled `go build -ldflags "-X main.version=$(git describe)"`
|
||||
// still does, and parsing it costs one case. Match is anchored at end so a tag whose
|
||||
// prerelease part itself contains hyphens (v1.0.0-earlyAccess) keeps that part in
|
||||
// Release.
|
||||
var describeSuffix = regexp.MustCompile(`-([0-9]+)-g([0-9a-f]+)$`)
|
||||
|
||||
// releaseTag matches a clean released semantic-version tag (vMAJOR.MINOR.PATCH
|
||||
@@ -55,11 +64,11 @@ var describeSuffix = regexp.MustCompile(`-([0-9]+)-g([0-9a-f]+)$`)
|
||||
// permissive on the prerelease so tags like v1.0.0-earlyAccess qualify.
|
||||
var releaseTag = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$`)
|
||||
|
||||
// parseBuildVersion splits a `git describe` build stamp into the fields the panel
|
||||
// version badge renders. Cases: a dev stamp "<tag>-N-gSHA" → Release=<tag>,
|
||||
// Commit=SHA, Dev=true; a clean release tag "vX.Y.Z[-pre]" → Release=tag, Dev=false;
|
||||
// anything else ("dev", "unknown", a bare short SHA, a dirty tree) → best-effort
|
||||
// Release with Dev=true.
|
||||
// parseBuildVersion splits a build stamp into the fields the panel version badge
|
||||
// renders. Cases: a dev stamp "<tag>+gSHA" (or the git-describe "<tag>-N-gSHA") →
|
||||
// Release=<tag>, Commit=SHA, Dev=true; a clean release tag "vX.Y.Z[-pre]" →
|
||||
// Release=tag, Dev=false; anything else ("dev", "unknown", a bare short SHA, a
|
||||
// dirty tree) → best-effort Release with Dev=true.
|
||||
func parseBuildVersion(raw string) buildInfo {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
@@ -70,6 +79,11 @@ func parseBuildVersion(raw string) buildInfo {
|
||||
dirty := core != raw
|
||||
|
||||
switch {
|
||||
case devSuffix.MatchString(core):
|
||||
m := devSuffix.FindStringSubmatch(core)
|
||||
bi.Release = core[:len(core)-len(m[0])]
|
||||
bi.Commit = m[1]
|
||||
bi.Dev = true
|
||||
case describeSuffix.MatchString(core):
|
||||
m := describeSuffix.FindStringSubmatch(core)
|
||||
bi.Release = core[:len(core)-len(m[0])]
|
||||
|
||||
@@ -57,3 +57,32 @@ func TestHandlerServesPanelAndConfig(t *testing.T) {
|
||||
t.Fatalf("api status = %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBuildVersionSplitsBothStampForms pins the two shapes a felis binary can
|
||||
// carry. The "+g<sha>" form is what deploy/bootstrap.sh's dev channel links in, and
|
||||
// it is the one that regressed: before it was parsed, a dev build fell through to the
|
||||
// default case and the badge rendered the whole stamp as the release with no commit.
|
||||
func TestParseBuildVersionSplitsBothStampForms(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
raw string
|
||||
release string
|
||||
commit string
|
||||
dev bool
|
||||
}{
|
||||
{"v1.2.3+g1a2b3c4", "v1.2.3", "1a2b3c4", true},
|
||||
{"v1.0.0-earlyAccess+g1a2b3c4", "v1.0.0-earlyAccess", "1a2b3c4", true},
|
||||
{"v1.0.0-earlyAccess-3-g1a2b3c4", "v1.0.0-earlyAccess", "1a2b3c4", true},
|
||||
{"v1.2.3", "v1.2.3", "", false},
|
||||
{"v1.0.0-earlyAccess", "v1.0.0-earlyAccess", "", false},
|
||||
{"v1.2.3+g1a2b3c4-dirty", "v1.2.3", "1a2b3c4", true},
|
||||
{"dev", "dev", "", true},
|
||||
} {
|
||||
t.Run(tc.raw, func(t *testing.T) {
|
||||
got := parseBuildVersion(tc.raw)
|
||||
if got.Release != tc.release || got.Commit != tc.commit || got.Dev != tc.dev {
|
||||
t.Errorf("parseBuildVersion(%q) = {Release:%q Commit:%q Dev:%v}, want {Release:%q Commit:%q Dev:%v}",
|
||||
tc.raw, got.Release, got.Commit, got.Dev, tc.release, tc.commit, tc.dev)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user