diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml new file mode 100644 index 000000000..cc1617688 --- /dev/null +++ b/.github/workflows/docs-sync.yml @@ -0,0 +1,324 @@ +name: Docs Sync + +on: + workflow_run: + workflows: ['Release'] + types: [completed] + branches: [prod] + + workflow_dispatch: + inputs: + days_back: + description: 'Number of days to look back for code changes' + required: false + default: '30' + type: string + +permissions: + contents: write + pull-requests: write + id-token: write + +concurrency: + group: docs-sync + cancel-in-progress: true + +jobs: + docs-sync: + runs-on: ubuntu-latest + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + timeout-minutes: 75 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Detect changes from release + if: github.event_name == 'workflow_run' + id: release_changes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Triggered by Release workflow completion." + + # Get the two most recent release tags + RELEASE_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') + PREV_TAG=$(gh release list --limit 2 --json tagName --jq '.[1].tagName') + + if [ -z "$RELEASE_TAG" ]; then + echo "No release tag found. Skipping." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Latest release: $RELEASE_TAG" + echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" + + if [ -z "$PREV_TAG" ]; then + echo "No previous tag found. Scanning all files in release tag." + CHANGED_FILES=$(git ls-tree -r --name-only "$RELEASE_TAG" -- \ + 'keeperhub/plugins/' \ + 'keeperhub/api/' \ + 'keeperhub/lib/' \ + 'lib/' \ + 'plugins/' \ + 'app/api/' \ + | grep -E '\.(ts|tsx|js|jsx)$' \ + | grep -vE '\.(test|spec|stories)\.' \ + | head -100 \ + || true) + else + echo "Previous release: $PREV_TAG" + CHANGED_FILES=$(git diff --name-only "$PREV_TAG".."$RELEASE_TAG" -- \ + 'keeperhub/plugins/' \ + 'keeperhub/api/' \ + 'keeperhub/lib/' \ + 'lib/' \ + 'plugins/' \ + 'app/api/' \ + | grep -E '\.(ts|tsx|js|jsx)$' \ + | grep -vE '\.(test|spec|stories)\.' \ + | head -100 \ + || true) + fi + + if [ -z "$CHANGED_FILES" ]; then + echo "No relevant code changes detected between tags." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ') + echo "Found $FILE_COUNT changed files." + echo "has_changes=true" >> "$GITHUB_OUTPUT" + + # Save file list to output using heredoc + echo "changed_files<> "$GITHUB_OUTPUT" + echo "$CHANGED_FILES" >> "$GITHUB_OUTPUT" + echo "CHANGED_FILES_EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Detect changes from manual trigger + if: github.event_name == 'workflow_dispatch' + id: manual_changes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + DAYS_BACK="${{ github.event.inputs.days_back }}" + + # Validate that days_back is a positive integer + if ! echo "$DAYS_BACK" | grep -qE '^[0-9]+$' || [ "$DAYS_BACK" -lt 1 ] || [ "$DAYS_BACK" -gt 365 ]; then + echo "Invalid days_back value: $DAYS_BACK. Must be a number between 1 and 365." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Manual trigger: scanning changes from the last $DAYS_BACK days." + + # Try to get the latest release tag for labeling + RELEASE_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || echo "manual-sync") + echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" + + CHANGED_FILES=$(git log --since="${DAYS_BACK} days ago" --name-only --pretty=format: -- \ + 'keeperhub/plugins/' \ + 'keeperhub/api/' \ + 'keeperhub/lib/' \ + 'lib/' \ + 'plugins/' \ + 'app/api/' \ + | sort -u \ + | grep -E '\.(ts|tsx|js|jsx)$' \ + | grep -vE '\.(test|spec|stories)\.' \ + | head -100 \ + || true) + + if [ -z "$CHANGED_FILES" ]; then + echo "No relevant code changes in the last $DAYS_BACK days." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ') + echo "Found $FILE_COUNT changed files." + echo "has_changes=true" >> "$GITHUB_OUTPUT" + + echo "changed_files<> "$GITHUB_OUTPUT" + echo "$CHANGED_FILES" >> "$GITHUB_OUTPUT" + echo "CHANGED_FILES_EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Skip if no changes + if: >- + (github.event_name == 'workflow_run' && steps.release_changes.outputs.has_changes != 'true') || + (github.event_name == 'workflow_dispatch' && steps.manual_changes.outputs.has_changes != 'true') + run: | + echo "No relevant code changes detected. Skipping docs sync." + + - name: Set release tag + if: >- + (github.event_name == 'workflow_run' && steps.release_changes.outputs.has_changes == 'true') || + (github.event_name == 'workflow_dispatch' && steps.manual_changes.outputs.has_changes == 'true') + id: tag + run: | + if [ "${{ github.event_name }}" = "workflow_run" ]; then + echo "release_tag=${{ steps.release_changes.outputs.release_tag }}" >> "$GITHUB_OUTPUT" + else + echo "release_tag=${{ steps.manual_changes.outputs.release_tag }}" >> "$GITHUB_OUTPUT" + fi + + - name: Validate Anthropic API key + if: steps.tag.outputs.release_tag != '' + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://api.anthropic.com/v1/messages \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "content-type: application/json" \ + -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}') + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" = "200" ]; then + echo "API key is valid and has sufficient credits." + else + echo "::error::Anthropic API pre-flight check failed (HTTP $HTTP_CODE): $BODY" + exit 1 + fi + + - name: Run Claude Code docs sync + if: steps.tag.outputs.release_tag != '' + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + base_branch: staging + branch_prefix: docs-sync- + prompt: | + REPO: ${{ github.repository }} + RELEASE TAG: ${{ steps.tag.outputs.release_tag }} + + CHANGED FILES: + ${{ github.event_name == 'workflow_run' && steps.release_changes.outputs.changed_files || steps.manual_changes.outputs.changed_files }} + + --- + + You are a documentation maintenance agent for KeeperHub, a blockchain automation + platform. A new release has been published and the code files listed above have + changed. Your job is to ensure the documentation in `/docs/` stays accurate and + in sync with the codebase. + + ## Documentation Infrastructure + + - Documentation lives in the `/docs/` directory at the repository root. + - The docs site uses Nextra 4 (a Next.js-based docs framework). Content is plain + Markdown with YAML frontmatter. + - Navigation is controlled by `_meta.json` files in each directory. The root + `_meta.json` at `/docs/_meta.json` defines the top-level sections. Each + subdirectory has its own `_meta.json` for page ordering within that section. + - There are approximately 33 documentation pages organized across these sections: + intro, getting-started, keepers, workflows, keeper-runs, notifications, + wallet-management, users-teams-orgs, practices, api, FAQ. + - Two sections are hidden from navigation: `api` and `plans-features`. + + ## Documentation Style Rules + + Follow these rules strictly when editing or creating documentation: + + - Every page starts with YAML frontmatter containing `title` and `description`. + - Use one `# H1` heading per page matching the frontmatter title. + - Use `## H2` for major sections, `### H3` for subsections. + - Write in a professional, instructive tone. Use second person ("you") for + instructions and third person for feature descriptions. + - Use short paragraphs, bullet lists, and tables. Use bold for emphasis and key + terms. + - Do NOT use emojis anywhere in the documentation. This is strictly enforced. + - Use code blocks for configuration examples, API responses, addresses, and code + snippets. + - File and directory naming convention is kebab-case. + + ## Your Task + + 1. Read each changed file listed above to understand what was modified. + Focus on: API endpoint changes, plugin interface changes, configuration + changes, new features, removed features, changed function signatures. + + 2. For each meaningful change, search the `/docs/` directory for related + documentation pages. Check: + - Are code examples in the docs still correct? + - Are API signatures, types, and endpoints still accurate? + - Are plugin configuration options still complete and correct? + - Are instructions and procedures still valid? + + 3. Fix ONLY what is broken or inaccurate: + - Update incorrect code examples to match the current code. + - Fix wrong API signatures, types, and endpoint paths. + - Update outdated configuration examples. + - Add minimal documentation for significant new features that have absolutely + no existing documentation coverage. Only do this for user-facing features. + - If a plugin gained new configuration options, add them to the relevant + docs table or list. + + 4. Do NOT do any of the following: + - Do NOT rewrite working documentation for style or tone improvements. + - Do NOT add changelog or release notes entries. + - Do NOT reorganize existing documentation structure or navigation. + - Do NOT add sections about internal implementation details. + - Do NOT modify documentation that is already accurate. + - Do NOT create documentation for test utilities, internal helpers, or + non-user-facing code. + + 5. If you create any new documentation pages: + - Use kebab-case filenames. + - Add the page to the appropriate `_meta.json` file. + - Include proper YAML frontmatter with title and description. + - Match the writing style and depth of existing pages in that section. + + 6. If after analyzing all changes you determine that no documentation updates + are needed (the docs are already accurate), report that finding and do not + create a PR. Simply state: "Documentation is up to date. No changes needed." + + 7. If you do make changes, commit them to the branch. The CI system will + automatically create a pull request from your commits. Use a commit message + format like: "docs: sync documentation with ${{ steps.tag.outputs.release_tag }}" + and include a summary of what was updated and why in the commit body. + claude_args: | + --model claude-sonnet-4-5-20250929 + --max-turns 30 + --allowedTools "Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*)" + + - name: Notify Discord on docs update + if: steps.claude.outcome == 'success' && steps.claude.outputs.branch_name != '' + continue-on-error: true + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_TAG="${{ steps.tag.outputs.release_tag }}" + BRANCH="${{ steps.claude.outputs.branch_name }}" + + # Find the PR created from this branch + PR_JSON=$(gh pr list --head "$BRANCH" --state open --json number,title,body --limit 1) + PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number // empty') + + if [ -z "$PR_NUMBER" ]; then + echo "No docs PR found. Skipping notification." + exit 0 + fi + + PR_URL="https://github.com/${{ github.repository }}/pull/$PR_NUMBER" + PR_BODY=$(echo "$PR_JSON" | jq -r '.[0].body // ""' | head -c 1500) + + # Build message safely with jq + MESSAGE=$(jq -n \ + --arg tag "$RELEASE_TAG" \ + --arg body "$PR_BODY" \ + --arg url "$PR_URL" \ + '"**Docs Update** for `" + $tag + "`\n\n" + $body + "\n\nPR: " + $url') + + # Send to Discord (jq output is already JSON-escaped string) + jq -n --argjson content "$MESSAGE" \ + '{"username": "KeeperHub Docs Bot", "content": $content}' \ + | curl -s -H "Content-Type: application/json" -d @- "$DISCORD_WEBHOOK" diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 000000000..169ddbc85 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,54 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize] + branches: + - staging + +jobs: + check-title: + runs-on: ubuntu-latest + + steps: + - name: Validate PR title follows conventional commit format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + # Allowed prefixes (must match release.yml classification) + PATTERN="^(feat|fix|bug|hotfix|breaking|chore|docs|refactor|test|ci|build|perf|style)(\([^)]*\))?[!]?:[[:space:]].+" + + if printf '%s' "$PR_TITLE" | grep -qiE "$PATTERN"; then + echo "PR title is valid: $PR_TITLE" + else + echo "----------------------------------------------" + echo " ERROR: PR title does not follow the required format" + echo "----------------------------------------------" + echo "" + echo " Got: $PR_TITLE" + echo "" + echo " Expected format:" + echo " : " + echo " (scope): " + echo "" + echo " Allowed types:" + echo " feat: New feature" + echo " fix: Bug fix" + echo " hotfix: Urgent bug fix" + echo " chore: Maintenance task" + echo " docs: Documentation" + echo " refactor: Code refactoring" + echo " test: Tests" + echo " ci: CI/CD changes" + echo " build: Build system" + echo " perf: Performance" + echo " style: Code style" + echo " breaking: Breaking change" + echo "" + echo " Examples:" + echo " feat: KEEP-1234 Add user authentication" + echo " fix(web3): KEEP-1234 Resolve wallet connection timeout" + echo " chore: Update dependencies" + echo "" + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..84bd4b190 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,316 @@ +name: Release + +on: + push: + branches: + - prod + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get previous release tag + id: prev_tag + run: | + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$PREV_TAG" ]; then + echo "No previous tags found. This will be the first release." + echo "tag=" >> "$GITHUB_OUTPUT" + echo "first_release=true" >> "$GITHUB_OUTPUT" + else + echo "Previous tag: $PREV_TAG" + echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT" + echo "first_release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Discover merged PRs since last release + id: discover_prs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PREV_TAG="${{ steps.prev_tag.outputs.tag }}" + FIRST_RELEASE="${{ steps.prev_tag.outputs.first_release }}" + + # Upper bound: the author date of HEAD (the prod push commit). + # This prevents including staging PRs merged after the prod push. + UNTIL=$(git log -1 --format=%aI HEAD) + echo "Upper bound (HEAD author date): $UNTIL" + + if [ "$FIRST_RELEASE" = "true" ]; then + # No previous tag: get all merged PRs to staging up to the prod push + echo "First release -- discovering all PRs merged to staging before $UNTIL" + PR_JSON=$(gh pr list \ + --state merged \ + --base staging \ + --json number,title,author,url,body,mergedAt \ + --limit 200 \ + --jq "[.[] | select(.mergedAt <= \"$UNTIL\")]") + else + # Get the date of the previous tag commit + SINCE=$(git log -1 --format=%aI "$PREV_TAG") + echo "Finding PRs merged after: $SINCE and before: $UNTIL" + + PR_JSON=$(gh pr list \ + --state merged \ + --base staging \ + --json number,title,author,url,mergedAt,body \ + --limit 200 \ + --jq "[.[] | select(.mergedAt > \"$SINCE\" and .mergedAt <= \"$UNTIL\")]") + fi + + PR_COUNT=$(echo "$PR_JSON" | jq 'length') + echo "Found $PR_COUNT merged PR(s) since last release." + + if [ "$PR_COUNT" -eq 0 ]; then + echo "has_prs=false" >> "$GITHUB_OUTPUT" + else + echo "has_prs=true" >> "$GITHUB_OUTPUT" + fi + + # Save PR JSON for subsequent steps + echo "PR_JSON<> "$GITHUB_ENV" + echo "$PR_JSON" >> "$GITHUB_ENV" + echo "PRJSONEOF" >> "$GITHUB_ENV" + + - name: Skip release if no PRs found + if: steps.discover_prs.outputs.has_prs == 'false' + run: | + echo "No merged PRs found since last release. Skipping release creation." + + - name: Determine version bump and classify PRs + if: steps.discover_prs.outputs.has_prs == 'true' + id: version + run: | + PREV_TAG="${{ steps.prev_tag.outputs.tag }}" + FIRST_RELEASE="${{ steps.prev_tag.outputs.first_release }}" + + HAS_BREAKING=false + HAS_FEAT=false + + BREAKING_PRS="" + FEAT_PRS="" + FIX_PRS="" + OTHER_PRS="" + + # Process each PR + while IFS= read -r pr_line; do + NUMBER=$(echo "$pr_line" | jq -r '.number') + TITLE=$(echo "$pr_line" | jq -r '.title') + AUTHOR=$(echo "$pr_line" | jq -r '.author.login') + URL=$(echo "$pr_line" | jq -r '.url') + BODY=$(echo "$pr_line" | jq -r '.body // ""') + + # Clean the title: remove leading/trailing whitespace + TITLE=$(printf '%s' "$TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + # Strip the prefix from the title for display + DISPLAY_TITLE=$(printf '%s' "$TITLE" | sed -E 's/^(feat|fix|bug|hotfix|breaking|chore|docs|refactor|test|ci|build|perf|style)(\([^)]*\))?[!]?:[[:space:]]*//') + + # If stripping produced empty string, use original title + if [ -z "$DISPLAY_TITLE" ]; then + DISPLAY_TITLE="$TITLE" + fi + + # Build the entry line + ENTRY="- ${DISPLAY_TITLE} ([#${NUMBER}](${URL})) @${AUTHOR}" + + # Classify by prefix + # Check for breaking changes: prefix "breaking:" or "!" before colon or BREAKING CHANGE in body + if printf '%s' "$TITLE" | grep -qiE "^breaking(\([^)]*\))?:"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + elif printf '%s' "$TITLE" | grep -qiE "^[a-z]+(\([^)]*\))?!:"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + elif printf '%s' "$BODY" | grep -q "BREAKING CHANGE"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + elif printf '%s' "$TITLE" | grep -qiE "^feat(\([^)]*\))?:"; then + HAS_FEAT=true + FEAT_PRS="${FEAT_PRS}${ENTRY}"$'\n' + elif printf '%s' "$TITLE" | grep -qiE "^(fix|bug|hotfix)(\([^)]*\))?:"; then + FIX_PRS="${FIX_PRS}${ENTRY}"$'\n' + else + OTHER_PRS="${OTHER_PRS}${ENTRY}"$'\n' + fi + done < <(echo "$PR_JSON" | jq -c '.[]') + + # Determine bump type + if [ "$HAS_BREAKING" = true ]; then + BUMP="major" + elif [ "$HAS_FEAT" = true ]; then + BUMP="minor" + else + BUMP="patch" + fi + + echo "Version bump: $BUMP" + + # Calculate new version + if [ "$FIRST_RELEASE" = "true" ]; then + # No previous tags: start at v0.1.0 + NEW_TAG="v0.1.0" + else + VERSION="${PREV_TAG#v}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + PATCH=$(echo "$VERSION" | cut -d. -f3) + + case "$BUMP" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}" + fi + + echo "New tag: $NEW_TAG" + + # Check if tag already exists + if git tag -l "$NEW_TAG" | grep -q "$NEW_TAG"; then + echo "Tag $NEW_TAG already exists. Aborting." + exit 1 + fi + + echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT" + echo "bump=$BUMP" >> "$GITHUB_OUTPUT" + + # Build release notes + REPO_URL="https://github.com/${{ github.repository }}" + NOTES="## What's Changed"$'\n' + + if [ -n "$BREAKING_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Breaking Changes"$'\n' + NOTES="${NOTES}${BREAKING_PRS}" + fi + + if [ -n "$FEAT_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Features"$'\n' + NOTES="${NOTES}${FEAT_PRS}" + fi + + if [ -n "$FIX_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Bug Fixes"$'\n' + NOTES="${NOTES}${FIX_PRS}" + fi + + if [ -n "$OTHER_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Other Changes"$'\n' + NOTES="${NOTES}${OTHER_PRS}" + fi + + if [ "$FIRST_RELEASE" = "true" ]; then + NOTES="${NOTES}"$'\n'"**Full Changelog**: ${REPO_URL}/commits/${NEW_TAG}" + else + NOTES="${NOTES}"$'\n'"**Full Changelog**: ${REPO_URL}/compare/${PREV_TAG}...${NEW_TAG}" + fi + + # Save notes to a file for gh release create + echo "$NOTES" > /tmp/release-notes.md + + echo "Release notes written to /tmp/release-notes.md" + + - name: Create GitHub Release + if: steps.discover_prs.outputs.has_prs == 'true' + id: create_release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + NEW_TAG="${{ steps.version.outputs.new_tag }}" + REPO_URL="https://github.com/${{ github.repository }}" + + gh release create "$NEW_TAG" \ + --title "$NEW_TAG" \ + --notes-file /tmp/release-notes.md \ + --target "${{ github.sha }}" + + RELEASE_URL="${REPO_URL}/releases/tag/${NEW_TAG}" + echo "release_url=$RELEASE_URL" >> "$GITHUB_OUTPUT" + echo "Release created: $RELEASE_URL" + + - name: Notify Discord + if: steps.discover_prs.outputs.has_prs == 'true' + continue-on-error: true + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + run: | + NEW_TAG="${{ steps.version.outputs.new_tag }}" + RELEASE_URL="${{ steps.create_release.outputs.release_url }}" + BUMP="${{ steps.version.outputs.bump }}" + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%S.000Z) + + # Read release notes and truncate for Discord embed description limit (4096 chars). + NOTES=$(cat /tmp/release-notes.md) + MAX_DESC_LENGTH=3800 + if [ ${#NOTES} -gt $MAX_DESC_LENGTH ]; then + NOTES="${NOTES:0:$MAX_DESC_LENGTH}"$'\n\n'"... [See full release notes](${RELEASE_URL})" + fi + + # Build the Discord JSON payload using jq for safe JSON escaping. + # jq handles all special characters (quotes, newlines, backslashes) automatically. + jq -n \ + --arg title "${NEW_TAG} Released" \ + --arg url "$RELEASE_URL" \ + --arg desc "$NOTES" \ + --arg footer "KeeperHub Release | ${BUMP} bump" \ + --arg ts "$TIMESTAMP" \ + '{ + username: "KeeperHub Release Bot", + embeds: [{ + title: $title, + url: $url, + description: $desc, + color: 5763719, + footer: { text: $footer }, + timestamp: $ts + }] + }' > /tmp/discord-payload.json + + # Send to Discord + HTTP_STATUS=$(curl -s -o /tmp/discord-response.txt -w "%{http_code}" \ + -H "Content-Type: application/json" \ + -d @/tmp/discord-payload.json \ + "$DISCORD_WEBHOOK") + + if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then + echo "Discord notification sent successfully (HTTP $HTTP_STATUS)." + else + echo "Discord notification failed (HTTP $HTTP_STATUS)." + cat /tmp/discord-response.txt + echo "" + echo "Release was created successfully. Discord notification failure is non-blocking." + fi + + - name: Notify Discord on release failure + if: failure() && steps.discover_prs.outputs.has_prs == 'true' + continue-on-error: true + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + run: | + curl -s -H "Content-Type: application/json" \ + -d "{\"username\": \"KeeperHub Release Bot\", \"content\": \"Release workflow failed. Please check the GitHub Actions logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + "$DISCORD_WEBHOOK" diff --git a/.gitignore b/.gitignore index ce8fdb46f..4752fcb94 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ tailwindcss-*.log # Claude lint output cache (read instead of re-running commands) .claude/lint-output.txt .claude/typecheck-output.txt + +.planning +.prompts \ No newline at end of file diff --git a/docs/api/_meta.json b/docs/api/_meta.json index 105204495..79cfc7d7f 100644 --- a/docs/api/_meta.json +++ b/docs/api/_meta.json @@ -6,6 +6,7 @@ "integrations": "Integrations", "chains": "Chains", "user": "User", + "organizations": "Organizations", "api-keys": "API Keys", "errors": "Errors" } diff --git a/docs/api/index.md b/docs/api/index.md index c022ea63c..969060d52 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -54,7 +54,8 @@ API requests are subject to rate limiting. Current limits: | [Executions](/api/executions) | Monitor workflow execution status and logs | | [Integrations](/api/integrations) | Manage notification and service integrations | | [Chains](/api/chains) | List supported blockchain networks | -| [User](/api/user) | User profile and preferences | +| [User](/api/user) | User profile, preferences, and address book | +| [Organizations](/api/organizations) | Organization membership management | | [API Keys](/api/api-keys) | Manage API keys for programmatic access | ## SDKs diff --git a/docs/api/organizations.md b/docs/api/organizations.md new file mode 100644 index 000000000..dcdecf2f5 --- /dev/null +++ b/docs/api/organizations.md @@ -0,0 +1,26 @@ +--- +title: "Organizations API" +description: "KeeperHub Organizations API - manage organization membership." +--- + +# Organizations API + +Manage organization membership programmatically. + +## Leave Organization + +```http +POST /api/organizations/{organizationId}/leave +``` + +Remove yourself from an organization. If you are the sole owner, you must transfer ownership by providing `newOwnerMemberId` in the request body. The new owner must be an accepted member of the organization. + +### Request Body + +```json +{ + "newOwnerMemberId": "member_456" +} +``` + +The `newOwnerMemberId` field is only required when you are the last remaining owner. diff --git a/docs/api/user.md b/docs/api/user.md index d3a3059f7..57ca11ec7 100644 --- a/docs/api/user.md +++ b/docs/api/user.md @@ -122,3 +122,110 @@ DELETE /api/user/rpc-preferences/{chainId} ``` Reverts to default RPC endpoints for the chain. + +## Change Password + +```http +POST /api/user/password +``` + +Change the password for a credential-based account. Requires the current password and a new password (minimum 8 characters). Not available for OAuth-only accounts. + +### Request Body + +```json +{ + "currentPassword": "old-password", + "newPassword": "new-password" +} +``` + +## Forgot Password + +```http +POST /api/user/forgot-password +``` + +Handles password reset via OTP. Supports two actions controlled by the `action` field in the request body. + +**Request OTP** (default when `action` is omitted or set to `"request"`): + +```json +{ + "email": "user@example.com" +} +``` + +**Reset password** (`action: "reset"`): + +```json +{ + "action": "reset", + "email": "user@example.com", + "otp": "123456", + "newPassword": "new-password" +} +``` + +The OTP expires after 5 minutes. OAuth-only accounts receive a notification email instead of a reset code. + +## Deactivate Account + +```http +POST /api/user/delete +``` + +Soft-deletes the authenticated user account. Requires a confirmation string in the request body. Invalidates all active sessions on success. Not available for anonymous users. + +### Request Body + +```json +{ + "confirmation": "DEACTIVATE" +} +``` + +## Address Book + +Manage saved Ethereum addresses scoped to the active organization. All address book endpoints require an active organization context. + +### List Address Book Entries + +```http +GET /api/address-book +``` + +Returns all address book entries for the active organization, ordered by creation date (newest first). + +### Create Address Book Entry + +```http +POST /api/address-book +``` + +#### Request Body + +```json +{ + "label": "Treasury Wallet", + "address": "0x..." +} +``` + +The address must be a valid Ethereum address. + +### Update Address Book Entry + +```http +PATCH /api/address-book/{entryId} +``` + +Update the label or address of an existing entry. Both fields are optional. + +### Delete Address Book Entry + +```http +DELETE /api/address-book/{entryId} +``` + +Removes the entry from the organization address book. diff --git a/docs/api/workflows.md b/docs/api/workflows.md index 3c8e92420..ff1457546 100644 --- a/docs/api/workflows.md +++ b/docs/api/workflows.md @@ -133,3 +133,36 @@ GET /api/workflows/{workflowId}/code ``` Generate SDK code for the workflow. + +## Claim Workflow + +```http +POST /api/workflows/{workflowId}/claim +``` + +Claim an anonymous workflow into the authenticated user's organization. Only the original creator of the anonymous workflow can claim it. + +## Workflow Taxonomy + +```http +GET /api/workflows/taxonomy +``` + +Returns distinct categories and protocols from all public workflows. Useful for building filter UIs. + +### Response + +```json +{ + "categories": ["defi", "nft"], + "protocols": ["uniswap", "aave"] +} +``` + +## Update Featured Status (Internal) + +```http +POST /api/hub/featured +``` + +Mark a workflow as featured in the hub. Requires internal service authentication (`hub` service). Accepts optional `category`, `protocol`, and `featuredOrder` fields alongside the `workflowId`. diff --git a/docs/keepers/configuration.md b/docs/keepers/configuration.md index 9c4f64972..8307f85cf 100644 --- a/docs/keepers/configuration.md +++ b/docs/keepers/configuration.md @@ -79,7 +79,7 @@ No additional configuration needed. Click the Run button to execute. | Function * | Read function to call (auto-populated from ABI) | | Parameters | Function input parameters | -KeeperHub automatically fetches the contract ABI from block explorers. +KeeperHub automatically fetches the contract ABI from block explorers. For proxy contracts, it detects the proxy pattern and fetches the implementation ABI automatically. Supported proxy standards include EIP-1967, EIP-1822 (UUPS), OpenZeppelin Transparent Proxy, EIP-1167 (minimal proxy), and Gnosis Safe. For EIP-2535 Diamond contracts, KeeperHub queries the Diamond Loupe interface to discover all facets and combines their ABIs into a single unified interface. ### Write Contract diff --git a/docs/users-teams-orgs/organizations.md b/docs/users-teams-orgs/organizations.md index dade18423..59a5db314 100644 --- a/docs/users-teams-orgs/organizations.md +++ b/docs/users-teams-orgs/organizations.md @@ -58,6 +58,16 @@ Workflows created within an organization are automatically shared with all membe - All members can view run history - All members can enable or disable workflows +## Leaving an Organization + +Members can leave an organization at any time: + +1. Open the Manage Organizations modal +2. Select the organization you want to leave +3. Click **Leave Organization** + +If you are the sole owner, you must transfer ownership to another accepted member before leaving. Select a member to promote to owner during the leave process. + ## Current Limitations ### No Role-Based Access diff --git a/docs/users-teams-orgs/users.md b/docs/users-teams-orgs/users.md index c34d202cf..ff8b26ada 100644 --- a/docs/users-teams-orgs/users.md +++ b/docs/users-teams-orgs/users.md @@ -31,7 +31,23 @@ Access your account settings to: - Update display name - Change email address - Manage authentication methods +- Change your password - View wallet information +- Deactivate your account + +## Password Management + +### Changing Your Password + +You can change your password from account settings. Enter your current password, then provide and confirm a new password. Passwords must be at least 8 characters. You will be signed out after changing your password and must sign in again. + +### Forgot Password + +If you forget your password, use the forgot password flow from the sign-in page. Enter your email address and a one-time verification code (OTP) will be sent to you. The code expires after 5 minutes. Enter the code along with your new password to complete the reset. + +### OAuth Users + +If you signed up with a social provider (Google, GitHub, or Vercel), your password is managed by that provider. The change password option will direct you to your provider's account settings. If you attempt a password reset, you will receive an email indicating which provider manages your account. ## Personal Workflows @@ -66,4 +82,7 @@ See [API Authentication](/docs/api/authentication) for details. - Your workflows and run data are private to you and your organizations - Para wallet private keys are never exposed -- Account deletion removes all associated data + +### Account Deactivation + +You can deactivate your account from account settings. To confirm, you must type **DEACTIVATE** in the confirmation dialog. Deactivation is a soft delete -- your data is preserved, but you will be signed out and unable to sign in. All active sessions are invalidated immediately. To reactivate a deactivated account, contact an administrator. diff --git a/docs/wallet-management/_meta.json b/docs/wallet-management/_meta.json index 884457250..cb70af84e 100644 --- a/docs/wallet-management/_meta.json +++ b/docs/wallet-management/_meta.json @@ -1,4 +1,5 @@ { "para": "Para Integration", - "gas": "Gas Management" + "gas": "Gas Management", + "address-book": "Address Book" } diff --git a/docs/wallet-management/address-book.md b/docs/wallet-management/address-book.md new file mode 100644 index 000000000..8eba5ade7 --- /dev/null +++ b/docs/wallet-management/address-book.md @@ -0,0 +1,46 @@ +--- +title: "Address Book" +description: "Save and manage frequently used blockchain addresses for quick reuse across workflows in KeeperHub." +--- + +# Address Book + +The address book lets you save and label blockchain addresses that your organization uses frequently. Saved addresses are available across all workflows, so you do not need to copy and paste the same addresses repeatedly. + +Address book entries are **organization-scoped** -- every member of your organization can view and use them. + +## Adding an Address + +There are two ways to save an address: + +**From the address book page**: Click **Add Address**, enter a label (for example, "Treasury Wallet") and a valid Ethereum address, then click **Save**. + +**From a workflow node field**: When you enter a valid address into any address-type field in the workflow builder, a **Save** button appears next to the input. Click it to open the save form with the address pre-filled. Provide a label and confirm. + +Both methods validate the address format before saving. Invalid addresses are rejected with an error message. + +## Editing an Address + +Click the **Edit** icon next to any entry in the address book table. You can update the label, the address, or both. The address is re-validated on save. + +## Removing an Address + +Click the **Delete** icon next to any entry. The entry is removed immediately. Removing an address book entry does not alter workflow nodes that already reference that address -- the address value remains in the node configuration. + +## Checksummed Address Display + +All addresses are stored in lowercase for consistency and displayed in **EIP-55 checksummed format**. This means mixed-case characters serve as a built-in integrity check, helping you verify that an address has not been corrupted. + +When you copy an address from the address book, the checksummed form is copied to your clipboard. + +## Using Addresses in Workflow Nodes + +When you focus an address-type input field in the workflow builder, a **popover** appears showing your saved addresses. You can search by label or address, then select an entry to populate the field. + +The selected bookmark is persisted in the node configuration. If you open the workflow later, the field retains its association with the address book entry. + +## Block Explorer Links + +KeeperHub generates block explorer links for addresses and transaction hashes based on the selected network. Clicking an address or transaction hash link opens the relevant page on the network's block explorer (for example, Etherscan for Ethereum Mainnet). + +Explorer URL construction uses the chain's configured `explorerUrl` and `explorerAddressPath`, so links work automatically for any supported network. diff --git a/docs/workflows/creating.md b/docs/workflows/creating.md index a192f9522..92eb5421c 100644 --- a/docs/workflows/creating.md +++ b/docs/workflows/creating.md @@ -132,6 +132,18 @@ The **Ask AI...** input at the bottom of the canvas lets you describe your autom - "Every hour, check if a contract's totalSupply changed and email me" - "When someone sends ETH to my wallet, log it to Slack" +## Importing from the Hub + +The **Hub** lists workflow templates shared by the community. To use a template: + +1. Browse the Hub from the main navigation +2. Select a workflow template +3. Click **Duplicate** to copy it into your workspace + +The copy is created with a unique name (e.g., "My Workflow (Copy)") and set to private visibility. Node configurations are preserved, but integration credentials are removed so you can assign your own connections. + +You can also duplicate any public workflow you are viewing by clicking the **Duplicate** button in the toolbar. + ## Workflow States | State | Description | diff --git a/keeperhub/components/organization/org-switcher.tsx b/keeperhub/components/organization/org-switcher.tsx index 78d7671a8..9f50b7ba0 100644 --- a/keeperhub/components/organization/org-switcher.tsx +++ b/keeperhub/components/organization/org-switcher.tsx @@ -24,7 +24,11 @@ import { useSession } from "@/lib/auth-client"; export function OrgSwitcher() { const { data: session } = useSession(); - const { organization, switchOrganization } = useOrganization(); + const { + organization, + switchOrganization, + isLoading: orgLoading, + } = useOrganization(); const { organizations, isLoading: orgsLoading } = useOrganizations(); const [open, setOpen] = useState(false); const [manageModalOpen, setManageModalOpen] = useState(false); @@ -33,7 +37,7 @@ export function OrgSwitcher() { // Auto-switch to first available org if no active org but user has orgs useEffect(() => { if ( - !(organization || orgsLoading) && + !(organization || orgsLoading || orgLoading) && organizations.length > 0 && !autoSwitching ) { @@ -46,6 +50,7 @@ export function OrgSwitcher() { organization, organizations, orgsLoading, + orgLoading, switchOrganization, autoSwitching, ]); @@ -126,9 +131,9 @@ export function OrgSwitcher() { {organizations.map((org) => ( { - await switchOrganization(org.id); + onSelect={() => { setOpen(false); + switchOrganization(org.id); }} > Previous release tag (default: auto-detect via git describe) + --base-branch Base branch for PR discovery (default: staging) + --help Show this help message + +EXAMPLES + # Auto-detect previous tag, discover PRs merged to staging + ./keeperhub/scripts/test-release.sh + + # Simulate from a specific tag + ./keeperhub/scripts/test-release.sh --prev-tag v0.3.0 + + # Use a different base branch + ./keeperhub/scripts/test-release.sh --base-branch main + +WHAT THIS DOES + 1. Discovers merged PRs since the previous tag (or all PRs if first release) + 2. Classifies PRs by conventional commit prefix + 3. Determines semver bump (breaking > feat > patch) + 4. Generates formatted release notes + +WHAT THIS DOES NOT DO + - Create git tags + - Create GitHub Releases + - Send Discord notifications +USAGE +} + +# ------------------------------------------------------------------ +# Argument parsing +# ------------------------------------------------------------------ +while [ $# -gt 0 ]; do + case "$1" in + --prev-tag) + PREV_TAG="$2" + shift 2 + ;; + --base-branch) + BASE_BRANCH="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# ------------------------------------------------------------------ +# Preflight checks +# ------------------------------------------------------------------ +if ! command -v gh &>/dev/null; then + echo "ERROR: gh CLI is not installed. Install it from https://cli.github.com/" + exit 1 +fi + +if ! gh auth status &>/dev/null; then + echo "ERROR: gh CLI is not authenticated. Run 'gh auth login' first." + exit 1 +fi + +if ! command -v jq &>/dev/null; then + echo "ERROR: jq is not installed. Install it with 'brew install jq'." + exit 1 +fi + +# Detect repo URL from git remote +REPO_URL=$(gh repo view --json url --jq '.url' 2>/dev/null || echo "") +if [ -z "$REPO_URL" ]; then + echo "ERROR: Could not determine repository URL. Run from inside a git repo with a GitHub remote." + exit 1 +fi + +echo "============================================================" +echo " KeeperHub Release Test" +echo "============================================================" +echo "" +echo "Repository: $REPO_URL" +echo "Base branch: $BASE_BRANCH" + +# ------------------------------------------------------------------ +# Step 1: Determine previous tag +# ------------------------------------------------------------------ +FIRST_RELEASE=false + +if [ -n "$PREV_TAG" ]; then + echo "Previous tag: $PREV_TAG (user-provided)" +else + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$PREV_TAG" ]; then + echo "Previous tag: (none -- first release)" + FIRST_RELEASE=true + else + echo "Previous tag: $PREV_TAG (auto-detected)" + fi +fi + +# ------------------------------------------------------------------ +# Step 2: Discover merged PRs +# ------------------------------------------------------------------ +echo "" +echo "============================================================" +echo " Discovered PRs" +echo "============================================================" +echo "" + +# Upper bound: the author date of HEAD +UNTIL=$(git log -1 --format=%aI HEAD) +echo "Upper bound (HEAD author date): $UNTIL" + +if [ "$FIRST_RELEASE" = "true" ]; then + echo "First release -- discovering all PRs merged to $BASE_BRANCH before $UNTIL" + PR_JSON=$(gh pr list \ + --state merged \ + --base "$BASE_BRANCH" \ + --json number,title,author,url,body,mergedAt \ + --limit 200 \ + --jq "[.[] | select(.mergedAt <= \"$UNTIL\")]") +else + SINCE=$(git log -1 --format=%aI "$PREV_TAG") + echo "Finding PRs merged after: $SINCE and before: $UNTIL" + + PR_JSON=$(gh pr list \ + --state merged \ + --base "$BASE_BRANCH" \ + --json number,title,author,url,mergedAt,body \ + --limit 200 \ + --jq "[.[] | select(.mergedAt > \"$SINCE\" and .mergedAt <= \"$UNTIL\")]") +fi + +PR_COUNT=$(echo "$PR_JSON" | jq 'length') +echo "" +echo "Found $PR_COUNT merged PR(s)." + +if [ "$PR_COUNT" -eq 0 ]; then + echo "" + echo "No PRs found. Nothing to release." + exit 0 +fi + +# Print discovered PRs +echo "" +echo "$PR_JSON" | jq -r '.[] | " #\(.number) \(.title) (@\(.author.login)) merged \(.mergedAt)"' + +# ------------------------------------------------------------------ +# Step 3: Classify PRs and determine version bump +# ------------------------------------------------------------------ +echo "" +echo "============================================================" +echo " Classification" +echo "============================================================" +echo "" + +HAS_BREAKING=false +HAS_FEAT=false + +BREAKING_PRS="" +FEAT_PRS="" +FIX_PRS="" +OTHER_PRS="" + +while IFS= read -r pr_line; do + NUMBER=$(echo "$pr_line" | jq -r '.number') + TITLE=$(echo "$pr_line" | jq -r '.title') + AUTHOR=$(echo "$pr_line" | jq -r '.author.login') + URL=$(echo "$pr_line" | jq -r '.url') + BODY=$(echo "$pr_line" | jq -r '.body // ""') + + # Clean the title: remove leading/trailing whitespace + TITLE=$(printf '%s' "$TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + # Strip the prefix from the title for display + DISPLAY_TITLE=$(printf '%s' "$TITLE" | sed -E 's/^(feat|fix|bug|hotfix|breaking|chore|docs|refactor|test|ci|build|perf|style)(\([^)]*\))?[!]?:[[:space:]]*//') + + # If stripping produced empty string, use original title + if [ -z "$DISPLAY_TITLE" ]; then + DISPLAY_TITLE="$TITLE" + fi + + # Build the entry line + ENTRY="- ${DISPLAY_TITLE} ([#${NUMBER}](${URL})) @${AUTHOR}" + + # Classify by prefix + CATEGORY="" + if printf '%s' "$TITLE" | grep -qiE "^breaking(\([^)]*\))?:"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + CATEGORY="BREAKING" + elif printf '%s' "$TITLE" | grep -qiE "^[a-z]+(\([^)]*\))?!:"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + CATEGORY="BREAKING (!)" + elif printf '%s' "$BODY" | grep -q "BREAKING CHANGE"; then + HAS_BREAKING=true + BREAKING_PRS="${BREAKING_PRS}${ENTRY}"$'\n' + CATEGORY="BREAKING (body)" + elif printf '%s' "$TITLE" | grep -qiE "^feat(\([^)]*\))?:"; then + HAS_FEAT=true + FEAT_PRS="${FEAT_PRS}${ENTRY}"$'\n' + CATEGORY="FEATURE" + elif printf '%s' "$TITLE" | grep -qiE "^(fix|bug|hotfix)(\([^)]*\))?:"; then + FIX_PRS="${FIX_PRS}${ENTRY}"$'\n' + CATEGORY="FIX" + else + OTHER_PRS="${OTHER_PRS}${ENTRY}"$'\n' + CATEGORY="OTHER" + fi + + printf ' %-16s #%s %s\n' "[$CATEGORY]" "$NUMBER" "$TITLE" +done < <(echo "$PR_JSON" | jq -c '.[]') + +# ------------------------------------------------------------------ +# Step 4: Version bump +# ------------------------------------------------------------------ +echo "" +echo "============================================================" +echo " Version Bump" +echo "============================================================" +echo "" + +if [ "$HAS_BREAKING" = true ]; then + BUMP="major" +elif [ "$HAS_FEAT" = true ]; then + BUMP="minor" +else + BUMP="patch" +fi + +echo "Bump type: $BUMP" + +if [ "$FIRST_RELEASE" = "true" ]; then + NEW_TAG="v0.1.0" +else + VERSION="${PREV_TAG#v}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + PATCH=$(echo "$VERSION" | cut -d. -f3) + + case "$BUMP" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}" +fi + +echo "New tag: $NEW_TAG" + +# Check if tag already exists +if git tag -l "$NEW_TAG" | grep -q "$NEW_TAG"; then + echo "" + echo "WARNING: Tag $NEW_TAG already exists locally." +fi + +# ------------------------------------------------------------------ +# Step 5: Formatted release notes +# ------------------------------------------------------------------ +echo "" +echo "============================================================" +echo " Formatted Release Notes" +echo "============================================================" +echo "" + +NOTES="## What's Changed"$'\n' + +if [ -n "$BREAKING_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Breaking Changes"$'\n' + NOTES="${NOTES}${BREAKING_PRS}" +fi + +if [ -n "$FEAT_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Features"$'\n' + NOTES="${NOTES}${FEAT_PRS}" +fi + +if [ -n "$FIX_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Bug Fixes"$'\n' + NOTES="${NOTES}${FIX_PRS}" +fi + +if [ -n "$OTHER_PRS" ]; then + NOTES="${NOTES}"$'\n'"### Other Changes"$'\n' + NOTES="${NOTES}${OTHER_PRS}" +fi + +if [ "$FIRST_RELEASE" = "true" ]; then + NOTES="${NOTES}"$'\n'"**Full Changelog**: ${REPO_URL}/commits/${NEW_TAG}" +else + NOTES="${NOTES}"$'\n'"**Full Changelog**: ${REPO_URL}/compare/${PREV_TAG}...${NEW_TAG}" +fi + +echo "$NOTES" + +echo "" +echo "============================================================" +echo " Done (dry run -- no tags or releases created)" +echo "============================================================" diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 43305ee21..a076d1bd4 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -4,6 +4,7 @@ import { index, integer, jsonb, + pgEnum, pgTable, text, timestamp, @@ -12,6 +13,26 @@ import { import type { IntegrationType } from "../types/integration"; import { generateId } from "../utils/id"; +// start custom keeperhub code // +// These enums are created by @workflow/world-postgres migrations in the public +// schema and referenced by workflow.workflow_runs / workflow.workflow_steps. +// Declaring them here prevents drizzle-kit from trying to drop them. +export const workflowRunStatus = pgEnum("status", [ + "pending", + "running", + "completed", + "failed", + "cancelled", +]); +export const workflowStepStatus = pgEnum("step_status", [ + "pending", + "running", + "completed", + "failed", + "cancelled", +]); +// end keeperhub code // + // Better Auth tables export const users = pgTable("users", { id: text("id").primaryKey(),