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
223 changes: 151 additions & 72 deletions .github/workflows/congrats.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Congratulate on Merge

# ─────────────────────────────────────────────────────────────────────────────
# PR Congratulations Workflow for OpenAgentHQ
# ─────────────────────────────────────────────────────────────────────────────
# Automatically posts a congratulatory comment on merged Pull Requests.
#
# Features:
Expand All @@ -10,86 +11,164 @@ name: Congratulate on Merge
#
# Trigger: pull_request_target → closed (only merged PRs targeting main)
# Permissions: pull-requests: write, contents: read (least privilege)
# ─────────────────────────────────────────────────────────────────────────────

name: PR Congratulations

on:
pull_request_target:
types: [closed]
branches: [main]

permissions:
pull-requests: write
contents: read

jobs:
congratulate:
name: Congratulate contributor
if: github.event.pull_request.merged == true
name: Post Congratulations
runs-on: ubuntu-latest

# Only run when the PR was actually merged (not just closed)
# and targets the main branch
if: >
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main'

steps:
- name: Log event
run: |
echo "PR #${{ github.event.pull_request.number }} merged by ${{ github.event.pull_request.user.login }}"
echo "Title: ${{ github.event.pull_request.title }}"

- name: Skip bots
id: check
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
run: |
if [ "$AUTHOR_TYPE" = "Bot" ]; then
echo "Author $AUTHOR is a bot — skipping congratulations."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Detect first contribution
if: steps.check.outputs.skip == 'false'
id: first
env:
GH_TOKEN: ${{ github.token }}
AUTHOR: ${{ github.event.pull_request.user.login }}
REPO: ${{ github.repository }}
run: |
COUNT=$(gh api "repos/$REPO/pulls?state=all&creator=$AUTHOR" \
--jq 'length')
echo "Total PRs by $AUTHOR (incl. this one): $COUNT"
if [ "$COUNT" -le 1 ]; then
echo "first=true" >> "$GITHUB_OUTPUT"
else
echo "first=false" >> "$GITHUB_OUTPUT"
fi

- name: Add labels
if: steps.check.outputs.skip == 'false' && steps.first.outputs.first == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
gh pr edit "$PR" --add-label "first contribution"
echo "Labeled PR #$PR as 'first contribution'."

- name: Post congratulatory comment
if: steps.check.outputs.skip == 'false'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
AUTHOR: ${{ github.event.pull_request.user.login }}
FIRST: ${{ steps.first.outputs.first }}
REPO: ${{ github.repository }}
run: |
if [ "$FIRST" = "true" ]; then
BODY=":tada: @$AUTHOR — congratulations on your **first contribution** to ModelDock! :tada

Your PR has been merged. We're thrilled to have you in the community — thank you for helping make local AI model management better for everyone.

If you're looking for what to tackle next, check out issues labeled \`good first issue\`."
else
BODY=":tada: Thanks @$AUTHOR — your PR has been merged! :tada

Appreciate the contribution to ModelDock. If you spot anything else to improve, we'd love another PR."
fi
gh pr comment "$PR" --body "$BODY"
echo "Posted congratulatory comment to PR #$PR."
# ─────────────────────────────────────────────────────────────────────
# Step 1: Detect first contribution and build comment
# ─────────────────────────────────────────────────────────────────────
- name: Detect first contribution and build comment
id: build-comment
uses: actions/github-script@v7
with:
script: |
// ── Extract PR metadata ──────────────────────────────────────
const pr = context.payload.pull_request;
const prNumber = pr.number;
const prTitle = pr.title;
const author = pr.user.login;
const authorType = pr.user.type;
const mergeCommitSha = pr.merge_commit_sha;
const labels = pr.labels.map(l => l.name);

// ── Skip bot authors ─────────────────────────────────────────
// GitHub marks bot accounts with type "Bot" or login ending in [bot]
if (authorType === 'Bot' || author.endsWith('[bot]')) {
core.info(`⏭️ Skipping bot author: ${author}`);
core.setOutput('action_taken', 'skipped_bot');
return;
}

core.info(`📋 PR #${prNumber}: "${prTitle}"`);
core.info(`👤 Author: ${author}`);
core.info(`🔀 Merge SHA: ${mergeCommitSha}`);

// ── Detect first contribution ────────────────────────────────
// Query merged PRs by this author targeting main.
// If count is 0 or 1 (this PR), it's their first contribution.
let isFirstContribution = false;
try {
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
q: `is:pr is:merged author:${author} base:main repo:${context.repo.owner}/${context.repo.repo}`,
per_page: 1,
});
// If 0 results, this is their first merged PR (the current one
// may not appear in search index yet). If exactly 1, it's this PR.
isFirstContribution = mergedPRs.total_count <= 1;
core.info(`🔍 Merged PRs by ${author} on main: ${mergedPRs.total_count}`);
} catch (error) {
core.warning(`⚠️ Could not query merged PRs: ${error.message}`);
// Default to false if API fails — don't break the workflow
isFirstContribution = false;
}

core.info(`🌟 First contribution: ${isFirstContribution}`);

// ── Check for special labels ─────────────────────────────────
const hasFirstContributionLabel = labels.some(
l => l.toLowerCase() === 'first contribution'
);
const hasGoodFirstIssueLabel = labels.some(
l => l.toLowerCase() === 'good first issue'
);
const showGreatFirstContribution = hasFirstContributionLabel || hasGoodFirstIssueLabel;

core.info(`🏷️ Labels: ${labels.join(', ') || '(none)'}`);
core.info(`🎊 Great first contribution: ${showGreatFirstContribution}`);

