Skip to content
Draft
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a554d5d
Automatic release tag creation on release branch merge
SimonRastikian Feb 20, 2026
bc3dff9
Renaming fil
SimonRastikian Feb 20, 2026
d52b961
Adding automatic Running workflows
SimonRastikian Feb 20, 2026
0b010db
Trigering workflows and storing the digest and image id
SimonRastikian Feb 20, 2026
7abcf95
Building the contract
SimonRastikian Feb 20, 2026
f783bf0
Contract hash
SimonRastikian Feb 20, 2026
ca7fe4e
create the draft release
SimonRastikian Feb 20, 2026
f3ee756
Change the title of the draft
SimonRastikian Feb 20, 2026
d1bbd5b
tabs issues
SimonRastikian Feb 20, 2026
b69168a
Cleaning up
SimonRastikian Feb 20, 2026
dae17c3
Do not use cliff for changelog
SimonRastikian Feb 20, 2026
1c0c5eb
No arbitrary sleep instead use gh watch
SimonRastikian Feb 20, 2026
072c4f6
Better waiting
SimonRastikian Feb 20, 2026
25037f4
Checking versioning and no inspect --raw for skopeo
SimonRastikian Feb 20, 2026
5e5c356
Release notes is not empty
SimonRastikian Feb 20, 2026
64c47d6
Making zizmor pass
SimonRastikian Feb 20, 2026
44a66cd
Avoid creating the tag via the GitHub API
SimonRastikian Feb 20, 2026
1b35dce
Fix critical issue
SimonRastikian Feb 20, 2026
23b7ffc
Merge branch 'main' into simon/gh-action-release-automation
SimonRastikian Feb 20, 2026
6632cff
Trigger on creation of tag or on merge PR
SimonRastikian Mar 2, 2026
e07d28a
lookup only
SimonRastikian Mar 2, 2026
ae272ce
Merge branch 'main' into simon/gh-action-release-automation
SimonRastikian Mar 2, 2026
fc2a03a
Merge branch 'main' into simon/gh-action-release-automation
SimonRastikian Mar 3, 2026
d7ebef2
Update .github/workflows/release.yml
SimonRastikian Mar 5, 2026
d6bf074
Merge branch 'main' into simon/gh-action-release-automation
SimonRastikian Mar 5, 2026
8475664
Install cargo near
SimonRastikian Mar 9, 2026
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
366 changes: 366 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
name: Release

# Prevent duplicate runs for the same workflow + branch combination.
# If a new run starts while one is in progress, the older run is cancelled.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# Trigger this workflow when:
# 1. A pull request targeting main is closed (filters to merged release PRs via job `if`).
# 2. A version tag (v*) is pushed directly (e.g. `git push origin v3.6.0`).
on:
push:
tags:
- '*.*.*'
pull_request:
branches:
- main
types:
- closed

jobs:
release-info:
name: "Extract release info"
# Run when a version tag is pushed OR a merged release/v* PR is closed.
if: >-
github.event_name == 'push'
|| (github.event.pull_request.merged == true
&& startsWith(github.event.pull_request.head.ref, 'release/v'))
runs-on: warp-ubuntu-2404-x64-2x
# Write permission on contents is required to push a new tag (PR path).
permissions:
contents: write

# Expose version and commit hash to downstream jobs.
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
short-sha: ${{ steps.version.outputs.short-sha }}

steps:
# Extract the semver version from the tag ref or branch name.
# Tag push: refs/tags/v3.6.0 -> version="3.6.0", tag="v3.6.0"
# PR merge: release/v3.6.0 -> version="3.6.0", tag="v3.6.0"
# Also compute the 7-char commit hash used as the Docker source tag.
- name: Extract version
id: version
env:
EVENT: ${{ github.event_name }}
TAG_REF: ${{ github.ref }}
TAG_SHA: ${{ github.sha }}
BRANCH: ${{ github.event.pull_request.head.ref }}
PR_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
if [[ "$EVENT" == "push" ]]; then
VERSION="${TAG_REF#refs/tags/v}"
SHA="$TAG_SHA"
else
VERSION="${BRANCH#release/v}"
SHA="$PR_SHA"
fi
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid semver: $VERSION"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "short-sha=${SHA::7}" >> "$GITHUB_OUTPUT"

