chore: import upstream snapshot with attribution
Integration Tests / melodic (push) Has been cancelled
Integration Tests / noetic (push) Has been cancelled
Integration Tests / humble (push) Has been cancelled
Integration Tests / jazzy (push) Has been cancelled
Ruff Lint & Format / ruff (push) Has been cancelled
Sync main to develop / Check if sync is needed (push) Has been cancelled
Sync main to develop / Sync main to develop (push) Has been cancelled
Integration Tests / melodic (push) Has been cancelled
Integration Tests / noetic (push) Has been cancelled
Integration Tests / humble (push) Has been cancelled
Integration Tests / jazzy (push) Has been cancelled
Ruff Lint & Format / ruff (push) Has been cancelled
Sync main to develop / Check if sync is needed (push) Has been cancelled
Sync main to develop / Sync main to develop (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
name: Track Clone Metrics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 8 * * *' # Run every day at 8am
|
||||
|
||||
|
||||
jobs:
|
||||
clone-stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper branch operations
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2.1.0
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Switch to metrics branch
|
||||
run: |
|
||||
# Checkout or create metrics branch
|
||||
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
|
||||
echo "📋 Checking out existing metrics branch..."
|
||||
git checkout -b metrics origin/metrics || git checkout metrics
|
||||
else
|
||||
echo "🆕 Creating new metrics branch..."
|
||||
git checkout -b metrics
|
||||
fi
|
||||
|
||||
- name: Fetch clone data
|
||||
env:
|
||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
mkdir -p .metrics
|
||||
# Fetch clone metrics (contains both daily breakdown and 14-day totals)
|
||||
curl -s -H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
https://api.github.com/repos/${{ github.repository }}/traffic/clones \
|
||||
> .metrics/clone_stats.json
|
||||
|
||||
echo "Clone metrics:"
|
||||
cat .metrics/clone_stats.json
|
||||
|
||||
- name: Update daily metrics
|
||||
run: |
|
||||
# Process each day from the clones array
|
||||
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Create daily CSV with header if it doesn't exist
|
||||
if [ ! -f .metrics/daily_clone_metrics.csv ]; then
|
||||
echo "date,total_clones,unique_cloners,last_updated" > .metrics/daily_clone_metrics.csv
|
||||
fi
|
||||
|
||||
echo "📊 Processing daily metrics..."
|
||||
jq -r '.clones[] | "\(.timestamp | split("T")[0]),\(.count),\(.uniques)"' .metrics/clone_stats.json | while IFS=',' read -r day_date count uniques; do
|
||||
echo "Processing $day_date: $count clones, $uniques unique"
|
||||
|
||||
# Check if this date already exists in the CSV
|
||||
if grep -q "^$day_date" .metrics/daily_clone_metrics.csv; then
|
||||
echo "📝 Updating existing entry for $day_date..."
|
||||
# Update existing entry
|
||||
awk -v date="$day_date" -v count="$count" -v uniques="$uniques" -v last_updated="$LAST_UPDATED" '
|
||||
BEGIN { FS=","; OFS="," }
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
|
||||
print $1, count, uniques, last_updated;
|
||||
updated=1;
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' .metrics/daily_clone_metrics.csv > .metrics/daily_clone_metrics_temp.csv
|
||||
mv .metrics/daily_clone_metrics_temp.csv .metrics/daily_clone_metrics.csv
|
||||
else
|
||||
echo "➕ Adding new daily entry for $day_date..."
|
||||
echo "$day_date,$count,$uniques,$LAST_UPDATED" >> .metrics/daily_clone_metrics.csv
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Daily metrics:"
|
||||
tail -n 5 .metrics/daily_clone_metrics.csv
|
||||
|
||||
- name: Update 14-day rolling metrics
|
||||
run: |
|
||||
# Process 14-day metrics
|
||||
COUNT_14D=$(jq '.count' .metrics/clone_stats.json)
|
||||
UNIQUES_14D=$(jq '.uniques' .metrics/clone_stats.json)
|
||||
DATE_ONLY=$(date -u +"%Y-%m-%d")
|
||||
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
echo "📊 Processing 14-day metrics... for date: $DATE_ONLY"
|
||||
echo "Processing values: $COUNT_14D clones, $UNIQUES_14D unique"
|
||||
|
||||
# Create 14-day CSV with header if it doesn't exist
|
||||
if [ ! -f .metrics/rolling_14d_clone_metrics.csv ]; then
|
||||
echo "date,total_clones_14d,unique_cloners_14d,last_updated" > .metrics/rolling_14d_clone_metrics.csv
|
||||
echo "📄 Created new 14-day rolling CSV file"
|
||||
fi
|
||||
|
||||
# Check if today's date already exists in the 14-day CSV
|
||||
if grep -q "^$DATE_ONLY" .metrics/rolling_14d_clone_metrics.csv; then
|
||||
echo "📝 Updating existing 14-day rolling entry for $DATE_ONLY..."
|
||||
# Update existing entry
|
||||
awk -v date="$DATE_ONLY" -v count="$COUNT_14D" -v uniques="$UNIQUES_14D" -v last_updated="$LAST_UPDATED" '
|
||||
BEGIN { FS=","; OFS=","; updated=0 }
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
|
||||
print $1, count, uniques, last_updated;
|
||||
updated=1;
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { if (!updated) print date, count, uniques, last_updated }
|
||||
' .metrics/rolling_14d_clone_metrics.csv > .metrics/rolling_14d_clone_metrics_temp.csv
|
||||
mv .metrics/rolling_14d_clone_metrics_temp.csv .metrics/rolling_14d_clone_metrics.csv
|
||||
echo "✅ Updated existing entry"
|
||||
else
|
||||
echo "➕ Adding new 14-day rolling entry for $DATE_ONLY..."
|
||||
echo "$DATE_ONLY,$COUNT_14D,$UNIQUES_14D,$LAST_UPDATED" >> .metrics/rolling_14d_clone_metrics.csv
|
||||
echo "✅ Added new entry"
|
||||
fi
|
||||
|
||||
echo "14-day rolling metrics:"
|
||||
tail -n 5 .metrics/rolling_14d_clone_metrics.csv
|
||||
|
||||
- name: Commit and push results
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "CloneMetricsBot[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Add both CSV files
|
||||
git add .metrics/daily_clone_metrics.csv .metrics/rolling_14d_clone_metrics.csv
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
echo "ℹ️ No changes to commit - CSV data is up to date"
|
||||
else
|
||||
echo "📝 Committing changes..."
|
||||
git commit -m "Automated update: repository clone metrics $(date)"
|
||||
|
||||
echo "🚀 Pushing to metrics branch..."
|
||||
git push --force-with-lease origin metrics
|
||||
fi
|
||||
@@ -0,0 +1,109 @@
|
||||
name: Track Download Metrics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Track View Metrics"] # exact name of PyPI workflow
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
download-stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper branch operations
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2.1.0
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Switch to metrics branch
|
||||
run: |
|
||||
# Checkout or create metrics branch
|
||||
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
|
||||
echo "📋 Checking out existing metrics branch..."
|
||||
git checkout -b metrics origin/metrics || git checkout metrics
|
||||
else
|
||||
echo "🆕 Creating new metrics branch..."
|
||||
git checkout -b metrics
|
||||
fi
|
||||
|
||||
- name: Fetch download data
|
||||
env:
|
||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
mkdir -p .metrics
|
||||
# Fetch download metrics from releases
|
||||
curl -s -H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases?per_page=100" \
|
||||
> .metrics/releases.json
|
||||
|
||||
echo "Download metrics:"
|
||||
jq '[.[] | {tag: .tag_name, date: .published_at, total_downloads: ([.assets[].download_count] | add), assets_count: (.assets | length)}]' .metrics/releases.json
|
||||
|
||||
- name: Update total download metrics
|
||||
run: |
|
||||
# Process each release from the releases array
|
||||
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Create total download CSV with header if it doesn't exist
|
||||
if [ ! -f .metrics/total_download_metrics.csv ]; then
|
||||
echo "date,release_tag,total_downloads,last_updated" > .metrics/total_download_metrics.csv
|
||||
fi
|
||||
|
||||
echo "📊 Processing total download metrics..."
|
||||
jq -r '.[] | "\(.published_at[0:10]),\(.tag_name),\([.assets[].download_count] | add)"' .metrics/releases.json | while IFS=',' read -r release_date tag_name total_downloads; do
|
||||
echo "Processing $release_date: $tag_name - $total_downloads downloads"
|
||||
|
||||
# Check if this release already exists in the CSV
|
||||
if grep -q ",$tag_name," .metrics/total_download_metrics.csv; then
|
||||
echo "📝 Updating existing entry for $tag_name..."
|
||||
# Update existing entry
|
||||
awk -v tag="$tag_name" -v downloads="$total_downloads" -v last_updated="$LAST_UPDATED" '
|
||||
BEGIN { FS=","; OFS="," }
|
||||
$2 == tag {
|
||||
print $1, $2, downloads, last_updated;
|
||||
updated=1;
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' .metrics/total_download_metrics.csv > .metrics/total_download_metrics_temp.csv
|
||||
mv .metrics/total_download_metrics_temp.csv .metrics/total_download_metrics.csv
|
||||
else
|
||||
echo "➕ Adding new download entry for $tag_name..."
|
||||
echo "$release_date,$tag_name,$total_downloads,$LAST_UPDATED" >> .metrics/total_download_metrics.csv
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total download metrics:"
|
||||
tail -n 5 .metrics/total_download_metrics.csv
|
||||
|
||||
- name: Commit and push results
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "DownloadMetricsBot[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Add CSV file
|
||||
git add .metrics/total_download_metrics.csv
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
echo "ℹ️ No changes to commit - CSV data is up to date"
|
||||
else
|
||||
echo "📝 Committing changes..."
|
||||
git commit -m "Automated update: repository download metrics $(date)"
|
||||
|
||||
echo "🚀 Pushing to metrics branch..."
|
||||
git push --force-with-lease origin metrics
|
||||
fi
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [develop, main]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ros-distro: [melodic, noetic, humble, jazzy]
|
||||
include:
|
||||
- ros-distro: melodic
|
||||
dockerfile: Dockerfile.ros1-melodic
|
||||
container-name: integration-ros-melodic
|
||||
- ros-distro: noetic
|
||||
dockerfile: Dockerfile.ros1-noetic
|
||||
container-name: integration-ros-noetic
|
||||
- ros-distro: humble
|
||||
dockerfile: Dockerfile.ros2-humble
|
||||
container-name: integration-ros2-humble
|
||||
- ros-distro: jazzy
|
||||
dockerfile: Dockerfile.ros2-jazzy
|
||||
container-name: integration-ros2-jazzy
|
||||
|
||||
name: ${{ matrix.ros-distro }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Start ROS container
|
||||
run: |
|
||||
ROS_DOCKERFILE=${{ matrix.dockerfile }} \
|
||||
ROS_CONTAINER_NAME=${{ matrix.container-name }} \
|
||||
docker compose -f tests/integration/docker-compose.yml up --build -d --wait
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Detect ROS version
|
||||
run: uv run python tests/integration/test_quick_detect.py
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
uv run pytest tests/integration/ -v \
|
||||
--ros-distro ${{ matrix.ros-distro }} --skip-compose
|
||||
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: |
|
||||
ROS_DOCKERFILE=${{ matrix.dockerfile }} \
|
||||
ROS_CONTAINER_NAME=${{ matrix.container-name }} \
|
||||
docker compose -f tests/integration/docker-compose.yml down --volumes --remove-orphans
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Merge branch into target
|
||||
|
||||
# Manual trigger only
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_branch:
|
||||
description: "Branch to merge from (default: the branch this workflow is run on)"
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
target_branch:
|
||||
description: "Branch to merge into"
|
||||
type: string
|
||||
required: true
|
||||
default: develop
|
||||
delete_source_branch:
|
||||
description: "Delete the source branch after a successful merge"
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
check_only:
|
||||
description: "Verify the merge succeeds without pushing"
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
merge-into-branch:
|
||||
runs-on: ubuntu-latest
|
||||
name: Merge '${{ inputs.source_branch || github.ref_name }}' into '${{ inputs.target_branch }}'
|
||||
env:
|
||||
SRC_BRANCH: ${{ inputs.source_branch || github.ref_name }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.target_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up git user
|
||||
run: |
|
||||
git config user.name "RobotMCP Bot"
|
||||
git config user.email "admin@robotmcp.ai"
|
||||
|
||||
# Make sure we have the source branch and a common ancestor to merge.
|
||||
git fetch origin "$SRC_BRANCH"
|
||||
if ! git merge-base "$TARGET_BRANCH" "origin/$SRC_BRANCH"; then
|
||||
echo "No common ancestor found for merging!"
|
||||
exit 1
|
||||
fi
|
||||
- name: Abort if conflict-resolution PR already open
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
SAFE_SRC="${SRC_BRANCH//\//_}"
|
||||
SAFE_TARGET="${TARGET_BRANCH//\//_}"
|
||||
EXISTING_PR=$(gh pr list \
|
||||
--base "$TARGET_BRANCH" \
|
||||
--state open \
|
||||
--json url,headRefName \
|
||||
--jq ".[] | select(.headRefName | startswith(\"${SAFE_SRC}-\") and endswith(\"/merge-into-${SAFE_TARGET}\")) | .url" \
|
||||
2>/dev/null | head -n1 || true)
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "::error::An open conflict-resolution PR already exists for '$SRC_BRANCH' -> '$TARGET_BRANCH': $EXISTING_PR"
|
||||
echo "::error::Resolve and merge that PR before re-running this workflow."
|
||||
exit 1
|
||||
fi
|
||||
- name: Attempt merge
|
||||
id: merge
|
||||
run: |
|
||||
set +e
|
||||
git merge \
|
||||
--no-ff \
|
||||
--no-edit \
|
||||
--commit \
|
||||
--message "Merged '$SRC_BRANCH' into '$TARGET_BRANCH'" \
|
||||
"origin/$SRC_BRANCH"
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "Merge conflicts detected; aborting in-runner merge."
|
||||
git merge --abort || true
|
||||
echo "conflict=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "conflict=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
exit 0
|
||||
- name: Push merged branch
|
||||
if: ${{ steps.merge.outputs.conflict == 'false' && !inputs.check_only }}
|
||||
run: git push origin "$TARGET_BRANCH"
|
||||
- name: Delete source branch
|
||||
if: ${{ inputs.delete_source_branch && steps.merge.outputs.conflict == 'false' && !inputs.check_only }}
|
||||
run: git push origin --delete "$SRC_BRANCH"
|
||||
- name: Create sync branch for conflict resolution
|
||||
id: sync-branch
|
||||
if: ${{ steps.merge.outputs.conflict == 'true' && !inputs.check_only }}
|
||||
run: |
|
||||
SAFE_SRC="${SRC_BRANCH//\//_}"
|
||||
SAFE_TARGET="${TARGET_BRANCH//\//_}"
|
||||
SHA_SHORT=$(git rev-parse --short "origin/$SRC_BRANCH")
|
||||
SYNC_BRANCH="${SAFE_SRC}-${SHA_SHORT}/merge-into-${SAFE_TARGET}"
|
||||
echo "Creating sync branch: $SYNC_BRANCH"
|
||||
# If a stale sync branch exists from a previously closed-but-unmerged PR, drop it.
|
||||
git push origin --delete "$SYNC_BRANCH" 2>/dev/null || true
|
||||
git branch -f "$SYNC_BRANCH" "origin/$SRC_BRANCH"
|
||||
git push origin "$SYNC_BRANCH"
|
||||
echo "SYNC_BRANCH=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
- name: Open conflict-resolution PR
|
||||
if: ${{ steps.merge.outputs.conflict == 'true' && !inputs.check_only }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SYNC_BRANCH: ${{ steps.sync-branch.outputs.SYNC_BRANCH }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base "$TARGET_BRANCH" \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "Conflict-resolution: merge '$SRC_BRANCH' into '$TARGET_BRANCH'" \
|
||||
--draft \
|
||||
--body "Automated merge of \`$SRC_BRANCH\` into \`$TARGET_BRANCH\` failed due to conflicts.
|
||||
|
||||
A temporary sync branch \`$SYNC_BRANCH\` was created off \`$SRC_BRANCH\` so conflicts can be resolved even when both branches are protected.
|
||||
|
||||
**Do not merge this PR through the GitHub UI.** Squash-merging a sync branch rewrites its commits, which inflates the commit count of later merges into \`$TARGET_BRANCH\`. Instead, a maintainer should:
|
||||
|
||||
1. Check out the sync branch locally:
|
||||
\`\`\`
|
||||
git fetch origin
|
||||
git checkout $SYNC_BRANCH
|
||||
\`\`\`
|
||||
|
||||
2. Merge '$TARGET_BRANCH' into the sync branch and resolve conflicts:
|
||||
\`\`\`
|
||||
git merge origin/$TARGET_BRANCH
|
||||
# resolve conflicts, then commit
|
||||
\`\`\`
|
||||
|
||||
3. Push the resolved sync branch:
|
||||
\`\`\`
|
||||
git push origin $SYNC_BRANCH
|
||||
\`\`\`
|
||||
|
||||
4. Re-run the 'Merge branch into target' workflow from this branch (\`$SYNC_BRANCH\`) with target branch '$TARGET_BRANCH'.
|
||||
|
||||
5. After the workflow completes, this PR will close automatically."
|
||||
- name: Fail on conflict
|
||||
if: ${{ steps.merge.outputs.conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Merge of '$SRC_BRANCH' into '$TARGET_BRANCH' has conflicts."
|
||||
exit 1
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Publish (PyPI + MCP Registry)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pypi:
|
||||
name: Publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # REQUIRED for PyPI Trusted Publishing (OIDC)
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate version matches tag
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
PYPROJECT_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml)
|
||||
SERVER_VERSION=$(grep -oP '"version":\s*"\K[^"]+' server.json | head -1)
|
||||
SERVER_PKG_VERSION=$(grep -oP '"version":\s*"\K[^"]+' server.json | tail -1)
|
||||
|
||||
ERRORS=0
|
||||
[ "$PYPROJECT_VERSION" != "$TAG_VERSION" ] && echo "pyproject.toml: $PYPROJECT_VERSION != $TAG_VERSION" && ERRORS=1
|
||||
[ "$SERVER_VERSION" != "$TAG_VERSION" ] && echo "server.json: $SERVER_VERSION != $TAG_VERSION" && ERRORS=1
|
||||
[ "$SERVER_PKG_VERSION" != "$TAG_VERSION" ] && echo "server.json package: $SERVER_PKG_VERSION != $TAG_VERSION" && ERRORS=1
|
||||
|
||||
if [ $ERRORS -eq 1 ]; then
|
||||
echo "Please update all version fields to $TAG_VERSION before creating the tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All versions match: $TAG_VERSION"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
run: pipx install uv
|
||||
|
||||
- name: Sync dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Run ruff lint
|
||||
run: uvx ruff check .
|
||||
|
||||
- name: Run ruff format check
|
||||
run: uvx ruff format --check .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -d tests ]; then
|
||||
uv run pytest -q -m "not slow and not integration" || [ $? -eq 5 ] # exit 5 = no tests collected
|
||||
else
|
||||
echo "No tests/ directory; skipping."
|
||||
fi
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: dist
|
||||
|
||||
registry:
|
||||
name: Publish to MCP Registry
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # REQUIRED for MCP Registry GitHub OIDC login
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Wait for PyPI metadata propagation
|
||||
run: sleep 20
|
||||
|
||||
- name: Install MCP Publisher
|
||||
run: |
|
||||
set -euo pipefail
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
||||
echo "Get the latest release version"
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest | jq -r '.tag_name')
|
||||
echo "Installing MCP Publisher version: $LATEST_VERSION"
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" \
|
||||
| tar xz mcp-publisher
|
||||
|
||||
- name: Login to MCP Registry
|
||||
run: ./mcp-publisher login github-oidc
|
||||
|
||||
- name: Publish to MCP Registry
|
||||
run: ./mcp-publisher publish
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Publish MCP Registry
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: publish-mcp-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mcp-registry:
|
||||
name: Publish to MCP Registry
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # REQUIRED for MCP Registry GitHub OIDC login
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install MCP Publisher
|
||||
run: |
|
||||
set -euo pipefail
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
||||
echo "Get the latest release version"
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest | jq -r '.tag_name')
|
||||
echo "Installing MCP Publisher version: $LATEST_VERSION"
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" \
|
||||
| tar xz mcp-publisher
|
||||
|
||||
- name: Login to MCP Registry
|
||||
run: ./mcp-publisher login github-oidc
|
||||
|
||||
- name: Publish to MCP Registry
|
||||
run: ./mcp-publisher publish
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Publish PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: publish-pypi-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pypi:
|
||||
name: Publish PyPI
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
run: pipx install uv
|
||||
|
||||
- name: Sync dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Run ruff lint
|
||||
run: uvx ruff check .
|
||||
|
||||
- name: Run ruff format check
|
||||
run: uvx ruff format --check .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -d tests ]; then
|
||||
uv run pytest -q -m "not slow and not integration" || [ $? -eq 5 ] # exit 5 = no tests collected
|
||||
else
|
||||
echo "No tests/ directory; skipping."
|
||||
fi
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: dist
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Ruff Lint & Format
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [develop, main]
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install Ruff
|
||||
run: pip install ruff
|
||||
|
||||
- name: Run Ruff check (lint)
|
||||
run: ruff check .
|
||||
|
||||
- name: Run Ruff format (verify formatting)
|
||||
run: ruff format --check .
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Sync main to develop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check if sync is needed
|
||||
outputs:
|
||||
skip: ${{ steps.check-sync.outputs.SKIP }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Check if sync is needed
|
||||
id: check-sync
|
||||
run: |
|
||||
# Use merge-tree to check if merging main into develop would produce
|
||||
# any content changes, without touching the working tree. This correctly
|
||||
# handles cherry-picks where the SHAs differ but the content is identical.
|
||||
if MERGE_TREE=$(git merge-tree --write-tree origin/develop origin/main 2>&1); then
|
||||
DEVELOP_TREE=$(git rev-parse origin/develop^{tree})
|
||||
echo "Merge result tree: $MERGE_TREE"
|
||||
echo "Develop tree: $DEVELOP_TREE"
|
||||
if [ "$MERGE_TREE" = "$DEVELOP_TREE" ]; then
|
||||
echo "Merge would not change develop. Skipping sync."
|
||||
echo "SKIP=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Merge would introduce changes. Sync needed."
|
||||
echo "SKIP=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "Merge would have conflicts. Sync needed."
|
||||
echo "SKIP=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
sync:
|
||||
needs: check
|
||||
if: needs.check.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
name: Sync main to develop
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up git user
|
||||
run: |
|
||||
git config user.name "RobotMCP Bot"
|
||||
git config user.email "admin@robotmcp.ai"
|
||||
- name: Create syncing branch off of main
|
||||
id: create-sync-branch
|
||||
run: |
|
||||
SYNC_BRANCH="main-$(git rev-parse --short HEAD)/sync"
|
||||
echo "Creating syncing branch: $SYNC_BRANCH"
|
||||
git switch --create "$SYNC_BRANCH"
|
||||
git push origin "$SYNC_BRANCH"
|
||||
echo "SYNC_BRANCH=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
- name: Create syncing PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SYNC_BRANCH: ${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base develop \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "chore: sync main to develop" \
|
||||
--draft \
|
||||
--body "Auto-syncing 'main' to 'develop'
|
||||
|
||||
If this PR does not merge automatically, there are conflicts between 'main' and 'develop'.
|
||||
|
||||
**Do not merge this PR through GitHub.** A maintainer should:
|
||||
|
||||
1. Pull this temporary syncing branch locally:
|
||||
\`\`\`
|
||||
git fetch origin
|
||||
git checkout $SYNC_BRANCH
|
||||
\`\`\`
|
||||
|
||||
2. Merge 'develop' into this branch and resolve conflicts:
|
||||
\`\`\`
|
||||
git merge origin/develop
|
||||
# resolve any conflicts, then commit
|
||||
\`\`\`
|
||||
|
||||
3. Push the resolved branch:
|
||||
\`\`\`
|
||||
git push origin $SYNC_BRANCH
|
||||
\`\`\`
|
||||
|
||||
4. Run the 'Merge branch into target' workflow from this branch with target branch 'develop'
|
||||
|
||||
5. After the workflow completes, this PR will close automatically"
|
||||
- name: Merge and push to develop
|
||||
run: |
|
||||
git switch develop
|
||||
git merge --no-ff --no-edit --commit --message "chore: sync main to develop" "${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}"
|
||||
git push origin develop
|
||||
- name: Cleanup sync branch
|
||||
continue-on-error: true
|
||||
env:
|
||||
SYNC_BRANCH: ${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}
|
||||
run: git push origin :"$SYNC_BRANCH"
|
||||
@@ -0,0 +1,151 @@
|
||||
name: Track View Metrics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Track Clone Metrics"] # exact name of PyPI workflow
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
view-stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper branch operations
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2.1.0
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Switch to metrics branch
|
||||
run: |
|
||||
# Checkout or create metrics branch
|
||||
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
|
||||
echo "📋 Checking out existing metrics branch..."
|
||||
git checkout -b metrics origin/metrics || git checkout metrics
|
||||
else
|
||||
echo "🆕 Creating new metrics branch..."
|
||||
git checkout -b metrics
|
||||
fi
|
||||
|
||||
- name: Fetch view data
|
||||
env:
|
||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
mkdir -p .metrics
|
||||
# Fetch view metrics (contains both daily breakdown and 14-day totals)
|
||||
curl -s -H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
https://api.github.com/repos/${{ github.repository }}/traffic/views \
|
||||
> .metrics/view_stats.json
|
||||
|
||||
echo "View metrics:"
|
||||
cat .metrics/view_stats.json
|
||||
|
||||
- name: Update daily metrics
|
||||
run: |
|
||||
# Process each day from the views array
|
||||
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Create daily CSV with header if it doesn't exist
|
||||
if [ ! -f .metrics/daily_view_metrics.csv ]; then
|
||||
echo "date,total_views,unique_visitors,last_updated" > .metrics/daily_view_metrics.csv
|
||||
fi
|
||||
|
||||
echo "📊 Processing daily metrics..."
|
||||
jq -r '.views[] | "\(.timestamp | split("T")[0]),\(.count),\(.uniques)"' .metrics/view_stats.json | while IFS=',' read -r day_date count uniques; do
|
||||
echo "Processing $day_date: $count views, $uniques unique"
|
||||
|
||||
# Check if this date already exists in the CSV
|
||||
if grep -q "^$day_date" .metrics/daily_view_metrics.csv; then
|
||||
echo "📝 Updating existing entry for $day_date..."
|
||||
# Update existing entry
|
||||
awk -v date="$day_date" -v count="$count" -v uniques="$uniques" -v last_updated="$LAST_UPDATED" '
|
||||
BEGIN { FS=","; OFS="," }
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
|
||||
print $1, count, uniques, last_updated;
|
||||
updated=1;
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' .metrics/daily_view_metrics.csv > .metrics/daily_view_metrics_temp.csv
|
||||
mv .metrics/daily_view_metrics_temp.csv .metrics/daily_view_metrics.csv
|
||||
else
|
||||
echo "➕ Adding new daily entry for $day_date..."
|
||||
echo "$day_date,$count,$uniques,$LAST_UPDATED" >> .metrics/daily_view_metrics.csv
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Daily metrics:"
|
||||
tail -n 5 .metrics/daily_view_metrics.csv
|
||||
|
||||
- name: Update 14-day rolling metrics
|
||||
run: |
|
||||
# Process 14-day metrics
|
||||
COUNT_14D=$(jq '.count' .metrics/view_stats.json)
|
||||
UNIQUES_14D=$(jq '.uniques' .metrics/view_stats.json)
|
||||
DATE_ONLY=$(date -u +"%Y-%m-%d")
|
||||
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
echo "📊 Processing 14-day metrics... for date: $DATE_ONLY"
|
||||
echo "Processing values: $COUNT_14D views, $UNIQUES_14D unique"
|
||||
|
||||
# Create 14-day CSV with header if it doesn't exist
|
||||
if [ ! -f .metrics/rolling_14d_view_metrics.csv ]; then
|
||||
echo "date,total_views_14d,unique_visitors_14d,last_updated" > .metrics/rolling_14d_view_metrics.csv
|
||||
echo "📄 Created new 14-day rolling CSV file"
|
||||
fi
|
||||
|
||||
# Check if today's date already exists in the 14-day CSV
|
||||
if grep -q "^$DATE_ONLY" .metrics/rolling_14d_view_metrics.csv; then
|
||||
echo "📝 Updating existing 14-day rolling entry for $DATE_ONLY..."
|
||||
# Update existing entry
|
||||
awk -v date="$DATE_ONLY" -v count="$COUNT_14D" -v uniques="$UNIQUES_14D" -v last_updated="$LAST_UPDATED" '
|
||||
BEGIN { FS=","; OFS=","; updated=0 }
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
|
||||
print $1, count, uniques, last_updated;
|
||||
updated=1;
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { if (!updated) print date, count, uniques, last_updated }
|
||||
' .metrics/rolling_14d_view_metrics.csv > .metrics/rolling_14d_view_metrics_temp.csv
|
||||
mv .metrics/rolling_14d_view_metrics_temp.csv .metrics/rolling_14d_view_metrics.csv
|
||||
echo "✅ Updated existing entry"
|
||||
else
|
||||
echo "➕ Adding new 14-day rolling entry for $DATE_ONLY..."
|
||||
echo "$DATE_ONLY,$COUNT_14D,$UNIQUES_14D,$LAST_UPDATED" >> .metrics/rolling_14d_view_metrics.csv
|
||||
echo "✅ Added new entry"
|
||||
fi
|
||||
|
||||
echo "14-day rolling metrics:"
|
||||
tail -n 5 .metrics/rolling_14d_view_metrics.csv
|
||||
|
||||
- name: Commit and push results
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "ViewMetricsBot[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Add both CSV files
|
||||
git add .metrics/daily_view_metrics.csv .metrics/rolling_14d_view_metrics.csv
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
echo "ℹ️ No changes to commit - CSV data is up to date"
|
||||
else
|
||||
echo "📝 Committing changes..."
|
||||
git commit -m "Automated update: repository view metrics $(date)"
|
||||
|
||||
echo "🚀 Pushing to metrics branch..."
|
||||
git push --force-with-lease origin metrics
|
||||
fi
|
||||
Reference in New Issue
Block a user