# Build the container image and publish it to GitHub Container Registry. # # The unit test 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. # # 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: publish 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' }} env: REGISTRY: ghcr.io 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 (same core count as a # GitHub-hosted runner); 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. GHCR_IMAGE overrides the whole owner/name, which is # what you need when this repository does not live on github.com -- see # the login step. - name: Resolve image name id: img run: | printf 'name=%s\n' \ "$(printf '%s' "${{ vars.GHCR_IMAGE || github.repository }}" | tr '[:upper:]' '[:lower:]')" \ >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@v3 # Skipped on pull requests: a PR from a fork has no write credentials, and # nothing is pushed from a PR anyway. # # On github.com the built-in GITHUB_TOKEN is enough. Running this from a # mirror or a self-hosted Actions runner (Gitea/Forgejo) means that token # authenticates to the wrong registry, so set GHCR_TOKEN to a GitHub PAT # with write:packages, and GHCR_USER/GHCR_IMAGE if the account name there # differs from the one here. - name: Log in to ghcr.io if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ vars.GHCR_USER || github.actor }} password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} - name: Derive tags and labels id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ steps.img.outputs.name }} 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 }} annotations: ${{ steps.meta.outputs.annotations }} # 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 # Without this every run recompiles openvpn3 from scratch. cache-from: type=gha cache-to: type=gha,mode=max provenance: mode=max sbom: true # 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: ${{ env.REGISTRY }}/${{ steps.img.outputs.name }}@${{ 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"