create asana mcp server guide #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Guide draft | |
| on: | |
| issues: | |
| types: [labeled] | |
| jobs: | |
| draft: | |
| if: github.event.label.name == 'guide:draft' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| concurrency: | |
| group: guide-draft-issue-${{ github.event.issue.number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| GH_TOKEN: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| steps: | |
| - name: Ensure factory labels | |
| run: | | |
| set -euo pipefail | |
| # Repo labels must exist before gh issue edit --add-label (missing label = hard fail). | |
| ensure_label() { | |
| local name="$1" color="$2" desc="$3" | |
| if gh label list --limit 100 --json name --jq '.[].name' | grep -Fxq "$name"; then | |
| return 0 | |
| fi | |
| gh label create "$name" --color "$color" --description "$desc" | |
| } | |
| ensure_label "guide:draft" "1D76DB" "Trigger guide draft factory" | |
| ensure_label "guide:in-progress" "FBCA04" "Guide draft factory running" | |
| ensure_label "guide:blocked" "D73A4A" "Guide draft factory blocked" | |
| - name: Preflight existing PR | |
| id: preflight | |
| run: | | |
| set -euo pipefail | |
| existing=$(gh pr list \ | |
| --state open \ | |
| --search "in:body \"#${ISSUE_NUMBER}\"" \ | |
| --json number,url,body,author \ | |
| --jq "[.[] | select(.body | test(\"(?i)(closes|fixes|resolves)\\\\s+#${ISSUE_NUMBER}\\\\b\"))]") | |
| count=$(echo "$existing" | jq 'length') | |
| refused=false | |
| existing_pr_url="" | |
| for i in $(seq 0 $((count - 1))); do | |
| author=$(echo "$existing" | jq -r ".[$i].author.login") | |
| # Only PRs from collaborators block; outside contributors don't count. | |
| if gh api "repos/${GH_REPO}/collaborators/${author}" --silent 2>/dev/null; then | |
| refused=true | |
| existing_pr_url=$(echo "$existing" | jq -r ".[$i].url") | |
| break | |
| fi | |
| done | |
| echo "refused=$refused" >> "$GITHUB_OUTPUT" | |
| echo "existing_pr_url=$existing_pr_url" >> "$GITHUB_OUTPUT" | |
| - name: Refuse existing PR | |
| if: steps.preflight.outputs.refused == 'true' | |
| env: | |
| EXISTING_PR_URL: ${{ steps.preflight.outputs.existing_pr_url }} | |
| run: | | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:draft" || true | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:blocked" || true | |
| gh issue comment "$ISSUE_NUMBER" --body "Refused to run: $EXISTING_PR_URL already targets this issue. Close it first, then re-add \`guide:draft\`." | |
| - name: Transition labels | |
| if: steps.preflight.outputs.refused != 'true' | |
| run: | | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:draft" || true | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:blocked" || true | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:in-progress" | |
| - name: Checkout main | |
| if: steps.preflight.outputs.refused != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| token: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| if: steps.preflight.outputs.refused != 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: scripts/cursor-sdk/package-lock.json | |
| - name: Install dependencies | |
| if: steps.preflight.outputs.refused != 'true' | |
| working-directory: scripts/cursor-sdk | |
| run: npm ci | |
| - name: Distill issue intent | |
| id: distill | |
| if: steps.preflight.outputs.refused != 'true' | |
| working-directory: scripts/cursor-sdk | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CURSOR_API_KEY:-}" ]; then | |
| echo "CURSOR_API_KEY secret is not set" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| # Fold issue comments into distill context so clarifications on the | |
| # ticket (not only the body) reach --notes on retry. | |
| comments=$(gh api "repos/${GH_REPO}/issues/${ISSUE_NUMBER}/comments" \ | |
| --jq '[.[].body] | join("\n\n---\n\n")' \ | |
| 2>/dev/null || echo "") | |
| if [ -n "${comments}" ]; then | |
| # Keep every line of this assignment indented — bare "## ..." at | |
| # column 0 ends the YAML `run: |` block and breaks the workflow. | |
| { | |
| printf '%s\n\n' "${ISSUE_BODY:-}" | |
| echo "## Issue thread (for clarifications)" | |
| printf '%s\n' "${comments}" | |
| } > "${RUNNER_TEMP}/issue-body-with-thread.txt" | |
| export ISSUE_BODY="$(cat "${RUNNER_TEMP}/issue-body-with-thread.txt")" | |
| fi | |
| set +e | |
| npm run resolve-issue -- --output "${RUNNER_TEMP}/resolved.json" | |
| code=$? | |
| set -e | |
| if [ ! -f "${RUNNER_TEMP}/resolved.json" ]; then | |
| echo "resolve-issue produced no resolved.json (exit ${code})" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| status=$(jq -r '.status' "${RUNNER_TEMP}/resolved.json") | |
| if [ "$status" = "ok" ]; then | |
| slug=$(jq -r '.slug' "${RUNNER_TEMP}/resolved.json") | |
| provider=$(jq -r '.provider' "${RUNNER_TEMP}/resolved.json") | |
| persona=$(jq -r '.persona // "it-admin"' "${RUNNER_TEMP}/resolved.json") | |
| notes=$(jq -r '.notes // ""' "${RUNNER_TEMP}/resolved.json") | |
| echo "slug=$slug" >> "$GITHUB_OUTPUT" | |
| echo "provider=$provider" >> "$GITHUB_OUTPUT" | |
| echo "persona=$persona" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "notes<<EOF" | |
| echo "$notes" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$status" = "needs_clarification" ]; then | |
| reason=$(jq -r '.reason' "${RUNNER_TEMP}/resolved.json") | |
| candidates=$(jq -r '(.candidates // []) | join(", ")' "${RUNNER_TEMP}/resolved.json") | |
| { | |
| echo "Distill needs clarification before drafting." | |
| echo | |
| echo "**Reason:** ${reason}" | |
| if [ -n "$candidates" ]; then | |
| echo | |
| echo "**Candidates:** ${candidates}" | |
| fi | |
| echo | |
| echo "Reply on this issue (or edit the body) clarifying which MCP server, then re-add \`guide:draft\`." | |
| } > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| echo "Unexpected distill status: ${status}" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| - name: Comment resolved intent | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PROVIDER: ${{ steps.distill.outputs.provider }} | |
| PERSONA: ${{ steps.distill.outputs.persona }} | |
| NOTES: ${{ steps.distill.outputs.notes }} | |
| run: | | |
| set -euo pipefail | |
| body_file="${RUNNER_TEMP}/resolved-comment.md" | |
| { | |
| echo "Resolved as \`${SLUG}\` (${PROVIDER}), persona \`${PERSONA}\`." | |
| if [ -n "${NOTES}" ]; then | |
| echo | |
| echo "Notes handed to the pipeline:" | |
| echo | |
| echo "> ${NOTES}" | |
| fi | |
| echo | |
| echo "Starting \`draft-guide\`…" | |
| } > "$body_file" | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Create branch | |
| id: branch | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| set -euo pipefail | |
| branch="guide/issue-${ISSUE_NUMBER}-${SLUG}" | |
| echo "name=$branch" >> "$GITHUB_OUTPUT" | |
| git config user.name "guide-factory[bot]" | |
| git config user.email "guide-factory[bot]@users.noreply.github.com" | |
| git checkout -b "$branch" | |
| - name: Draft guide | |
| id: draft | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| working-directory: scripts/cursor-sdk | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PERSONA: ${{ steps.distill.outputs.persona }} | |
| NOTES: ${{ steps.distill.outputs.notes }} | |
| run: | | |
| set -euo pipefail | |
| args=(--overwrite) | |
| if [ -n "${PERSONA}" ] && [ "${PERSONA}" != "it-admin" ]; then | |
| args+=(--persona "${PERSONA}") | |
| fi | |
| if [ -n "${NOTES}" ]; then | |
| args+=(--notes "${NOTES}") | |
| fi | |
| set +e | |
| npm run draft-guide -- "${SLUG}" "${args[@]}" | |
| code=$? | |
| set -e | |
| # Prefer the newest run record for this slug (written even on unconverged). | |
| record="" | |
| if compgen -G "${GITHUB_WORKSPACE}/retro/runs/*-${SLUG}.json" > /dev/null; then | |
| record=$(ls -t "${GITHUB_WORKSPACE}"/retro/runs/*-"${SLUG}".json | head -n1) | |
| echo "record=${record}" >> "$GITHUB_OUTPUT" | |
| cp "$record" "${RUNNER_TEMP}/run-record.json" | |
| fi | |
| # CLI: 0 = converged, 2 = unconverged/blocked/failed guide status, | |
| # 1 = hard failure. Exit 2 still wrote files — open a PR for humans. | |
| if [ "$code" -eq 0 ]; then | |
| echo "outcome=converged" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$code" -eq 2 ]; then | |
| if [ -d "${GITHUB_WORKSPACE}/guides/${SLUG}" ]; then | |
| echo "outcome=unconverged" >> "$GITHUB_OUTPUT" | |
| echo "draft-guide exited 2 (unconverged/blocked/failed). Opening a draft PR with whatever was written for human review." > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 0 | |
| fi | |
| echo "draft-guide exited 2 and guides/${SLUG}/ is missing" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| echo "draft-guide exited ${code} (hard failure; see workflow logs)" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit "$code" | |
| - name: Commit and push | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| BRANCH: ${{ steps.branch.outputs.name }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| set -euo pipefail | |
| git add "guides/${SLUG}/" || true | |
| # Matching run records for this slug (any timestamp prefix). | |
| if compgen -G "retro/runs/*-${SLUG}.json" > /dev/null; then | |
| git add retro/runs/*-"${SLUG}".json | |
| fi | |
| if git diff --cached --quiet; then | |
| echo "No guide or run-record changes to commit" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| git commit -m "Draft guide: ${SLUG} (issue #${ISSUE_NUMBER})" | |
| git push --force-with-lease origin "$BRANCH" | |
| - name: Open draft PR | |
| id: open_pr | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| BRANCH: ${{ steps.branch.outputs.name }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PROVIDER: ${{ steps.distill.outputs.provider }} | |
| OUTCOME: ${{ steps.draft.outputs.outcome }} | |
| run: | | |
| set -euo pipefail | |
| title="Draft guide: ${PROVIDER} (#${ISSUE_NUMBER})" | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| title="Draft guide (unconverged): ${PROVIDER} (#${ISSUE_NUMBER})" | |
| fi | |
| title="${title:0:256}" | |
| body_file="${RUNNER_TEMP}/pr-body.md" | |
| { | |
| echo "Closes #${ISSUE_NUMBER}" | |
| echo | |
| echo "Factory draft of \`guides/${SLUG}/\` via \`draft-guide\` (Cursor SDK)." | |
| echo | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| echo "**Pipeline status:** unconverged (reviewers still had blockers after max rounds)." | |
| echo "See the issue comment **Pipeline review** for unresolved blockers and open questions." | |
| echo "Do not merge until those are settled." | |
| echo | |
| fi | |
| echo "Human review still required — agents never commit; this Action did." | |
| } > "$body_file" | |
| # Append a short blocker summary into the PR body when present. | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" | |
| fi | |
| pr_url=$(gh pr create --draft --base main --head "$BRANCH" --title "$title" --body-file "$body_file" | tail -n1) | |
| echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT" | |
| - name: Comment pipeline review on issue | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| PR_URL: ${{ steps.open_pr.outputs.pr_url }} | |
| OUTCOME: ${{ steps.draft.outputs.outcome }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| set -euo pipefail | |
| body_file="${RUNNER_TEMP}/pipeline-review-comment.md" | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "${PR_URL}" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" > "$body_file" | |
| else | |
| { | |
| echo "## Pipeline review" | |
| echo | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| echo "Draft PR opened (pipeline **unconverged**): ${PR_URL}" | |
| else | |
| echo "Draft PR opened: ${PR_URL}" | |
| fi | |
| echo | |
| echo "_No run record found to summarize blockers / open questions._" | |
| } > "$body_file" | |
| fi | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Mark blocked on failure | |
| if: steps.preflight.outputs.refused != 'true' && failure() | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| reason="(no reason file written; check workflow logs)" | |
| if [ -f "${RUNNER_TEMP}/failure_reason.txt" ]; then | |
| reason=$(cat "${RUNNER_TEMP}/failure_reason.txt") | |
| fi | |
| body_file="${RUNNER_TEMP}/failure-comment.md" | |
| { | |
| echo "\`guide:draft\` run failed (no PR opened)." | |
| echo | |
| echo "$reason" | |
| echo | |
| echo "**Workflow run:** ${RUN_URL}" | |
| echo | |
| } > "$body_file" | |
| # If a run record exists from a partial draft, surface blockers here too. | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" || true | |
| elif [ -n "${SLUG}" ] && compgen -G "${GITHUB_WORKSPACE}/retro/runs/*-${SLUG}.json" > /dev/null; then | |
| record=$(ls -t "${GITHUB_WORKSPACE}"/retro/runs/*-"${SLUG}".json | head -n1) | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "$record" "" "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" || true | |
| else | |
| { | |
| echo "Reply on this issue with clarifications, then re-add \`guide:draft\`." | |
| } >> "$body_file" | |
| fi | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:blocked" || true | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Always remove in-progress | |
| if: always() && steps.preflight.outputs.refused != 'true' | |
| run: gh issue edit "$ISSUE_NUMBER" --remove-label "guide:in-progress" || true |