diff --git a/.gitea/workflows/build-image.yml b/.gitea/workflows/build-image.yml new file mode 100644 index 0000000..87a9e87 --- /dev/null +++ b/.gitea/workflows/build-image.yml @@ -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- +# push tag v1.2.3 -> 1.2.3, 1.2, latest, sha- +# 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"