# Create the tag via the GitHub API so we don't need git credentials.
# Points the tag at the PR's merge commit SHA.
# Skipped on tag push since the tag already exists.
- name: Create tag
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
gh api "repos/$GITHUB_REPOSITORY/git/refs" \
--method POST \
-f ref="refs/tags/$TAG" \
-f sha="$SHA"

# Trigger the existing Docker retag workflows for each image.
# These are the same workflows you'd normally trigger manually from the Actions UI.
retag-docker-images:
name: "Retag Docker images"
needs: release-info
runs-on: warp-ubuntu-2404-x64-2x
permissions:
actions: write

# Expose image digests to downstream jobs.
outputs:
mpc_node_manifest_digest: ${{ steps.digests.outputs.mpc_node_manifest_digest }}
mpc_node_image_id: ${{ steps.digests.outputs.mpc_node_image_id }}
mpc_node_gcp_manifest_digest: ${{ steps.digests.outputs.mpc_node_gcp_manifest_digest }}
mpc_node_gcp_image_id: ${{ steps.digests.outputs.mpc_node_gcp_image_id }}
mpc_launcher_manifest_digest: ${{ steps.digests.outputs.mpc_launcher_manifest_digest }}
mpc_launcher_image_id: ${{ steps.digests.outputs.mpc_launcher_image_id }}

steps:
# Dispatch each retag workflow and immediately capture its run ID.
# A brief sleep after each dispatch lets the run appear in the API
# before we query for it.
- name: Retag mpc-launcher
id: launcher
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ needs.release-info.outputs.short-sha }}
VERSION: ${{ needs.release-info.outputs.version }}
run: |
gh workflow run docker_launcher_release.yml \
--repo "$REPO" \
-f source-tag="main-${SHORT_SHA}" \
-f release-tag="${VERSION}"
sleep 5
RUN_ID=$(gh run list --workflow=docker_launcher_release.yml --repo "$REPO" --limit=1 --json databaseId -q '.[0].databaseId')
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"

- name: Retag mpc-node-gcp
id: node-gcp
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ needs.release-info.outputs.short-sha }}
VERSION: ${{ needs.release-info.outputs.version }}
run: |
gh workflow run docker_node_release.yml \
--repo "$REPO" \
-f source-tag="main-${SHORT_SHA}" \
-f release-tag="${VERSION}" \
-f repository="mpc-node-gcp"
sleep 5
RUN_ID=$(gh run list --workflow=docker_node_release.yml --repo "$REPO" --limit=1 --json databaseId -q '.[0].databaseId')
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"

- name: Retag mpc-node
id: node
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ needs.release-info.outputs.short-sha }}
VERSION: ${{ needs.release-info.outputs.version }}
run: |
gh workflow run docker_node_release.yml \
--repo "$REPO" \
-f source-tag="main-${SHORT_SHA}" \
-f release-tag="${VERSION}" \
-f repository="mpc-node"
sleep 5
RUN_ID=$(gh run list --workflow=docker_node_release.yml --repo "$REPO" --limit=1 --json databaseId -q '.[0].databaseId')
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"

# Wait for all three dispatched runs to complete.
# --exit-status makes gh run watch fail if the workflow itself fails.
- name: Wait for retag workflows
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
LAUNCHER_RUN: ${{ steps.launcher.outputs.run_id }}
NODE_GCP_RUN: ${{ steps.node-gcp.outputs.run_id }}
NODE_RUN: ${{ steps.node.outputs.run_id }}
run: |
for RUN_ID in $LAUNCHER_RUN $NODE_GCP_RUN $NODE_RUN; do
gh run watch "$RUN_ID" --repo "$REPO" --exit-status
done

