Merge pull request 'ci(installer): add pr-checks, release, and project-tree sync workflows' (#1) from ci/add-workflows into main
Reviewed-on: #1 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
54
.gitea/scripts/gen_tree.py
Normal file
54
.gitea/scripts/gen_tree.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render an ASCII tree of tracked files, read from stdin (one path per line).
|
||||
|
||||
Used by the `sync-project-tree` workflow to regenerate this repo's PROJECT_TREE.md
|
||||
snapshot in the RunicGateway/docs repo. Feed it `git ls-files`:
|
||||
|
||||
git ls-files | python3 .gitea/scripts/gen_tree.py <root-label>
|
||||
|
||||
Deterministic ordering: directories before files, each group sorted
|
||||
case-insensitively with the raw name as a tiebreak. Output uses the classic
|
||||
`tree(1)` box-drawing style so the result is stable across runs and platforms.
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
def build(paths):
|
||||
root = {}
|
||||
for p in paths:
|
||||
p = p.strip().replace("\\", "/")
|
||||
if not p:
|
||||
continue
|
||||
node = root
|
||||
for part in p.split("/"):
|
||||
node = node.setdefault(part, {})
|
||||
return root
|
||||
|
||||
|
||||
def render(node, prefix, lines):
|
||||
entries = list(node.items())
|
||||
# directories (non-empty children dict) before files, then case-insensitive name
|
||||
entries.sort(key=lambda kv: (0 if kv[1] else 1, kv[0].lower(), kv[0]))
|
||||
for i, (name, child) in enumerate(entries):
|
||||
last = i == len(entries) - 1
|
||||
branch = "└── " if last else "├── "
|
||||
suffix = "/" if child else ""
|
||||
lines.append(f"{prefix}{branch}{name}{suffix}")
|
||||
if child:
|
||||
render(child, prefix + (" " if last else "│ "), lines)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", newline="\n")
|
||||
except AttributeError:
|
||||
pass
|
||||
root_label = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
tree = build(sys.stdin.read().splitlines())
|
||||
lines = [f"{root_label}/"]
|
||||
render(tree, "", lines)
|
||||
sys.stdout.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
118
.gitea/workflows/pr-checks.yml
Normal file
118
.gitea/workflows/pr-checks.yml
Normal file
@@ -0,0 +1,118 @@
|
||||
# Gate every pull request into `main` on the same Rust checks the release runs,
|
||||
# so a formatting slip, a lint regression, or a failing test can't reach the
|
||||
# deployable branch.
|
||||
#
|
||||
# Mirrors RunicGateway/link's pr-checks.yml — same gates in the same order as
|
||||
# release.yml, so a green PR means the release will get past its own gates too.
|
||||
# The one structural difference is the crate guard below.
|
||||
#
|
||||
# ── Crate guard ──────────────────────────────────────────────────────────────
|
||||
# This repo is in the planning phase and has no Cargo project yet (the design of
|
||||
# record is docs/installer/PLAN.md; Phase 1 is what creates the crate). Rather
|
||||
# than leave the repo ungated until then — or land a workflow that red-Xes every
|
||||
# governance/docs PR — the gates are conditional on a root Cargo.toml existing.
|
||||
# Before the crate lands, the job reports green with a notice. The moment
|
||||
# Phase 1 adds Cargo.toml the gates arm themselves; nothing here has to change.
|
||||
#
|
||||
# The crate is expected at the REPO ROOT (not a subdirectory like link/sidecar):
|
||||
# this repo's sole product is the one installer binary, so there is nothing to
|
||||
# namespace it against.
|
||||
#
|
||||
# Enforcement (one-time, in the Gitea UI):
|
||||
# Repository Settings → Branches → Branch Protection (rule for `main`)
|
||||
# • Enable Status Check
|
||||
# • Status check patterns: PR Checks / *
|
||||
# Note: Gitea only lists a context in its dropdown after it has reported once,
|
||||
# so let this workflow run on one PR first. The `PR Checks / *` glob matches
|
||||
# without needing the dropdown.
|
||||
#
|
||||
# Runner: the same self-hosted `ubuntu-latest` runner release.yml uses. Rust is
|
||||
# not assumed to be preinstalled, so the toolchain step bootstraps it the same
|
||||
# way release.yml does (minus the MinGW cross-compile deps — PRs build for the
|
||||
# host only; the Windows cross-build stays a release-time concern).
|
||||
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# A newer push to the same PR cancels the in-flight run.
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
rust-gates:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Detect whether a crate exists yet
|
||||
id: detect
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f Cargo.toml ]; then
|
||||
echo "crate=true" >> "$GITHUB_OUTPUT"
|
||||
echo "==> Cargo.toml found — running the full gate set."
|
||||
else
|
||||
echo "crate=false" >> "$GITHUB_OUTPUT"
|
||||
echo "==> No Cargo.toml at the repo root yet (planning phase)."
|
||||
echo " Skipping fmt/clippy/test. These gates arm themselves as"
|
||||
echo " soon as Phase 1 lands the crate — see docs/installer/PLAN.md."
|
||||
fi
|
||||
|
||||
# One job runs all three gates on purpose: installing the toolchain costs
|
||||
# far more than the checks themselves, so splitting fmt/clippy/test into
|
||||
# parallel jobs would pay that cost three times for no wall-clock win.
|
||||
- name: Install Rust toolchain (rustfmt + clippy)
|
||||
if: ${{ steps.detect.outputs.crate == 'true' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y --no-install-recommends \
|
||||
build-essential curl ca-certificates git
|
||||
|
||||
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 clippy
|
||||
cargo --version && cargo fmt --version && cargo clippy --version
|
||||
|
||||
# Keyed on Cargo.lock: dependency builds are reused until a dep actually
|
||||
# changes. A cache miss only makes the run slower, never wrong.
|
||||
- name: Cache cargo registry and build dir
|
||||
if: ${{ steps.detect.outputs.crate == 'true' }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
# Cheapest gate first — parses only, no compile, so a formatting slip
|
||||
# fails in seconds instead of after a full build.
|
||||
- name: cargo fmt --check
|
||||
if: ${{ steps.detect.outputs.crate == 'true' }}
|
||||
run: cargo fmt --check
|
||||
|
||||
# --all-targets covers tests and examples, not just the binary.
|
||||
# -D warnings makes a lint a failure, so the crate starts clean at this bar
|
||||
# and anything new is a regression introduced by the PR.
|
||||
- name: cargo clippy
|
||||
if: ${{ steps.detect.outputs.crate == 'true' }}
|
||||
run: cargo clippy --locked --all-targets -- -D warnings
|
||||
|
||||
# --locked matches release.yml: it also proves Cargo.lock is in sync with
|
||||
# Cargo.toml, rather than letting the build silently update it.
|
||||
- name: cargo test
|
||||
if: ${{ steps.detect.outputs.crate == 'true' }}
|
||||
run: cargo test --locked
|
||||
289
.gitea/workflows/release.yml
Normal file
289
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,289 @@
|
||||
# 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 ──────────────────────────────────────────────────────────────
|
||||
# The repo is in the planning phase. With no Cargo.toml there is nothing to
|
||||
# build, so the plan step forces RELEASE=false and the job exits green having
|
||||
# done nothing. It starts cutting real releases the moment Phase 1 lands the
|
||||
# crate — 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 bump commit + tag and create the release.
|
||||
# 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 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
||||
echo "Tag v${VERSION} already exists — nothing to release."
|
||||
RELEASE=false
|
||||
fi
|
||||
|
||||
{
|
||||
echo "## ${BIN} v${VERSION}"
|
||||
echo
|
||||
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
|
||||
FIXES="$(echo "$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 "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi
|
||||
echo "$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 "==> release=${RELEASE} version=${VERSION} bump=${BUMP} last_tag=${LAST_TAG:-<none>}"
|
||||
|
||||
# ── RUST ADAPTER: toolchain + cross-compile deps ─────────────────────
|
||||
- name: Install Rust toolchain, Windows target, and MinGW linker
|
||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y --no-install-recommends \
|
||||
build-essential gcc-mingw-w64-x86-64 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}"
|
||||
|
||||
- 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}"
|
||||
|
||||
# ── 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/${WINDOWS_TARGET}/release/${BIN}.exe" "dist/${BIN}-windows-x86_64.exe"
|
||||
( cd dist && sha256sum "${BIN}-linux-x86_64" "${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 "installer-ci"
|
||||
git config user.email "ci@whitlocktech.com"
|
||||
git remote set-url origin \
|
||||
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
||||
|
||||
git add Cargo.toml 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
|
||||
git tag "${TAG}"
|
||||
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}-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
|
||||
111
.gitea/workflows/sync-project-tree.yml
Normal file
111
.gitea/workflows/sync-project-tree.yml
Normal file
@@ -0,0 +1,111 @@
|
||||
name: sync-project-tree
|
||||
|
||||
# Keeps this repo's file-layout snapshot (docs/installer/PROJECT_TREE.md in the
|
||||
# RunicGateway/docs repo) current. On every push to `main` it regenerates the
|
||||
# tree from tracked files and, if it changed, opens (or force-updates) a pull
|
||||
# request against the docs repo. It never writes to the docs repo's `main`
|
||||
# directly. Auth reuses the same REGISTRY_USER / REGISTRY_TOKEN secrets the
|
||||
# other workflows use (the token needs repo read/write on RunicGateway/docs).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: sync-project-tree
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITEA_HOST: gitea.whitlocktech.com
|
||||
DOCS_REPO: RunicGateway/docs
|
||||
SELF_REPO: RunicGateway/installer
|
||||
DOCS_PATH: installer/PROJECT_TREE.md
|
||||
TREE_TITLE: Runic Gateway installer
|
||||
ROOT_LABEL: installer
|
||||
PR_BRANCH: chore/sync-installer-tree
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out this repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Ensure python3 is available
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v python3 >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq python3; }
|
||||
|
||||
- name: Render PROJECT_TREE.md from tracked files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p _sync
|
||||
{
|
||||
printf '# %s — Project Tree\n\n' "${TREE_TITLE}"
|
||||
printf '> **Auto-generated.** This file is maintained by the `sync-project-tree` CI workflow in\n'
|
||||
printf '> the [`%s`](https://%s/%s) repository, which\n' "${SELF_REPO}" "${GITEA_HOST}" "${SELF_REPO}"
|
||||
printf '> opens a pull request here whenever the tracked file layout on `main` changes. Do not edit\n'
|
||||
printf '> by hand — changes will be overwritten by the next sync.\n\n'
|
||||
printf 'A snapshot of the tracked files in the repository (build output, dependencies, and other\n'
|
||||
printf 'git-ignored paths are excluded).\n\n'
|
||||
printf '```text\n'
|
||||
git ls-files | python3 .gitea/scripts/gen_tree.py "${ROOT_LABEL}"
|
||||
printf '```\n'
|
||||
} > _sync/PROJECT_TREE.md
|
||||
echo "----- generated ${DOCS_PATH} -----"
|
||||
cat _sync/PROJECT_TREE.md
|
||||
|
||||
- name: Open or update the docs PR if the tree changed
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Secrets can carry a trailing CR/LF depending on how they were pasted;
|
||||
# strip line breaks before they land in a URL or Authorization header.
|
||||
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
||||
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
||||
API="https://${GITEA_HOST}/api/v1/repos/${DOCS_REPO}"
|
||||
REMOTE="https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${DOCS_REPO}.git"
|
||||
|
||||
git clone --depth 1 "${REMOTE}" docs_repo
|
||||
cd docs_repo
|
||||
git config user.name "runic-docs-bot"
|
||||
git config user.email "ci@whitlocktech.com"
|
||||
|
||||
mkdir -p "$(dirname "${DOCS_PATH}")"
|
||||
cp ../_sync/PROJECT_TREE.md "${DOCS_PATH}"
|
||||
git add "${DOCS_PATH}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "PROJECT_TREE.md already up to date — nothing to sync."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHORT_SHA="$(echo "${GITHUB_SHA:-local}" | cut -c1-7)"
|
||||
git checkout -B "${PR_BRANCH}"
|
||||
git commit -m "docs(tree): sync ${DOCS_PATH} from ${SELF_REPO}@${SHORT_SHA} [skip ci]"
|
||||
git push --force "${REMOTE}" "HEAD:${PR_BRANCH}"
|
||||
|
||||
# Open a PR only if one isn't already open for this branch (a force-push
|
||||
# to an existing open PR's head updates it in place).
|
||||
OPEN="$(curl -sSf -H "Authorization: token ${CI_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=50" \
|
||||
| jq --arg b "${PR_BRANCH}" '[.[] | select(.head.ref == $b)] | length')"
|
||||
if [ "${OPEN}" = "0" ]; then
|
||||
curl -sSf -X POST "${API}/pulls" \
|
||||
-H "Authorization: token ${CI_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n \
|
||||
--arg head "${PR_BRANCH}" \
|
||||
--arg base "main" \
|
||||
--arg title "docs(tree): sync ${DOCS_PATH}" \
|
||||
--arg body "Automated project-tree sync from [\`${SELF_REPO}\`](https://${GITEA_HOST}/${SELF_REPO}), regenerated from tracked files on \`main\`. Merge once the layout looks right; the workflow will keep this branch current until then." \
|
||||
'{head: $head, base: $base, title: $title, body: $body}')" \
|
||||
>/dev/null
|
||||
echo "Opened a new docs PR for ${PR_BRANCH}."
|
||||
else
|
||||
echo "Existing open docs PR for ${PR_BRANCH} was updated via force-push."
|
||||
fi
|
||||
Reference in New Issue
Block a user