#!/bin/sh # github_issue_sync.sh # Synchronizes GitHub and Linear issues with Taskwarrior # Set new line and tab for word splitting IFS=' ' # Logger with timestamp log() { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" } # Function to trim leading and trailing whitespace using sed trim_whitespace() { input="$1" echo "$input" | sed 's/^[ \t]*//;s/[ \t]*$//' } # Validate necessary environment variables validate_env_vars() { oldIFS="$IFS" IFS=' ' required_vars="api userid" for var in $required_vars; do val=$(pass show "api/linear/$var") if [ -z "$val" ]; then echo "Error: Environment variable $var is not set." >&2 exit 1 fi done IFS="$oldIFS" } # Retrieve and format GitHub issues get_github_issues() { issues=$(gh api -X GET /search/issues \ -f q='is:issue is:open assignee:TheSiahxyz' \ --jq '.items[] | {id: .number, description: .title, repository: .repository_url, html_url: .html_url}') || { echo "Error: Unable to fetch GitHub issues" >&2 return 1 } echo "$issues" } # Retrieve and format Linear issues get_linear_issues() { issues=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Authorization: $LINEAR_API_KEY" \ --data '{"query": "query { user(id: \"'"$LINEAR_USER_ID"'\") { id name assignedIssues(filter: { state: { name: { nin: [\"Released\", \"Canceled\"] } } }) { nodes { id title url } } } }"}' \ https://api.linear.app/graphql | jq -c '.data.user.assignedIssues.nodes[] | {id: .id, description: .title, repository: "linear", html_url: .url}') || { echo "Error: Unable to fetch Linear issues" >&2 return 1 } echo "$issues" } # Synchronize a single issue with Taskwarrior sync_to_taskwarrior() { issue_line="$1" issue_id=$(echo "$issue_line" | jq -r '.id') issue_description=$(echo "$issue_line" | jq -r '.description') issue_repo_name=$(echo "$issue_line" | jq -r '.repository' | awk -F/ '{print $NF}') issue_url=$(echo "$issue_line" | jq -r '.html_url') log "Processing Issue ID: $issue_id, Description: $issue_description" task_uuid=$(get_task_id_by_description "$issue_description") if [ -z "$task_uuid" ]; then log "Creating new task for issue: $issue_description" task_uuid=$(create_task "$issue_description" "+$issue_repo_name" "project:$issue_repo_name") if [ -n "$task_uuid" ]; then annotate_task "$task_uuid" "$issue_url" log "Task created and annotated for: $issue_description" else log "Error: Failed to create task for: $issue_description" >&2 fi else log "Task already exists for: $issue_description (UUID: $task_uuid)" fi } # Mark a GitHub issue as completed in Taskwarrior sync_github_issue() { task_description="$1" task_uuid=$(get_task_id_by_description "$task_description") if [ -n "$task_uuid" ]; then mark_task_completed "$task_uuid" log "Task marked as completed: $task_description (UUID: $task_uuid)" else log "Task UUID not found for: $task_description" >&2 fi } # Compare existing Taskwarrior tasks with current issues and mark as completed if not present compare_and_display_tasks_not_in_issues() { existing_task_descriptions="$1" issues_descriptions="$2" log "Starting comparison of Taskwarrior tasks and current issues." existing_task_descriptions_array=$(echo "$existing_task_descriptions" | tr '\n' ' ') issues_descriptions_array=$(echo "$issues_descriptions" | tr '\n' ' ') for task_description in $existing_task_descriptions_array; do trimmed_task_description=$(trim_whitespace "$task_description") issue_exists=false for issue_description in $issues_descriptions_array; do trimmed_issue_description=$(trim_whitespace "$issue_description") if [ "$(echo "$trimmed_task_description" | tr '[:upper:]' '[:lower:]')" = "$(echo "$trimmed_issue_description" | tr '[:upper:]' '[:lower:]')" ]; then issue_exists=true break fi done if [ "$issue_exists" = false ]; then log "No matching issue found for task: $trimmed_task_description. Marking as completed." sync_github_issue "$trimmed_task_description" fi done log "Comparison of Taskwarrior tasks and issues completed." } # Retrieve existing Taskwarrior task descriptions with +github or +linear tags and pending status get_existing_task_descriptions() { task +github or +linear status:pending export | jq -r '.[] | .description' } # Log retrieved issues log_issues() { issue_type="$1" issues="$2" log "Retrieved $issue_type issues: $(echo "$issues" | jq '.')" } # Synchronize all issues to Taskwarrior sync_issues_to_taskwarrior() { issues="$1" echo "$issues" | jq -c '.' | while IFS= read -r line; do sync_to_taskwarrior "$line" done } # Main function to orchestrate the synchronization main() { if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "Not a git repository." exit 1 fi validate_env_vars github_issues=$(get_github_issues) linear_issues=$(get_linear_issues) if [ -z "$github_issues" ] && [ -z "$linear_issues" ]; then log "No issues retrieved from GitHub or Linear. Exiting." exit 0 fi if [ -n "$github_issues" ]; then log_issues "GitHub" "$github_issues" sync_issues_to_taskwarrior "$github_issues" fi if [ -n "$linear_issues" ]; then log_issues "Linear" "$linear_issues" sync_issues_to_taskwarrior "$linear_issues" fi existing_task_descriptions=$(get_existing_task_descriptions) compare_and_display_tasks_not_in_issues "$existing_task_descriptions" "$( echo "$github_issues" | jq -r '.description' echo "$linear_issues" | jq -r '.description' )" } main