All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m15s
An existing tag was treated as "nothing to release", unconditionally. That is wrong in the one case it matters: a tag with no release behind it means an earlier 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 been pushed. Standing down on the tag alone makes that permanent. Every later run sees the tag, sets RELEASE=false, and the release never appears; the version is unpublishable forever. The tag now decides nothing on its own. The API does: 200 -> a release exists, stand down 404 -> tag without release, reuse the tag and publish what is missing else -> refuse and exit 1 The last arm matters as much as the others. A 000 from a network failure or a 401 from a bad token is not evidence of absence, and guessing "no release" would republish over a good one. Note the 404 arm deliberately overrides the RELEASE=false decided just above it: with the tag in place there are no releasable commits after it, so the normal path always stands down -- which is why this could never self-heal on its own. Two consequences handled with it. The changelog range is now previous-tag..this-tag on a recovery run, since a run finishing an earlier one has nothing after the tag and would otherwise publish an empty change list. And the tagging step tolerates the tag already existing, because `git tag` on an existing name fails under `set -e` while pushing an identical tag is a harmless no-op -- a push that does fail there means the remote tag points somewhere else, which should stop the run. This is the same handling installer/release.yml already carries; link was the copy that still had the trap. Verified by extracting this step and driving it through four scenarios against a real clone with the live tags, with curl stubbed to return each status: a normal patch bump (v1.1.1, release=true), a tag with no release (recovers, release=true), a tag with a release (stands down), and an unreachable API (exit 1, publishes nothing). Co-Authored-By: Claude <noreply@anthropic.com>
391 lines
21 KiB
YAML
391 lines
21 KiB
YAML
# Automated build + release for the uo-link Rust sidecar.
|
|
#
|
|
# 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) │
|
|
# └───────────────────────────────────────────────────────────────────┘
|
|
#
|
|
# To retarget this engine at a C#/Node/Docker/static project later, only the
|
|
# "Rust adapter" steps change — the plan + release steps consume just
|
|
# {version, changelog, artifacts} and know nothing about Rust.
|
|
#
|
|
# 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/link):
|
|
# REGISTRY_USER — Gitea username the token below belongs to
|
|
# REGISTRY_TOKEN — Gitea access token. For image builds it needed
|
|
# write:package; THIS workflow additionally needs
|
|
# `write:repository` so it can push the bump commit + tag
|
|
# and create the release. Grant that scope to the token.
|
|
# The final step also dispatches RunicGateway/installer's
|
|
# bundle workflow, so the token ideally has write there too
|
|
# — but that is a nicety, not a requirement: without it the
|
|
# step warns and the installer's nightly cron picks the
|
|
# release up instead.
|
|
# Also: `main` must accept a direct push from that user (disable branch
|
|
# protection for it, or add it as an exception) — the bump commit lands on main.
|
|
#
|
|
# The bump commit carries `[skip ci]`, so it does not re-trigger this workflow.
|
|
|
|
name: Release sidecar
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: release-sidecar
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
REPO: RunicGateway/link
|
|
WORKDIR: sidecar
|
|
BIN: uo-link-sidecar
|
|
LINUX_TARGET: x86_64-unknown-linux-gnu
|
|
WINDOWS_TARGET: x86_64-pc-windows-gnu
|
|
# Ampere/Graviton instances and Pi-class boxes are a realistic ServUO home,
|
|
# and the shard dials the sidecar out on loopback — so wherever the shard
|
|
# runs, this binary has to run too (installer PLAN.md §5.2).
|
|
ARM64_TARGET: aarch64-unknown-linux-gnu
|
|
# Notified after a release so the installer's compat matrix picks up this
|
|
# version immediately rather than at its next nightly run (PLAN.md §7.2).
|
|
INSTALLER_REPO: RunicGateway/installer
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
# Don't loop on our own bump commit (belt-and-suspenders with [skip ci]).
|
|
# 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
|
|
|
|
CARGO_VERSION="$(grep -m1 '^version' "${WORKDIR}/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
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')"
|
|
REL_HTTP="$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token ${CI_TOKEN}" "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/^/- /'
|
|
} > 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 "==> release=${RELEASE} version=${VERSION} bump=${BUMP} last_tag=${LAST_TAG:-<none>}"
|
|
|
|
# ── 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
|
|
# The arm64 cross toolchain is not optional for this crate: sqlx's
|
|
# sqlite feature pulls libsqlite3-sys, which compiles bundled SQLite
|
|
# from C, so a Rust-only cross build fails at the first .c file.
|
|
#
|
|
# libc6-dev-arm64-cross is named explicitly because gcc-aarch64-linux-gnu
|
|
# only *recommends* it, and this install is --no-install-recommends: the
|
|
# compiler arrives without arm64 libc headers and SQLite's build dies on
|
|
# `bits/libc-header-start.h: No such file or directory`. Verified by
|
|
# reproducing both the failure and the fix in a rust:1-slim-bookworm
|
|
# container.
|
|
$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}\"/" "${WORKDIR}/Cargo.toml"
|
|
grep -m1 '^version' "${WORKDIR}/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 --manifest-path "${WORKDIR}/Cargo.toml" --workspace
|
|
|
|
# ── RUST ADAPTER: gates ──────────────────────────────────────────────
|
|
- name: cargo fmt --check
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
working-directory: sidecar
|
|
run: cargo fmt --check
|
|
|
|
- name: cargo test
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
working-directory: sidecar
|
|
run: cargo test --locked
|
|
|
|
# ── RUST ADAPTER: build both targets ─────────────────────────────────
|
|
- name: cargo build --release (Linux)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
working-directory: sidecar
|
|
run: cargo build --release --locked --target "${LINUX_TARGET}"
|
|
|
|
- name: cargo build --release (Windows, cross via MinGW)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
working-directory: sidecar
|
|
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}"
|
|
|
|
# Same shape as the Windows step: a linker for Rust's output and a CC/AR
|
|
# pair for the cc-crate build of bundled SQLite.
|
|
- name: cargo build --release (Linux arm64, cross via aarch64-linux-gnu)
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
working-directory: sidecar
|
|
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) ────────────────────
|
|
- name: Package artifacts and SHA256SUMS
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
cp "${WORKDIR}/target/${LINUX_TARGET}/release/${BIN}" "dist/${BIN}-linux-x86_64"
|
|
cp "${WORKDIR}/target/${ARM64_TARGET}/release/${BIN}" "dist/${BIN}-linux-aarch64"
|
|
cp "${WORKDIR}/target/${WINDOWS_TARGET}/release/${BIN}.exe" "dist/${BIN}-windows-x86_64.exe"
|
|
# Every artifact must appear here: the installer verifies its download
|
|
# against these sums, and `sha256sum -c` passes silently over a file
|
|
# this list does not mention.
|
|
( 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 ───────────────────────
|
|
- name: Commit version bump and push tag
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ steps.plan.outputs.version }}"
|
|
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. Passing them via
|
|
# env (not inline ${{ }}) also keeps a newline from breaking this script.
|
|
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
git config user.name "uo-link-ci"
|
|
git config user.email "ci@whitlocktech.com"
|
|
git remote set-url origin \
|
|
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
|
|
|
git add "${WORKDIR}/Cargo.toml" "${WORKDIR}/Cargo.lock"
|
|
if ! git diff --cached --quiet; then
|
|
git commit -m "chore(release): bump version to ${TAG} [skip ci]"
|
|
git push origin "HEAD:main"
|
|
else
|
|
echo "Version unchanged (first release) — no bump commit needed."
|
|
fi
|
|
# 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
|
|
|
|
# ── Recompose the installer's bundle manifest ────────────────────────
|
|
# The installer does not resolve "latest" at run time — it installs the
|
|
# exact combination named by a published bundle (docs/installer/PLAN.md
|
|
# §7.1). So a sidecar release that nobody recomposes around is a release
|
|
# no operator will ever be offered. This step tells the installer repo to
|
|
# rebuild that manifest now, instead of leaving the new version invisible
|
|
# until its nightly cron.
|
|
#
|
|
# DISPATCH, DON'T WAIT (PLAN.md §7.3). Gitea's workflow-dispatch endpoint
|
|
# returns no run handle, so there is nothing to poll: a waiting step would
|
|
# have to guess which run is its own and hold a runner idle to do it. The
|
|
# bundle job runs its own gates regardless of who started it.
|
|
#
|
|
# A failure here is a WARNING, never a failure of this job. The release is
|
|
# already published and correct by this point; failing the run would
|
|
# misreport that. The installer's nightly cron recomposes from whatever
|
|
# the latest releases actually are, so a dropped dispatch self-heals — it
|
|
# costs latency, not correctness.
|
|
#
|
|
# `repository_dispatch` is deliberately not used: support for it is
|
|
# uncertain on this Gitea version, while dispatching an existing
|
|
# workflow_dispatch workflow via the API works today.
|
|
- name: Ask the installer repo to recompose its bundle
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
HTTP="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"ref":"main"}' \
|
|
"https://${GITEA_HOST}/api/v1/repos/${INSTALLER_REPO}/actions/workflows/bundle.yml/dispatches" || echo 000)"
|
|
case "$HTTP" in
|
|
20*) echo "Dispatched ${INSTALLER_REPO} bundle.yml (HTTP ${HTTP}) — not waiting for it." ;;
|
|
403|404)
|
|
echo "::warning::Could not dispatch ${INSTALLER_REPO} bundle.yml (HTTP ${HTTP}). REGISTRY_TOKEN likely lacks write:repository on that repo. Release ${{ steps.plan.outputs.tag }} is published and fine; its bundle will be composed by the installer's nightly cron instead." ;;
|
|
*)
|
|
echo "::warning::Dispatching ${INSTALLER_REPO} bundle.yml returned HTTP ${HTTP}. Release ${{ steps.plan.outputs.tag }} is published and fine; the nightly cron will recompose the bundle." ;;
|
|
esac
|