- name: Install skopeo
run: |
sudo apt-get update
sudo apt-get install -y skopeo

# Inspect each retagged image and store Manifest digest and Image ID as outputs.
- name: Get image digests
id: digests
env:
RELEASE_TAG: ${{ needs.release-info.outputs.version }}
run: |
for REPO in mpc-launcher mpc-node-gcp mpc-node; do
SAFE_NAME="${REPO//-/_}"
MANIFEST_DIGEST=$(skopeo inspect docker://nearone/$REPO:$RELEASE_TAG | jq -r '.Digest')
# Use --override-os/--override-arch to resolve multi-arch manifest lists
# to the platform-specific image manifest, where .config.digest is the Image ID.
IMAGE_ID=$(skopeo inspect --raw --override-os linux --override-arch amd64 docker://nearone/$REPO:$RELEASE_TAG | jq -r '.config.digest')
echo "${SAFE_NAME}_manifest_digest=$MANIFEST_DIGEST" >> "$GITHUB_OUTPUT"
echo "${SAFE_NAME}_image_id=$IMAGE_ID" >> "$GITHUB_OUTPUT"
done

# Build the smart contract using reproducible builds, rename the artifacts
# with the release version, and package them into a .tar.gz archive.
# This job runs in parallel with retag-docker-images since both only
# depend on release-info.
build-contract:
name: "Build contract"
needs: release-info
# Uses a 16x runner because the reproducible contract build is resource-intensive.
runs-on: warp-ubuntu-2404-x64-16x
permissions:
contents: read

# Expose contract hash to downstream jobs.
outputs:
contract-hash: ${{ steps.contract-hash.outputs.sha256 }}

steps:
# Checkout the release tag so we build the correct code in both trigger paths.
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ needs.release-info.outputs.tag }}
persist-credentials: false

# Rust cache: lookup-only + save-if:false keeps this step inert so that
# no poisoned cache content can influence release artifacts (zizmor cache-poisoning).
# The actual build happens inside a Docker container (reproducible-wasm)
# so host caching provides limited benefit here anyway.
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: false
cache-provider: "warpbuild"
lookup-only: true

- name: Install cargo-binstall
uses: taiki-e/install-action@d4422f254e595ee762a758628fe4f16ce050fa2e # v2.67.28
with:
tool: cargo-binstall

- name: Install cargo-near
run: |
sudo apt-get update && sudo apt-get install --assume-yes libudev-dev
cargo binstall --force --no-confirm --locked cargo-near@0.19.1 --pkg-url="{ repo }/releases/download/{ name }-v{ version }/{ name }-{ target }.{ archive-format }"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Install wasm-opt
run: |
cargo binstall --force --no-confirm --locked wasm-opt@0.116.1
echo "${HOME}/.cargo/bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Build the contract with reproducible-wasm to ensure deterministic output.
# This produces mpc_contract.wasm and mpc_contract_abi.json
# under target/near/mpc_contract/.
- name: Build contract
run: cargo near build reproducible-wasm --manifest-path crates/contract/Cargo.toml
Comment on lines +249 to +250
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will this work without installing cargo-near first?


# Compute the SHA-256 hash of the compiled contract .wasm before renaming.
# Used later in the GitHub release notes so users can verify contract integrity.
- name: Compute contract hash
id: contract-hash
run: |
CONTRACT_HASH=$(sha256sum target/near/mpc_contract/mpc_contract.wasm | awk '{print $1}')
echo "sha256=$CONTRACT_HASH" >> "$GITHUB_OUTPUT"

# Rename the build outputs to include the release version and
# compress them into a single .tar.gz archive for distribution.
# e.g. mpc-contract-v3.6.0.wasm, mpc-contract-v3.6.0-abi.json
- name: Package contract artifacts
env:
VERSION: ${{ needs.release-info.outputs.version }}
run: |
cd target/near/mpc_contract/
mv mpc_contract.wasm "mpc-contract-v${VERSION}.wasm"
mv mpc_contract_abi.json "mpc-contract-v${VERSION}-abi.json"
tar -czf "mpc-contract-v${VERSION}.tar.gz" "mpc-contract-v${VERSION}.wasm" "mpc-contract-v${VERSION}-abi.json"

