# CI on the self-hosted Gitea, ported from .gitlab-ci.yml during the move off # GitLab (2026-09-22). Gitea reads .gitea/workflows and ignores .github/ once # this directory exists; .github/workflows is the GitHub side, below. # # WHERE THE BUILD RUNS. Gitea push-mirrors this repository to GitHub, and the # org variable BUILD_ON picks which forge does the heavy work: # * unset (or anything but `github`): every job here runs, as it always did, # and GitHub's workflow skips all of its jobs. # * `github`: the build and publish jobs here are skipped, GitHub Actions # runs .github/workflows/ci.yml on its hosted runners (native arm64, no # QEMU), and the `github` job below waits for the commit status that run # posts back, passing or failing with it. So this run's result is still # the one that counts, for a PR's checks as for anything that merges on # green CI. The variable is set on both forges, and must agree. # If GitHub is ever unavailable, unsetting BUILD_ON here is the whole # fallback: the jobs below take over again unchanged. weekly-release.yml and # announce.yml run here whatever BUILD_ON says. # # Every job runs in an image pinned by digest (tag in the trailing comment), # and the only action used is coffey-labs/actions/checkout pinned by SHA. The # instance resolves short `uses:` against itself, never GitHub, so nothing # unreviewed can be pulled in. Read the comment for the version; the digest is # what runs. # # Jobs run on the runner's `ci-net` network and clone from Gitea's internal # address, never through the Cloudflare-proxied public name, which caps # request bodies at 100 MB. Images go to the registry's own DNS-only name # (vars.REGISTRY, an org variable). # # The weekly release is its own workflow, weekly-release.yml. # # Not ported: # * cleanup.yml pruned GHCR with dataaxiom/ghcr-cleanup-action; on Gitea # that belongs in the package cleanup rules (owner settings -> Packages), # not in a workflow. name: ci on: push: branches: [main] # Only date tags publish (v2026.9.21, v2026.9.21.2). The repository still # carries the inherited v1.0.x tags, and a tag of any other shape pushed # by hand is not a release. tags: - 'v[0-9][0-9][0-9][0-9].[0-9]+.[0-9]+' - 'v[0-9][0-9][0-9][0-9].[0-9]+.[0-9]+.[0-9]+' pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # A release tag is built and tested again before its image is published. build: if: ${{ vars.BUILD_ON != 'github' }} runs-on: light container: image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim env: NPM_CONFIG_CACHE: ${{ github.workspace }}/.npm steps: - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec - run: npm ci --ignore-scripts - run: npm run typecheck - run: npm run lint - run: npm run i18n:check - run: npm test - run: sh docker/listen-ipv6.test.sh - run: npm run build # A changed upstream file without the AGPL 5(a) notice, as inbuxa-server's # fork-checks job catches for the server. Seconds and no npm install, so it # runs here whatever BUILD_ON says. It diffs against the upstream release # this fork is built on, hence the full fetch. notices: runs-on: light container: image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim steps: - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec with: fetch-depth: 0 - run: command -v git >/dev/null || { apt-get update -qq && apt-get install -y -qq --no-install-recommends git >/dev/null; } - run: git config --global --add safe.directory "$PWD" && node tools/notice-check.mjs # ----------------------------------------------------------- publish ------ # Port of publish.yml, to the owner's own registry now that GHCR went with # the GitHub account: /inbuxa/inbuxa-admin, the same path the # GitLab registry used. # # Tag-driven. A release cut with the job's own token raises no event on # Gitea (as on GitHub), so weekly-release.yml creates its release with # RELEASE_TOKEN; the tag that makes is an ordinary push, and starts this. # # The tag must agree with inbuxa-version.json at the commit it names -- the # property release.yml was built around: the tree a tag points at reports # the version the tag claims. A tag placed beside an unbumped file fails # here rather than publishing an image that reports the wrong version. # # Both architectures build under QEMU on this amd64 host, where publish.yml # had a native arm64 runner. That is slow -- tens of minutes for npm ci and # the Vite build through instruction translation -- and tolerable for a # weekly tag, which is why this is tag-only. If arm64 starts timing out, the # fix is an arm64 runner, not dropping the platform. # # The push logs in with PACKAGE_TOKEN (jcoffey-dev, write:package): Gitea's # per-job token is refused by the container registry. The registry hands out # its push tokens from its own name, so unlike on GitLab nothing here has to # be pointed at a public address. publish: if: ${{ vars.BUILD_ON != 'github' && startsWith(github.ref, 'refs/tags/') }} needs: [build] runs-on: docker container: image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli volumes: - /var/run/docker.sock:/var/run/docker.sock env: DOCKER_BUILDKIT: "1" REGISTRY: ${{ vars.REGISTRY }} IMAGE: ${{ vars.REGISTRY }}/${{ github.repository }} PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} READ_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec - run: | set -eu apk add --no-cache -q jq curl VERSION="$(jq -er .version inbuxa-version.json)" if [ "$GITHUB_REF_NAME" != "v$VERSION" ]; then echo "Tag $GITHUB_REF_NAME names a commit whose inbuxa-version.json says $VERSION." >&2 echo "Refusing to publish an image that would report the wrong version." >&2 exit 1 fi echo "VERSION=$VERSION" >> "$GITHUB_ENV" # :latest belongs to the newest series: once a final release numbered # by series exists (v27.1, v27.1.3), a dated release from release/2026 # leaves it alone. A failed check stops here rather than guess. releases="$(curl -fsS -H "Authorization: token $READ_TOKEN" "$CI_SERVER_INTERNAL/api/v1/repos/$GITHUB_REPOSITORY/releases?draft=false&pre-release=false&limit=50")" series="$(printf '%s' "$releases" | jq -er '[.[].tag_name | select(test("^v[0-9]{2}\\."))] | first // ""')" if [ -n "$series" ]; then echo "$series is out: :latest stays on its series"; echo "LATEST_TAG=" >> "$GITHUB_ENV" else echo "no series release yet: :latest moves"; echo "LATEST_TAG=--tag $IMAGE:latest" >> "$GITHUB_ENV" fi - run: | test -n "$REGISTRY" test -n "$PACKAGE_TOKEN" || { echo "PACKAGE_TOKEN secret is not set on this repository" >&2; exit 1; } echo "$PACKAGE_TOKEN" | docker login -u jcoffey-dev --password-stdin "$REGISTRY" docker run --privileged --rm tonistiigi/binfmt --install arm64 docker buildx create --use --name gitea-builder --driver docker-container || docker buildx use gitea-builder # Attestations are off, as they were in publish.yml: they add manifests # of their own to the index. - run: | docker buildx build \ --platform linux/amd64,linux/arm64 \ --provenance=false --sbom=false \ --tag "$IMAGE:$VERSION" \ $LATEST_TAG \ --push . docker buildx imagetools inspect "$IMAGE:$VERSION" # Gitea keeps a container package on its owner; linking it shows it on # the repository's Packages tab. Idempotent. - run: | curl -fsS -o /dev/null -X POST -H "Authorization: token $PACKAGE_TOKEN" \ "$CI_SERVER_INTERNAL/api/v1/packages/${GITHUB_REPOSITORY%%/*}/container/${GITHUB_REPOSITORY#*/}/-/link/${GITHUB_REPOSITORY#*/}" \ || echo "package already linked (or link refused); not fatal" - if: always() run: docker logout "$REGISTRY" || true # ------------------------------------------------------------ github ------ # With BUILD_ON=github, the work above happens in GitHub Actions, which # posts one commit status back here when it finishes: "github/ci (branch)" # for a branch push, "github/ci (tag)" for a tag. This job waits for that # status on the commit under test -- the PR's head for a pull request -- and # passes or fails with it. Nothing arriving within the timeout means GitHub # never built the commit (a mirror that failed to sync, or GitHub being # down): check the mirror, or unset BUILD_ON to build here. github: if: ${{ vars.BUILD_ON == 'github' }} # Its own runner label with plenty of slots: this job only polls, but holds a slot # for as long as the GitHub build takes, and must not starve the build runners. runs-on: wait timeout-minutes: 150 container: image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim env: TOKEN: ${{ secrets.GITHUB_TOKEN }} SHA: ${{ github.event.pull_request.head.sha || github.sha }} CONTEXT: github/ci (${{ github.ref_type == 'tag' && 'tag' || 'branch' }}) steps: - run: apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates curl jq >/dev/null - shell: bash run: | set -uo pipefail url="$CI_SERVER_INTERNAL/api/v1/repos/$GITHUB_REPOSITORY/commits/$SHA/statuses?limit=50" echo "waiting for '$CONTEXT' on $SHA" while :; do state="$(curl -fsS -H "Authorization: token $TOKEN" "$url" \ | jq -r --arg c "$CONTEXT" '[.[] | select(.context == $c)] | sort_by(.id) | last | .status // empty')" case "$state" in success) echo "GitHub reported success"; exit 0 ;; failure|error) echo "GitHub reported $state -- see the status's link for the run" >&2; exit 1 ;; esac sleep 20 done