Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 102 additions & 159 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,210 +3,153 @@ name: Release Artifacts
on:
push:
tags:
- '*'
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag name to attach artifacts to (e.g., v1.0.0). The tag must exist in the repository.'
description: 'Tag name to attach artifacts to (e.g., v0.1.0). The tag must exist in the repository.'
required: true
type: string

jobs:
build-and-upload:
name: Build & Upload Soroban Artifacts
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Determine tag name and workflow branch
- name: Determine release tag
id: tag
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="$INPUT_TAG"
# For workflow_dispatch, use the branch where workflow is running
WORKFLOW_REF="${{ github.ref }}"
else
TAG="${GITHUB_REF#refs/tags/}"
# For tag pushes, use default branch (main) to get script
# The script must exist in the default branch for workflow_dispatch to work anyway
WORKFLOW_REF="main"
TAG="${GITHUB_REF_NAME}"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "workflow_ref=$WORKFLOW_REF" >> $GITHUB_OUTPUT
echo "Determined tag: $TAG"
echo "Workflow ref for script: $WORKFLOW_REF"

- name: Checkout workflow branch (to get script)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Using release tag: $TAG"

- name: Checkout code at tag
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
ref: ${{ steps.tag.outputs.workflow_ref }}
submodules: recursive
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }}

- name: Save extraction script
- name: Stage extract-docs script from workflow ref
env:
WORKFLOW_SHA: ${{ github.sha }}
run: |
# Save the script to a temp location that won't be affected by tag checkout
mkdir -p /tmp/workflow-scripts
if [ -f "scripts/extract-artifacts.js" ]; then
cp scripts/extract-artifacts.js /tmp/workflow-scripts/extract-artifacts.js
echo "✓ Saved script from workflow branch"
else
echo "Warning: scripts/extract-artifacts.js not found in workflow branch"
fi
git cat-file -p "${WORKFLOW_SHA}:soroban/scripts/extract-docs.js" \
> /tmp/extract-docs.js
chmod +x /tmp/extract-docs.js
echo "Staged extract-docs.js from ${WORKFLOW_SHA}"

- name: Checkout code at tag
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
submodules: recursive
fetch-depth: 0
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }}
targets: wasm32v1-none,wasm32-unknown-unknown

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Install Stellar CLI
uses: stellar/stellar-cli@v26.0.0

