Files
mt5-quant_new/.github/workflows/release.yml
T

548 lines
23 KiB
YAML

name: Build and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g., v1.32.0)'
required: true
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
jobs:
# ── Build binaries ───────────────────────────────────────────────────────────
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: macos-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: macos-cargo-
- name: Build release binary
run: cargo build --release
- name: Package binary
run: |
mkdir -p dist/mcp-mt5-quant-macos
cp target/release/mt5-quant dist/mcp-mt5-quant-macos/
cp -r config dist/mcp-mt5-quant-macos/
cp README.md dist/mcp-mt5-quant-macos/
cp -r docs dist/mcp-mt5-quant-macos/
cd dist
tar -czf mcp-mt5-quant-macos-arm64.tar.gz mcp-mt5-quant-macos
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-binary
path: dist/mcp-mt5-quant-macos-arm64.tar.gz
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
- name: Build release binary
run: cargo build --release
- name: Package binary
run: |
mkdir -p dist/mcp-mt5-quant-linux
cp target/release/mt5-quant dist/mcp-mt5-quant-linux/
cp -r config dist/mcp-mt5-quant-linux/
cp README.md dist/mcp-mt5-quant-linux/
cp -r docs dist/mcp-mt5-quant-linux/
cd dist
tar -czf mcp-mt5-quant-linux-x64.tar.gz mcp-mt5-quant-linux
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-binary
path: dist/mcp-mt5-quant-linux-x64.tar.gz
# ── Cargo Publish ────────────────────────────────────────────────────────────
cargo-publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for crates.io Trusted Publishing (OIDC)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-publish-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-publish-
- name: Get crates.io authentication token
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
id: auth
- name: Publish to crates.io
run: cargo publish --no-verify --allow-dirty
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
# ── GitHub Release ───────────────────────────────────────────────────────────
release:
needs: [build-macos, build-linux, cargo-publish]
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download macOS artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: macos-binary
path: dist
- name: Download Linux artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: linux-binary
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_TAG }}
files: |
dist/mcp-mt5-quant-macos-arm64.tar.gz
dist/mcp-mt5-quant-linux-x64.tar.gz
draft: false
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── MCP Package ──────────────────────────────────────────────────────────────
# Builds the installable MCP package (binary + config + docs),
# computes its SHA256, and uploads to the release. Also attests the FINAL
# release bytes — the macOS tarball is re-packaged here (server.json added),
# so attesting in the build jobs would cover digests that never ship.
build-mcp-package:
needs: release
runs-on: macos-latest
permissions:
contents: write # Attach the package to the GitHub Release
id-token: write # Required for build provenance attestation (OIDC)
attestations: write # Required to publish the attestation
outputs:
sha256: ${{ steps.compute-sha256.outputs.sha256 }}
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download macOS binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: macos-binary
path: dist
- name: Download Linux binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: linux-binary
path: dist
- name: Build MCP package
run: |
mkdir -p mcp-package/mcp-server/bin
cd dist && tar -xzf mcp-mt5-quant-macos-arm64.tar.gz && cd ..
cp dist/mcp-mt5-quant-macos/mt5-quant mcp-package/mcp-server/bin/
cp -r config mcp-package/
cp -r docs mcp-package/
cp README.md mcp-package/
cp server.json mcp-package/
cd mcp-package && tar -czf ../mcp-mt5-quant-macos-arm64.tar.gz . && cd ..
# Attest the exact bytes attached to the Release: the re-packaged macOS
# tarball at the repo root and the pass-through Linux tarball from the
# artifact — so attested digests match the published assets.
- name: Generate build provenance attestation (macOS)
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: mcp-mt5-quant-macos-arm64.tar.gz
- name: Generate build provenance attestation (Linux)
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/mcp-mt5-quant-linux-x64.tar.gz
- name: Compute SHA256
id: compute-sha256
run: |
SHA256=$(shasum -a 256 mcp-mt5-quant-macos-arm64.tar.gz | awk '{print $1}')
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "MCP Package SHA256: $SHA256"
- name: Upload MCP package to release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ env.RELEASE_TAG }}
files: mcp-mt5-quant-macos-arm64.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload MCP package artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mcp-package
path: mcp-mt5-quant-macos-arm64.tar.gz
# ── Update server.json SHA256 ────────────────────────────────────────────────
# Updates server.json on master via an auto-merged PR instead of a direct
# push: the protect-master ruleset requires build/lint/cargo-deny status
# checks, which github-actions[bot] cannot bypass on a direct push. The PR
# runs those checks legitimately, then auto-merges once green. This job
# blocks until the merge lands so mcp-publish reads the patched server.json.
#
# Bot identity (RELEASE_PAT): when the RELEASE_PAT secret exists (a
# fine-grained personal access token owned by masdevid with Contents+PR
# write), the branch push and the PR are attributed to masdevid instead of
# github-actions[bot]. GitHub only holds pull_request workflows for manual
# approval when the PR author is an OUTSIDE contributor — same-owner PRs
# run their checks immediately, making releases fully hands-off. Without
# the secret everything falls back to GITHUB_TOKEN: the release still
# completes, but the server.json PR's required status checks sit in
# "action_required" until manually approved once per release.
update-server-json:
needs: build-mcp-package
runs-on: ubuntu-latest
permissions:
contents: write # push the ci/* release branch
pull-requests: write # create the PR and enable auto-merge
env:
# Single bot identity used for BOTH the git push and the gh pr calls —
# GitHub ties workflow-trigger approval to the PR-authoring identity,
# so push and PR must match. Resolves to RELEASE_PAT when configured,
# otherwise the default GITHUB_TOKEN, so nothing breaks before the
# secret is added. Declared here instead of interpolated inline so the
# token never lands on a command line; GitHub masks it in logs anyway.
RELEASE_PAT_OR_TOKEN: ${{ secrets.RELEASE_PAT || github.token }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: master
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}
- name: Patch server.json
env:
PKG_SHA256: ${{ needs.build-mcp-package.outputs.sha256 }}
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
run: |
VERSION="${RELEASE_TAG#v}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
VERSION="$VERSION" PKG_SHA256="$PKG_SHA256" python3 - <<'PYEOF'
import json, os, re
sha256 = os.environ['PKG_SHA256']
version = os.environ['VERSION']
with open('server.json') as f:
data = json.load(f)
data['version'] = version
for pkg in data.get('packages', []):
pkg['version'] = version
pkg['fileSha256'] = sha256
if 'identifier' in pkg:
pkg['identifier'] = re.sub(
r'/v[0-9]+\.[0-9]+\.[0-9]+/',
f'/v{version}/',
pkg['identifier']
)
with open('server.json', 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
print(f"Patched: version={version}, sha256={sha256[:16]}...")
PYEOF
- name: Commit server.json to release branch
id: commit
run: |
BRANCH="ci/server-json-sha-${VERSION}"
# Authenticate ALL pushes (stale-branch cleanup + branch upload)
# with the SAME identity that creates the PR below: GitHub ties
# workflow-trigger approval to the PR author, so a mismatch here
# would defeat the RELEASE_PAT hands-off path. Token comes from the
# job-level env (never interpolated inline, never echoed).
git remote set-url --push origin \
"https://x-access-token:${RELEASE_PAT_OR_TOKEN}@github.com/masdevid/mt5-quant.git"
# Drop any stale branch/PR left over from a previous attempt at
# this release so every run starts from a clean slate
git push origin --delete "$BRANCH" 2>/dev/null || true
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add server.json
if git diff --cached --quiet; then
echo "server.json unchanged — no commit needed"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
# Never add a CI-skip marker to this commit message: under the
# protect-master ruleset the server.json PR carries required
# status checks, and a CI-skip marker suppresses
# pull_request-triggered workflows, so those checks never report
# and auto-merge hangs until timeout. Omitting it is safe —
# release.yml triggers only on tags (no loop risk) and
# GITHUB_TOKEN-attributed merges never retrigger workflows.
git commit -m "ci: update server.json SHA256 for v${VERSION}"
git push origin "$BRANCH"
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "Committed server.json update to ${BRANCH}"
fi
- name: Create PR and enable auto-merge
if: steps.commit.outputs.changed == 'true'
run: |
BRANCH="ci/server-json-sha-${VERSION}"
# Same rule as above: no CI-skip marker in the title — GitHub
# honors markers on PR titles too, which would suppress the
# required checks.
GH_TOKEN="$RELEASE_PAT_OR_TOKEN" gh pr create \
--title "ci: update server.json SHA256 for v${VERSION}" \
--body "Automated MCP package hash update." \
--base master \
--head "$BRANCH"
GH_TOKEN="$RELEASE_PAT_OR_TOKEN" gh pr merge "$BRANCH" --auto --squash
# Block until required checks (build, lint, cargo-deny x4) finish
# and auto-merge completes — mcp-publish depends on this job and
# checks out master expecting the patched server.json
DEADLINE=$((SECONDS + 1800))
while (( SECONDS < DEADLINE )); do
STATE=$(GH_TOKEN="$RELEASE_PAT_OR_TOKEN" gh pr view "$BRANCH" --json state --jq .state)
case "$STATE" in
MERGED)
echo "server.json PR merged to master"
exit 0
;;
CLOSED)
echo "::error::server.json PR was closed without merging" >&2
exit 1
;;
esac
sleep 20
done
echo "::error::Timed out after 30m waiting for server.json PR auto-merge (required checks: build, lint, cargo-deny x4)" >&2
exit 1
# ── MCP Registry Publish ─────────────────────────────────────────────────────
mcp-publish:
needs: [build-mcp-package, update-server-json]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for GitHub OIDC
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: master # Pull master so server.json has the correct SHA256
- name: Download MCP package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: mcp-package
path: .
- name: Install mcp-publisher
id: install-publisher
env:
# Hash-pinned download (OpenSSF Scorecard Pinned-Dependencies):
# PUB_SHA256 is the mcp-publisher_linux_amd64.tar.gz entry from
# registry_1.8.1_checksums.txt shipped in the same release.
# Bump both values together when upgrading.
PUB_VERSION: v1.8.1
PUB_SHA256: a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc
run: |
URL="https://github.com/modelcontextprotocol/registry/releases/download/${PUB_VERSION}/mcp-publisher_linux_amd64.tar.gz"
echo "Downloading: $URL"
if ! curl -fsSL -o "$RUNNER_TEMP/mcp-publisher.tar.gz" "$URL"; then
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Download failed: $URL"
exit 0
fi
# Never execute unverified bytes: hard-fail the job on any digest
# mismatch (upstream re-release or tampering)
ACTUAL=$(sha256sum "$RUNNER_TEMP/mcp-publisher.tar.gz" | awk '{print $1}')
if [[ "$ACTUAL" != "$PUB_SHA256" ]]; then
echo "::error::SHA256 mismatch for mcp-publisher ${PUB_VERSION}: expected ${PUB_SHA256}, got ${ACTUAL}"
exit 1
fi
echo "SHA256 verified: $ACTUAL"
EXTRACT_DIR="$(mktemp -d)"
if ! tar -xzf "$RUNNER_TEMP/mcp-publisher.tar.gz" -C "$EXTRACT_DIR"; then
rm -rf "$EXTRACT_DIR" "$RUNNER_TEMP/mcp-publisher.tar.gz"
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Extraction failed"
exit 0
fi
if [[ -f "$EXTRACT_DIR/mcp-publisher" ]]; then
chmod +x "$EXTRACT_DIR/mcp-publisher"
sudo mv "$EXTRACT_DIR/mcp-publisher" /usr/local/bin/mcp-publisher
echo "installed=true" >> "$GITHUB_OUTPUT"
echo "Installed: $(mcp-publisher --version 2>/dev/null || echo 'ok')"
else
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Binary not found in archive"
fi
rm -rf "$EXTRACT_DIR" "$RUNNER_TEMP/mcp-publisher.tar.gz"
- name: Validate server.json before publish
id: validate
run: |
python3 - <<'PYEOF'
import json, sys
with open('server.json') as f:
data = json.load(f)
print(f" name: {data.get('name')}")
print(f" version: {data.get('version')}")
ok = True
for pkg in data.get('packages', []):
sha = pkg.get('fileSha256', '')
ident = pkg.get('identifier', '')
print(f" sha256: {sha}")
print(f" url: {ident}")
if sha in ('', 'TBD_CI_WILL_UPDATE', 'TBD_UPDATED_BY_CI'):
print('ERROR: fileSha256 is a placeholder — publish aborted')
ok = False
sys.exit(0 if ok else 1)
PYEOF
- name: Publish to MCP Registry
if: steps.install-publisher.outputs.installed == 'true' && steps.validate.outcome == 'success'
run: |
echo "=== Authenticating with GitHub OIDC ==="
mcp-publisher login github-oidc || {
echo "OIDC login failed"
exit 1
}
echo "=== Publishing ==="
mcp-publisher publish && echo "✓ Published to MCP Registry" || {
echo ""
echo "══════════════════════════════════════════════════════════"
echo " AUTOMATED MCP PUBLISH FAILED — manual steps:"
echo "══════════════════════════════════════════════════════════"
echo " 1. Download: https://github.com/modelcontextprotocol/registry/releases"
echo " 2. ./mcp-publisher login github"
echo " 3. ./mcp-publisher publish"
echo " Web UI: https://registry.modelcontextprotocol.io"
echo "══════════════════════════════════════════════════════════"
exit 0 # Don't fail the workflow — release succeeded
}
- name: Skip notice
if: steps.install-publisher.outputs.installed != 'true'
run: |
echo "mcp-publisher not installed — skipping automated publish"
echo "Manual: https://registry.modelcontextprotocol.io"
# ── Release Summary ──────────────────────────────────────────────────────────
release-info:
needs: [mcp-publish]
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
steps:
- name: Summary
run: |
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Release ${RELEASE_TAG} complete! "
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "GitHub Release:"
echo " https://github.com/masdevid/mt5-quant/releases/tag/${RELEASE_TAG}"
echo ""
echo "MCP Registry: io.github.masdevid/mt5-quant"
echo ""
echo "── MCP Client Registration ──────────────────────────────────"
echo ""
echo "Claude Code:"
echo " claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant"
echo ""
echo "Windsurf (~/.codeium/windsurf/mcp_config.json):"
echo ' {"mcpServers": {"io.github.masdevid/mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
echo ""
echo "Cursor / VS Code (~/.cursor/mcp.json):"
echo ' {"mcpServers": {"mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
echo ""
echo "─────────────────────────────────────────────────────────────"