# Upload the .tar.gz as a GitHub Actions artifact so downstream jobs
# (e.g. creating the GitHub release) can download and attach it.
- name: Upload contract artifact
uses: actions/upload-artifact@ea165f8d65b6db9a6b7e75b195db6a7b2bcf4bac # v4.6.2
with:
name: mpc-contract
path: target/near/mpc_contract/mpc-contract-v${{ needs.release-info.outputs.version }}.tar.gz

# Create a draft GitHub release with the changelog, Docker image info,
# and the contract artifact attached.
# Runs after both retag-docker-images and build-contract complete.
create-release:
name: "Create draft GitHub release"
needs: [release-info, retag-docker-images, build-contract]
runs-on: warp-ubuntu-2404-x64-2x
permissions:
contents: write

steps:
# Checkout the release tag to read the correct CHANGELOG.md.
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ needs.release-info.outputs.tag }}
persist-credentials: false

# Extract the changelog section for this version from CHANGELOG.md.
# Sections are delimited by "## [<version>]" headers, so we print
# everything between the current version's header and the next one.
- name: Extract changelog
env:
VERSION: ${{ needs.release-info.outputs.version }}
run: |
awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found" CHANGELOG.md > release-notes-raw.md
if [ ! -s release-notes-raw.md ]; then
echo "::error::No changelog found for version ${VERSION} in CHANGELOG.md"
exit 1
fi

# Download the contract .tar.gz uploaded by the build-contract job.
- name: Download contract artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: mpc-contract

# Compose the full release body: changelog + Docker images + contract hash.
- name: Compose release body
env:
VERSION: ${{ needs.release-info.outputs.version }}
NODE_MANIFEST: ${{ needs.retag-docker-images.outputs.mpc_node_manifest_digest }}
NODE_IMAGE_ID: ${{ needs.retag-docker-images.outputs.mpc_node_image_id }}
NODE_GCP_MANIFEST: ${{ needs.retag-docker-images.outputs.mpc_node_gcp_manifest_digest }}
NODE_GCP_IMAGE_ID: ${{ needs.retag-docker-images.outputs.mpc_node_gcp_image_id }}
LAUNCHER_MANIFEST: ${{ needs.retag-docker-images.outputs.mpc_launcher_manifest_digest }}
LAUNCHER_IMAGE_ID: ${{ needs.retag-docker-images.outputs.mpc_launcher_image_id }}
CONTRACT_HASH: ${{ needs.build-contract.outputs.contract-hash }}
run: |
cp release-notes-raw.md release-notes.md
cat >> release-notes.md <<EOF

## Docker images

nearone/mpc-node:${VERSION}
Manifest digest: ${NODE_MANIFEST}
Image ID: ${NODE_IMAGE_ID}

nearone/mpc-node-gcp:${VERSION}
Manifest digest: ${NODE_GCP_MANIFEST}
Image ID: ${NODE_GCP_IMAGE_ID}

nearone/mpc-launcher:${VERSION}
Manifest digest: ${LAUNCHER_MANIFEST}
Image ID: ${LAUNCHER_IMAGE_ID}

## MPC contract

digest: sha256:${CONTRACT_HASH}
EOF

# Create a draft release on GitHub using the gh CLI.
# --draft: the release is not published yet, allowing manual review before going live.
# --notes-file: uses the composed release notes (changelog + Docker images + contract hash).
# The contract .tar.gz is passed as a positional argument, which gh attaches as a release asset.
- name: Create draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-info.outputs.tag }}
VERSION: ${{ needs.release-info.outputs.version }}
run: |
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "MPC ${VERSION}" \
--notes-file release-notes.md \
--draft \
"mpc-contract-v${VERSION}.tar.gz"
Loading