# ============================================================================= # build-and-publish (multi-distro matrix) # # Builds twiy as a Debian .deb for each target distro in parallel: # - trixie (Debian 13) -> uploaded to NEXUS_REPO_TRIXIE # - raccoon (Ubuntu 26.04 LTS) -> uploaded to NEXUS_REPO_RACCOON # # Each matrix job spins up a Docker container of the target distro on the # Gitea runner host, builds nginx + modules INSIDE the container so apt deps # and ldd resolution match what end users have, then uploads the resulting # .deb to that distro's Nexus apt-hosted repository. # # Required repository secrets: # NEXUS_USER, NEXUS_PASS, NEXUS_URL (shared) # NEXUS_REPO_TRIXIE (Debian 13 target) # NEXUS_REPO_RACCOON (Ubuntu 26.04 target) # ============================================================================= name: build-and-publish on: push: branches: [master] workflow_dispatch: jobs: build: # Runner is just a docker host; build OS is determined by matrix.image. runs-on: ubuntu-22.04 strategy: # If trixie fails, still finish raccoon (and vice versa) — surface both. fail-fast: false matrix: target: [trixie, raccoon] include: - target: trixie image: debian:13 nexus_repo_secret: NEXUS_REPO_TRIXIE - target: raccoon image: ubuntu:26.04 nexus_repo_secret: NEXUS_REPO_RACCOON steps: - name: Checkout source uses: actions/checkout@v4 - name: Build nginx and assemble .deb inside ${{ matrix.image }} id: pkg env: TARGET: ${{ matrix.target }} IMAGE: ${{ matrix.image }} run: | set -euo pipefail mkdir -p dist # The whole compile + .deb assembly happens inside the target distro # container. Output is dropped into ./dist/ (mounted from the runner) # so the publish step on the host can grab it. sudo docker run --rm \ -v "$PWD:/repo" \ -w /repo \ -e TARGET="$TARGET" \ "$IMAGE" \ bash -euxc ' # build script handles its own apt-get install (per-distro list) touch /.dockerenv bash build/${TARGET}.sh new bash build/${TARGET}.sh build bash build/${TARGET}.sh postfix PKG_NAME="twiy" NGINX_VER="$(nginx -v 2>&1 | awk -F/ "{print \$2}")" # Append CI run number AND target so each rebuild is a strictly- # greater Debian revision. Without this, `apt upgrade twiy` would # be a no-op when upstream nginx hasnt moved, so packaging fixes # wouldnt reach users who already have the package installed. # The ~target suffix keeps trixie/raccoon versions distinct in # case any introspection ever compares them. VERSION="${NGINX_VER}-${GITHUB_RUN_NUMBER:-1}~${TARGET}" ARCH="amd64" PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}" DEB_DIR="${PKG_DIR}/DEBIAN" # The *_temp dirs under /usr/local/nginx are nginxs compiled-in # defaults for client_body / proxy / fastcgi / uwsgi / scgi temp # storage (no --http-*-temp-path was passed to ./configure). They # must exist before `nginx -t` runs, so we ship them empty in the # .deb and the postinst chowns them to the nginx user. mkdir -p "${PKG_DIR}/usr/sbin" "${PKG_DIR}/nginx" \ "${PKG_DIR}/etc/systemd/system" "${PKG_DIR}/var/log/nginx" \ "${PKG_DIR}/usr/lib" "${PKG_DIR}/usr/local/lib" \ "${PKG_DIR}/hostdata/default/public_html" \ "${PKG_DIR}/usr/nginx_lua" \ "${PKG_DIR}/usr/local/nginx/client_body_temp" \ "${PKG_DIR}/usr/local/nginx/proxy_temp" \ "${PKG_DIR}/usr/local/nginx/fastcgi_temp" \ "${PKG_DIR}/usr/local/nginx/uwsgi_temp" \ "${PKG_DIR}/usr/local/nginx/scgi_temp" cp /usr/sbin/nginx "${PKG_DIR}/usr/sbin/" cp -R /nginx/* "${PKG_DIR}/nginx/" || true cp /etc/systemd/system/nginx.service "${PKG_DIR}/etc/systemd/system/" cp -R /hostdata/default "${PKG_DIR}/hostdata/" || true cp -R /usr/nginx_lua "${PKG_DIR}/usr/" || true # Bundle every shared library nginx links against. ldd resolves # against THIS containers libraries (not the runner host) so the # .deb gets the correct per-distro libs. for lib in $(ldd /usr/sbin/nginx | grep "=> /" | awk "{print \$3}"); do cp "$lib" "${PKG_DIR}/usr/lib/" || true done # ---- DEBIAN/control -------------------------------------------- mkdir -p "${DEB_DIR}" cat > "${DEB_DIR}/control" < Description: Nginx L7 DDoS Protection (The-World-Is-Yours), built by RAWeb CI for ${TARGET}. EOF # ---- DEBIAN/postinst ------------------------------------------- cat > "${DEB_DIR}/postinst" <<"EOFPOSTINST" #!/bin/bash # Idempotent: safe on first install, upgrade, and reinstall. useradd -r -d /usr/local/nginx -s /bin/false nginx 2>/dev/null || true install -d -o nginx -g nginx -m 0755 \ /usr/local/nginx \ /usr/local/nginx/client_body_temp \ /usr/local/nginx/proxy_temp \ /usr/local/nginx/fastcgi_temp \ /usr/local/nginx/uwsgi_temp \ /usr/local/nginx/scgi_temp \ /var/log/nginx chown -R nginx:nginx /var/log/nginx /nginx /usr/local/nginx 2>/dev/null || true systemctl daemon-reload 2>/dev/null || true systemctl enable nginx.service 2>/dev/null || true systemctl restart nginx.service 2>/dev/null || true exit 0 EOFPOSTINST chmod 755 "${DEB_DIR}/postinst" dpkg-deb --build "${PKG_DIR}" cp "${PKG_DIR}.deb" /repo/dist/ # Hand ownership back to the runner UID so the host job can read. chown $(stat -c "%u:%g" /repo) /repo/dist/$(basename "${PKG_DIR}.deb") # Stash version for the publish step. echo "${PKG_NAME}_${VERSION}_${ARCH}.deb" > /repo/dist/${TARGET}.name echo "${VERSION}" > /repo/dist/${TARGET}.version echo "${PKG_NAME}" > /repo/dist/${TARGET}.pkg ' # Surface the artifact paths for the next step. DEB_FILE="$PWD/dist/$(cat dist/${TARGET}.name)" PKG_NAME="$(cat dist/${TARGET}.pkg)" VERSION="$(cat dist/${TARGET}.version)" { echo "deb_file=${DEB_FILE}" echo "version=${VERSION}" echo "pkg_name=${PKG_NAME}" } >> "$GITHUB_OUTPUT" ls -la "${DEB_FILE}" sha256sum "${DEB_FILE}" # ───────────────────────────────────────────────────────────────────────── # Publish to Nexus (runs on the runner host, not in the build container). # Same security posture as the previous workflow: # * tmpfs scratch dir for credentials # * trap covers EXIT INT TERM HUP # * netrc auth (no -u user:pass on cmdline → no /proc leak) # * NEXUS_HOST derived from NEXUS_URL so forks don't have to edit YAML # The matrix-driven secret indirection picks the right per-distro repo. # ───────────────────────────────────────────────────────────────────────── - name: Publish to Nexus (${{ matrix.target }}) env: NEXUS_USER: ${{ secrets.NEXUS_USER }} NEXUS_PASS: ${{ secrets.NEXUS_PASS }} NEXUS_URL: ${{ secrets.NEXUS_URL }} NEXUS_REPO: ${{ secrets[matrix.nexus_repo_secret] }} DEB_FILE: ${{ steps.pkg.outputs.deb_file }} PKG_NAME: ${{ steps.pkg.outputs.pkg_name }} TARGET: ${{ matrix.target }} run: | set -euo pipefail umask 077 SECDIR="$(mktemp -d -p /dev/shm twiy-XXXXXXXX 2>/dev/null \ || mktemp -d -t twiy-XXXXXXXX)" chmod 700 "$SECDIR" cleanup() { find "$SECDIR" -type f -exec shred -uz {} + 2>/dev/null || true rm -rf "$SECDIR" } trap cleanup EXIT INT TERM HUP NEXUS_HOST="$(printf '%s' "$NEXUS_URL" | awk -F/ '{print $3}')" printf 'machine %s login %s password %s\n' \ "$NEXUS_HOST" "$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc" unset NEXUS_USER NEXUS_PASS # Replace the prior version of this same package in this same repo, # if any. Best-effort: missing prior is not an error. (apt-hosted # repos in Nexus retain every upload otherwise.) OLD_ID="$(curl -fsS --netrc-file "$SECDIR/netrc" \ "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO" \ | PKG_NAME="$PKG_NAME" python3 -c ' import sys, json, os for c in json.load(sys.stdin).get("items", []): if c.get("name") == os.environ["PKG_NAME"]: print(c["id"]); break ' || true)" if [ -n "$OLD_ID" ]; then curl -fsS -X DELETE --netrc-file "$SECDIR/netrc" \ "$NEXUS_URL/service/rest/v1/components/$OLD_ID" -o /dev/null fi HTTP="$(curl -sS --netrc-file "$SECDIR/netrc" \ -o "$SECDIR/upload.body" -w '%{http_code}' \ -X POST -F "apt.asset=@$DEB_FILE" \ "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO")" case "$HTTP" in 201|204) echo "[$TARGET] uploaded $(basename "$DEB_FILE") to $NEXUS_URL/repository/$NEXUS_REPO/" ;; *) echo "[$TARGET] upload failed (HTTP $HTTP)"; head -c 400 "$SECDIR/upload.body"; exit 1 ;; esac