v1.0.0 installed and then died on every boot:
module "uo" failed to load — {"stage":"register","reason":"Cannot find
module './commands/guild.command'"}
`server/commands/` arrived with the Teams cutover (2d1d91e, `/guild`). The
release assembles the tarball from an include list, that list was hardcoded in
release.yml, and it was never told about the new directory — so the bundle
shipped without it and the module was dead on the operator's box.
Nothing caught it, and that is the more interesting half. Every PR check runs
against the whole repo — `frozen-manifest` even installs the module into core by
tarring the entire tree — but a release is a SUBSET of the repo, and the subset
exists nowhere except the release. The pre-publish check in release.yml only
stats the paths `module.json` declares, and a file reached by a require inside
`register()` is named in none of them, so it passed on a bundle that could not
load.
The include list stays an include list — release.yml's header makes that case
and it still holds. What changes is that it is declared ONCE, in ci/bundle.json,
with two readers instead of one:
• release.yml assembles from it (via jq) rather than from its own copy.
• server/scripts/checkBundle.js asks, in PR checks, whether it still covers
everything `server/index.js` reaches — following requires transitively and
through function bodies, which is where index.js deliberately puts them.
And the release gains a real loadability check: `checkBundle.js --bundle` walks
the ASSEMBLED tree and asserts every relative require resolves inside it. Asked
of the artifact rather than the source, so it also catches a half-failed copy or
a list naming a path that has moved.
Requiring the entry point would not have worked as a check: index.js requires
inside `register()` because require order is load-bearing (`core.init(ctx)` must
run before anything under `router/`), so requiring it evaluates one line and
reports success on a bundle missing every router it has.
Both modes were verified against the real defect — each fails with `commands`
removed and passes with it present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz
445 lines
22 KiB
YAML
445 lines
22 KiB
YAML
# Build and publish the installable bundle: `module-uo-<version>.tar.gz` plus the
|
|
# manifest carrying its sha256 (docs/website/MODULE_SYSTEM.md §2.3, §2.5).
|
|
#
|
|
# ── What a release IS here ──────────────────────────────────────────────────
|
|
#
|
|
# **An operator never builds anything** (MODULE_SYSTEM.md §1.14 — the constraint
|
|
# the whole module system is shaped around). So a release is not source: it is the
|
|
# directory core's loader expects to find at `modules/uo/`, already assembled —
|
|
# the prebuilt client chunk, the one runtime dependency installed, the schema
|
|
# fragment and the OpenAPI fragment — packed as it will be unpacked. Phase 4's
|
|
# admin install downloads the tarball, verifies it against the `sha256` in the
|
|
# manifest, and unpacks it onto the volume. Nothing runs `npm` on the way.
|
|
#
|
|
# ── The version is DERIVED, and the declaration is a floor ──────────────────
|
|
#
|
|
# This file used to release only when a merge to `main` left `module.json` at a
|
|
# version with no release yet — the version DECLARED, never computed, on the
|
|
# argument that two sources for one number is how they drift. That was true and
|
|
# it was still the wrong trade: it makes every bundle cost a second reviewed PR
|
|
# whose entire content is a number, and between 2026-08-12 and 2026-08-19 it cost
|
|
# this repo *every* bundle — v0.3.0 was the only release while nine phases of
|
|
# Teams work landed, because nothing in them touched that line.
|
|
#
|
|
# So the engine `link` and `installer` already run is adopted here (MODULE_SYSTEM
|
|
# §2.7.1, decision 19 as amended):
|
|
#
|
|
# feat!: / BREAKING CHANGE -> major feat: -> minor fix|perf: -> patch
|
|
# nothing releasable -> no release is cut
|
|
# (first ever run, no tag) -> releases what module.json declares
|
|
#
|
|
# **The declared version is kept as a floor, not deleted.** If `module.json` names
|
|
# a version above the newest tag, that version releases — which is the old model
|
|
# exactly, surviving as the special case it always was. Raising it by hand is
|
|
# still how you say "this one is a minor, whatever the subjects imply", and it is
|
|
# still the natural place to move when a `coreApi` bump forces the question. What
|
|
# no longer happens is a merge full of `feat:` producing nothing.
|
|
#
|
|
# The number that ships is therefore the TAG, and CI writes it into the
|
|
# `module.json` inside the bundle at assembly time. The committed `module.json` is
|
|
# a floor and a starting point, not a record of the last release — `link` reached
|
|
# the same arrangement with `Cargo.toml`, for the same reason: a release engine
|
|
# that has to commit a bump back to `main` stops working the day someone protects
|
|
# the branch, and this one is protected.
|
|
#
|
|
# ── The backdoor ────────────────────────────────────────────────────────────
|
|
#
|
|
# `workflow_dispatch` publishes on demand, for the case the rules above cannot
|
|
# reach: `module.json` changed in a way worth shipping — a widened `coreApi`, a
|
|
# new mount, a capability — with no releasable code behind it. Leave `version`
|
|
# blank to bump the newest tag by `bump` (default `patch`), or name an exact
|
|
# version to publish that. A dispatch releases even when nothing in the log is
|
|
# releasable; that is the entire point of pressing the button.
|
|
#
|
|
# Re-running on a version that is already released is a no-op, so a rerun after an
|
|
# unrelated failure is safe. A tag that exists with no release behind it is NOT a
|
|
# no-op — see the recovery branch in the plan step.
|
|
#
|
|
# This workflow still never writes to a branch. It tags and publishes, so `main`
|
|
# needs no push exception.
|
|
#
|
|
# Prerequisites (Settings → Actions → Secrets on RunicGateway/Module-uo):
|
|
# REGISTRY_TOKEN — Gitea access token with `write:repository`, to push the tag
|
|
# and create the release.
|
|
|
|
name: Release
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Exact version to publish (e.g. 0.4.1). Blank = bump the newest tag by the level below.'
|
|
required: false
|
|
default: ''
|
|
bump:
|
|
description: 'Bump level when version is blank: patch | minor | major'
|
|
required: false
|
|
default: 'patch'
|
|
|
|
concurrency:
|
|
group: release-module-uo
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
REPO: RunicGateway/Module-uo
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
# Full history: the plan step reads every tag and every subject since the
|
|
# newest one, and a shallow clone has neither.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
|
|
- name: Plan the release (version + changelog)
|
|
id: plan
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
EVENT: ${{ github.event_name }}
|
|
IN_VERSION: ${{ github.event.inputs.version }}
|
|
IN_BUMP: ${{ github.event.inputs.bump }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
git fetch --tags --force >/dev/null 2>&1 || true
|
|
|
|
DECLARED="$(node -p "require('./module.json').version")"
|
|
LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)"
|
|
CURRENT="${LAST_TAG#v}"
|
|
RANGE="${LAST_TAG:+${LAST_TAG}..}HEAD"
|
|
echo "module.json declares ${DECLARED}; newest tag is ${LAST_TAG:-<none>}"
|
|
|
|
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
|
|
}
|
|
|
|
# `sort -V` orders version strings, so the higher of two is its last
|
|
# line. Used rather than a hand-rolled field compare because 0.10.0 vs
|
|
# 0.9.0 is exactly the comparison a string sort gets wrong.
|
|
higher() { printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1; }
|
|
|
|
rank() { case "$1" in major) echo 3 ;; minor) echo 2 ;; patch) echo 1 ;; *) echo 0 ;; esac; }
|
|
bigger_bump() { if [ "$(rank "$1")" -ge "$(rank "$2")" ]; then echo "$1"; else echo "$2"; fi; }
|
|
|
|
VERSION=""
|
|
if [ -n "${IN_VERSION:-}" ]; then
|
|
# The backdoor's exact form. Deliberately unvalidated against the log:
|
|
# a human typed it, and the already-released check below is the only
|
|
# guard that matters.
|
|
VERSION="${IN_VERSION}"
|
|
echo "dispatch: publishing the requested version ${VERSION}"
|
|
else
|
|
LEVEL="$BUMP"
|
|
# A dispatch with nothing releasable in the log still releases — that
|
|
# is what the button is for. Where the log DOES say something, the
|
|
# larger of the two wins rather than the input: pressing the button on
|
|
# a log full of `feat:` without touching the dropdown would otherwise
|
|
# publish its `patch` default over a minor's worth of work, and a
|
|
# version that undersells its own contents cannot be taken back.
|
|
if [ "${EVENT:-}" = workflow_dispatch ]; then
|
|
LEVEL="$(bigger_bump "$LEVEL" "${IN_BUMP:-patch}")"
|
|
if [ "$BUMP" = none ]; then
|
|
echo "dispatch: nothing releasable in the log, bumping ${LEVEL} anyway"
|
|
elif [ "$LEVEL" != "$BUMP" ]; then
|
|
echo "dispatch: the log says ${BUMP}, the run asked for ${LEVEL} — taking ${LEVEL}"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$CURRENT" ]; then
|
|
VERSION="$DECLARED" # first ever release: ship what is declared
|
|
elif [ "$LEVEL" != none ]; then
|
|
VERSION="$(bump "$CURRENT" "$LEVEL")"
|
|
fi
|
|
|
|
# The floor. A `module.json` above the newest tag releases at that
|
|
# version even when the log says nothing and even when the log says
|
|
# patch — which is the pre-2026-08-19 model, kept as a special case.
|
|
if [ -n "$CURRENT" ] && [ "$DECLARED" != "$CURRENT" ] \
|
|
&& [ "$(higher "$DECLARED" "$CURRENT")" = "$DECLARED" ]; then
|
|
if [ -z "$VERSION" ] || [ "$(higher "$DECLARED" "$VERSION")" = "$DECLARED" ]; then
|
|
echo "module.json declares ${DECLARED}, above both ${CURRENT} and the derived version — releasing that."
|
|
VERSION="$DECLARED"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
RELEASE=true
|
|
if [ -z "$VERSION" ]; then
|
|
RELEASE=false
|
|
VERSION="$CURRENT"
|
|
echo "Nothing releasable since ${LAST_TAG} (no feat/fix/perf/breaking subject) — standing down."
|
|
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 what happened on servuo-plugins' first release,
|
|
# where absent 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. Note this deliberately OVERRIDES the RELEASE=false
|
|
# above: with the tag in place there is nothing releasable after it, so
|
|
# the normal path would stand down, which is why it could never
|
|
# self-heal. Anything other than 200/404 — a network failure, a bad
|
|
# token — is not evidence of absence, and guessing "no" would publish
|
|
# over a good release, so refuse instead.
|
|
REUSE_TAG=false
|
|
if [ -n "$VERSION" ] && 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)"
|
|
case "$REL_HTTP" in
|
|
200) echo "v${VERSION} is already released — nothing to do."; RELEASE=false ;;
|
|
404) 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 ;;
|
|
*) echo "::error::Could not determine whether v${VERSION} is released (HTTP ${REL_HTTP}). Refusing to guess."; exit 1 ;;
|
|
esac
|
|
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)"
|
|
CL_RANGE="${PREV_TAG:+${PREV_TAG}..}v${VERSION}"
|
|
SINCE="$PREV_TAG"
|
|
else
|
|
CL_RANGE="$RANGE"
|
|
SINCE="$LAST_TAG"
|
|
fi
|
|
CL_SUBJECTS="$(git log --no-merges --format='%s' $CL_RANGE || true)"
|
|
|
|
{
|
|
echo "## module-uo v${VERSION}"
|
|
echo
|
|
echo "Install from the website's Admin → Modules screen by pasting the URL of"
|
|
echo "\`module-uo-${VERSION}.json\`, or unpack the tarball onto the modules volume"
|
|
echo "as \`modules/uo/\`. Requires a core whose \`MODULE_API_VERSION\` satisfies"
|
|
echo "\`$(node -p "require('./module.json').coreApi")\`."
|
|
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** — the \`sha256\` in \`module-uo-${VERSION}.json\` is the"
|
|
echo "trust anchor, and the website verifies it before unpacking."
|
|
echo
|
|
echo '```bash'
|
|
echo "sha256sum -c SHA256SUMS --ignore-missing"
|
|
echo '```'
|
|
} > dist/CHANGELOG.md
|
|
|
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "release=${RELEASE}" >> "$GITHUB_OUTPUT"
|
|
echo "reuse_tag=${REUSE_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
echo "==> release=${RELEASE} version=${VERSION} bump=${BUMP} declared=${DECLARED} last_tag=${LAST_TAG:-<none>}"
|
|
|
|
# Before anything is built or tagged, so a repo without secrets fails
|
|
# legibly rather than half-publishing: the tag push can succeed on the
|
|
# credential actions/checkout left in the local git config while the
|
|
# release API call 401s, leaving the repo tagged and unreleased.
|
|
- name: Verify release credentials are configured
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" ]; then
|
|
echo "::error::Missing Actions secret REGISTRY_TOKEN (needs write:repository) on ${REPO}."
|
|
exit 1
|
|
fi
|
|
echo "Release credentials present."
|
|
|
|
- name: Build the client chunk
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
npm ci --prefix client
|
|
npm run build --prefix client
|
|
|
|
# `--omit=dev` and then PACKED: express, express-validator and swagger-autogen
|
|
# are build- and test-time only — the shipped half is handed express on `ctx`
|
|
# (MODULE_API.md §2.3) — and `ws` is the one runtime dependency. Node resolves
|
|
# it by walking up from `modules/uo/server/`, which is why it ships inside the
|
|
# tarball rather than being installed on the operator's box.
|
|
- name: Install the shipped runtime dependency
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: npm ci --omit=dev --prefix server
|
|
|
|
# ── Assemble exactly what an operator's volume gets ──────────────────
|
|
#
|
|
# Stated as an INCLUDE list, not an exclude list. An exclude list ships
|
|
# whatever it forgot: the day someone adds `server/tools/` with a scratch
|
|
# credential in it, an exclude list packs it and nobody finds out.
|
|
#
|
|
# The list itself lives in `ci/bundle.json`, not here, because it has a
|
|
# second reader: `server/scripts/checkBundle.js` runs in PR checks and asks
|
|
# whether the list still covers everything `server/index.js` reaches. It
|
|
# was hardcoded in this file until v1.0.0 shipped without `server/commands/`
|
|
# — added by the Teams cutover, never added here — and the module died at
|
|
# the register stage on the operator's box. One declaration, two readers,
|
|
# so the next directory cannot go missing quietly.
|
|
- name: Assemble the bundle
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ steps.plan.outputs.version }}"
|
|
OUT="dist/module-uo-${VERSION}"
|
|
rm -rf "$OUT" && mkdir -p "$OUT"
|
|
|
|
# The manifest core reads — with the RELEASED version written into it.
|
|
# The committed `module.json` is a floor, not a record of the last
|
|
# release (see the header), so copying it verbatim would ship a bundle
|
|
# whose `installed_modules` row and admin screen disagree with the tag
|
|
# it came from. This is the one place the derived number becomes the
|
|
# module's own.
|
|
jq --arg v "$VERSION" '.version = $v' module.json > "$OUT/module.json"
|
|
|
|
# The two fragments, and the licence the code is under — a bundle that
|
|
# ships GPL code without its licence is not distributable.
|
|
for f in $(jq -r '.root[]' ci/bundle.json); do
|
|
cp "$f" "$OUT/"
|
|
done
|
|
|
|
# The server half, minus what never runs inside core's process.
|
|
mkdir -p "$OUT/server"
|
|
for d in $(jq -r '.server[]' ci/bundle.json); do
|
|
cp -r "server/$d" "$OUT/server/"
|
|
done
|
|
cp -r server/node_modules "$OUT/server/"
|
|
|
|
# The client half is the BUILT chunk only. `client/src` is 5,000 lines
|
|
# of source an operator has no use for and core will never read.
|
|
mkdir -p "$OUT/client/dist"
|
|
cp client/dist/entry.js "$OUT/client/dist/"
|
|
|
|
# Prove the bundle is loadable before it is published: these are the
|
|
# paths core's loader resolves out of module.json, and a release whose
|
|
# entry point is missing fails on an operator's box with a
|
|
# `startup_failed` row instead of here. The version assertion guards the
|
|
# rewrite above — a bundle that still carries the declared version would
|
|
# install under a number that is not the one it was released as.
|
|
node -e '
|
|
const fs = require("fs"), path = require("path");
|
|
const [root, want] = process.argv.slice(1);
|
|
const m = JSON.parse(fs.readFileSync(path.join(root, "module.json"), "utf8"));
|
|
if (m.version !== want) {
|
|
console.error(`bundle declares ${m.version}, but this is release ${want}`);
|
|
process.exit(1);
|
|
}
|
|
for (const p of [m.server, m.schema, m.purge, m.client.entry, "swagger-fragment.json"]) {
|
|
if (!fs.existsSync(path.join(root, p))) { console.error("bundle is missing " + p); process.exit(1); }
|
|
}
|
|
console.log("bundle contents check: ok");
|
|
' "$OUT" "$VERSION"
|
|
|
|
# ── And that it can actually LOAD ─────────────────────────────────
|
|
#
|
|
# The check above stats the paths `module.json` declares, which is a
|
|
# real question but a shallow one: v1.0.0 passed it and was still
|
|
# missing `server/commands/`, because a file reached only by a require
|
|
# inside `register()` is named nowhere in `module.json`. This resolves
|
|
# every relative require in the assembled tree and asserts the target is
|
|
# in it — asked of the artifact, so it also catches a copy that half
|
|
# failed or a list naming a path that has since moved.
|
|
#
|
|
# Run from the SOURCE tree (`server/scripts/` never ships) against the
|
|
# assembled bundle.
|
|
node server/scripts/checkBundle.js --bundle "$OUT"
|
|
|
|
tar -C dist -czf "dist/module-uo-${VERSION}.tar.gz" "module-uo-${VERSION}"
|
|
rm -rf "$OUT"
|
|
|
|
SHA="$(sha256sum "dist/module-uo-${VERSION}.tar.gz" | cut -d' ' -f1)"
|
|
SIZE="$(stat -c%s "dist/module-uo-${VERSION}.tar.gz")"
|
|
|
|
# The install manifest. Same shape as the installer's bundle JSON — a
|
|
# per-asset sha256 fetched over HTTPS, no signatures — because that is
|
|
# the model this project already has and a second one would be a second
|
|
# thing to get right (MODULE_SYSTEM.md §1.11).
|
|
jq -n \
|
|
--arg id "$(node -p "require('./module.json').id")" \
|
|
--arg name "$(node -p "require('./module.json').name")" \
|
|
--arg version "$VERSION" \
|
|
--arg coreApi "$(node -p "require('./module.json').coreApi")" \
|
|
--arg artifact "module-uo-${VERSION}.tar.gz" \
|
|
--arg sha256 "$SHA" \
|
|
--argjson size "$SIZE" \
|
|
--arg url "https://${GITEA_HOST}/${REPO}/releases/download/v${VERSION}/module-uo-${VERSION}.tar.gz" \
|
|
'{schema:1, id:$id, name:$name, version:$version, coreApi:$coreApi,
|
|
artifact:$artifact, url:$url, sha256:$sha256, size:$size}' \
|
|
> "dist/module-uo-${VERSION}.json"
|
|
|
|
echo "${SHA} module-uo-${VERSION}.tar.gz" > dist/SHA256SUMS
|
|
cat "dist/module-uo-${VERSION}.json"
|
|
|
|
# Skipped on a recovery run: the tag is already there and is the thing being
|
|
# published against.
|
|
- name: Tag the release
|
|
if: ${{ steps.plan.outputs.release == 'true' && steps.plan.outputs.reuse_tag != 'true' }}
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.plan.outputs.tag }}"
|
|
git config user.name 'Runic Gateway CI'
|
|
git config user.email 'ci@whitlocktech.net'
|
|
git tag -a "$TAG" -m "module-uo ${TAG}"
|
|
git push origin "$TAG"
|
|
|
|
- name: Create the Gitea release and upload the bundle
|
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.plan.outputs.tag }}"
|
|
VERSION="${{ steps.plan.outputs.version }}"
|
|
API="https://${GITEA_HOST}/api/v1/repos/${REPO}"
|
|
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 "$(cat dist/CHANGELOG.md)" \
|
|
'{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \
|
|
| jq -r '.id')"
|
|
echo "Created release ${TAG} (id=${REL_ID})"
|
|
|
|
for f in "module-uo-${VERSION}.tar.gz" "module-uo-${VERSION}.json" 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
|