diff --git a/.gitea/workflows/build-publish.yml b/.gitea/workflows/build-publish.yml index 77c344f..b17a8f3 100644 --- a/.gitea/workflows/build-publish.yml +++ b/.gitea/workflows/build-publish.yml @@ -1,3 +1,21 @@ +# ============================================================================= +# 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 runs DIRECTLY INSIDE a container of the target distro +# (Gitea Actions `container:` directive, not nested docker-in-docker). +# act_runner mounts the workspace into the container, so apt/ldd/dpkg-deb +# all see the target distro's libraries — no host contamination. +# +# Required repository secrets (set up by the API provisioning script): +# NEXUS_URL (shared) +# NEXUS_USER_TRIXIE, NEXUS_PASS_TRIXIE, NEXUS_REPO_TRIXIE +# NEXUS_USER_RACCOON, NEXUS_PASS_RACCOON, NEXUS_REPO_RACCOON +# Each NEXUS_USER_* is a least-privilege user scoped to ONE apt-hosted repo. +# ============================================================================= name: build-and-publish on: @@ -9,6 +27,7 @@ jobs: build: runs-on: ubuntu-22.04 strategy: + # If trixie fails, still finish raccoon (and vice versa) — surface both. fail-fast: false matrix: target: [trixie, raccoon] @@ -16,108 +35,124 @@ jobs: - target: trixie image: debian:13 nexus_repo_secret: NEXUS_REPO_TRIXIE + nexus_user_secret: NEXUS_USER_TRIXIE + nexus_pass_secret: NEXUS_PASS_TRIXIE - target: raccoon image: ubuntu:26.04 nexus_repo_secret: NEXUS_REPO_RACCOON + nexus_user_secret: NEXUS_USER_RACCOON + nexus_pass_secret: NEXUS_PASS_RACCOON + + # Run all steps directly inside the target distro's container. Gitea's + # act_runner mounts the workspace and injects node so actions/checkout + # works. No nested docker calls needed. + container: + image: ${{ matrix.image }} steps: + # The default debian/ubuntu images lack git + ca-certificates, which + # actions/checkout needs. Cheaper to install just those two here than + # to bake a custom image. + - name: Bootstrap checkout deps + run: | + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + git ca-certificates + - name: Checkout source uses: actions/checkout@v4 - - name: Build nginx and assemble .deb inside ${{ matrix.image }} + - name: Build nginx and assemble .deb (${{ matrix.target }}) id: pkg env: TARGET: ${{ matrix.target }} - IMAGE: ${{ matrix.image }} run: | set -euo pipefail - mkdir -p dist - sudo docker run --rm \ - -v "$PWD:/repo" \ - -w /repo \ - -e TARGET="$TARGET" \ - "$IMAGE" \ - bash -euxc ' - touch /.dockerenv - bash build/${TARGET}.sh new - bash build/${TARGET}.sh build - bash build/${TARGET}.sh postfix + # /.dockerenv tells build/${TARGET}.sh's postfix step to skip systemctl + 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}")" - VERSION="${NGINX_VER}-${GITHUB_RUN_NUMBER:-1}~${TARGET}" - ARCH="amd64" - PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}" - DEB_DIR="${PKG_DIR}/DEBIAN" - 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" + 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" - 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 - 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 + # The *_temp dirs under /usr/local/nginx are nginx's 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" - # ---- 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" + 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 - dpkg-deb --build "${PKG_DIR}" - cp "${PKG_DIR}.deb" /repo/dist/ + # Bundle every shared library nginx links against. ldd resolves + # against THIS container's 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 - # 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") + # ---- 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 - # 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 - ' + # ---- 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}" + DEB_FILE="${PKG_DIR}.deb" - # 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}" @@ -126,10 +161,15 @@ jobs: ls -la "${DEB_FILE}" sha256sum "${DEB_FILE}" + + # Each matrix target uses its own dedicated Nexus user (ci-trixie / + # ci-raccoon) whose role is scoped to that ONE apt-hosted repo. + # Verified at provisioning time: each user gets 403 trying to touch + # the other repo. Admin credentials are NOT used by CI. - name: Publish to Nexus (${{ matrix.target }}) env: - NEXUS_USER: ${{ secrets.NEXUS_USER }} - NEXUS_PASS: ${{ secrets.NEXUS_PASS }} + NEXUS_USER: ${{ secrets[matrix.nexus_user_secret] }} + NEXUS_PASS: ${{ secrets[matrix.nexus_pass_secret] }} NEXUS_URL: ${{ secrets.NEXUS_URL }} NEXUS_REPO: ${{ secrets[matrix.nexus_repo_secret] }} DEB_FILE: ${{ steps.pkg.outputs.deb_file }} @@ -139,6 +179,11 @@ jobs: set -euo pipefail umask 077 + # Need curl + python3 to talk to Nexus REST. python3 came with the + # build deps; curl was installed by `bash build/X.sh new`. Add + # explicitly anyway since this step is independent. + apt-get install -y -q --no-install-recommends curl python3 ca-certificates >/dev/null + SECDIR="$(mktemp -d -p /dev/shm twiy-XXXXXXXX 2>/dev/null \ || mktemp -d -t twiy-XXXXXXXX)" chmod 700 "$SECDIR" @@ -152,6 +197,8 @@ jobs: printf 'machine %s login %s password %s\n' \ "$NEXUS_HOST" "$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc" unset NEXUS_USER NEXUS_PASS + + # Replace prior version of this same package in this same repo. OLD_ID="$(curl -fsS --netrc-file "$SECDIR/netrc" \ "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO" \ | PKG_NAME="$PKG_NAME" python3 -c '