diff --git a/.github/workflows/publish-apt.yml b/.github/workflows/publish-apt.yml new file mode 100644 index 0000000..7726218 --- /dev/null +++ b/.github/workflows/publish-apt.yml @@ -0,0 +1,62 @@ +name: Publish APT repo to S3/CloudFront + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Tag to publish (e.g. v1.9.0). Leave empty to use latest release." + required: false + type: string + backfill_all: + description: "Build/publish repo for ALL releases." + required: false + default: false + type: boolean + +permissions: + id-token: write + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + env: + PKG_NAME: newt + SUITE: stable + COMPONENT: main + REPO_BASE_URL: https://repo.dev.fosrl.io/apt + + AWS_REGION: ${{ vars.AWS_REGION }} + S3_BUCKET: ${{ vars.S3_BUCKET }} + S3_PREFIX: ${{ vars.S3_PREFIX }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} + + INPUT_TAG: ${{ inputs.tag }} + BACKFILL_ALL: ${{ inputs.backfill_all }} + EVENT_TAG: ${{ github.event.release.tag_name }} + GH_REPO: ${{ github.repository }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y dpkg-dev apt-utils gnupg curl jq gh + + - name: Install nfpm + run: curl -fsSL https://github.com/goreleaser/nfpm/releases/latest/download/nfpm_Linux_x86_64.tar.gz | sudo tar -xz -C /usr/local/bin nfpm + + - name: Publish APT repo + env: + GH_TOKEN: ${{ github.token }} + APT_GPG_PRIVATE_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }} + APT_GPG_PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }} + run: ./scripts/publish-apt.sh diff --git a/scripts/nfpm.yaml.tmpl b/scripts/nfpm.yaml.tmpl new file mode 100644 index 0000000..f9a7d60 --- /dev/null +++ b/scripts/nfpm.yaml.tmpl @@ -0,0 +1,11 @@ +name: __PKG_NAME__ +arch: __ARCH__ +platform: linux +version: __VERSION__ +section: net +priority: optional +maintainer: fosrl +description: Newt - userspace tunnel client and TCP/UDP proxy +contents: + - src: build/newt + dst: /usr/bin/newt diff --git a/scripts/publish-apt.sh b/scripts/publish-apt.sh new file mode 100644 index 0000000..eba6315 --- /dev/null +++ b/scripts/publish-apt.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---- required env ---- +: "${GH_REPO:?}" +: "${S3_BUCKET:?}" +: "${AWS_REGION:?}" +: "${CLOUDFRONT_DISTRIBUTION_ID:?}" +: "${PKG_NAME:?}" +: "${SUITE:?}" +: "${COMPONENT:?}" +: "${APT_GPG_PRIVATE_KEY:?}" + +S3_PREFIX="${S3_PREFIX:-}" +if [[ -n "${S3_PREFIX}" && "${S3_PREFIX}" != */ ]]; then + S3_PREFIX="${S3_PREFIX}/" +fi + +WORKDIR="$(pwd)" +mkdir -p repo/apt assets build + +echo "${APT_GPG_PRIVATE_KEY}" | gpg --batch --import >/dev/null 2>&1 || true + +KEYID="$(gpg --list-secret-keys --with-colons | awk -F: '$1=="sec"{print $5; exit}')" +if [[ -z "${KEYID}" ]]; then + echo "ERROR: No GPG secret key available after import." + exit 1 +fi + +# Determine which tags to process +TAGS="" +if [[ "${BACKFILL_ALL:-false}" == "true" ]]; then + echo "Backfill mode: collecting all release tags..." + TAGS="$(gh release list -R "${GH_REPO}" --limit 200 --json tagName --jq '.[].tagName')" +else + if [[ -n "${INPUT_TAG:-}" ]]; then + TAGS="${INPUT_TAG}" + elif [[ -n "${EVENT_TAG:-}" ]]; then + TAGS="${EVENT_TAG}" + else + echo "No tag provided; using latest release tag..." + TAGS="$(gh release view -R "${GH_REPO}" --json tagName --jq '.tagName')" + fi +fi + +echo "Tags to process:" +printf '%s\n' "${TAGS}" + +# Pull existing repo from S3 so we keep older versions +echo "Sync existing repo from S3..." +aws s3 sync "s3://${S3_BUCKET}/${S3_PREFIX}apt/" repo/apt/ >/dev/null 2>&1 || true + +# Build and add packages +while IFS= read -r TAG; do + [[ -z "${TAG}" ]] && continue + echo "=== Processing tag: ${TAG} ===" + + rm -rf assets build + mkdir -p assets build + + gh release download "${TAG}" -R "${GH_REPO}" -p "newt_linux_amd64" -D assets + gh release download "${TAG}" -R "${GH_REPO}" -p "newt_linux_arm64" -D assets + + VERSION="${TAG#v}" + + for arch in amd64 arm64; do + bin="assets/newt_linux_${arch}" + if [[ ! -f "${bin}" ]]; then + echo "ERROR: Missing release asset: ${bin}" + exit 1 + fi + + install -Dm755 "${bin}" "build/newt" + + # Create nfpm config from template file (no heredoc here) + sed \ + -e "s/__PKG_NAME__/${PKG_NAME}/g" \ + -e "s/__ARCH__/${arch}/g" \ + -e "s/__VERSION__/${VERSION}/g" \ + scripts/nfpm.yaml.tmpl > nfpm.yaml + + nfpm package -p deb -f nfpm.yaml -t "build/${PKG_NAME}_${VERSION}_${arch}.deb" + done + + mkdir -p "repo/apt/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/" + cp -v build/*.deb "repo/apt/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/" + +done <<< "${TAGS}" + +# Regenerate metadata +cd repo/apt + +for arch in amd64 arm64; do + mkdir -p "dists/${SUITE}/${COMPONENT}/binary-${arch}" + dpkg-scanpackages -a "${arch}" pool > "dists/${SUITE}/${COMPONENT}/binary-${arch}/Packages" + gzip -fk "dists/${SUITE}/${COMPONENT}/binary-${arch}/Packages" +done + +# Release file with hashes +cat > apt-ftparchive.conf < "dists/${SUITE}/Release" + +# Sign Release +cd "dists/${SUITE}" + +gpg --batch --yes --pinentry-mode loopback \ + ${APT_GPG_PASSPHRASE:+--passphrase "${APT_GPG_PASSPHRASE}"} \ + --local-user "${KEYID}" \ + --clearsign -o InRelease Release + +gpg --batch --yes --pinentry-mode loopback \ + ${APT_GPG_PASSPHRASE:+--passphrase "${APT_GPG_PASSPHRASE}"} \ + --local-user "${KEYID}" \ + -abs -o Release.gpg Release + +# Export public key into apt repo root +cd ../../.. +gpg --batch --yes --armor --export "${KEYID}" > public.key + +# Upload to S3 +echo "Uploading to S3..." +aws s3 sync "${WORKDIR}/repo/apt" "s3://${S3_BUCKET}/${S3_PREFIX}apt/" --delete + +# Invalidate metadata +echo "CloudFront invalidation..." +aws cloudfront create-invalidation \ + --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \ + --paths "/${S3_PREFIX}apt/dists/*" "/${S3_PREFIX}apt/public.key" + +echo "Done. Repo base: ${REPO_BASE_URL}"