CI: build and publish the container image to the Gitea registry
build image / image (push) Canceled after 0s

Gitea reads .gitea/workflows in preference to .github/workflows, so this
supersedes the ghcr.io workflow here without deleting it: that one stays
correct for github.com, and the two cannot race.

Registry cache rather than type=gha -- a self-hosted act_runner supplies
ACTIONS_CACHE_URL/ACTIONS_RUNTIME_TOKEN inconsistently, and without a
cache every run recompiles the openvpn3 core from scratch. Attestations
are off because they turn the push into a multi-manifest index that
Gitea's package view renders as junk entries.
This commit is contained in:
2026-07-29 09:37:33 +00:00
parent 40b47a8f66
commit 7171de7a12
+163
View File
@@ -0,0 +1,163 @@
# Build the container image and publish it to the Gitea container registry.
#
# Why this lives in .gitea/workflows and not .github/workflows: Gitea looks for
# .gitea/workflows first and only falls back to .github/workflows when that
# directory does not exist. So the presence of this file makes Gitea ignore
# .github/workflows/publish-image.yml entirely. That file is still correct for
# github.com/ghcr.io and is deliberately left in place -- the two cannot race,
# and neither is dead code. Deleting this directory silently re-activates the
# ghcr.io workflow, which will fail here for want of a GitHub PAT.
#
# The unit suite runs *inside* the image build (the Dockerfile's builder stage
# ends with ./build/tests/ovg_tests), so a failing test fails the publish. There
# is deliberately no separate test job duplicating that -- see the note in
# .github/workflows/publish-image.yml, which makes the same choice.
#
# Tags produced:
# push to master -> master, sha-<short>
# push tag v1.2.3 -> 1.2.3, 1.2, latest, sha-<short>
# pull request -> built and smoke-tested, never pushed
#
# `latest` follows the newest release tag, not the branch head. Until the first
# v* tag exists, the tag to pull is `master`.
name: build image
on:
push:
branches: [master]
tags: ["v*"]
pull_request:
branches: [master]
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Superseding a PR build is free; killing a release build half way through is
# not.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
image:
runs-on: ubuntu-latest
# A cold build compiles the openvpn3 core and lwIP from source. Configure +
# compile alone measured 1m39s on 4 cores; on top of that the image build
# also does an apt install, two git clones, and the test suite. The ceiling
# is set well above any of that because the failure mode it guards against
# -- a cache miss on a runner that is also being slow -- is the one where a
# tight limit turns a slow build into a red one.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
# Registry paths must be lowercase and github.repository is not guaranteed
# to be. Both halves are overridable: REGISTRY for a different Gitea host,
# IMAGE for an owner/name that differs from this repository's.
- name: Resolve image reference
id: img
env:
REGISTRY: ${{ vars.REGISTRY || 'git.sfclub.cc' }}
IMAGE: ${{ vars.IMAGE || github.repository }}
run: |
set -eu
printf 'ref=%s\n' \
"$(printf '%s/%s' "$REGISTRY" "$IMAGE" | tr '[:upper:]' '[:lower:]')" \
>> "$GITHUB_OUTPUT"
printf 'registry=%s\n' "$REGISTRY" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
# Skipped on pull requests: nothing is pushed from a PR, and a PR from a
# fork has no access to secrets anyway.
#
# REGISTRY_USER/REGISTRY_TOKEN match the naming in cloud/workspace-image.
# The fallback is Gitea's own per-job token, which can push to this
# instance's registry under the actor's namespace -- enough for the common
# case, so the workflow is not dead on arrival before secrets are set.
- name: Log in to the Gitea registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ steps.img.outputs.registry }}
username: ${{ secrets.REGISTRY_USER || github.actor }}
password: ${{ secrets.REGISTRY_TOKEN || secrets.GITEA_TOKEN }}
- name: Derive tags and labels
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.img.outputs.ref }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
# The default flavor (latest=auto) adds `latest` on a semver tag push
# and nowhere else, which is the intent stated at the top of the file.
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# amd64 only, matching the project's stated target. arm64 is not known
# to be broken -- it is untested, and cross-building the openvpn3 core
# under QEMU costs the better part of an hour per run. Enabling it
# means adding docker/setup-qemu-action above and verifying lwIP's
# unaligned-access assumptions, not just editing this line.
platforms: linux/amd64
build-args: |
OVG_WITH_TUNNEL=ON
OVG_RUN_TESTS=1
# Registry-backed cache rather than type=gha: the GitHub Actions cache
# backend needs ACTIONS_CACHE_URL/ACTIONS_RUNTIME_TOKEN, which a
# self-hosted act_runner supplies inconsistently. A registry cache
# needs only the login two steps up. Without one of the two, every run
# recompiles openvpn3 from scratch.
cache-from: type=registry,ref=${{ steps.img.outputs.ref }}:buildcache
cache-to: ${{ github.event_name != 'pull_request' && format('type=registry,ref={0}:buildcache,mode=max', steps.img.outputs.ref) || '' }}
# Off on purpose. Attestations turn the push into a multi-manifest
# index with unknown/unknown platform entries, which Gitea's registry
# renders as junk "images" in the package view and which some pull
# paths mishandle. ghcr.io copes; this does not need to.
provenance: false
# Verifies the artifact that was actually published, by digest rather than
# by tag. Cheap, and it covers the two things a green build still would
# not: that the runtime stage carries the shared libraries the binary
# needs, and that the config baked into the image parses.
#
# --check needs the credentials file, which is deliberately not in the
# image; a throwaway one is enough to get the config validated.
- name: Smoke test the published image
if: github.event_name != 'pull_request'
env:
IMAGE: ${{ steps.img.outputs.ref }}@${{ steps.build.outputs.digest }}
run: |
set -eux
docker run --rm "$IMAGE" --version
printf 'ci:changeme\n' > "$RUNNER_TEMP/socks5.auth"
docker run --rm \
-v "$RUNNER_TEMP/socks5.auth:/etc/openvpngate/socks5.auth:ro" \
"$IMAGE" -c /etc/openvpngate/openvpngate.conf --check
- name: Summary
if: github.event_name != 'pull_request'
run: |
{
echo "### Published"
echo
echo '```'
echo "${{ steps.meta.outputs.tags }}"
echo '```'
echo
echo "digest: \`${{ steps.build.outputs.digest }}\`"
} >> "$GITHUB_STEP_SUMMARY"