Phase 2 of docs/modules/rust/PLAN.md. Phase 1 built five guards and ran them by hand; this repo had no workflows at all, so nothing gated the branch that gets released and there was no way to release it. Three pieces: - **release.yml** — the derived-version engine link, installer and Module-uo already run (conventional-commit subjects since the newest tag; module.json's version survives as a floor; workflow_dispatch as the backdoor), assembling the bundle from an include list and publishing the tarball, the install manifest carrying its sha256, and SHA256SUMS. The tag is the number that ships and CI stamps it into the bundle's own module.json. - **pr-checks.yml** — server tests, check:imports, check:bundle, check:swagger, the client build, client tests and check:externals, plus frozen-manifest. - **frozen-manifest** — clones core at the sha pinned in ci/core-ref.json, generates its route table without this module and with it, and takes the difference. It ran locally against that exact ref: six routes, all documented, no core route moved. That is the first proof by a running core that /rust collides with nothing — phase 1 could only check it by reading, because core mounts /status and /version at a tier root where the loader's own collision probe cannot see them. The bundle carries no node_modules, because the shipped half declares no runtime dependencies (org lead, phase 2). checkBundle.js holds both halves of that: the include list still covers everything server/index.js reaches, and no dependency has appeared without the release learning to pack it. Verified by breaking it — dropping "model" from the list names the exact edit and exits 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
443 lines
22 KiB
YAML
443 lines
22 KiB
YAML
# Build and publish the installable bundle: `module-rust-<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/rust/`, already assembled —
|
|
# the prebuilt client chunk, the schema fragment and the OpenAPI fragment — packed
|
|
# as it will be unpacked. Core'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.
|
|
#
|
|
# **And nothing is installed into the bundle either**, which is where this repo
|
|
# differs from Module-uo: the shipped half declares no runtime dependencies, so
|
|
# there is no `npm ci --omit=dev` and no `server/node_modules` in the tarball (org
|
|
# lead, phase 2). That is a decision worth being loud about rather than a detail —
|
|
# `server/scripts/checkBundle.js` fails the PR that adds a dependency without also
|
|
# teaching this file to install and pack it, because a bundle that declares an
|
|
# import it does not carry fails the same way a missing directory does.
|
|
#
|
|
# ── The version is DERIVED, and the declaration is a floor ──────────────────
|
|
#
|
|
# The engine `link`, `installer` and `Module-uo` already run (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
|
|
#
|
|
# Module-uo learned this the expensive way: it released only when a merge left
|
|
# `module.json` at a version with no release yet — the version DECLARED, never
|
|
# computed — and between 2026-08-12 and 2026-08-19 that cost it *every* bundle,
|
|
# because nine phases of work landed without anyone touching that line.
|
|
#
|
|
# **The declared version is kept as a floor, not deleted.** If `module.json` names
|
|
# a version above the newest tag, that version releases. Raising it by hand is how
|
|
# you say "this one is a minor, whatever the subjects imply", and it is the natural
|
|
# place to move when a `coreApi` bump forces the question.
|
|
#
|
|
# 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 — 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. That state is not theoretical:
|
|
# `servuo-plugins`' first release pushed its tag and then 401'd on the release API
|
|
# because the secret was absent, and without the recovery branch the repo would
|
|
# have been stuck there permanently.
|
|
#
|
|
# This workflow never writes to a branch. It tags and publishes, so `main` needs
|
|
# no push exception.
|
|
#
|
|
# Prerequisites (Settings → Actions → Secrets on RunicGateway/Module-Rust):
|
|
# 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.1.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-rust
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
REPO: RunicGateway/Module-Rust
|
|
|
|
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.
|
|
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-rust v${VERSION}"
|
|
echo
|
|
echo "Install from the website's Admin → Modules screen by pasting the URL of"
|
|
echo "\`module-rust-${VERSION}.json\`, or unpack the tarball onto the modules volume"
|
|
echo "as \`modules/rust/\`. Requires a core whose \`MODULE_API_VERSION\` satisfies"
|
|
echo "\`$(node -p "require('./module.json').coreApi")\`."
|
|
echo
|
|
echo "A Rust server also needs the other two halves of the bridge:"
|
|
echo "[Rust-Link](https://${GITEA_HOST}/RunicGateway/Rust-Link) (the sidecar) and"
|
|
echo "[Rust-Plugins](https://${GITEA_HOST}/RunicGateway/Rust-Plugins) (the Oxide/Carbon plugin)."
|
|
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-rust-${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
|
|
|
|
# ── 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. One
|
|
# declaration, two readers, so a new 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-rust-${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. No
|
|
# node_modules: the shipped half declares no runtime dependencies, and
|
|
# check:bundle is what keeps that true.
|
|
mkdir -p "$OUT/server"
|
|
for d in $(jq -r '.server[]' ci/bundle.json); do
|
|
cp -r "server/$d" "$OUT/server/"
|
|
done
|
|
|
|
# The client half is the BUILT chunk only. `client/src` is 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 carried 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: Module-uo's v1.0.0 passed exactly that 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-rust-${VERSION}.tar.gz" "module-rust-${VERSION}"
|
|
rm -rf "$OUT"
|
|
|
|
SHA="$(sha256sum "dist/module-rust-${VERSION}.tar.gz" | cut -d' ' -f1)"
|
|
SIZE="$(stat -c%s "dist/module-rust-${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-rust-${VERSION}.tar.gz" \
|
|
--arg sha256 "$SHA" \
|
|
--argjson size "$SIZE" \
|
|
--arg url "https://${GITEA_HOST}/${REPO}/releases/download/v${VERSION}/module-rust-${VERSION}.tar.gz" \
|
|
'{schema:1, id:$id, name:$name, version:$version, coreApi:$coreApi,
|
|
artifact:$artifact, url:$url, sha256:$sha256, size:$size}' \
|
|
> "dist/module-rust-${VERSION}.json"
|
|
|
|
echo "${SHA} module-rust-${VERSION}.tar.gz" > dist/SHA256SUMS
|
|
cat "dist/module-rust-${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-rust ${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-rust-${VERSION}.tar.gz" "module-rust-${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
|