diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml index 61aa10b..0bccccc 100644 --- a/.github/workflows/congrats.yml +++ b/.github/workflows/congrats.yml @@ -1,5 +1,6 @@ -name: Congratulate on Merge - +# ───────────────────────────────────────────────────────────────────────────── +# PR Congratulations Workflow for OpenAgentHQ +# ───────────────────────────────────────────────────────────────────────────── # Automatically posts a congratulatory comment on merged Pull Requests. # # Features: @@ -10,11 +11,13 @@ 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 @@ -22,74 +25,150 @@ permissions: 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}`); + } \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bffa537..d8636c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 \ No newline at end of file diff --git a/docs/images/modeldock.png b/docs/images/modeldock.png new file mode 100644 index 0000000..fa338ec Binary files /dev/null and b/docs/images/modeldock.png differ