From b15b6624cbeaba430a48a9e4c09ef963bbe45bd3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 4 Apr 2026 22:21:03 -0700 Subject: [PATCH] Add Forgejo namespace release workflow --- .forgejo/workflows/release.yml | 60 ++++++++++ Scripts/ci/build-release-artifacts.sh | 20 ++++ Scripts/ci/ensure-nix.sh | 157 ++++++++++++++++++++++++++ Scripts/ci/publish-forgejo-release.sh | 65 +++++++++++ 4 files changed, 302 insertions(+) create mode 100644 .forgejo/workflows/release.yml create mode 100755 Scripts/ci/build-release-artifacts.sh create mode 100755 Scripts/ci/ensure-nix.sh create mode 100755 Scripts/ci/publish-forgejo-release.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..3d1e92a --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + name: Release Build + runs-on: namespace-profile-linux-medium + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + with: + token: ${{ github.token }} + fetch-depth: 0 + + - name: Bootstrap Nix + shell: bash + run: | + set -euo pipefail + chmod +x Scripts/ci/ensure-nix.sh + Scripts/ci/ensure-nix.sh + + - name: Build release artifacts + shell: bash + env: + RELEASE_REF: ${{ github.ref_name }} + run: | + set -euo pipefail + ref="${RELEASE_REF:-manual-${GITHUB_SHA::7}}" + export RELEASE_REF="${ref}" + chmod +x Scripts/ci/build-release-artifacts.sh + nix develop .#ci -c Scripts/ci/build-release-artifacts.sh + + - name: Upload release artifacts + uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: burrow-release-${{ github.ref_name }} + path: dist/* + if-no-files-found: error + + - name: Publish Forgejo release + if: startsWith(github.ref, 'refs/tags/') + shell: bash + env: + RELEASE_TAG: ${{ github.ref_name }} + API_URL: ${{ github.api_url }} + REPOSITORY: ${{ github.repository }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + chmod +x Scripts/ci/publish-forgejo-release.sh + nix develop .#ci -c Scripts/ci/publish-forgejo-release.sh diff --git a/Scripts/ci/build-release-artifacts.sh b/Scripts/ci/build-release-artifacts.sh new file mode 100755 index 0000000..20b4c06 --- /dev/null +++ b/Scripts/ci/build-release-artifacts.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "${repo_root}" + +release_ref="${RELEASE_REF:-manual-${GITHUB_SHA:-unknown}}" +target="x86_64-unknown-linux-gnu" +out_dir="${repo_root}/dist" +staging="${out_dir}/burrow-${release_ref}-${target}" + +mkdir -p "${staging}" + +cargo build --locked --release -p burrow --bin burrow +install -m 0755 target/release/burrow "${staging}/burrow" +cp README.md "${staging}/README.md" + +tarball="${out_dir}/burrow-${release_ref}-${target}.tar.gz" +tar -C "${out_dir}" -czf "${tarball}" "$(basename "${staging}")" +shasum -a 256 "${tarball}" > "${tarball}.sha256" diff --git a/Scripts/ci/ensure-nix.sh b/Scripts/ci/ensure-nix.sh new file mode 100755 index 0000000..14be895 --- /dev/null +++ b/Scripts/ci/ensure-nix.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_nix_profile() { + local candidate + for candidate in \ + "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" \ + "${HOME}/.nix-profile/etc/profile.d/nix.sh" + do + if [[ -f "${candidate}" ]]; then + # shellcheck disable=SC1090 + . "${candidate}" + return 0 + fi + done + return 1 +} + +linux_cp_supports_preserve() { + cp --help 2>&1 | grep -q -- '--preserve' +} + +ensure_root_owned_home() { + if [[ "$(id -u)" -ne 0 ]]; then + return 0 + fi + + if [[ ! -d "${HOME}" ]] || [[ ! -O "${HOME}" ]]; then + export HOME="/root" + fi + + mkdir -p "${HOME}" +} + +ensure_linux_nixbld_accounts() { + if [[ "$(id -u)" -ne 0 ]]; then + return 0 + fi + + if command -v getent >/dev/null 2>&1 && getent group nixbld >/dev/null 2>&1; then + return 0 + fi + + if command -v addgroup >/dev/null 2>&1 && ! command -v groupadd >/dev/null 2>&1; then + addgroup -S nixbld >/dev/null 2>&1 || true + for i in $(seq 1 10); do + adduser -S -D -H -h /var/empty -s /sbin/nologin -G nixbld "nixbld${i}" >/dev/null 2>&1 || true + done + return 0 + fi + + if command -v groupadd >/dev/null 2>&1; then + groupadd -r nixbld >/dev/null 2>&1 || true + for i in $(seq 1 10); do + useradd \ + --system \ + --no-create-home \ + --home-dir /var/empty \ + --shell /usr/sbin/nologin \ + --gid nixbld \ + "nixbld${i}" >/dev/null 2>&1 || true + done + return 0 + fi + + echo "linux nix bootstrap requires nixbld group creation support" >&2 + exit 1 +} + +ensure_linux_nix_bootstrap_prereqs() { + if linux_cp_supports_preserve; then + ensure_root_owned_home + ensure_linux_nixbld_accounts + return 0 + fi + + if command -v apk >/dev/null 2>&1; then + apk add --no-cache coreutils xz >/dev/null + elif command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y >/dev/null + apt-get install -y coreutils xz-utils >/dev/null + elif command -v dnf >/dev/null 2>&1; then + dnf install -y coreutils xz >/dev/null + elif command -v yum >/dev/null 2>&1; then + yum install -y coreutils xz >/dev/null + else + echo "linux nix bootstrap requires GNU cp but no supported package manager was found" >&2 + exit 1 + fi + + linux_cp_supports_preserve || { + echo "linux nix bootstrap still lacks GNU cp after installing prerequisites" >&2 + exit 1 + } + + ensure_root_owned_home + ensure_linux_nixbld_accounts +} + +if ! command -v nix >/dev/null 2>&1; then + if ! command -v curl >/dev/null 2>&1; then + echo "curl is required to install nix" >&2 + exit 1 + fi + + case "$(uname -s)" in + Linux) + ensure_linux_nix_bootstrap_prereqs + curl -fsSL https://nixos.org/nix/install | sh -s -- --no-daemon + ;; + Darwin) + installer="$(mktemp -t burrow-nix.XXXXXX)" + trap 'rm -f "${installer}"' EXIT + curl -fsSL -o "${installer}" https://install.determinate.systems/nix + chmod +x "${installer}" + if command -v sudo >/dev/null 2>&1; then + if sudo -n true 2>/dev/null; then + sudo -n sh "${installer}" install --no-confirm + else + sudo sh "${installer}" install --no-confirm + fi + else + sh "${installer}" install --no-confirm + fi + ;; + *) + echo "unsupported platform for nix bootstrap: $(uname -s)" >&2 + exit 1 + ;; + esac +fi + +source_nix_profile || true +export PATH="${HOME}/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:${PATH}" + +config_root="${XDG_CONFIG_HOME:-$HOME/.config}" +config_file="${config_root}/nix/nix.conf" +if [[ -e "${config_file}" && ! -w "${config_file}" ]]; then + config_root="$(mktemp -d -t burrow-nix-config.XXXXXX)" + export XDG_CONFIG_HOME="${config_root}" + config_file="${XDG_CONFIG_HOME}/nix/nix.conf" +fi + +mkdir -p "$(dirname -- "${config_file}")" +cat > "${config_file}" <<'EOF' +experimental-features = nix-command flakes +sandbox = true +fallback = true +substituters = https://cache.nixos.org +trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= +EOF + +command -v nix >/dev/null 2>&1 || { + echo "nix is still unavailable after bootstrap" >&2 + exit 1 +} diff --git a/Scripts/ci/publish-forgejo-release.sh b/Scripts/ci/publish-forgejo-release.sh new file mode 100755 index 0000000..338f71b --- /dev/null +++ b/Scripts/ci/publish-forgejo-release.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${API_URL:?API_URL is required}" +: "${REPOSITORY:?REPOSITORY is required}" +: "${RELEASE_TAG:?RELEASE_TAG is required}" +: "${TOKEN:?TOKEN is required}" + +release_api="${API_URL}/repos/${REPOSITORY}/releases" +tag_api="${release_api}/tags/${RELEASE_TAG}" +release_json="$(mktemp)" +create_json="$(mktemp)" +trap 'rm -f "${release_json}" "${create_json}"' EXIT + +status="$( + curl -sS -o "${release_json}" -w '%{http_code}' \ + -H "Authorization: token ${TOKEN}" \ + "${tag_api}" +)" + +if [[ "${status}" == "404" ]]; then + jq -n \ + --arg tag "${RELEASE_TAG}" \ + --arg name "Burrow ${RELEASE_TAG}" \ + '{ + tag_name: $tag, + target_commitish: $tag, + name: $name, + body: "Automated prerelease built on Forgejo Namespace runners.", + draft: false, + prerelease: true + }' > "${create_json}" + + curl -fsS \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d @"${create_json}" \ + "${release_api}" > "${release_json}" +elif [[ "${status}" != "200" ]]; then + echo "failed to query Forgejo release for ${RELEASE_TAG} (HTTP ${status})" >&2 + cat "${release_json}" >&2 + exit 1 +fi + +release_id="$(jq -r '.id' "${release_json}")" +if [[ -z "${release_id}" || "${release_id}" == "null" ]]; then + echo "Forgejo release payload is missing an id" >&2 + cat "${release_json}" >&2 + exit 1 +fi + +for file in dist/*; do + name="$(basename "${file}")" + asset_id="$(jq -r --arg name "${name}" '.assets[]? | select(.name == $name) | .id' "${release_json}" | head -n1)" + if [[ -n "${asset_id}" ]]; then + curl -fsS -X DELETE \ + -H "Authorization: token ${TOKEN}" \ + "${release_api}/${release_id}/assets/${asset_id}" >/dev/null + fi + + curl -fsS \ + -H "Authorization: token ${TOKEN}" \ + -F "attachment=@${file}" \ + "${release_api}/${release_id}/assets?name=${name}" >/dev/null +done