Files
jcoffey-dev 25083c8388
ci / announce (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m15s
ci / test (pull_request) Skipped
ci: retry release file uploads over HTTP/1.1
inbuxa-server's first GitHub-built release lost 50 MB uploads to
Gitea's release API through Cloudflare (curl 92, HTTP/2 PROTOCOL_ERROR)
on both runs of its binaries job. This job uploads the same way. Uploads
now go over HTTP/1.1, and the asset listing and uploads retry 5 times.
2026-09-30 16:49:45 -07:00

277 lines
13 KiB
YAML

# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# CI on GitHub Actions, for the GitHub copy of this repository.
#
# The repository lives on the self-hosted Gitea; GitHub holds a push mirror
# that Gitea updates on every commit. Nothing is merged here -- pull requests
# happen on Gitea, and their branches reach GitHub as ordinary pushes, which is
# why this workflow runs on `push` and not on `pull_request`.
#
# Which forge does the building is one switch, the variable BUILD_ON, set on
# both forges at the organization level:
#
# BUILD_ON=github every job here runs; .gitea/workflows/ci.yml skips its
# test job and waits for the status this workflow reports
# back instead.
# unset / other every job here skips; Gitea runs the tests. Releases need
# GitHub: they are built on native amd64 and arm64 runners.
#
# The result goes back to Gitea as one commit status, "github/ci (branch)" or
# "github/ci (tag)", which is what Gitea's `github` job waits on.
#
# v* tags build a release: the tag must be on main and name the version in
# Cargo.toml; each architecture is built on its own native runner by
# scripts/build-release.sh, and the archives plus SHA256SUMS are attached to
# the Gitea Release -- Gitea is where the install guide points. The Release is
# created as a draft, filled, then published, so it is never visible with
# assets missing; if an upload fails, a Release this run created is deleted.
# Gitea announces the release once this run has reported success.
#
# The binaries link glibc dynamically, so they are built on Ubuntu 22.04: the
# oldest glibc GitHub offers, and so the widest range of servers they run on.
#
# Configuration, all at the organization level on GitHub: variables BUILD_ON,
# GITEA_URL; secret GITEA_TOKEN (write:repository on Gitea).
#
# Every `uses:` is pinned to a full commit SHA, with the release it was in the
# trailing comment. Nothing schedules here: schedules belong to Gitea.
name: ci
on:
push:
branches: ['**']
tags: ['**']
# For a run GitHub queued and then orphaned. Run it from a tag to redo that
# tag's release; attaching skips any file already on the Release.
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref_type != 'tag' }}
permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL }}
CONTEXT: github/ci (${{ github.ref_type }})
CARGO_TERM_COLOR: never
jobs:
# Tells Gitea a result is on its way, so a status that is still missing
# reads as "running" rather than "never started".
pending:
if: vars.BUILD_ON == 'github'
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-d "$(jq -n --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}')"
# The unit and mock tests. The #[ignore]d ones need live servers or
# containers and run by hand (README, "Testing").
test:
if: vars.BUILD_ON == 'github'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-test-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: cargo-test-${{ runner.os }}-${{ runner.arch }}-
# rustup is on the runner; rust-toolchain.toml picks the toolchain.
- run: rustup show active-toolchain || rustup toolchain install
- run: cargo test --locked
# Guards shared by both architectures, checked once.
version:
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
runs-on: ubuntu-latest
env:
TAG: ${{ github.ref_name }}
steps:
# Full history: the ancestry check cannot be answered from a shallow
# clone.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# A release can never describe code that was not reviewed onto main,
# and its tag must be the version the binary reports.
- run: |
git fetch --quiet origin main
git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \
|| { echo "::error::$TAG is not on main"; exit 1; }
want="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
[ "$TAG" = "$want" ] || { echo "::error::$TAG does not match Cargo.toml ($want)"; exit 1; }
build:
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
needs: [test, version]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: true
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
- arch: arm64
runner: ubuntu-22.04-arm
env:
TAG: ${{ github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- run: rustup show active-toolchain || rustup toolchain install
- run: |
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist
sha256sum dist/*.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: archive-${{ matrix.arch }}
path: dist/*.tar.gz
retention-days: 7
overwrite: true
if-no-files-found: error
release:
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
needs: [build]
runs-on: ubuntu-latest
env:
TAG: ${{ github.ref_name }}
steps:
# The release notes are this version's section of CHANGELOG.md, so the
# release, and the announcement made from it, say what changed. Checked
# out first: a checkout cleans the workspace, and would take dist/ with
# it if it ran after the download.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: CHANGELOG.md
sparse-checkout-cone-mode: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
pattern: archive-*
merge-multiple: true
- run: |
(cd dist && sha256sum ./*.tar.gz | sed 's| \./| |' > SHA256SUMS)
cat dist/SHA256SUMS
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
API="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY"
auth=(-H "Authorization: token $GITEA_TOKEN")
# Everything the release carries has to be here before a release is made.
for f in inbuxa-migrate-linux-amd64.tar.gz inbuxa-migrate-linux-arm64.tar.gz SHA256SUMS; do
[ -s "dist/$f" ] || { echo "::error::dist/$f is missing; not creating a release"; exit 1; }
done
files="Binaries for linux/amd64 and linux/arm64. Verify with SHA256SUMS."
notes="$(awk -v v="${TAG#v}" 'index($0, "## [" v "]") == 1 {f = 1; next}
f && (/^## / || /^---$/) {exit}
f' CHANGELOG.md | sed -e '/./,$!d')"
if [ -n "$notes" ]; then notes="$notes"$'\n\n'"$files"; else notes="$files"; fi
# Reuse the Release if the tag already has one (a re-run), else make a
# draft of our own.
created=0
id="$(curl -fsS "${auth[@]}" "$API/releases/tags/$TAG" 2>/dev/null | jq -r '.id // empty' || true)"
if [ -z "$id" ]; then
id="$(curl -fsS "${auth[@]}" -H 'Content-Type: application/json' \
-d "$(jq -n --arg t "$TAG" --arg b "$notes" '{tag_name:$t, name:$t, draft:true, body:$b}')" \
"$API/releases" | jq -r '.id // empty')"
[ -n "$id" ] || { echo "::error::could not create the release"; exit 1; }
created=1
fi
# The uploads cross Cloudflare, which dropped 50 MB HTTP/2 uploads
# part-way for inbuxa-server v2026.9.30.1 (curl 92). HTTP/1.1, and retry.
retry=(--retry 5 --retry-all-errors --retry-delay 15)
have="$(curl -fsS "${retry[@]}" "${auth[@]}" "$API/releases/$id/assets" | jq -r '.[].name')"
for f in dist/*; do
n="$(basename "$f")"
if grep -qxF "$n" <<<"$have"; then echo "already attached: $n"; continue; fi
echo "uploading $n"
curl -fsS --http1.1 "${retry[@]}" -o /dev/null "${auth[@]}" --form "attachment=@$f" "$API/releases/$id/assets?name=$n" || {
[ "$created" = 1 ] && curl -sS -o /dev/null "${auth[@]}" -X DELETE "$API/releases/$id"
exit 1
}
done
if [ "$created" = 1 ]; then
curl -fsS -o /dev/null "${auth[@]}" -H 'Content-Type: application/json' -X PATCH \
-d '{"draft":false}' "$API/releases/$id"
fi
# ---------------------------------------------------- github release ------
# Copies this tag's Gitea release -- notes and files -- to a GitHub release,
# so the replica's Releases page, and anyone watching it, keeps up. Gitea's
# release is the real one; this is left out of the report to Gitea, so a
# failure here cannot fail a release. PR and issue numbers in the notes are
# rewritten to Gitea links: on GitHub a bare #16 is some other PR.
github-release:
if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }}
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL }}
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
steps:
- run: |
set -euo pipefail
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "GitHub already has a release for $TAG"; exit 0
fi
# The Gitea release exists by now if this run made it; allow a few
# minutes either way.
code=0
for _ in $(seq 1 15); do
code="$(curl -sS -o rel.json -w '%{http_code}' "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$TAG")"
[ "$code" = 200 ] && break
sleep 20
done
if [ "$code" != 200 ]; then echo "No Gitea release for $TAG; nothing to copy"; exit 0; fi
if [ "$(jq -r .draft rel.json)" = true ]; then echo "The Gitea release is a draft; not copying"; exit 0; fi
export BASE="$(jq -r '.html_url | sub("/releases/tag/.*$"; "")' rel.json)"
jq -r '.body // ""' rel.json | perl -pe 's{(?<![\w/&\[])#(\d+)\b}{[#$1]($ENV{BASE}/pulls/$1)}g' > notes.md
printf '\n\n_Mirrored from [the Gitea release](%s); report issues on [Gitea](%s/issues)._\n' \
"$(jq -r .html_url rel.json)" "$BASE" >> notes.md
files=()
mkdir -p files
while IFS=$'\t' read -r name url; do
curl -fsSL -o "files/$name" "$url"; files+=("files/$name")
done < <(jq -r '.assets[]? | [.name, .browser_download_url] | @tsv' rel.json)
title="$(jq -r '.name // ""' rel.json)"; [ -n "$title" ] || title="$TAG"
if [ "$(jq -r .prerelease rel.json)" = true ]; then kind=--prerelease; else kind=--latest; fi
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$title" \
--notes-file notes.md "$kind" "${files[@]}"
echo "created the GitHub release for $TAG with ${#files[@]} file(s)"
report:
if: always() && vars.BUILD_ON == 'github'
needs: [pending, test, version, build, release]
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
STATE: ${{ contains(needs.*.result, 'failure') && 'failure' || (contains(needs.*.result, 'cancelled') && 'cancelled' || 'success') }}
run: |
# A cancelled run was superseded by a newer run for the same commit (the
# mirror can push one commit twice); that run reports. Posting "failure"
# here would fail the Gitea check while the real build is still going.
if [ "$STATE" = cancelled ]; then echo "cancelled: leaving the result to the newer run"; exit 0; fi
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-d "$(jq -n --arg s "$STATE" --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}')"