- name: Build contracts
run: forge build
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
soroban/target
key: ${{ runner.os }}-cargo-soroban-${{ hashFiles('soroban/**/Cargo.toml', 'soroban/**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-soroban-

- name: Extract contract artifacts
id: extract
- name: Discover Soroban contracts
working-directory: soroban
run: |
# Use the script from the workflow branch (saved earlier)
if [ -f "/tmp/workflow-scripts/extract-artifacts.js" ]; then
echo "Using script from workflow branch"
node /tmp/workflow-scripts/extract-artifacts.js
else
echo "Error: Extraction script not found. Make sure scripts/extract-artifacts.js exists in your workflow branch."
cargo metadata --format-version 1 --no-deps > /tmp/soroban-metadata.json
jq -r '.packages[] as $pkg | $pkg.targets[] | select(.kind | index("cdylib")) | [$pkg.name, .name] | @tsv' \
/tmp/soroban-metadata.json > /tmp/soroban-contracts.tsv

if [ ! -s /tmp/soroban-contracts.tsv ]; then
echo "Error: no Soroban cdylib contracts found in the workspace"
exit 1
fi

- name: Create release if it doesn't exist
id: create-release
echo "Contracts selected for release:"
cat /tmp/soroban-contracts.tsv

- name: Build Soroban contracts
working-directory: soroban
run: |
while IFS="$(printf '\t')" read -r package target; do
echo "Building $package ($target)"
stellar contract build --package "$package" --optimize
done < /tmp/soroban-contracts.tsv

- name: Package release assets
working-directory: soroban
run: |
asset_dir="$GITHUB_WORKSPACE/release-assets"
mkdir -p "$asset_dir"

while IFS="$(printf '\t')" read -r package target; do
release_wasm=""
for target_dir in \
target/wasm32v1-none/release \
target/wasm32-unknown-unknown/release; do
if [ -f "$target_dir/${target}.optimized.wasm" ]; then
release_wasm="$target_dir/${target}.optimized.wasm"
break
elif [ -f "$target_dir/${target}.wasm" ]; then
release_wasm="$target_dir/${target}.wasm"
break
fi
done

if [ -z "$release_wasm" ]; then
echo "Error: no WASM found for ${target} under target/wasm32v1-none or target/wasm32-unknown-unknown"
exit 1
fi

echo "Using $release_wasm"

cp "$release_wasm" "$asset_dir/${package}.wasm"
stellar contract info interface \
--wasm "$asset_dir/${package}.wasm" \
--output json-formatted > "$asset_dir/${package}.contractspec.json"
node /tmp/extract-docs.js \
"$asset_dir/${package}.contractspec.json" \
"$asset_dir/${package}.docs.json"
done < /tmp/soroban-contracts.tsv

echo "Release assets:"
ls -lh "$asset_dir"

- name: Create release if it does not exist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
TAG="${{ steps.tag.outputs.tag }}"

# Check if release already exists
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release $TAG already exists - will add artifacts to existing release"
echo "exists=true" >> $GITHUB_OUTPUT
echo "release_action=update" >> $GITHUB_OUTPUT
echo "Release $TAG already exists; artifacts will be uploaded to it."
else
echo "Creating new release $TAG"
# Verify the tag exists in the repository
if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
echo "Error: Tag $TAG does not exist in the repository"
echo "Available tags:"
git tag | head -10
exit 1
fi

gh release create "$TAG" \
--repo "${{ github.repository }}" \
--verify-tag \
--title "Release $TAG" \
--notes "Contract artifacts for $TAG" \
--draft=false \
--prerelease=false

if [ $? -eq 0 ]; then
echo "Successfully created release $TAG"
echo "exists=false" >> $GITHUB_OUTPUT
echo "release_action=create" >> $GITHUB_OUTPUT
else
echo "Warning: Failed to create release (may have been created concurrently)"
echo "exists=true" >> $GITHUB_OUTPUT
echo "release_action=update" >> $GITHUB_OUTPUT
fi
--notes "Soroban contract artifacts for $TAG"
fi

- name: Upload artifacts to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
RELEASE_ACTION="${{ steps.create-release.outputs.release_action }}"

# Verify release exists
if ! gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Error: Release $TAG does not exist and could not be created"
exit 1
fi

echo "Release action: $RELEASE_ACTION"
echo "Tag: $TAG"
echo ""

# Upload each contract's artifacts
if [ -d "artifacts" ] && [ "$(ls -A artifacts)" ]; then
# Create a list of files to upload
find artifacts -type f > /tmp/artifacts_list.txt

# Create temporary directory for renamed files
mkdir -p /tmp/release-assets

upload_count=0
failed_count=0

# Process each file
while IFS= read -r artifact_file; do
filename=$(basename "$artifact_file")
contract_name=$(basename $(dirname "$artifact_file"))

# Create a descriptive name for the asset
# Copy file to temp location with desired name for upload
asset_name="${contract_name}/${filename}"
temp_asset_path="/tmp/release-assets/${asset_name}"

# Create subdirectory structure in temp location
mkdir -p "$(dirname "$temp_asset_path")"

# Copy file to temp location with desired name
cp "$artifact_file" "$temp_asset_path"

echo "Uploading $artifact_file as $asset_name"

# Use --clobber to overwrite existing assets if they exist
upload_output=$(gh release upload "$TAG" "$temp_asset_path" \
--repo "${{ github.repository }}" \
--clobber 2>&1)
upload_exit_code=$?

if [ $upload_exit_code -eq 0 ]; then
echo "✓ Successfully uploaded $asset_name"
upload_count=$((upload_count + 1))
else
echo "✗ Failed to upload $asset_name"
echo " Error: $upload_output"
failed_count=$((failed_count + 1))
fi
done < /tmp/artifacts_list.txt

# Cleanup
rm -f /tmp/artifacts_list.txt
rm -rf /tmp/release-assets

echo ""
echo "=========================================="
echo "Upload Summary:"
echo " Tag: $TAG"
echo " Release action: $RELEASE_ACTION"
echo " Files uploaded: $upload_count"
echo " Files failed: $failed_count"
echo " Contracts processed: $(find artifacts -mindepth 1 -maxdepth 1 -type d | wc -l)"
echo "=========================================="

if [ $failed_count -gt 0 ]; then
echo "Error: Some files failed to upload"
exit 1
fi

if [ $upload_count -eq 0 ]; then
echo "Warning: No files were uploaded"
exit 1
fi
else
echo "Error: No artifacts found to upload"
exit 1
fi
gh release upload "$TAG" release-assets/* \
--repo "${{ github.repository }}" \
--clobber
10 changes: 10 additions & 0 deletions soroban/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,13 @@ members = [
"predicate-client",
"example-compliant-token",
]

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
6 changes: 6 additions & 0 deletions soroban/predicate-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pub struct PredicateRegistryContract;
#[contractimpl]
impl PredicateRegistryContract {
/// Initialize the registry with an owner address.
///
/// # Arguments
///
/// * `owner` - Address with administrative privileges. Can register and
/// deregister attesters, and propose a new owner via the two-step
/// `transfer_ownership` / `accept_ownership` flow.
pub fn __constructor(e: &Env, owner: Address) {
e.storage().instance().set(&OWNER, &owner);
}
Expand Down
Loading
Loading