// ── Build the comment body ───────────────────────────────────
let comment = `🎉 Congratulations @${author}!\n\n`;
comment += `Your pull request has been successfully merged into **main**. 🚀\n\n`;
comment += `Thank you for contributing to OpenAgentHQ and helping improve the project.\n\n`;
comment += `We truly appreciate your contribution and hope to see you back with more amazing PRs!\n\n`;
comment += `Happy Open Sourcing! ❤️`;

// Append first contribution message
if (isFirstContribution) {
comment += `\n\n🌟 This is your first merged contribution to this repository.\n`;
comment += `Welcome to the OpenAgentHQ contributors family!`;
}

// Append great first contribution message
if (showGreatFirstContribution) {
comment += `\n\n🎊 Great first contribution!`;
}

// ── Set outputs for logging step ─────────────────────────────
core.setOutput('pr_number', prNumber);
core.setOutput('pr_title', prTitle);
core.setOutput('author', author);
core.setOutput('merge_commit_sha', mergeCommitSha);
core.setOutput('is_first_contribution', isFirstContribution);
core.setOutput('action_taken', 'commented');
core.setOutput('comment_body', comment);

core.info(`✅ Comment built successfully (${comment.length} chars)`);

# ─────────────────────────────────────────────────────────────────────
# Step 2: Post the comment on the PR
# ─────────────────────────────────────────────────────────────────────
- name: Post comment on PR
if: steps.build-comment.outputs.action_taken == 'commented'
uses: actions/github-script@v7
with:
script: |
const commentBody = `${{ steps.build-comment.outputs.comment_body }}`;
const prNumber = parseInt('${{ steps.build-comment.outputs.pr_number }}', 10);

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody,
});

core.info(`💬 Comment posted on PR #${prNumber}`);

# ─────────────────────────────────────────────────────────────────────
# Step 3: Structured logging for audit trail
# ─────────────────────────────────────────────────────────────────────
- name: Log workflow execution
if: always()
uses: actions/github-script@v7
with:
script: |
const actionTaken = '${{ steps.build-comment.outputs.action_taken }}';

core.startGroup('📊 Workflow Execution Summary');
core.info(`PR Number: ${{ steps.build-comment.outputs.pr_number }}`);
core.info(`PR Title: ${{ steps.build-comment.outputs.pr_title }}`);
core.info(`Author: ${{ steps.build-comment.outputs.author }}`);
core.info(`Merge Commit SHA: ${{ steps.build-comment.outputs.merge_commit_sha }}`);
core.info(`First Contrib.: ${{ steps.build-comment.outputs.is_first_contribution }}`);
core.info(`Action Taken: ${actionTaken}`);
core.endGroup();

if (actionTaken === 'skipped_bot') {
core.info('⏭️ No action taken — author is a bot.');
} else if (actionTaken === 'commented') {
core.info('✅ Congratulations comment posted successfully.');
} else {
core.warning(`⚠️ Unknown action: ${actionTaken}`);
}
114 changes: 66 additions & 48 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,71 +1,89 @@
# Release Workflow for modeldock
# Triggered by GitHub Release publish or version tag push
# Builds package and publishes to PyPI using Trusted Publishing (OIDC)

name: Release

on:
release:
types: [published]
push:
tags:
- "v*.*.*"
workflow_dispatch:
- "v*"

permissions:
contents: read
id-token: write # Required for Trusted Publishing (OIDC)

env:
PYTHON_DEFAULT: "3.11"

jobs:
build-and-publish:
name: Build & publish to PyPI
# ──────────────────────────────────────────────────────────────
# Build & Publish to PyPI
# ──────────────────────────────────────────────────────────────
publish:
name: Build & Publish to PyPI
runs-on: ubuntu-latest
environment: pypi
permissions:
# Required for trusted publishing (OIDC); remove if using API token.
id-token: write
# Required to create the GitHub Release via gh release create.
contents: write
environment:
name: pypi
url: https://pypi.org/p/modeldock/

steps:
- name: Checkout
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.12
python-version: ${{ env.PYTHON_DEFAULT }}

- name: Sync dependencies
run: uv sync --extra dev --extra ollama
- name: Install build tools
run: pip install build twine

- name: Verify version consistency
run: |
PKG_VER=$(uv run python -c "import tomllib,sys; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
INIT_VER=$(uv run python -c "import modeldock; print(modeldock.__version__)")
TAG=${GITHUB_REF_NAME#v}
echo "pyproject=$PKG_VER __init__=$INIT_VER tag=$TAG"
if [ "$PKG_VER" != "$INIT_VER" ] || [ "$PKG_VER" != "$TAG" ]; then
echo "Version mismatch: pyproject=$PKG_VER __init__=$INIT_VER tag=$TAG"
exit 1
fi
- name: Build package
run: python -m build

- name: Run quality gates
run: |
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src
uv run bandit -r src
uv run pytest
- name: Verify package
run: twine check dist/*

- name: Build distributions
run: uv build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 30

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# Uses trusted publishing (OIDC) by default. To use a token instead,
# set repository-url and pass the token via secrets.PYPI_API_TOKEN.
# Uses Trusted Publishing (OIDC) - no API token needed
# Requires configuring PyPI trusted publisher for:
# Repository: OpenAgentHQ/modeldock
# Workflow: release.yml
# Environment: pypi

- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--generate-notes
# ──────────────────────────────────────────────────────────────
# Create GitHub Release Artifacts
# ──────────────────────────────────────────────────────────────
release-artifacts:
name: Upload Release Artifacts
runs-on: ubuntu-latest
needs: publish
permissions:
contents: write

steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Upload wheel to release
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
dist/*.tar.gz
draft: false
prerelease: false
Binary file added docs/images/modeldock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading