Skip to content

Automatically release to crates.io #1031

Automatically release to crates.io

Automatically release to crates.io #1031

Workflow file for this run

name: CI
on:
push:
tags:
- "*"
pull_request:
jobs:
# Check formatting and run clippy lints
linting:
strategy:
fail-fast: false
matrix:
rust:
- stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy -- -D warnings
# Build the workspace with the feature permutations not built by default
features:
strategy:
fail-fast: false
matrix:
feature-args:
- "--no-default-features"
- "--no-default-features --features serialization"
- "--no-default-features --features serial"
- "--no-default-features --features tls"
- "--no-default-features --features tls-aws-lc"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build the workspace with the features
run: cargo build --release -p dnp3 -p dnp3-ffi -p dnp3-ffi-java ${{ matrix.feature-args }}
# Run the unit tests on Windows and Linux
test:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
rust:
- stable
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Run Rust unit tests
run: cargo test
# Build API documentation packages
documentation:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@stable
- name: Install doxygen 1.9.5
run: wget -q https://github.com/stepfunc/ci-files/raw/main/doxygen/doxygen-1.9.5.linux.bin.tar.gz -O- | sudo tar --strip-components=1 -C /usr -xz doxygen-1.9.5
- name: Build FFI and JNI
run: cargo build --release -p dnp3-ffi -p dnp3-ffi-java
- name: Build Rustdoc
run: cargo doc -p dnp3 --no-deps
- name: C bindings
run: cargo run --bin dnp3-bindings -- --c --doxygen --no-tests
- name: .NET bindings
run: cargo run --bin dnp3-bindings -- --dotnet --doxygen --no-tests
- name: Java bindings
run: cargo run --bin dnp3-bindings -- --java
- name: Extract documentation
run: |
mkdir -p ~/doc
cp -a target/doc ~/doc/rust
cp -a ffi/bindings/c/generated/doc/c ~/doc/c
cp -a ffi/bindings/c/generated/doc/cpp ~/doc/cpp
cp -a ffi/bindings/dotnet/dnp3/doc ~/doc/dotnet
cp -a ffi/bindings/java/dnp3/target/apidocs ~/doc/java
rm ffi/bindings/c/generated/logo.png ffi/bindings/c/generated/doxygen-awesome.css
- name: Upload documentation
uses: actions/upload-artifact@v4
with:
name: doc-api
path: ~/doc
# Build bindings on Windows x64 [64-bit MSVC (Windows 7+) (x86_64-pc-windows-msvc)] and x86 [32-bit MSVC (Windows 7+) (i686-pc-windows-msvc)]
bindings-windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc # 64-bit MSVC (Windows 7+)
test: true
- target: i686-pc-windows-msvc # 32-bit MSVC (Windows 7+)
test: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install NASM
run: |
choco install nasm
echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Create FFI modules DIR
run: mkdir ffi-modules\${{ matrix.target }}
- name: Build FFI
run: cargo build -p dnp3-ffi --release --target ${{ matrix.target }} --no-default-features --features serial,tls-aws-lc
- name: Build JNI
run: cargo build -p dnp3-ffi-java --release --target ${{ matrix.target }} --no-default-features --features serial,tls-aws-lc
- name: Copy the FFI and JNI libs
shell: pwsh
run: |
Copy-Item -Path ./target/${{ matrix.target }}/release/dnp3_ffi.dll -Destination ffi-modules/${{ matrix.target }}
Copy-Item -Path ./target/${{ matrix.target }}/release/dnp3_ffi.dll.lib -Destination ffi-modules/${{ matrix.target }}
Copy-Item -Path ./target/${{ matrix.target }}/release/dnp3_ffi_java.dll -Destination ffi-modules/${{ matrix.target }}
- name: Upload compiled FFI modules
uses: actions/upload-artifact@v4
with:
name: ffi-modules-${{ matrix.target }}
path: ffi-modules
- name: Test C Bindings
if: ${{ matrix.test }}
run: cargo run --bin dnp3-bindings -- --c -r ${{ matrix.target }} -a ./target/${{ matrix.target }}/release
- name: Test .NET Bindings
if: ${{ matrix.test }}
run: cargo run --bin dnp3-bindings -- --dotnet -r ${{ matrix.target }} -a ./target/${{ matrix.target }}/release
- name: Test Java
if: ${{ matrix.test }}
run: cargo run --bin dnp3-bindings -- --java -r ${{ matrix.target }} -a ./target/${{ matrix.target }}/release
# Build bindings on MacOS [64-bit macOS (10.7+, Lion+) (x86_64-apple-darwin)]
bindings-macos:
strategy:
fail-fast: false
matrix:
include:
- runner: macos-14
target: aarch64-apple-darwin
- runner: macos-13
target: x86_64-apple-darwin
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Create FFI modules dir
run: mkdir -p ffi-modules/${{ matrix.target }}
- name: Build FFI
run: cargo build -p dnp3-ffi --release --no-default-features --features serial,tls-aws-lc
- name: Build JNI
run: cargo build -p dnp3-ffi-java --release --no-default-features --features serial,tls-aws-lc
- name: Copy the FFI and JNI libs
run: |
cp ./target/release/libdnp3_ffi.dylib ./ffi-modules/${{ matrix.target }}
cp ./target/release/libdnp3_ffi_java.dylib ./ffi-modules/${{ matrix.target }}
- name: Upload compiled FFI modules
uses: actions/upload-artifact@v4
with:
name: ffi-modules-${{ matrix.target }}
path: ffi-modules
- name: Test .NET bindings
run: cargo run --bin dnp3-bindings -- --dotnet
- name: Test Java bindings
run: cargo run --bin dnp3-bindings -- --java
# Cross-compilation for Linux to produce portable C and JNI libraries
bindings-linux:
env:
# By default, MUSL will not produce a cdylib with dynamic linkage to MUSL LIB C
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-C target-feature=-crt-static"
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-C target-feature=-crt-static"
CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_RUSTFLAGS: "-C target-feature=-crt-static"
strategy:
fail-fast: false
matrix:
cases:
- target: x86_64-unknown-linux-gnu # 64-bit Linux (kernel 2.6.32+, glibc 2.11+)
features: "tls-aws-lc"
- target: i686-unknown-linux-gnu # 32-bit Linux (kernel 3.2+, glibc 2.17+)
features: "tls-aws-lc"
- target: x86_64-unknown-linux-musl # 64-bit Linux with MUSL
features: "tls"
- target: arm-unknown-linux-gnueabihf # ARMv6 Linux, hardfloat (kernel 3.2, glibc 2.17)
features: "tls"
- target: arm-unknown-linux-musleabihf # ARMv6 Linux with MUSL, hardfloat
features: "tls"
- target: aarch64-unknown-linux-gnu # ARM64 Linux (kernel 4.2, glibc 2.17+)
features: "tls-aws-lc"
- target: aarch64-unknown-linux-musl # ARM64 Linux with MUSL
features: "tls"
- target: armv7-unknown-linux-gnueabihf # ARMv7 Linux, hardfloat (kernel 3.2, glibc 2.17)
features: "tls"
- target: arm-unknown-linux-gnueabi # ARMv6 Linux (kernel 3.2, glibc 2.17)
features: "tls"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.cases.target }}
- name: Install Rust Cross
run: cargo install cross
- name: Create FFI modules dir
run: mkdir -p ffi-modules/${{ matrix.cases.target }}
- name: Build FFI
run: cross build -p dnp3-ffi --release --target ${{ matrix.cases.target }} --no-default-features --features serial,${{ matrix.cases.features }}
- name: Build JNI
run: cross build -p dnp3-ffi-java --release --target ${{ matrix.cases.target }} --no-default-features --features serial,${{ matrix.cases.features }}
- name: Copy the FFI and JNI libs
run: |
cp ./target/${{ matrix.cases.target }}/release/libdnp3_ffi.so ./ffi-modules/${{ matrix.cases.target }}
cp ./target/${{ matrix.cases.target }}/release/libdnp3_ffi_java.so ./ffi-modules/${{ matrix.cases.target }}
- name: Upload compiled FFI modules
uses: actions/upload-artifact@v4
with:
name: ffi-modules-${{ matrix.cases.target }}
path: ffi-modules
guide:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
working-directory: guide
run: npm ci
- name: Build guide
working-directory: guide
run: |
npm run build
mkdir -p ~/doc/guide
mv build/* ~/doc/guide
- name: Upload guide
uses: actions/upload-artifact@v4
with:
name: doc-guide
path: ~/doc
# Package all the generated bindings
packaging:
needs: [documentation, guide, bindings-windows, bindings-macos, bindings-linux]
runs-on: ubuntu-latest
steps:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Cargo CycloneDx
run: cargo install cargo-cyclonedx
- name: Install custom allow-list tool
run: cargo install --git https://github.com/stepfunc/bom-tools.git
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled FFI
uses: actions/download-artifact@v4
with:
path: ffi-modules
pattern: ffi-modules-*
merge-multiple: true
- name: Create SBOMs
run: |
for dir in ffi-modules/*; do
target=`basename "${dir}"`
cargo cyclonedx -f json --target $target
mv ./ffi/dnp3-ffi/dnp3-ffi.cdx.json ffi-modules/$target
mv ./ffi/dnp3-ffi-java/dnp3-ffi-java.cdx.json ffi-modules/$target
done
- name: Create FFI third-party-licenses.txt
run: allow-list gen-licenses-dir -l ffi-modules -b dnp3-ffi.cdx.json -c allowed.json > third-party-licenses.txt
- name: Create FFI third-party-licenses-java.txt
run: allow-list gen-licenses-dir -l ffi-modules -b dnp3-ffi-java.cdx.json -c allowed.json > third-party-licenses-java.txt
- name: Package C/C++ bindings
run: cargo run --bin dnp3-bindings -- --c --package ./ffi-modules --options ./packaging.json -f third-party-licenses.txt
- name: Package .NET bindings
run: cargo run --bin dnp3-bindings -- --dotnet --package ./ffi-modules --options ./packaging.json -f third-party-licenses.txt
- name: Package Java bindings
run: cargo run --bin dnp3-bindings -- --java --package ./ffi-modules --options ./packaging.json -f third-party-licenses-java.txt
- name: Upload C/C++ bindings
uses: actions/upload-artifact@v4
with:
name: c-bindings
path: ffi/bindings/c/generated/*
- name: Upload .NET bindings
uses: actions/upload-artifact@v4
with:
name: dotnet-bindings
path: ffi/bindings/dotnet/nupkg/dnp3*
- name: Upload Java bindings
uses: actions/upload-artifact@v4
with:
name: java-bindings-jar
path: ffi/bindings/java/dnp3/target/*.jar
- name: Upload Java pom.xml
uses: actions/upload-artifact@v4
with:
name: java-bindings-pom
path: ffi/bindings/java/dnp3/pom.xml
# Run the conformance tests
conformance:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Build JNI
run: cargo build --release -p dnp3-ffi-java
- name: Build Java bindings
run: cargo run --release --bin dnp3-bindings -- --java
- name: Install Java bindings
shell: bash
run: (cd ffi/bindings/java/dnp3 && sudo mvn install)
- name: Checkout dnp4s
uses: actions/checkout@v4
with:
repository: stepfunc/dnp4s
ssh-key: ${{ secrets.DNP4S_SSH_KEY }}
ref: scala-2.13
path: dnp4s
- name: Build dnp4s
working-directory: dnp4s
run: sudo mvn --batch-mode install
- name: Run the conformance tests
working-directory: conformance
run: sudo mvn --batch-mode scala:run
- name: Upload conformance test results
if: always()
uses: actions/upload-artifact@v4
with:
name: conformance-results
path: conformance/results
# Quality gate to ensure all tests pass before releases
quality-gate:
needs: [linting, test, features, conformance]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Quality checks passed
run: echo "✅ All quality checks passed successfully"
# Release jobs - each can run independently and is idempotent
release-crates-io:
needs: [quality-gate]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io
run: |
VERSION=${{github.ref_name}}
if curl -f -s "https://crates.io/api/v1/crates/dnp3/$VERSION" > /dev/null 2>&1; then
echo "✅ dnp3 $VERSION already published to crates.io - skipping"
else
echo "Publishing dnp3 $VERSION to crates.io..."
cargo publish -p dnp3 --token ${{ secrets.CRATES_PUBLISH_TOKEN }}
fi
release-docs:
needs: [packaging, quality-gate]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download documentation artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: doc-*
- name: Checkout stepfunc/docs
uses: actions/checkout@v4
with:
repository: stepfunc/docs
ssh-key: ${{ secrets.SFIO_DOCS_SSH_KEY }}
path: docs
- name: Upload docs
working-directory: docs
run: |
git config user.name github-actions
git config user.email github-actions@github.com
# Pull latest changes to avoid conflicts
git pull
# Remove and recreate docs directory
rm -rf ./dnp3/${{github.ref_name}}
mkdir -p ./dnp3/${{github.ref_name}}
cp -a ../artifacts/doc-api/* ./dnp3/${{github.ref_name}}
cp -a ../artifacts/doc-guide/* ./dnp3/${{github.ref_name}}
# Check if there are actual changes to commit
git add -A
if git diff --staged --quiet; then
echo "No changes to commit - docs already up to date"
else
git commit -m "[dnp3] release ${{github.ref_name}}"
git push
fi
release-maven:
needs: [packaging, quality-gate]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download Java artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: java-bindings-*
- name: Import PGP key
uses: crazy-max/ghaction-import-gpg@v3
with:
gpg-private-key: ${{ secrets.SFIO_PGP_PRIVATE_KEY }}
passphrase: ${{ secrets.SFIO_PGP_PRIVATE_KEY_PASSPHRASE }}
- name: Deploy Java to Maven Central
shell: bash
working-directory: artifacts
run: |
# Check if artifact already exists or is being published
if curl -f -s -o /dev/null "https://repo1.maven.org/maven2/io/stepfunc/dnp3/${{github.ref_name}}/dnp3-${{github.ref_name}}.jar"; then
echo "✅ Artifact io.stepfunc:dnp3:${{github.ref_name}} already exists in Maven Central - skipping deployment"
else
echo "Artifact not found in Maven Central - checking if deployment is in progress..."
# Create base64 encoded token for Authorization header
AUTH_TOKEN=$(echo -n "${{ secrets.MAVEN_CENTRAL_USERNAME }}:${{ secrets.MAVEN_CENTRAL_TOKEN }}" | base64 -w 0)
# Try to upload - if it fails due to duplicate, that means it's already being processed
echo "Attempting to upload to Central Portal..."
# Create Maven repository structure (starting from groupId)
mkdir -p io/stepfunc/dnp3/${{github.ref_name}}
# Copy artifacts to bundle directory
cp java-bindings-pom/pom.xml io/stepfunc/dnp3/${{github.ref_name}}/dnp3-${{github.ref_name}}.pom
cp java-bindings-jar/dnp3-${{github.ref_name}}.jar io/stepfunc/dnp3/${{github.ref_name}}/
cp java-bindings-jar/dnp3-${{github.ref_name}}-sources.jar io/stepfunc/dnp3/${{github.ref_name}}/
cp java-bindings-jar/dnp3-${{github.ref_name}}-javadoc.jar io/stepfunc/dnp3/${{github.ref_name}}/
# Sign all files
cd io/stepfunc/dnp3/${{github.ref_name}}
for file in *.jar *.pom; do
gpg --batch --yes --pinentry-mode loopback --passphrase "${{ secrets.SFIO_PGP_PRIVATE_KEY_PASSPHRASE }}" --armor --detach-sign "$file"
done
# Generate checksums
for file in *.jar *.pom; do
md5sum "$file" | cut -d' ' -f1 > "$file.md5"
sha1sum "$file" | cut -d' ' -f1 > "$file.sha1"
done
# Create bundle zip
cd ../../../..
zip -r bundle.zip io/
# Upload bundle to Central Portal
echo "Uploading bundle to Central Portal..."
RESPONSE=$(curl -X POST \
https://central.sonatype.com/api/v1/publisher/upload \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "bundle=@bundle.zip;type=application/octet-stream" \
-F "publishingType=AUTOMATIC" \
-w "\nHTTP_STATUS:%{http_code}" \
-s)
# Extract HTTP status code
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '$d') # Remove last line with status code
echo "Response: $BODY"
echo "HTTP Status: $HTTP_STATUS"
# Handle response
if [[ $HTTP_STATUS -ge 200 && $HTTP_STATUS -lt 300 ]]; then
# The API returns the deployment ID as plain text, not JSON
DEPLOYMENT_ID=$(echo "$BODY" | tr -d '[:space:]')
echo "✅ Upload successful! Deployment ID: $DEPLOYMENT_ID"
echo "The deployment will be automatically published to Maven Central after validation."
elif [[ $HTTP_STATUS -eq 400 ]] && [[ "$BODY" =~ "duplicate" || "$BODY" =~ "already exists" || "$BODY" =~ "PUBLISHED" ]]; then
echo "✅ Version ${{github.ref_name}} is already uploaded or being processed - skipping deployment"
else
echo "❌ Upload failed with status $HTTP_STATUS"
echo "Error details: $BODY"
exit 1
fi
fi
release-nuget:
needs: [packaging, quality-gate]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download .NET artifacts
uses: actions/download-artifact@v4
with:
name: dotnet-bindings
path: artifacts/dotnet-bindings
- name: Publish NuGet package
shell: bash
run: |
# Use --skip-duplicate to make this idempotent
dotnet nuget push $(find artifacts/dotnet-bindings/dnp3*.nupkg) \
-s https://api.nuget.org/v3/index.json \
-k ${{ secrets.SFIO_NUGET_KEY }} \
--skip-duplicate
create-github-release:
needs: [release-docs, release-maven, release-nuget, release-crates-io]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download all release artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Package C Bindings and Conformance Results
run: |
mkdir release
cd artifacts/c-bindings
zip -r ../../release/dnp3-${{github.ref_name}}.zip .
cd ../conformance-results
zip -r ../../release/conformance-results.zip .
- name: Create GitHub release
uses: softprops/action-gh-release@v1
with:
draft: true
files: |
release/*.zip
artifacts/dotnet-bindings/dnp3*
artifacts/java-bindings-jar/*.jar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}