Repository: GoogleCloudPlatform/magic-modules
Pull request #13969: go-github set and get function conversion
Author: login: ScottSuarez
Created: 2025-05-15T17:38:46Z | Merged: 2025-05-16T22:09:06Z

<!--
Complete the self-review checklist to help speed up the review process: https://googlecloudplatform.github.io/magic-modules/contribute/review-pr/

If your PR is still work in progress, please create it in draft mode.

Put a description of what this PR is for here, along with any references to issues that this resolves or contributes to.
For example: Fixes https://github.com/hashicorp/terraform-provider-google/issues/ISSUE_ID
-->

**Release Note Template for Downstream PRs (will be copied)**

See [Write release notes](https://googlecloudplatform.github.io/magic-modules/contribute/release-notes/) for guidance.

```release-note:none

```



Diff:
diff --git a/.ci/magician/github/README.md b/.ci/magician/github/README.md
new file mode 100644
index 000000000000..987b87408af2
--- /dev/null
+++ b/.ci/magician/github/README.md
@@ -0,0 +1,24 @@
+# GitHub Integration Tests
+
+## Overview
+This directory contains an interface for the GitHub client that make real API calls to GitHub's API. The tests in `integration_test.go` are isolated with build tags to prevent accidental execution.
+
+## Build Tags
+This file uses Go build tags (`//go:build integration`) which:
+- Exclude these tests from normal test execution (`go test ./...`)
+- Require explicit opt-in (`go test -tags=integration`)
+- Prevent accidental execution of tests that make real API calls and may have side effects
+
+## Usage
+
+### Requirements
+- GitHub API token with appropriate permissions
+- Token set as environment variable: `GITHUB_API_TOKEN`
+
+### Running Tests
+```bash
+# Run all integration tests
+GITHUB_API_TOKEN=your_token_here go test -v -tags=integration ./github
+
+# Run specific test
+GITHUB_API_TOKEN=your_token_here go test -v -tags=integration -run TestIntegrationGetPullRequest ./github
\ No newline at end of file
diff --git a/.ci/magician/github/get.go b/.ci/magician/github/get.go
index e39dfe25ddc3..667c8b498f0d 100644
--- a/.ci/magician/github/get.go
+++ b/.ci/magician/github/get.go
@@ -18,9 +18,18 @@ package github
 import (
 	"fmt"
 	utils "magician/utility"
+	"strconv"
 	"time"
+
+	gh "github.com/google/go-github/v68/github"
+)
+
+const (
+	defaultOwner = "GoogleCloudPlatform"
+	defaultRepo  = "magic-modules"
 )
 
+// Types for external interface compatibility
 type User struct {
 	Login string `json:"login"`
 }
@@ -47,26 +56,44 @@ type PullRequestComment struct {
 	CreatedAt time.Time `json:"created_at"`
 }
 
-func (gh *Client) GetPullRequest(prNumber string) (PullRequest, error) {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/%s", prNumber)
-
-	var pullRequest PullRequest
+// GetPullRequest fetches a single pull request
+func (c *Client) GetPullRequest(prNumber string) (PullRequest, error) {
+	num, err := strconv.Atoi(prNumber)
+	if err != nil {
+		return PullRequest{}, err
+	}
 
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, &pullRequest, nil)
+	pr, _, err := c.gh.PullRequests.Get(c.ctx, defaultOwner, defaultRepo, num)
+	if err != nil {
+		return PullRequest{}, err
+	}
 
-	return pullRequest, err
+	return convertGHPullRequest(pr), nil
 }
 
-func (gh *Client) GetPullRequests(state, base, sort, direction string) ([]PullRequest, error) {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/pulls?state=%s&base=%s&sort=%s&direction=%s", state, base, sort, direction)
+// GetPullRequests fetches multiple pull requests
+func (c *Client) GetPullRequests(state, base, sort, direction string) ([]PullRequest, error) {
+	opts := &gh.PullRequestListOptions{
+		State:     state,
+		Base:      base,
+		Sort:      sort,
+		Direction: direction,
+	}
 
-	var pullRequests []PullRequest
+	prs, _, err := c.gh.PullRequests.List(c.ctx, defaultOwner, defaultRepo, opts)
+	if err != nil {
+		return nil, err
+	}
 
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, &pullRequests, nil)
+	result := make([]PullRequest, len(prs))
+	for i, pr := range prs {
+		result[i] = convertGHPullRequest(pr)
+	}
 
-	return pullRequests, err
+	return result, nil
 }
 
+// GetPullRequestRequestedReviewers gets requested reviewers for a PR
 func (gh *Client) GetPullRequestRequestedReviewers(prNumber string) ([]User, error) {
 	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/pulls/%s/requested_reviewers", prNumber)
 
@@ -82,6 +109,7 @@ func (gh *Client) GetPullRequestRequestedReviewers(prNumber string) ([]User, err
 	return requestedReviewers.Users, nil
 }
 
+// GetPullRequestPreviousReviewers gets previous reviewers for a PR
 func (gh *Client) GetPullRequestPreviousReviewers(prNumber string) ([]User, error) {
 	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/pulls/%s/reviews", prNumber)
 
@@ -107,66 +135,97 @@ func (gh *Client) GetPullRequestPreviousReviewers(prNumber string) ([]User, erro
 	return result, nil
 }
 
-func (gh *Client) GetCommitMessage(owner, repo, sha string) (string, error) {
-	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", owner, repo, sha)
-
-	var commit struct {
-		Commit struct {
-			Message string `json:"message"`
-		} `json:"commit"`
-	}
-
-	err := utils.RequestCall(url, "GET", gh.token, &commit, nil)
+// GetCommitMessage gets a commit message
+func (c *Client) GetCommitMessage(owner, repo, sha string) (string, error) {
+	commit, _, err := c.gh.Repositories.GetCommit(c.ctx, owner, repo, sha, nil)
 	if err != nil {
 		return "", err
 	}
 
-	return commit.Commit.Message, nil
-}
+	if commit.Commit != nil && commit.Commit.Message != nil {
+		return *commit.Commit.Message, nil
+	}
 
-func (gh *Client) GetPullRequestComments(prNumber string) ([]PullRequestComment, error) {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/%s/comments", prNumber)
+	return "", fmt.Errorf("no commit message found")
+}
 
-	var comments []PullRequestComment
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, &comments, nil)
+// GetPullRequestComments gets all comments on a PR, handling pagination
+func (c *Client) GetPullRequestComments(prNumber string) ([]PullRequestComment, error) {
+	num, err := strconv.Atoi(prNumber)
 	if err != nil {
 		return nil, err
 	}
-	return comments, nil
-}
 
-func (gh *Client) GetTeamMembers(organization, team string) ([]User, error) {
-	url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/members", organization, team)
+	var allComments []*gh.IssueComment
+	opts := &gh.IssueListCommentsOptions{
+		ListOptions: gh.ListOptions{
+			PerPage: 100,
+		},
+	}
 
-	var members []User
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, &members, nil)
-	if err != nil {
-		return nil, err
+	for {
+		comments, resp, err := c.gh.Issues.ListComments(c.ctx, defaultOwner, defaultRepo, num, opts)
+		if err != nil {
+			return nil, err
+		}
+
+		allComments = append(allComments, comments...)
+
+		if resp.NextPage == 0 {
+			break // No more pages
+		}
+
+		// Set up for the next page
+		opts.Page = resp.NextPage
 	}
-	return members, nil
+
+	return convertGHComments(allComments), nil
 }
 
-func (gh *Client) IsOrgMember(author, org string) bool {
-	url := fmt.Sprintf("https://api.github.com/orgs/%s/members/%s", org, author)
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, nil, nil)
-	return err == nil
+// GetTeamMembers gets all members of a team, handling pagination
+func (c *Client) GetTeamMembers(organization, team string) ([]User, error) {
+	var allMembers []*gh.User
+	opts := &gh.TeamListTeamMembersOptions{
+		ListOptions: gh.ListOptions{
+			PerPage: 100,
+		},
+	}
+
+	for {
+		members, resp, err := c.gh.Teams.ListTeamMembersBySlug(c.ctx, organization, team, opts)
+		if err != nil {
+			return nil, err
+		}
+
+		allMembers = append(allMembers, members...)
+
+		if resp.NextPage == 0 {
+			break // No more pages
+		}
+
+		// Set up for the next page
+		opts.Page = resp.NextPage
+	}
+
+	return convertGHUsers(allMembers), nil
 }
 
-func (gh *Client) IsTeamMember(organization, teamSlug, username string) bool {
-	type TeamMembership struct {
-		URL   string `json:"url"`
-		Role  string `json:"role"`
-		State string `json:"state"`
+// IsOrgMember checks if a user is a member of an organization
+func (c *Client) IsOrgMember(username, org string) bool {
+	isMember, _, err := c.gh.Organizations.IsMember(c.ctx, org, username)
+	if err != nil {
+		return false
 	}
 
-	url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s", organization, teamSlug, username)
-	var membership TeamMembership
-	err := utils.RequestCallWithRetry(url, "GET", gh.token, &membership, nil)
+	return isMember
+}
 
+// IsTeamMember checks if a user is a member of a team
+func (c *Client) IsTeamMember(organization, teamSlug, username string) bool {
+	membership, _, err := c.gh.Teams.GetTeamMembershipBySlug(c.ctx, organization, teamSlug, username)
 	if err != nil {
 		return false
 	}
 
-	// User is considered a member if state is "active"
-	return membership.State == "active"
+	return membership != nil && membership.State != nil && *membership.State == "active"
 }
diff --git a/.ci/magician/github/integration_test.go b/.ci/magician/github/integration_test.go
new file mode 100644
index 000000000000..bf5a649f27a1
--- /dev/null
+++ b/.ci/magician/github/integration_test.go
@@ -0,0 +1,310 @@
+//go:build integration
+// +build integration
+
+/*
+* Copyright 2025 Google LLC. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+* Integration tests - makes real GitHub API calls.
+* NOT run during normal test execution (go test).
+* Requires:
+*   - GITHUB_API_TOKEN environment variable
+*   - Run with: go test -tags=integration
+ */
+
+package github
+
+import (
+	"fmt"
+	"os"
+	"testing"
+	"time"
+)
+
+// https://github.com/GoogleCloudPlatform/magic-modules
+const (
+	testNonMember     = "bananaman5000"
+	testRepo          = "magic-modules"
+	testOwner         = "GoogleCloudPlatform"
+	testPRNumber      = "13969"                                    // replace this with an actual PR Number
+	testPRCommitSha   = "4a8409686810551655eea2533e939cc5344e83e2" // replace this with an actual SHA
+	testMainCommitSha = "fd910977cf24595d2c04e3f0a369a82c79fdb8f8" // replace this with an actual SHA
+	testLabel         = "terraform-3.0"
+	testOrg           = "GoogleCloudPlatform"
+	testTeam          = "terraform"
+	workflowFileName  = "test-tpg.yml"
+)
+
+func skipIfNoToken(t *testing.T) *Client {
+	token := os.Getenv("GITHUB_API_TOKEN")
+	if token == "" {
+		t.Skip("Skipping integration test: GITHUB_API_TOKEN environment variable not set")
+	}
+
+	return NewClient(token)
+}
+
+func TestIntegrationGetPullRequest(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	pr, err := client.GetPullRequest(testPRNumber)
+	if err != nil {
+		t.Fatalf("GetPullRequest failed: %v", err)
+	}
+
+	t.Logf("PR Title: %s", pr.Title)
+	if pr.Number == 0 {
+		t.Error("Expected PR number to be non-zero")
+	}
+	if pr.Title == "" {
+		t.Error("Expected PR title to be non-empty")
+	}
+}
+
+func TestIntegrationGetPullRequests(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	prs, err := client.GetPullRequests("open", "main", "created", "desc")
+	if err != nil {
+		t.Fatalf("GetPullRequests failed: %v", err)
+	}
+
+	t.Logf("Found %d PRs", len(prs))
+}
+
+func TestIntegrationGetCommitMessage(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	// You'll need a valid commit SHA for this test
+	if testMainCommitSha == "HEAD" {
+		t.Skip("Skipping GetCommitMessage test: need a valid commit SHA")
+	}
+
+	message, err := client.GetCommitMessage(testOwner, testRepo, testMainCommitSha)
+	if err != nil {
+		t.Fatalf("GetCommitMessage failed: %v", err)
+	}
+
+	t.Logf("Commit message: %s", message)
+	if message == "" {
+		t.Error("Expected commit message to be non-empty")
+	}
+}
+
+func TestIntegrationGetPullRequestComments(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	comments, err := client.GetPullRequestComments(testPRNumber)
+	if err != nil {
+		t.Fatalf("GetPullRequestComments failed: %v", err)
+	}
+
+	t.Logf("Found %d comments", len(comments))
+	for i, comment := range comments {
+		t.Logf("Comment %d: %s by %s", i+1, comment.Body[:min(len(comment.Body), 50)], comment.User.Login)
+	}
+}
+
+func TestIntegrationGetTeamMembers(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	members, err := client.GetTeamMembers(testOrg, testTeam)
+	if err != nil {
+		t.Logf("GetTeamMembers failed: %v", err)
+		t.Skip("Skipping team member test - might not have access to the specified team")
+	}
+
+	t.Logf("Found %d team members", len(members))
+	for i, member := range members {
+		t.Logf("Member %d: %s", i+1, member.Login)
+	}
+}
+
+func TestIntegrationIsOrgMember(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	isMember := client.IsOrgMember(testOwner, testOrg)
+	t.Logf("Is %s a member of %s: %v", testOwner, testOrg, isMember)
+
+	if !isMember {
+		t.Errorf("Note: Expected %s to be a member of %s, but they're not", testOwner, testOrg)
+	}
+
+	isMember = client.IsOrgMember(testNonMember, testOrg)
+	if isMember {
+		t.Errorf("Expected %s to not be a member of %s, but they are", testNonMember, testOrg)
+	} else {
+		t.Logf("Is %s not a member of %s: %v", testNonMember, testOrg, isMember)
+	}
+}
+
+func TestIntegrationIsTeamMember(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	isMember := client.IsTeamMember(testOrg, testTeam, testOwner)
+	if !isMember {
+		t.Errorf("Expected %s to be a member of team %s in org %s, but they're not", testOwner, testTeam, testOrg)
+	} else {
+		t.Logf("Is %s a member of team %s in org %s: %v", testOwner, testTeam, testOrg, isMember)
+	}
+
+	isMember = client.IsTeamMember(testOrg, testTeam, testNonMember)
+	if isMember {
+		t.Errorf("Expected %s to not be a member of team %s in org %s, but they are", testNonMember, testTeam, testOrg)
+	} else {
+		t.Logf("Is %s not a member of team %s in org %s: %v", testNonMember, testTeam, testOrg, isMember)
+	}
+}
+
+func TestIntegrationPostAndUpdateComment(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	// First post a comment
+	comment := fmt.Sprintf("Test comment from integration test at %s", time.Now().Format(time.RFC3339))
+	err := client.PostComment(testPRNumber, comment)
+	if err != nil {
+		t.Fatalf("PostComment failed: %v", err)
+	}
+
+	// Get the comment ID
+	comments, err := client.GetPullRequestComments(testPRNumber)
+	if err != nil {
+		t.Fatalf("GetPullRequestComments failed: %v", err)
+	}
+
+	var commentID int
+	for _, c := range comments {
+		if c.Body == comment {
+			commentID = c.ID
+			break
+		}
+	}
+
+	if commentID == 0 {
+		t.Fatal("Could not find the comment we just posted")
+	}
+
+	// Update the comment
+	updatedComment := fmt.Sprintf("Updated test comment from integration test at %s", time.Now().Format(time.RFC3339))
+	err = client.UpdateComment(testPRNumber, updatedComment, commentID)
+	if err != nil {
+		t.Fatalf("UpdateComment failed: %v", err)
+	}
+
+	t.Logf("Successfully posted and updated comment with ID: %d", commentID)
+}
+
+func TestIntegrationAddAndRemoveLabels(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	err := client.AddLabels(testPRNumber, []string{testLabel})
+	if err != nil {
+		t.Fatalf("AddLabels failed: %v", err)
+	}
+
+	// Then remove the label
+	err = client.RemoveLabel(testPRNumber, testLabel)
+	if err != nil {
+		t.Fatalf("RemoveLabel failed: %v", err)
+	}
+
+	t.Logf("Successfully added and removed label: %s", testLabel)
+}
+
+func TestIntegrationPostBuildStatus(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	// You'll need a valid commit SHA for this test
+	if testPRCommitSha == "HEAD" {
+		t.Skip("Skipping PostBuildStatus test: need a valid commit SHA")
+	}
+
+	err := client.PostBuildStatus(
+		testPRNumber,
+		"integration-test",
+		"success",
+		"https://example.com/integration-test",
+		testPRCommitSha,
+	)
+	if err != nil {
+		t.Errorf("PostBuildStatus failed: %v", err)
+	}
+
+	err = client.PostBuildStatus(
+		testPRNumber,
+		"integration-test-failed",
+		"failure",
+		"https://example.com/integration-test-fail",
+		testPRCommitSha,
+	)
+	if err != nil {
+		t.Errorf("PostBuildStatus failed: %v", err)
+	}
+
+	t.Logf("Successfully posted build status")
+}
+
+func TestIntegrationCreateWorkflowDispatchEvent(t *testing.T) {
+	client := skipIfNoToken(t)
+
+	// Skip this test by default as it can have side effects
+	if os.Getenv("RUN_WORKFLOW_DISPATCH_TEST") != "true" {
+		t.Skip("Skipping workflow dispatch test: set RUN_WORKFLOW_DISPATCH_TEST=true to run")
+	}
+
+	if err := client.CreateWorkflowDispatchEvent("test-tpg.yml", map[string]any{
+		"owner":     "modular-magician",
+		"repo":      testRepo,
+		"branch":    "main",
+		"pr-number": testPRNumber,
+		"sha":       testPRCommitSha,
+	}); err != nil {
+		t.Errorf("error creating workflow dispatch event: %v", err)
+	}
+
+	t.Logf("Successfully triggered workflow dispatch event")
+}
+
+// TestIntegrationMergePullRequest is commented out as it has permanent effects
+// Uncomment and run only if you're sure you want to merge the PR
+/*
+ func TestIntegrationMergePullRequest(t *testing.T) {
+	 client := skipIfNoToken(t)
+
+	 // Skip this test by default as it has permanent effects
+	 if os.Getenv("RUN_MERGE_PR_TEST") != "true" {
+		 t.Skip("Skipping merge PR test: set RUN_MERGE_PR_TEST=true to run")
+	 }
+
+	 // You'll need a valid commit SHA for this test
+	 if testPRCommitSha == "HEAD" {
+		 t.Skip("Skipping MergePullRequest test: need a valid commit SHA")
+	 }
+
+	 err := client.MergePullRequest(testOwner, testRepo, testPRNumber, testPRCommitSha)
+	 if err != nil {
+		 t.Fatalf("MergePullRequest failed: %v", err)
+	 }
+
+	 t.Logf("Successfully merged pull request")
+ }
+*/
+
+// Helper function to get minimum of two integers
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
diff --git a/.ci/magician/github/interface_conversion.go b/.ci/magician/github/interface_conversion.go
new file mode 100644
index 000000000000..3ca9f35a1ba2
--- /dev/null
+++ b/.ci/magician/github/interface_conversion.go
@@ -0,0 +1,70 @@
+package github
+
+import (
+	gh "github.com/google/go-github/v68/github"
+)
+
+// Convert from GitHub types to our types
+func convertGHPullRequest(pr *gh.PullRequest) PullRequest {
+	if pr == nil {
+		return PullRequest{}
+	}
+
+	var labels []Label
+	if pr.Labels != nil {
+		for _, l := range pr.Labels {
+			if l.Name != nil {
+				labels = append(labels, Label{Name: *l.Name})
+			}
+		}
+	}
+
+	return PullRequest{
+		HTMLUrl:        pr.GetHTMLURL(),
+		Number:         pr.GetNumber(),
+		Title:          pr.GetTitle(),
+		User:           User{Login: pr.GetUser().GetLogin()},
+		Body:           pr.GetBody(),
+		Labels:         labels,
+		MergeCommitSha: pr.GetMergeCommitSHA(),
+		Merged:         pr.GetMerged(),
+	}
+}
+
+func convertGHUser(user *gh.User) User {
+	if user == nil {
+		return User{}
+	}
+	return User{
+		Login: user.GetLogin(),
+	}
+}
+
+func convertGHUsers(users []*gh.User) []User {
+	result := make([]User, len(users))
+	for i, u := range users {
+		result[i] = convertGHUser(u)
+	}
+	return result
+}
+
+func convertGHComment(comment *gh.IssueComment) PullRequestComment {
+	if comment == nil {
+		return PullRequestComment{}
+	}
+
+	return PullRequestComment{
+		User:      convertGHUser(comment.User),
+		Body:      comment.GetBody(),
+		ID:        int(comment.GetID()),
+		CreatedAt: comment.GetCreatedAt().Time,
+	}
+}
+
+func convertGHComments(comments []*gh.IssueComment) []PullRequestComment {
+	result := make([]PullRequestComment, len(comments))
+	for i, c := range comments {
+		result[i] = convertGHComment(c)
+	}
+	return result
+}
diff --git a/.ci/magician/github/set.go b/.ci/magician/github/set.go
index 15ac9996ca22..e56b5a03f5b5 100644
--- a/.ci/magician/github/set.go
+++ b/.ci/magician/github/set.go
@@ -17,61 +17,64 @@ package github
 
 import (
 	"fmt"
-	utils "magician/utility"
+	"strconv"
 	"strings"
 	"time"
-)
 
-func (gh *Client) PostBuildStatus(prNumber, title, state, targetURL, commitSha string) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/statuses/%s", commitSha)
+	utils "magician/utility"
 
-	postBody := map[string]string{
-		"context":    title,
-		"state":      state,
-		"target_url": targetURL,
+	gh "github.com/google/go-github/v68/github"
+)
+
+// PostBuildStatus creates a commit status for a specific SHA
+func (c *Client) PostBuildStatus(prNumber, title, state, targetURL, commitSha string) error {
+	repoStatus := &gh.RepoStatus{
+		Context:   gh.Ptr(title),
+		State:     gh.Ptr(state),
+		TargetURL: gh.Ptr(targetURL),
 	}
 
-	err := utils.RequestCallWithRetry(url, "POST", gh.token, nil, postBody)
+	_, _, err := c.gh.Repositories.CreateStatus(c.ctx, defaultOwner, defaultRepo, commitSha, repoStatus)
 	if err != nil {
 		return err
 	}
 
 	fmt.Printf("Successfully posted build status to pull request %s\n", prNumber)
-
 	return nil
 }
 
-func (gh *Client) PostComment(prNumber, comment string) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/%s/comments", prNumber)
+// PostComment adds a comment to a pull request
+func (c *Client) PostComment(prNumber, comment string) error {
+	num, err := strconv.Atoi(prNumber)
+	if err != nil {
+		return err
+	}
 
-	body := map[string]string{
-		"body": comment,
+	issueComment := &gh.IssueComment{
+		Body: gh.Ptr(comment),
 	}
 
-	err := utils.RequestCallWithRetry(url, "POST", gh.token, nil, body)
+	_, _, err = c.gh.Issues.CreateComment(c.ctx, defaultOwner, defaultRepo, num, issueComment)
 	if err != nil {
 		return err
 	}
 
 	fmt.Printf("Successfully posted comment to pull request %s\n", prNumber)
-
 	return nil
 }
 
-func (gh *Client) UpdateComment(prNumber, comment string, id int) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/comments/%d", id)
-
-	body := map[string]string{
-		"body": comment,
+// UpdateComment updates an existing comment
+func (c *Client) UpdateComment(prNumber, comment string, id int) error {
+	issueComment := &gh.IssueComment{
+		Body: gh.Ptr(comment),
 	}
 
-	err := utils.RequestCallWithRetry(url, "PATCH", gh.token, nil, body)
+	_, _, err := c.gh.Issues.EditComment(c.ctx, defaultOwner, defaultRepo, int64(id), issueComment)
 	if err != nil {
 		return err
 	}
 
 	fmt.Printf("Successfully updated comment %d in pull request %s\n", id, prNumber)
-
 	return nil
 }
 
@@ -111,26 +114,29 @@ func (gh *Client) RemovePullRequestReviewers(prNumber string, reviewers []string
 	return nil
 }
 
-func (gh *Client) AddLabels(prNumber string, labels []string) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/%s/labels", prNumber)
-
-	body := map[string][]string{
-		"labels": labels,
+// AddLabels adds labels to an issue or pull request
+func (c *Client) AddLabels(prNumber string, labels []string) error {
+	num, err := strconv.Atoi(prNumber)
+	if err != nil {
+		return err
 	}
-	err := utils.RequestCallWithRetry(url, "POST", gh.token, nil, body)
 
+	_, _, err = c.gh.Issues.AddLabelsToIssue(c.ctx, defaultOwner, defaultRepo, num, labels)
 	if err != nil {
 		return fmt.Errorf("failed to add %q labels: %s", labels, err)
 	}
 
 	return nil
-
 }
 
-func (gh *Client) RemoveLabel(prNumber, label string) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/issues/%s/labels/%s", prNumber, label)
-	err := utils.RequestCallWithRetry(url, "DELETE", gh.token, nil, nil)
+// RemoveLabel removes a label from an issue or pull request
+func (c *Client) RemoveLabel(prNumber, label string) error {
+	num, err := strconv.Atoi(prNumber)
+	if err != nil {
+		return err
+	}
 
+	_, err = c.gh.Issues.RemoveLabelForIssue(c.ctx, defaultOwner, defaultRepo, num, label)
 	if err != nil {
 		return fmt.Errorf("failed to remove %s label: %s", label, err)
 	}
@@ -138,19 +144,24 @@ func (gh *Client) RemoveLabel(prNumber, label string) error {
 	return nil
 }
 
-func (gh *Client) CreateWorkflowDispatchEvent(workflowFileName string, inputs map[string]any) error {
-	url := fmt.Sprintf("https://api.github.com/repos/GoogleCloudPlatform/magic-modules/actions/workflows/%s/dispatches", workflowFileName)
-	err := utils.RequestCallWithRetry(url, "POST", gh.token, nil, map[string]any{
-		"ref":    "main",
-		"inputs": inputs,
-	})
+// CreateWorkflowDispatchEvent triggers a workflow run
+func (c *Client) CreateWorkflowDispatchEvent(workflowFileName string, inputs map[string]any) error {
+	stringInputs := make(map[string]interface{})
+	for k, v := range inputs {
+		stringInputs[k] = v
+	}
 
+	event := gh.CreateWorkflowDispatchEventRequest{
+		Ref:    "main",
+		Inputs: stringInputs,
+	}
+
+	_, err := c.gh.Actions.CreateWorkflowDispatchEventByFileName(c.ctx, defaultOwner, defaultRepo, workflowFileName, event)
 	if err != nil {
 		return fmt.Errorf("failed to create workflow dispatch event: %s", err)
 	}
 
 	fmt.Printf("Successfully created workflow dispatch event for %s with inputs %v\n", workflowFileName, inputs)
-
 	return nil
 }
 
diff --git a/.ci/magician/utility/utils.go b/.ci/magician/utility/utils.go
index 0325c1090d85..1c55326bc84a 100644
--- a/.ci/magician/utility/utils.go
+++ b/.ci/magician/utility/utils.go
@@ -23,6 +23,7 @@ import (
 	"math"
 	"net/http"
 	"os"
+	"strings"
 	"time"
 
 	"golang.org/x/exp/slices"
@@ -51,21 +52,37 @@ func defaultRetryConfig() retryConfig {
 // makeHTTPRequest performs the actual HTTP request and returns the response
 func makeHTTPRequest(url, method, credentials string, body any) (*http.Response, []byte, error) {
 	client := &http.Client{}
-	jsonBody, err := json.Marshal(body)
-	if err != nil {
-		return nil, nil, fmt.Errorf("error marshaling JSON: %s", err)
+
+	fmt.Println("")
+	fmt.Println("request url: ", url)
+
+	var reqBody io.Reader
+	if body != nil {
+		switch v := body.(type) {
+		case []byte:
+			// Body is already serialized, use directly
+			reqBody = bytes.NewBuffer(v)
+			rbString := strings.TrimSpace(string(v))
+			fmt.Println("request body (raw bytes): ", rbString)
+		default:
+			// Body needs serialization
+			jsonBody, err := json.Marshal(body)
+			if err != nil {
+				return nil, nil, fmt.Errorf("error marshaling JSON: %s", err)
+			}
+			reqBody = bytes.NewBuffer(jsonBody)
+			fmt.Println("request body (serialized): ", string(jsonBody))
+		}
 	}
-	req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonBody))
+
+	req, err := http.NewRequest(method, url, reqBody)
 	if err != nil {
 		return nil, nil, fmt.Errorf("error creating request: %s", err)
 	}
+
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", credentials))
 	req.Header.Set("Content-Type", "application/json")
 	req.Header.Set("Accept", "application/json")
-
-	fmt.Println("")
-	fmt.Println("request url: ", url)
-	fmt.Println("request body: ", string(jsonBody))
 	fmt.Println("")
 
 	resp, err := client.Do(req)


Reviews:
user: login: melinath
body: Overall seems pretty straightforward. A couple small things.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-16T16:51:26Z
user: login: ScottSuarez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:46:56Z
user: login: ScottSuarez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:47:13Z
user: login: ScottSuarez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:47:35Z
user: login: melinath
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T21:21:17Z
user: login: melinath
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T21:22:22Z

Review comments:
diff_hunk: @@ -0,0 +1,310 @@
+//go:build integration
path: .ci/magician/github/integration_test.go
user: login: melinath
body: I'm a little hesitant about this file. I see it's using build tags so it won't build by default, and we're not running it in CI, but it's not a common pattern for us.

Could you add a README file explaining how this is intended to be used?
created_at: 2025-05-16T16:42:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,84 @@
+package github
+
+import (
+	gh "github.com/google/go-github/v68/github"
+)
+
+// Convert from GitHub types to our types
+func convertGHPullRequest(pr *gh.PullRequest) PullRequest {
+	if pr == nil {
+		return PullRequest{}
+	}
+
+	labels := make([]Label, 0)
path: .ci/magician/github/interface_conversion.go
user: login: melinath
body: nit: style

```suggestion
	var labels []Label
```
created_at: 2025-05-16T16:42:50Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,4 @@
-name: "Basic PR checks"
+name: "pull-request"
path: .github/workflows/basic-pr-checks.yml
user: login: melinath
body: could you move this change to a separate PR since it's unrelated?
created_at: 2025-05-16T16:47:29Z
author_association: MEMBER
diff_hunk: @@ -23,6 +23,7 @@ import (
 	"math"
 	"net/http"
 	"os"
+	"strings"
path: .ci/magician/utility/utils.go
user: login: melinath
body: Could you move this file's changes to a separate PR for clarity, since it's not related?
created_at: 2025-05-16T16:49:44Z
author_association: MEMBER
diff_hunk: @@ -23,6 +23,7 @@ import (
 	"math"
 	"net/http"
 	"os"
+	"strings"
path: .ci/magician/utility/utils.go
user: login: ScottSuarez
body: This is related and needed. The RounderTripper passes a []Byte object and the other passes and generic go object that needs to be serialized. 
created_at: 2025-05-16T18:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-name: "Basic PR checks"
+name: "pull-request"
path: .github/workflows/basic-pr-checks.yml
user: login: ScottSuarez
body: done
created_at: 2025-05-16T18:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+//go:build integration
path: .ci/magician/github/integration_test.go
user: login: ScottSuarez
body: added a readme, I am okay removing this altogether if you want.
created_at: 2025-05-16T18:47:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +23,7 @@ import (
 	"math"
 	"net/http"
 	"os"
+	"strings"
path: .ci/magician/utility/utils.go
user: login: melinath
body: ah, okay!
created_at: 2025-05-16T21:21:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,310 @@
+//go:build integration
path: .ci/magician/github/integration_test.go
user: login: melinath
body: I think let's keep it for now.
created_at: 2025-05-16T21:22:09Z
author_association: MEMBER

PR comments:
user: login: ScottSuarez
created_at: 2025-05-15T17:49:09Z
body: a comment
author_association: CONTRIBUTOR
user: login: ScottSuarez
created_at: 2025-05-15T18:02:29Z
body: Updated test comment from integration test at 2025-05-15T11:02:30-07:00
author_association: CONTRIBUTOR
user: login: ScottSuarez
created_at: 2025-05-15T18:03:58Z
body: Updated test comment from integration test at 2025-05-15T11:03:58-07:00
author_association: CONTRIBUTOR
user: login: ScottSuarez
created_at: 2025-05-15T18:04:27Z
body: Updated test comment from integration test at 2025-05-15T11:04:27-07:00
author_association: CONTRIBUTOR
user: login: modular-magician
created_at: 2025-05-15T18:28:25Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-15T18:50:59Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-15T20:53:57Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-15T20:55:39Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-16T18:56:01Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-16T18:56:02Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR hasn't generated any diffs, but I'll let you know if a future commit does.





author_association: COLLABORATOR