diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4a6086c..05687b3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -54,9 +54,34 @@ jobs: - name: Test run: dotnet test "Resgrid Audio.sln" --configuration Release --no-build --verbosity normal + check-approval: + name: Verify PR was approved + needs: build-and-test + if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master' + runs-on: ubuntu-latest + environment: BuildEnv + + permissions: + pull-requests: read + + steps: + - name: Verify at least one approved review + uses: actions/github-script@v7 + with: + script: | + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + const approved = reviews.some(r => r.state === 'APPROVED'); + if (!approved) { + core.setFailed('PR must have at least one approved review before publishing.'); + } + publish-apps: name: Publish app assets - needs: build-and-test + needs: check-approval if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master' runs-on: windows-latest environment: BuildEnv @@ -122,7 +147,7 @@ jobs: docker-build-and-push: name: Publish Docker image - needs: build-and-test + needs: check-approval if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master' runs-on: ubuntu-latest environment: BuildEnv diff --git a/.gitignore b/.gitignore index b8065d5..7dc0732 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,4 @@ Web/Resgrid.Services/App_Data/Resgrid.Web.Services.XML #Common/AssemblyInfo.cs /node_modules .vs/ +/.idea diff --git a/Dockerfile b/Dockerfile index 8f6d306..e8877b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,55 @@ COPY . . RUN dotnet restore "Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj" -p:TargetFramework=net10.0 RUN dotnet publish "Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj" -c Release -f net10.0 -o /app/publish /p:UseAppHost=false +# Download the LocalXpose CLI binary. +# LocalXpose distributes only a rolling "latest" build from S3 — there are no +# versioned release URLs. SHA256 values below are sourced from the AUR PKGBUILD +# (https://aur.archlinux.org/packages/localxpose-cli, last updated 2025-02-04) +# and must be updated whenever loclx publishes a new binary. +# Supported Docker architectures: linux/amd64, linux/arm64, linux/386, linux/arm +FROM debian:bookworm-slim AS loclx-download +ARG TARGETARCH=amd64 +ARG LOCLX_SHA256_AMD64=03c6d1d35dfd0acb673473314c1384156ed2bfcb96e581b3e0bb398fef45fb88 +ARG LOCLX_SHA256_ARM64=a423e0ce90fcab7044b4f0244fb8483fe12acf30361fff0e37d5abc2dae2da91 +ARG LOCLX_SHA256_386=2534e0056ba5c1e4d55322b2b975a4945104604be15bef9df01c933ad4352804 +ARG LOCLX_SHA256_ARM=83e5484169ea28f05fe221056374bbdaac129850b607bdc243326a06c22575e4 +RUN apt-get update && apt-get install -y --no-install-recommends wget zstd ca-certificates \ + && case "${TARGETARCH}" in \ + amd64) sha="${LOCLX_SHA256_AMD64}" ;; \ + arm64) sha="${LOCLX_SHA256_ARM64}" ;; \ + 386) sha="${LOCLX_SHA256_386}" ;; \ + arm) sha="${LOCLX_SHA256_ARM}" ;; \ + *) echo "Unsupported arch: ${TARGETARCH}" >&2 ; exit 1 ;; \ + esac \ + && wget -q -O /tmp/loclx.pkg.tar.zst \ + "https://loclx-client.s3.amazonaws.com/loclx-linux-${TARGETARCH}.pkg.tar.zst" \ + && echo "${sha} /tmp/loclx.pkg.tar.zst" | sha256sum -c - \ + && mkdir -p /tmp/loclx-extract \ + && tar --zstd -xf /tmp/loclx.pkg.tar.zst -C /tmp/loclx-extract \ + && find /tmp/loclx-extract -type f -name 'loclx' \ + -exec install -m 755 {} /usr/local/bin/loclx \; \ + && [ -x /usr/local/bin/loclx ] \ + && rm -rf /tmp/loclx.pkg.tar.zst /tmp/loclx-extract /var/lib/apt/lists/* + FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final WORKDIR /app COPY --from=build /app/publish . +COPY --from=loclx-download /usr/local/bin/loclx /usr/local/bin/loclx +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +# Relay configuration ENV RELAY_Mode=smtp ENV RELAY_Smtp__Port=2525 +# LocalXpose tunnel configuration (all optional — tunnel is disabled by default). +# Set LOCLX_ENABLED=true to activate the tunnel. +# Provide LOCLX_TOKEN with your LocalXpose access token. +# Optionally set LOCLX_RESERVED_ENDPOINT= to use a reserved endpoint. +# Alternatively, mount a tunnels YAML file at /etc/resgrid/loclx-tunnels.yaml. +ENV LOCLX_ENABLED=false + EXPOSE 2525 -ENTRYPOINT ["dotnet", "Resgrid.Audio.Relay.Console.dll"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..c0bd953 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Entrypoint for the Resgrid Relay container. +# +# Optionally starts a LocalXpose TCP tunnel before launching the relay, +# allowing the SMTP port to be reached from outside a firewall. +# +# Configuration — choose one approach: +# +# APPROACH 1: environment variables +# LOCLX_ENABLED=true enable the tunnel (default: false) +# LOCLX_TOKEN= LocalXpose access token (required) +# LOCLX_RESERVED_ENDPOINT= optional reserved endpoint, e.g. smtp.loclx.io:25 +# RELAY_Smtp__Port=2525 SMTP port the relay listens on (default: 2525) +# +# APPROACH 2: config file (takes precedence over env vars when present) +# Mount a tunnels YAML file into the container: +# -v /host/loclx-tunnels.yaml:/etc/resgrid/loclx-tunnels.yaml +# and set LOCLX_ENABLED=true and LOCLX_TOKEN=. +# +# Example loclx-tunnels.yaml: +# tunnels: +# - name: smtp +# type: tcp +# to: localhost:2525 +# reserved: smtp.loclx.io:25 +# +set -e + +if [ "${LOCLX_ENABLED:-false}" = "true" ]; then + SMTP_PORT="${RELAY_Smtp__Port:-2525}" + + if [ -n "${LOCLX_TOKEN}" ]; then + echo "[localxpose] Authenticating..." + export LX_ACCESS_TOKEN="${LOCLX_TOKEN}" + loclx auth login + else + echo "[localxpose] WARNING: LOCLX_TOKEN is not set; tunnel may fail to authenticate." + fi + + if [ -f "/etc/resgrid/loclx-tunnels.yaml" ]; then + echo "[localxpose] Starting tunnel from /etc/resgrid/loclx-tunnels.yaml..." + loclx tunnel -c /etc/resgrid/loclx-tunnels.yaml & + elif [ -n "${LOCLX_RESERVED_ENDPOINT}" ]; then + echo "[localxpose] Starting reserved TCP tunnel to localhost:${SMTP_PORT} via ${LOCLX_RESERVED_ENDPOINT}..." + loclx tunnel tcp --to "localhost:${SMTP_PORT}" --reserved-endpoint "${LOCLX_RESERVED_ENDPOINT}" & + else + echo "[localxpose] Starting ephemeral TCP tunnel to localhost:${SMTP_PORT}..." + loclx tunnel tcp --to "localhost:${SMTP_PORT}" & + fi + + echo "[localxpose] Tunnel started (PID: $!)." +fi + +exec dotnet /app/Resgrid.Audio.Relay.Console.dll