All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m56s
The first release attempt failed at packaging:
cp: cannot stat 'target/aarch64-unknown-linux-gnu/release/runicgateway-installer':
No such file or directory
installer#10 added linux-aarch64 in three of the four places it belongs — the
rustup target, the `cp` into dist/, and the SHA256SUMS line — but never added a
build step for it. Nothing ever produced the binary, so the run got all the way
to packaging before noticing. No tag or release was created, so a retry is clean.
Two changes:
- Build arm64, with the same linker/CC/AR env pattern the Windows cross build
already uses.
- Name `libc6-dev-arm64-cross` in the apt install. gcc-aarch64-linux-gnu only
*recommends* it and this step runs --no-install-recommends, so without it the
Rust half builds and then `ring` (under ureq's rustls) dies compiling C on a
missing bits/libc-header-start.h.
Verified by reproducing CI in rust:1-slim-bookworm — the same apt line including
--no-install-recommends, then the same cargo invocation. Builds clean and emits
a 4.6 MB binary at exactly the path the packaging step reads.
Co-Authored-By: Claude <noreply@anthropic.com>
412 lines
22 KiB
YAML
412 lines
22 KiB
YAML
# Automated build + release for the Runic Gateway installer.
|
|
#
|
|
# Trigger: every push to `main` (i.e. every merged PR).
|
|
#
|
|
# Flow (two conceptual halves, kept separate on purpose):
|
|
#
|
|
# ┌── RELEASE ENGINE (language-agnostic) ─────────────────────────────┐
|
|
# │ reads: latest v* git tag + conventional-commit subjects │
|
|
# │ produces: next version, changelog, and (at the end) the release │
|
|
# └───────────────────────────────────────────────────────────────────┘
|
|
# ┌── RUST ADAPTER (the only Rust-specific part) ─────────────────────┐
|
|
# │ consumes: the version │
|
|
# │ produces: the artifacts (linux bin, windows exe, SHA256SUMS) │
|
|
# └───────────────────────────────────────────────────────────────────┘
|
|
#
|
|
# This is RunicGateway/link's release.yml with the adapter retargeted at this
|
|
# repo's crate — exactly the reuse its header anticipated. Differences from link:
|
|
#
|
|
# • The crate lives at the REPO ROOT, not in a subdirectory.
|
|
# • The crate guard (below) — this repo has no Cargo project yet.
|
|
# • Artifact names follow docs/installer/PLAN.md §3.
|
|
#
|
|
# ── Crate guard ──────────────────────────────────────────────────────────────
|
|
# With no Cargo.toml at the repo root there is nothing to build, so the plan step
|
|
# forces RELEASE=false and the job exits green having done nothing.
|
|
#
|
|
# That guard is what makes the `edge` branch work. Phase 1 (installer core) and
|
|
# Phase 2 (sidecar + service) land on `edge`, so `main` stays crate-free and this
|
|
# workflow keeps standing down — an installer binary that syncs the overlay but
|
|
# cannot install the sidecar is not something to publish to operators. The first
|
|
# release is cut by the `edge → main` cutover, with no edit required here.
|
|
#
|
|
# ── Unsigned releases ────────────────────────────────────────────────────────
|
|
# Per PLAN.md §3, installer binaries are deliberately UNSIGNED: SHA256SUMS is
|
|
# the trust anchor. That makes the checksum step below load-bearing rather than
|
|
# a nicety — do not drop it, and keep SHA256SUMS attached to every release.
|
|
#
|
|
# Version bump (conventional commits since the last v* tag):
|
|
# feat!: / BREAKING CHANGE -> major feat: -> minor fix|perf: -> patch
|
|
# nothing releasable -> no release is cut
|
|
# (first ever run, no tag) -> releases the current Cargo.toml version as-is
|
|
#
|
|
# Prerequisites (Settings → Actions → Secrets on RunicGateway/installer):
|
|
# REGISTRY_USER — Gitea username the token below belongs to
|
|
# REGISTRY_TOKEN — Gitea access token with `write:repository`, so it can push
|
|
# the release tag and create the release.
|
|
#
|
|
# `main` needs NO push exception: this workflow tags and publishes, and never
|
|
# writes to a branch. Keeping it that way is deliberate — a first release that
|
|
# depends on a write to a protected branch fails at the worst possible moment.
|
|
|
|
name: Release installer
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: release-installer
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
REPO: RunicGateway/installer
|
|
BIN: runicgateway-installer
|
|
LINUX_TARGET: x86_64-unknown-linux-gnu
|
|
WINDOWS_TARGET: x86_64-pc-windows-gnu
|
|
# The installer has to run wherever the sidecar it installs can run, and link
|
|
# publishes an arm64 Linux binary from v1.2.0 (PLAN.md §5.2, step 4 of 4).
|
|
ARM64_TARGET: aarch64-unknown-linux-gnu
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
# Vestigial since this workflow stopped writing a bump commit, and kept as
|
|
# belt-and-braces in case one ever returns.
|
|
# Quoted because the expression contains a colon (`chore(release):`), which an
|
|
# unquoted YAML scalar would misparse as a mapping value.
|
|
if: "${{ !contains(github.event.head_commit.message, 'chore(release): bump version') }}"
|
|
steps:
|
|
- name: Check out full history (need tags + commit log for the bump)
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
# ── RELEASE ENGINE: decide the next version + changelog ──────────────
|
|
- name: Plan the release (version + changelog)
|
|
id: plan
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
git fetch --tags --force >/dev/null 2>&1 || true
|
|
|
|
# Planning-phase guard: nothing to build without a crate.
|
|
if [ ! -f Cargo.toml ]; then
|
|
echo "No Cargo.toml at the repo root yet (planning phase) — nothing to release."
|
|
echo "This job arms itself when Phase 1 lands the crate. See docs/installer/PLAN.md."
|
|
echo "release=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
CARGO_VERSION="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
|
|
LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)"
|
|
if [ -n "$LAST_TAG" ]; then RANGE="${LAST_TAG}..HEAD"; else RANGE="HEAD"; fi
|
|
|
|
SUBJECTS="$(git log --no-merges --format='%s' $RANGE || true)"
|
|
BODIES="$(git log --no-merges --format='%B' $RANGE || true)"
|
|
|
|
BUMP=none
|
|
if echo "$BODIES" | grep -qE 'BREAKING[ -]CHANGE' ; then BUMP=major; fi
|
|
if echo "$SUBJECTS" | grep -qE '^[a-z]+(\([^)]+\))?!:' ; then BUMP=major; fi
|
|
if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^feat(\([^)]+\))?:' ; then BUMP=minor; fi
|
|
if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^(fix|perf)(\([^)]+\))?:'; then BUMP=patch; fi
|
|
|
|
bump() { # <x.y.z> <major|minor|patch> -> bumped
|
|
IFS=. read -r MA MI PA <<< "$1"
|
|
case "$2" in
|
|
major) echo "$((MA+1)).0.0" ;;
|
|
minor) echo "${MA}.$((MI+1)).0" ;;
|
|
patch) echo "${MA}.${MI}.$((PA+1))" ;;
|
|
esac
|
|
}
|
|
|
|
RELEASE=true
|
|
if [ -z "$LAST_TAG" ]; then
|
|
VERSION="$CARGO_VERSION" # first release: ship what's in Cargo.toml
|
|
elif [ "$BUMP" = none ]; then
|
|
RELEASE=false # no feat/fix/breaking since last tag
|
|
VERSION="${LAST_TAG#v}"
|
|
else
|
|
VERSION="$(bump "${LAST_TAG#v}" "$BUMP")"
|
|
fi
|
|
|
|
# An existing tag is NOT automatically "nothing to do". A tag with no
|
|
# release behind it means a previous run tagged and then died before
|
|
# publishing — which is exactly what happened on servuo-plugins' first
|
|
# release, where absent REGISTRY_* secrets took the release API call to
|
|
# 401 after the tag had already been pushed. Standing down on the tag
|
|
# alone makes that state permanent: every later run sees the tag, sets
|
|
# RELEASE=false, and the release never appears. Note this deliberately
|
|
# OVERRIDES the RELEASE=false decided just above — with the tag in
|
|
# place there are no releasable commits after it, so the normal path
|
|
# would stand down, which is exactly why it could never self-heal.
|
|
REUSE_TAG=false
|
|
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
|
REL_HTTP="$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: token $(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" \
|
|
"https://${GITEA_HOST}/api/v1/repos/${REPO}/releases/tags/v${VERSION}" || echo 000)"
|
|
if [ "$REL_HTTP" = "200" ]; then
|
|
echo "Tag v${VERSION} already has a release — nothing to do."
|
|
RELEASE=false
|
|
elif [ "$REL_HTTP" = "404" ]; then
|
|
echo "::warning::Tag v${VERSION} exists but has no release — a previous run failed after tagging. Reusing the tag and publishing the release it is missing."
|
|
REUSE_TAG=true
|
|
RELEASE=true
|
|
else
|
|
# Anything else (000 from a network failure, 401/403 from a bad
|
|
# token) is not evidence of absence. Guessing "no release" would
|
|
# re-publish over a good one, so refuse instead.
|
|
echo "::error::Could not determine whether a release exists for v${VERSION} (HTTP ${REL_HTTP}). Refusing to guess."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Changelog range. A recovery run has nothing after the tag, so
|
|
# summarize what the tag itself contains rather than emitting an empty
|
|
# list: the range that produced it, i.e. previous-tag..this-tag.
|
|
if [ "$REUSE_TAG" = true ]; then
|
|
PREV_TAG="$(git describe --tags --match 'v*' --abbrev=0 "v${VERSION}^" 2>/dev/null || true)"
|
|
if [ -n "$PREV_TAG" ]; then CL_RANGE="${PREV_TAG}..v${VERSION}"; else CL_RANGE="v${VERSION}"; fi
|
|
SINCE="$PREV_TAG"
|
|
else
|
|
CL_RANGE="$RANGE"
|
|
SINCE="$LAST_TAG"
|
|
fi
|
|
CL_SUBJECTS="$(git log --no-merges --format='%s' $CL_RANGE || true)"
|
|
|
|
{
|
|
echo "## ${BIN} v${VERSION}"
|
|
echo
|
|
FEATS="$(echo "$CL_SUBJECTS" | grep -E '^feat' || true)"
|
|
FIXES="$(echo "$CL_SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
|
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
|
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
|
echo "### All changes"
|
|
if [ -n "$SINCE" ]; then echo "Since ${SINCE}:"; fi
|
|
echo "$CL_SUBJECTS" | sed 's/^/- /'
|
|
echo
|
|
echo "### Verifying this download"
|
|
echo
|
|
echo "Releases are **unsigned** — \`SHA256SUMS\` is the trust anchor. Verify before running:"
|
|
echo
|
|
echo '```bash'
|
|
echo "sha256sum -c SHA256SUMS --ignore-missing # Linux"
|
|
echo '```'
|
|
echo
|
|
echo '```powershell'
|
|
echo "Get-FileHash .\\${BIN}-windows-x86_64.exe -Algorithm SHA256 # Windows, compare to SHA256SUMS"
|
|
echo '```'
|
|
echo
|
|
echo "Windows will show a SmartScreen \"unrecognized app\" prompt; this is expected for unsigned binaries."
|
|
} > dist/CHANGELOG.md
|
|
|
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "release=${RELEASE}" >> "$GITHUB_OUTPUT"
|
|
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
echo "reuse_tag=${REUSE_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "==> release=${RELEASE} version=${VERSION} bump=${BUMP} reuse_tag=${REUSE_TAG} last_tag=${LAST_TAG:-<none>}"
|
|
|
|
# ── Credential preflight ─────────────────────────────────────────────
|
|
# Runs BEFORE anything is built or pushed, and only when this run intends
|
|
# to publish, so a docs:/chore:-only merge (or the pre-crate no-op) stays
|
|
# green on a repo with no secrets.
|
|
#
|
|
# Learned from servuo-plugins' first release: REGISTRY_USER and
|
|
# REGISTRY_TOKEN were empty, but the tag push SUCCEEDED anyway, because
|
|
# actions/checkout leaves an `http.<host>.extraheader` credential in the
|
|
# local git config — so `git remote set-url` to a URL with empty
|
|
# credentials still authenticated through that leftover header. The
|
|
# release API call had no such fallback and 401'd, leaving the repo tagged
|
|
# but unreleased. Checking up front makes that an immediate, legible
|
|
# failure instead of a half-published release.
|
|
- name: Verify release credentials are configured
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
MISSING=""
|
|
[ -n "$(printf '%s' "${REGISTRY_USER:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_USER"
|
|
[ -n "$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_TOKEN"
|
|
if [ -n "$MISSING" ]; then
|
|
echo "::error::Missing Actions secret(s):${MISSING}. Set them under Settings → Actions → Secrets on ${REPO}. REGISTRY_TOKEN needs the write:repository scope to push the bump commit, the tag, and create the release."
|
|
exit 1
|
|
fi
|
|
echo "Release credentials present."
|
|
|
|
# ── RUST ADAPTER: toolchain + cross-compile deps ─────────────────────
|
|
- name: Install Rust toolchain, cross targets, and their linkers
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
|
$SUDO apt-get update
|
|
# libc6-dev-arm64-cross is named explicitly on purpose: gcc-aarch64-linux-gnu only
|
|
# *recommends* it, and this install runs --no-install-recommends. Without it the Rust
|
|
# half of the arm64 build succeeds and then `ring` (under ureq's rustls) dies compiling
|
|
# C, on a missing bits/libc-header-start.h.
|
|
$SUDO apt-get install -y --no-install-recommends \
|
|
build-essential gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu libc6-dev-arm64-cross \
|
|
curl ca-certificates git jq
|
|
|
|
if ! command -v cargo >/dev/null 2>&1; then
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --profile minimal --default-toolchain stable
|
|
fi
|
|
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
|
export PATH="${HOME}/.cargo/bin:${PATH}"
|
|
rustup component add rustfmt
|
|
rustup target add "${WINDOWS_TARGET}"
|
|
rustup target add "${ARM64_TARGET}"
|
|
|
|
- name: Set the crate version to match the release
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ steps.plan.outputs.version }}"
|
|
# Replace only the [package] version (the first `version = "..."`).
|
|
sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"${VERSION}\"/" Cargo.toml
|
|
grep -m1 '^version' Cargo.toml
|
|
# Bumping the manifest version desyncs this crate's own entry in
|
|
# Cargo.lock, which would make the `--locked` fmt/test/build steps below
|
|
# fail ("cannot update the lock file ... --locked was passed"). Sync just
|
|
# the workspace member(s) into the lock — dependency pins are untouched.
|
|
cargo update --workspace
|
|
|
|
# ── RUST ADAPTER: gates ──────────────────────────────────────────────
|
|
- name: cargo fmt --check
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: cargo fmt --check
|
|
|
|
- name: cargo test
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: cargo test --locked
|
|
|
|
# ── RUST ADAPTER: build both targets ─────────────────────────────────
|
|
- name: cargo build --release (Linux)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: cargo build --release --locked --target "${LINUX_TARGET}"
|
|
|
|
- name: cargo build --release (Windows, cross via MinGW)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
|
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
|
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
|
|
run: cargo build --release --locked --target "${WINDOWS_TARGET}"
|
|
|
|
# The installer has to run wherever the sidecar it installs can run, and link publishes an
|
|
# arm64 Linux binary (PLAN.md §5.2). Without this step the target is installed and the
|
|
# artifact is packaged, but nothing ever builds it — which is exactly how the first release
|
|
# attempt failed, at `cp: cannot stat target/aarch64-unknown-linux-gnu/release/...`.
|
|
- name: cargo build --release (Linux arm64, cross)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
|
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
|
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
|
|
run: cargo build --release --locked --target "${ARM64_TARGET}"
|
|
|
|
# ── RUST ADAPTER: package artifacts (+ checksums) ────────────────────
|
|
# SHA256SUMS is the trust anchor for these unsigned binaries (PLAN.md §3),
|
|
# so it ships with every release and the docs lead with the verify command.
|
|
- name: Package artifacts and SHA256SUMS
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
cp "target/${LINUX_TARGET}/release/${BIN}" "dist/${BIN}-linux-x86_64"
|
|
cp "target/${ARM64_TARGET}/release/${BIN}" "dist/${BIN}-linux-aarch64"
|
|
cp "target/${WINDOWS_TARGET}/release/${BIN}.exe" "dist/${BIN}-windows-x86_64.exe"
|
|
# Every artifact must be listed: `sha256sum -c` passes silently over a
|
|
# file the sums do not mention, and an operator verifying a download
|
|
# would get a pass on a binary nobody vouched for.
|
|
( cd dist && sha256sum "${BIN}-linux-x86_64" "${BIN}-linux-aarch64" "${BIN}-windows-x86_64.exe" > SHA256SUMS )
|
|
ls -l dist && echo "----" && cat dist/SHA256SUMS
|
|
|
|
# ── RELEASE ENGINE: commit the bump, tag, push ───────────────────────
|
|
# Tag only — `main` is never pushed to.
|
|
#
|
|
# This step used to commit the version bump back to main first, and it has
|
|
# never executed in any repo that carries it: an EMPTY template expression
|
|
# written literally in the comment below (the `$`+`{{ }}` token, spelled
|
|
# out here for that reason) makes the runner fail to build the script and
|
|
# skip the step WITHOUT failing the job. link/release.yml carried the same
|
|
# bug for six releases, which is why its Cargo.toml still says 0.1.0 while
|
|
# its tags reach v1.1.1 — the release API creates the tag when it
|
|
# publishes, so the pipeline worked by accident.
|
|
#
|
|
# It also would have been declined if it had run: `main` is protected, and
|
|
# the bundle job proved that on 2026-08-05 (`pre-receive hook declined`).
|
|
# A first release must not depend on a write to a protected branch.
|
|
#
|
|
# So the tag is the version, as in servuo-plugins. The version is still
|
|
# written into Cargo.toml before building, so a released binary
|
|
# self-reports correctly; it is simply not committed back. The next
|
|
# version is computed from the newest tag, never from the file.
|
|
- name: Push the release tag
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.plan.outputs.tag }}"
|
|
# Secrets can arrive with a trailing newline (depending on how they were
|
|
# pasted); a stray CR/LF corrupts the remote URL ("credential url cannot
|
|
# be parsed"). Strip line breaks before building the URL. They are passed
|
|
# via env rather than interpolated into this script, so a newline cannot
|
|
# break it — do NOT write a template token literally in a comment here,
|
|
# or the runner will skip this step without failing the job.
|
|
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
git config user.name "installer-ci"
|
|
git config user.email "ci@whitlocktech.com"
|
|
git remote set-url origin \
|
|
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
|
# The tag may already exist when finishing a run that died after
|
|
# tagging (see the plan step). `git tag` on an existing name fails
|
|
# under `set -e`; pushing an identical existing tag is a harmless
|
|
# no-op. A push that fails here means the remote tag points somewhere
|
|
# else, which SHOULD stop the run.
|
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
echo "Tag ${TAG} already exists — reusing it."
|
|
else
|
|
git tag "${TAG}"
|
|
fi
|
|
git push origin "${TAG}"
|
|
|
|
# ── RELEASE ENGINE: create the Gitea release + upload assets ─────────
|
|
- name: Create Gitea release and upload assets
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.plan.outputs.tag }}"
|
|
API="https://${GITEA_HOST}/api/v1/repos/${REPO}"
|
|
BODY="$(cat dist/CHANGELOG.md)"
|
|
# Same newline hygiene as the push step: a stray CR/LF in the token would
|
|
# corrupt the Authorization header.
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
|
|
REL_ID="$(curl -sSf -X POST "${API}/releases" \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg tag "$TAG" --arg body "$BODY" \
|
|
'{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \
|
|
| jq -r '.id')"
|
|
echo "Created release ${TAG} (id=${REL_ID})"
|
|
|
|
for f in "${BIN}-linux-x86_64" "${BIN}-linux-aarch64" "${BIN}-windows-x86_64.exe" SHA256SUMS; do
|
|
curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-F "attachment=@dist/${f}" >/dev/null
|
|
echo " uploaded ${f}"
|
|
done
|