name: Build and Release node-app .deb (native cdylib)
on:
push:
tags:
- 'v*'
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version tag (defaults to manifest.version)"
required: false
type: string
permissions:
contents: write
jobs:
prepare-release:
name: Resolve version + ensure GitHub Release exists
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.resolve.outputs.version }}
name: ${{ steps.resolve.outputs.name }}
tag: ${{ steps.resolve.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve version
id: resolve
run: |
NAME=$(jq -r .name manifest.json)
VERSION="${{ github.event.inputs.version }}"
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
VERSION="${{ github.event.release.tag_name }}"
fi
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
fi
fi
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
VERSION=$(jq -r .version manifest.json)
fi
VERSION="${VERSION#v}"
TAG="v${VERSION}"
echo "name=${NAME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Resolved tag=${TAG} (name=${NAME})"
- name: Create tag + release if missing
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.resolve.outputs.tag }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::notice::release $TAG already exists; will upload to it"
else
gh release create "$TAG" \
--title "$TAG" \
--notes "Release $TAG" \
--target "${{ github.sha }}"
echo "::notice::created release $TAG @ ${{ github.sha }}"
fi
build-and-publish:
name: Build node-app-${{ needs.prepare-release.outputs.name }} (${{ matrix.arch }})
needs: prepare-release
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
include:
- arch: amd64
rust_target: x86_64-unknown-linux-gnu
- arch: arm64
rust_target: aarch64-unknown-linux-gnu
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sync Cargo.toml + manifest.json to resolved version
run: |
V="${{ needs.prepare-release.outputs.version }}"
sed -i "s/^version = .*/version = \"${V}\"/" Cargo.toml
jq --arg v "$V" '.version=$v' manifest.json > manifest.json.tmp \
&& mv manifest.json.tmp manifest.json
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}
- name: Install cross-compile linker (arm64)
if: matrix.arch == 'arm64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
mkdir -p .cargo
cat > .cargo/config.toml <<'EOF'
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
EOF
# If your app pulls in any cc-rs-built native deps (e.g. rusqlite's
# bundled SQLite), without these overrides cc would invoke host
# x86_64 gcc and emit x86_64 objects.
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> "$GITHUB_ENV"
- name: Install cargo-deb
run: cargo install cargo-deb --locked
- name: Write LICENSE
run: |
if [ ! -f LICENSE ]; then
cat > LICENSE <<'EOF'
Licensed under either of
* Apache License, Version 2.0
(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license
(LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
EOF
fi
- name: Build .deb
run: cargo deb --target ${{ matrix.rust_target }}
- name: Rename .deb to aggregator-expected format
run: |
SRC=$(find target/${{ matrix.rust_target }}/debian -maxdepth 1 -name '*.deb' | head -1)
if [ -z "$SRC" ]; then
echo "ERROR: no .deb produced by cargo deb" >&2
ls -la target/${{ matrix.rust_target }}/debian/ || true
exit 1
fi
DEST="node-app-${{ needs.prepare-release.outputs.name }}_${{ needs.prepare-release.outputs.version }}_${{ matrix.arch }}.deb"
cp "$SRC" "$DEST"
echo "DEB_NAME=${DEST}" >> "$GITHUB_ENV"
echo "=== Built ==="
ls -lh "$DEST"
dpkg-deb -I "$DEST"
echo "=== Contents ==="
dpkg-deb -c "$DEST"
- name: Sign .deb (GPG detached)
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
echo "$GPG_PRIVATE_KEY" | gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --import
gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --detach-sign --armor "${DEB_NAME}"
- name: Publish manifest sidecar
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
mkdir -p Manifests
SIDECAR="Manifests/node-app-${{ needs.prepare-release.outputs.name }}_${{ needs.prepare-release.outputs.version }}.json"
cp manifest.json "$SIDECAR"
gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --detach-sign --armor "$SIDECAR"
echo "SIDECAR=$SIDECAR" >> "$GITHUB_ENV"
- name: Upload artifacts to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ needs.prepare-release.outputs.tag }}" \
"${DEB_NAME}" "${DEB_NAME}.asc" \
"${SIDECAR}" "${SIDECAR}.asc" \
--clobber
notify-aggregator:
name: Notify apt-repo indexer
needs: [prepare-release, build-and-publish]
runs-on: ubuntu-22.04
steps:
- name: Dispatch update-apt-repo.yml on econ-v1/node-releases
env:
GH_TOKEN: ${{ secrets.APT_REPO_DISPATCH_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::APT_REPO_DISPATCH_TOKEN not set — skipping aggregator dispatch"
exit 0
fi
TAG="${{ needs.prepare-release.outputs.tag }}"
set +e
for attempt in 1 2 3 4 5; do
gh workflow run update-apt-repo.yml \
--repo econ-v1/node-releases \
--ref main \
-F version="${TAG}" \
-F source_repo="${{ github.repository }}"
if [ $? -eq 0 ]; then
echo "::notice::aggregator dispatched on attempt $attempt"
exit 0
fi
echo "::warning::dispatch attempt $attempt failed; retrying in 5s"
sleep 5
done
echo "::warning::aggregator dispatch failed after 5 attempts; release artifacts are still uploaded"