From 4ecbe57c920e5722b5513908bff78f4d3bd59973 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sat, 14 Dec 2024 06:48:55 -0500 Subject: [PATCH 01/20] feat: add github_release_asset data source This addresses issue #2513 and adds support for a `github_release_asset` data source. Example of passing acceptance tests: ``` GITHUB_ORGANIZATION=mterwill \ GITHUB_OWNER=mterwill \ TF_ACC=1 \ go test -v ./... -run ^TestAccGithubReleaseAssetDataSource ? github.com/integrations/terraform-provider-github/v6 [no test files] === RUN TestAccGithubReleaseAssetDataSource === RUN TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID === RUN TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_anonymous_account provider_utils.go:51: GITHUB_TOKEN environment variable should be empty provider_utils.go:74: Skipping TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_anonymous_account which requires anonymous mode === RUN TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_individual_account === RUN TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_organization_account --- PASS: TestAccGithubReleaseAssetDataSource (11.65s) --- PASS: TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID (11.65s) --- SKIP: TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_anonymous_account (0.00s) --- PASS: TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_individual_account (8.90s) --- PASS: TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID/with_an_organization_account (2.75s) PASS ok github.com/integrations/terraform-provider-github/v6/github 12.434s ``` Signed-off-by: Mike Ball --- github/config.go | 6 +- github/data_source_github_release_asset.go | 161 ++++++++++++++++++ .../data_source_github_release_asset_test.go | 84 +++++++++ github/provider.go | 1 + website/docs/d/release_asset.html.markdown | 84 +++++++++ 5 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 github/data_source_github_release_asset.go create mode 100644 github/data_source_github_release_asset_test.go create mode 100644 website/docs/d/release_asset.html.markdown diff --git a/github/config.go b/github/config.go index 869677fc42..857e35e98c 100644 --- a/github/config.go +++ b/github/config.go @@ -189,7 +189,11 @@ func (injector *previewHeaderInjectorTransport) RoundTrip(req *http.Request) (*h header := req.Header.Get(name) if header == "" { header = value - } else { + // NOTE: Some API endpoints expect a single Accept: application/octet-stream header. + // If one has been set, it's necessary to preserve it as-is, without + // appending previewHeaders value. + // See https://github.com/google/go-github/pull/3392 + } else if !(strings.ToLower(name) == "accept" && header == "application/octet-stream") { header = strings.Join([]string{header, value}, ",") } req.Header.Set(name, header) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go new file mode 100644 index 0000000000..5d9246d52b --- /dev/null +++ b/github/data_source_github_release_asset.go @@ -0,0 +1,161 @@ +package github + +import ( + "context" + "io" + "strconv" + "strings" + + "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubReleaseAsset() *schema.Resource { + return &schema.Resource{ + Description: "Retrieve information about a GitHub release asset.", + ReadContext: dataSourceGithubReleaseAssetRead, + + Schema: map[string]*schema.Schema{ + "asset_id": { + Type: schema.TypeInt, + Required: true, + Description: "ID of the release asset to retrieve", + }, + "owner": { + Type: schema.TypeString, + Required: true, + Description: "Owner of the repository", + }, + "repository": { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository to retrieve the release asset from", + }, + "body": { + Type: schema.TypeString, + Computed: true, + Description: "The release asset body", + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "URL of the asset", + }, + "node_id": { + Type: schema.TypeString, + Computed: true, + Description: "Node ID of the asset", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "File name of the asset", + }, + "label": { + Type: schema.TypeString, + Computed: true, + Description: "Label for the asset", + }, + "content_type": { + Type: schema.TypeString, + Computed: true, + Description: "MIME type of the asset", + }, + "size": { + Type: schema.TypeInt, + Computed: true, + Description: "Asset size in bytes", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date the asset was created", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date the asset was updated", + }, + "browser_download_url": { + Type: schema.TypeString, + Computed: true, + Description: "Browser URL from which the release asset can be downloaded", + }, + }, + } +} + +func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + repository := d.Get("repository").(string) + owner := d.Get("owner").(string) + + client := meta.(*Owner).v3client + + var err error + var asset *github.ReleaseAsset + + assetID := int64(d.Get("asset_id").(int)) + asset, _, err = client.Repositories.GetReleaseAsset(ctx, owner, repository, assetID) + if err != nil { + return diag.FromErr(err) + } + + var respBody io.ReadCloser + clientCopy := client.Client() + respBody, _, err = client.Repositories.DownloadReleaseAsset(ctx, owner, repository, assetID, clientCopy) + if err != nil { + return diag.FromErr(err) + } + defer respBody.Close() + + buf := new(strings.Builder) + _, err = io.Copy(buf, respBody) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(asset.GetID(), 10)) + err = d.Set("body", buf.String()) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("url", asset.URL) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("node_id", asset.NodeID) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("name", asset.Name) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("label", asset.Label) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("content_type", asset.ContentType) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("size", asset.Size) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("created_at", asset.CreatedAt.String()) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("created_at", asset.UpdatedAt.String()) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("browser_download_url", asset.BrowserDownloadURL) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go new file mode 100644 index 0000000000..b2bb1abb8c --- /dev/null +++ b/github/data_source_github_release_asset_test.go @@ -0,0 +1,84 @@ +package github + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubReleaseAssetDataSource(t *testing.T) { + + testReleaseRepository := "go-github-issue-demo-1" + if os.Getenv("GITHUB_TEMPLATE_REPOSITORY") != "" { + testReleaseRepository = os.Getenv("GITHUB_TEMPLATE_REPOSITORY") + } + + testReleaseAssetID := "151970555" + if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_ID") != "" { + testReleaseAssetID = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_ID") + } + + testReleaseAssetName := "foo.txt" + if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_NAME") != "" { + testReleaseAssetName = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_NAME") + } + + testReleaseAssetContent := "Hello, world!\n" + if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_CONTENT") != "" { + testReleaseAssetContent = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_CONTENT") + } + + testReleaseOwner := testOrganizationFunc() + + t.Run("queries specified asset ID", func(t *testing.T) { + + config := fmt.Sprintf(` + data "github_release_asset" "test" { + repository = "%s" + owner = "%s" + asset_id = "%s" + } + `, testReleaseRepository, testReleaseOwner, testReleaseAssetID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "asset_id", testReleaseAssetID, + ), + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "name", testReleaseAssetName, + ), + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "body", testReleaseAssetContent, + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + testCase(t, anonymous) + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + }) + +} diff --git a/github/provider.go b/github/provider.go index 4f857d27c0..9e9a058892 100644 --- a/github/provider.go +++ b/github/provider.go @@ -267,6 +267,7 @@ func Provider() *schema.Provider { "github_organization_webhooks": dataSourceGithubOrganizationWebhooks(), "github_ref": dataSourceGithubRef(), "github_release": dataSourceGithubRelease(), + "github_release_asset": dataSourceGithubReleaseAsset(), "github_repositories": dataSourceGithubRepositories(), "github_repository": dataSourceGithubRepository(), "github_repository_autolink_references": dataSourceGithubRepositoryAutolinkReferences(), diff --git a/website/docs/d/release_asset.html.markdown b/website/docs/d/release_asset.html.markdown new file mode 100644 index 0000000000..7b26202241 --- /dev/null +++ b/website/docs/d/release_asset.html.markdown @@ -0,0 +1,84 @@ +--- +layout: "github" +page_title: "GitHub: github_release_asset" +description: |- + Get information on a GitHub release asset. +--- + +# github\_release\_asset + +Use this data source to retrieve information about a GitHub release asset. + +## Example Usage +To retrieve the latest release that is present in a repository: + +```hcl +data "github_release" "example" { + repository = "example-repository" + owner = "example-owner" + retrieve_by = "latest" +} +``` + +To retrieve a specific release asset from a repository based on its ID: + +```hcl +data "github_release_asset" "example" { + repository = "example-repository" + owner = "example-owner" + asset_id = 12345 +} +``` + +To retrieve the first release asset associated with the the latest release in a repository: + +```hcl +data "github_release" "example" { + repository = "example-repository" + owner = "example-owner" + retrieve_by = "latest" +} + +data "github_release_asset" "example" { + repository = "example-repository" + owner = "example-owner" + asset_id = data.github_release.example.assets[0].id +} +``` + +To retrieve all release assets associated with the the latest release in a repository: + +```hcl +data "github_release" "example" { + repository = "example-repository" + owner = "example-owner" + retrieve_by = "latest" +} + +data "github_release_asset" "example" { + count = length(data.github_release.example.assets) + repository = "example-repository" + owner = "example-owner" + asset_id = data.github_release.example.assets[count.index].id +} +``` + +## Argument Reference + +* `repository` - (Required) Name of the repository to retrieve the release from +* `owner` - (Required) Owner of the repository +* `asset_id` - (Required) ID of the release asset to retrieve + +## Attributes Reference + +* `id` - ID of the asset +* `url` - URL of the asset +* `node_id` - Node ID of the asset +* `name` - The file name of the asset +* `label` - Label for the asset +* `content_type` - MIME type of the asset +* `size` - Asset size in bytes +* `created_at` - Date the asset was created +* `updated_at` - Date the asset was last updated +* `browser_download_url` - Browser URL from which the release asset can be downloaded +* `body` - The release asset body From 1b6916a13abaf9236791c84f2346bec210856204 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 21 Dec 2025 07:07:31 -0500 Subject: [PATCH 02/20] chore(config): test previewHeaderInjectorTransport.RoundTrip Per request of @deiga (https://github.com/integrations/terraform-provider-github/pull/2514#issuecomment-3677836222), this adds tests for previewHeaderInjectorTransport.RoundTrip logic around application/octest-stream header handling. See https://github.com/google/go-github/pull/3392 for context. Signed-off-by: Mike Ball --- github/config_test.go | 215 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/github/config_test.go b/github/config_test.go index 67d6223031..06eccb34ca 100644 --- a/github/config_test.go +++ b/github/config_test.go @@ -2,6 +2,8 @@ package github import ( "context" + "net/http" + "net/http/httptest" "testing" "github.com/shurcooL/githubv4" @@ -300,3 +302,216 @@ func TestAccConfigMeta(t *testing.T) { } }) } + +func TestPreviewHeaderInjectorTransport_RoundTrip(t *testing.T) { + tests := []struct { + name string + previewHeaders map[string]string + existingHeaders map[string]string + expectedHeaders map[string]string + expectRoundTripCall bool + }{ + { + name: "empty preview headers", + previewHeaders: map[string]string{}, + existingHeaders: map[string]string{"User-Agent": "test"}, + expectedHeaders: map[string]string{"User-Agent": "test"}, + expectRoundTripCall: true, + }, + { + name: "add new preview header", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + }, + existingHeaders: map[string]string{}, + expectedHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + }, + expectRoundTripCall: true, + }, + { + name: "append to existing header", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.preview+json", + }, + existingHeaders: map[string]string{ + "Accept": "application/json", + }, + expectedHeaders: map[string]string{ + "Accept": "application/json,application/vnd.github.preview+json", + }, + expectRoundTripCall: true, + }, + { + name: "preserve existing Accept application/octet-stream", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.preview+json", + }, + existingHeaders: map[string]string{ + "Accept": "application/octet-stream", + }, + expectedHeaders: map[string]string{ + "Accept": "application/octet-stream", + }, + expectRoundTripCall: true, + }, + { + name: "preserve existing accept application/octet-stream (lowercase)", + previewHeaders: map[string]string{ + "accept": "application/vnd.github.preview+json", + }, + existingHeaders: map[string]string{ + "accept": "application/octet-stream", + }, + expectedHeaders: map[string]string{ + "Accept": "application/octet-stream", + }, + expectRoundTripCall: true, + }, + { + name: "preserve existing Accept application/octet-stream (mixed case)", + previewHeaders: map[string]string{ + "AcCePt": "application/vnd.github.preview+json", + }, + existingHeaders: map[string]string{ + "Accept": "application/octet-stream", + }, + expectedHeaders: map[string]string{ + "Accept": "application/octet-stream", + }, + expectRoundTripCall: true, + }, + { + name: "multiple preview headers", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + existingHeaders: map[string]string{}, + expectedHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + expectRoundTripCall: true, + }, + { + name: "append multiple preview headers to existing", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + existingHeaders: map[string]string{ + "Accept": "application/json", + "X-GitHub-Api-Version": "2021-01-01", + }, + expectedHeaders: map[string]string{ + "Accept": "application/json,application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2021-01-01,2022-11-28", + }, + expectRoundTripCall: true, + }, + { + name: "non-accept headers always append", + previewHeaders: map[string]string{ + "X-Custom-Header": "preview-value", + }, + existingHeaders: map[string]string{ + "X-Custom-Header": "application/octet-stream", + }, + expectedHeaders: map[string]string{ + "X-Custom-Header": "application/octet-stream,preview-value", + }, + expectRoundTripCall: true, + }, + { + name: "accept header with different value appends", + previewHeaders: map[string]string{ + "Accept": "application/vnd.github.preview+json", + }, + existingHeaders: map[string]string{ + "Accept": "application/json", + }, + expectedHeaders: map[string]string{ + "Accept": "application/json,application/vnd.github.preview+json", + }, + expectRoundTripCall: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock RoundTripper that records the request + var capturedRequest *http.Request + mockRT := &mockRoundTripper{ + roundTripFunc: func(req *http.Request) (*http.Response, error) { + capturedRequest = req + return &http.Response{ + StatusCode: http.StatusOK, + Body: http.NoBody, + }, nil + }, + } + + injector := &previewHeaderInjectorTransport{ + rt: mockRT, + previewHeaders: tt.previewHeaders, + } + + // Create a test request with existing headers + req := httptest.NewRequest(http.MethodGet, "https://api.github.com/test", nil) + for name, value := range tt.existingHeaders { + req.Header.Set(name, value) + } + + // Execute RoundTrip + resp, err := injector.RoundTrip(req) + + // Verify no error + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify response + if resp == nil { + t.Fatal("expected non-nil response") + } + + // Verify RoundTrip was called on the underlying transport + if tt.expectRoundTripCall && capturedRequest == nil { + t.Fatal("expected RoundTrip to be called on underlying transport") + } + + // Verify headers in the captured request + if capturedRequest != nil { + for name, expectedValue := range tt.expectedHeaders { + actualValue := capturedRequest.Header.Get(name) + if actualValue != expectedValue { + t.Errorf("header %q: expected %q, got %q", name, expectedValue, actualValue) + } + } + + // Verify no unexpected headers were added + for name := range capturedRequest.Header { + if _, exists := tt.expectedHeaders[name]; !exists { + // Allow headers that were in existingHeaders but not in expectedHeaders + if _, wasExisting := tt.existingHeaders[name]; !wasExisting { + t.Errorf("unexpected header %q: %q", name, capturedRequest.Header.Get(name)) + } + } + } + } + }) + } +} + +// mockRoundTripper is a mock implementation of http.RoundTripper for testing +type mockRoundTripper struct { + roundTripFunc func(*http.Request) (*http.Response, error) +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if m.roundTripFunc != nil { + return m.roundTripFunc(req) + } + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} From a79408bfbf6da405a177cf712dd5dff1d50f552e Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 8 Jan 2026 09:35:57 -0500 Subject: [PATCH 03/20] chore: github_release_asset data source tests use new test patterns Per code review feedback from @deiga (https://github.com/integrations/terraform-provider-github/pull/2514#issuecomment-3709368981), this updates the github_release_asset data source tests to use the new testing patterns implemented in https://github.com/integrations/terraform-provider-github/pull/2986 This was tested via: ``` $ TF_ACC=1 \ go test -v ./... -run ^TestAccGithubReleaseAssetDataSource ? github.com/integrations/terraform-provider-github/v6 [no test files] === RUN TestAccGithubReleaseAssetDataSource === RUN TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID --- PASS: TestAccGithubReleaseAssetDataSource (5.91s) --- PASS: TestAccGithubReleaseAssetDataSource/queries_specified_asset_ID (5.91s) PASS ok github.com/integrations/terraform-provider-github/v6/github 6.788s ``` Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 2 +- .../data_source_github_release_asset_test.go | 66 ++++++++----------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 5d9246d52b..030ee8b7bc 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v67/github" + "github.com/google/go-github/v81/github" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index b2bb1abb8c..142f0bef72 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -10,27 +10,31 @@ import ( func TestAccGithubReleaseAssetDataSource(t *testing.T) { - testReleaseRepository := "go-github-issue-demo-1" - if os.Getenv("GITHUB_TEMPLATE_REPOSITORY") != "" { - testReleaseRepository = os.Getenv("GITHUB_TEMPLATE_REPOSITORY") - } + testReleaseRepository := testAccConf.testPublicRepository - testReleaseAssetID := "151970555" - if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_ID") != "" { - testReleaseAssetID = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_ID") + // NOTE: the default repository, owner, asset ID, asset name, and asset content + // values can be overridden with GH_TEST* environment variables to exercise + // tests against different release assets in development. + if os.Getenv("GH_TEST_REPOSITORY") != "" { + testReleaseRepository = os.Getenv("GH_TEST_REPOSITORY") } - testReleaseAssetName := "foo.txt" - if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_NAME") != "" { - testReleaseAssetName = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_NAME") + // The terraform-provider-github_6.4.0_manifest.json asset ID from + // https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0 + testReleaseAssetID := "207956097" + if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_ID") != "" { + testReleaseAssetID = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_ID") } - testReleaseAssetContent := "Hello, world!\n" - if os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_CONTENT") != "" { - testReleaseAssetContent = os.Getenv("GITHUB_TEMPLATE_REPOSITORY_RELEASE_ASSET_CONTENT") + testReleaseAssetName := "terraform-provider-github_6.4.0_manifest.json" + if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_NAME") != "" { + testReleaseAssetName = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_NAME") } - testReleaseOwner := testOrganizationFunc() + testReleaseAssetContent := "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}\n" + if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_CONTENT") != "" { + testReleaseAssetContent = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_CONTENT") + } t.Run("queries specified asset ID", func(t *testing.T) { @@ -40,7 +44,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { owner = "%s" asset_id = "%s" } - `, testReleaseRepository, testReleaseOwner, testReleaseAssetID) + `, testReleaseRepository, testAccConf.testPublicRepositoryOwner, testReleaseAssetID) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( @@ -54,31 +58,15 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { ), ) - testCase := func(t *testing.T, mode string) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, mode) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: config, - Check: check, - }, + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, }, - }) - } - - t.Run("with an anonymous account", func(t *testing.T) { - testCase(t, anonymous) - }) - - t.Run("with an individual account", func(t *testing.T) { - testCase(t, individual) + }, }) - - t.Run("with an organization account", func(t *testing.T) { - testCase(t, organization) - }) - }) - } From bf8808d8b51693d0a7ae60de83c1c4f818ec53df Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 12 Jan 2026 09:39:07 -0500 Subject: [PATCH 04/20] chore(data_source_github_release_asset): use if err := ...; err != nil pattern Per code review feedback (https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2681828757) from @stevehipwell, this tweaks the `data_source_github_release_asset` to use the `if err := ...; err != nil` pattern towards the goal of improving readability. Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 34 ++++++++-------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 030ee8b7bc..7c83431458 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -110,50 +110,40 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat defer respBody.Close() buf := new(strings.Builder) - _, err = io.Copy(buf, respBody) - if err != nil { + if _, err := io.Copy(buf, respBody); err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(asset.GetID(), 10)) - err = d.Set("body", buf.String()) - if err != nil { + + if err := d.Set("body", buf.String()); err != nil { return diag.FromErr(err) } - err = d.Set("url", asset.URL) - if err != nil { + if err := d.Set("url", asset.URL); err != nil { return diag.FromErr(err) } - err = d.Set("node_id", asset.NodeID) - if err != nil { + if err := d.Set("node_id", asset.NodeID); err != nil { return diag.FromErr(err) } - err = d.Set("name", asset.Name) - if err != nil { + if err := d.Set("name", asset.Name); err != nil { return diag.FromErr(err) } - err = d.Set("label", asset.Label) - if err != nil { + if err := d.Set("label", asset.Label); err != nil { return diag.FromErr(err) } - err = d.Set("content_type", asset.ContentType) - if err != nil { + if err := d.Set("content_type", asset.ContentType); err != nil { return diag.FromErr(err) } - err = d.Set("size", asset.Size) - if err != nil { + if err := d.Set("size", asset.Size); err != nil { return diag.FromErr(err) } - err = d.Set("created_at", asset.CreatedAt.String()) - if err != nil { + if err := d.Set("created_at", asset.CreatedAt.String()); err != nil { return diag.FromErr(err) } - err = d.Set("created_at", asset.UpdatedAt.String()) - if err != nil { + if err := d.Set("created_at", asset.UpdatedAt.String()); err != nil { return diag.FromErr(err) } - err = d.Set("browser_download_url", asset.BrowserDownloadURL) - if err != nil { + if err := d.Set("browser_download_url", asset.BrowserDownloadURL); err != nil { return diag.FromErr(err) } From 44ea6342fc6764a5ff8f3adc4d4af2807eeb5f82 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 12 Jan 2026 09:43:04 -0500 Subject: [PATCH 05/20] chore(data_source_github_release_asset): improve terseness Use idiomatic `:=` pattern over `var`-pre-defined variables, per code review feedback from @stevehipwell: https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2681826073 Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 7c83431458..fa904c30fe 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/google/go-github/v81/github" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -92,18 +91,14 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat client := meta.(*Owner).v3client - var err error - var asset *github.ReleaseAsset - assetID := int64(d.Get("asset_id").(int)) - asset, _, err = client.Repositories.GetReleaseAsset(ctx, owner, repository, assetID) + asset, _, err := client.Repositories.GetReleaseAsset(ctx, owner, repository, assetID) if err != nil { return diag.FromErr(err) } - var respBody io.ReadCloser clientCopy := client.Client() - respBody, _, err = client.Repositories.DownloadReleaseAsset(ctx, owner, repository, assetID, clientCopy) + respBody, _, err := client.Repositories.DownloadReleaseAsset(ctx, owner, repository, assetID, clientCopy) if err != nil { return diag.FromErr(err) } From 144aff8e8c3dcf9932ec857de60fecf0863bc98e Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 12 Jan 2026 10:12:54 -0500 Subject: [PATCH 06/20] chore(data_source_github_release_asset_test): inline ComposeTestCheckFunc This inlines `resource.ComposeTestCheckFunc` to improve readability (rather than define it as a variable), per code review feedback from @stevehipwell: https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2681887132 Signed-off-by: Mike Ball --- .../data_source_github_release_asset_test.go | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 142f0bef72..3964c46c4b 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -46,25 +46,23 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { } `, testReleaseRepository, testAccConf.testPublicRepositoryOwner, testReleaseAssetID) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "data.github_release_asset.test", "asset_id", testReleaseAssetID, - ), - resource.TestCheckResourceAttr( - "data.github_release_asset.test", "name", testReleaseAssetName, - ), - resource.TestCheckResourceAttr( - "data.github_release_asset.test", "body", testReleaseAssetContent, - ), - ) - resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: config, - Check: check, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "asset_id", testReleaseAssetID, + ), + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "name", testReleaseAssetName, + ), + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "body", testReleaseAssetContent, + ), + ), }, }, }) From 771aef79443114b244bc019133c4b0a4dbb4f1f3 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 12 Jan 2026 10:37:38 -0500 Subject: [PATCH 07/20] chore(data_source_github_release_asset): source config from acc_test.go Per code review feedback from @stevehipwell, this simplifies test configuration: - source test configuration from `testAccConf`, as done elsewhere - remove the ability to override test configuration via `GH_TEST_*` env vars; this is arguably premature optimization and can be added to `acc_test.go` if it's needed in the future Signed-off-by: Mike Ball --- github/acc_test.go | 18 ++++++++--- .../data_source_github_release_asset_test.go | 31 +++---------------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/github/acc_test.go b/github/acc_test.go index c267467476..8be239d9c7 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -49,6 +49,9 @@ type testAccConfig struct { testPublicRepository string testPublicRepositoryOwner string testPublicReleaseId int + testPublicRelaseAssetId string + testPublicRelaseAssetName string + testPublicReleaseAssetContent string testPublicTemplateRepository string testPublicTemplateRepositoryOwner string testGHActionsAppInstallationId int @@ -105,11 +108,16 @@ func TestMain(m *testing.M) { } config := testAccConfig{ - baseURL: baseURL, - authMode: authMode, - testPublicRepository: "terraform-provider-github", - testPublicRepositoryOwner: "integrations", - testPublicReleaseId: 186531906, + baseURL: baseURL, + authMode: authMode, + testPublicRepository: "terraform-provider-github", + testPublicRepositoryOwner: "integrations", + testPublicReleaseId: 186531906, + // The terraform-provider-github_6.4.0_manifest.json asset ID from + // https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0 + testPublicRelaseAssetId: "207956097", + testPublicRelaseAssetName: "terraform-provider-github_6.4.0_manifest.json", + testPublicReleaseAssetContent: "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}\n", testPublicTemplateRepository: "template-repository", testPublicTemplateRepositoryOwner: "template-repository", testGHActionsAppInstallationId: 15368, diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 3964c46c4b..9415e7b942 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -2,7 +2,6 @@ package github import ( "fmt" - "os" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -10,31 +9,11 @@ import ( func TestAccGithubReleaseAssetDataSource(t *testing.T) { + testRepositoryOwner := testAccConf.testPublicRepositoryOwner testReleaseRepository := testAccConf.testPublicRepository - - // NOTE: the default repository, owner, asset ID, asset name, and asset content - // values can be overridden with GH_TEST* environment variables to exercise - // tests against different release assets in development. - if os.Getenv("GH_TEST_REPOSITORY") != "" { - testReleaseRepository = os.Getenv("GH_TEST_REPOSITORY") - } - - // The terraform-provider-github_6.4.0_manifest.json asset ID from - // https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0 - testReleaseAssetID := "207956097" - if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_ID") != "" { - testReleaseAssetID = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_ID") - } - - testReleaseAssetName := "terraform-provider-github_6.4.0_manifest.json" - if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_NAME") != "" { - testReleaseAssetName = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_NAME") - } - - testReleaseAssetContent := "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}\n" - if os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_CONTENT") != "" { - testReleaseAssetContent = os.Getenv("GH_TEST_REPOSITORY_RELEASE_ASSET_CONTENT") - } + testReleaseAssetID := testAccConf.testPublicRelaseAssetId + testReleaseAssetName := testAccConf.testPublicRelaseAssetName + testReleaseAssetContent := testAccConf.testPublicReleaseAssetContent t.Run("queries specified asset ID", func(t *testing.T) { @@ -44,7 +23,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { owner = "%s" asset_id = "%s" } - `, testReleaseRepository, testAccConf.testPublicRepositoryOwner, testReleaseAssetID) + `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, From a663444ed6f4d2e80e18b2377cfbf46030792375 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 16 Jan 2026 06:15:02 -0500 Subject: [PATCH 08/20] chore(data_source_github_release_asset): remove unnecessary TestCase.Providers Per code review feedback from @deiga, this removes unnecessary Providers configuration from the data_source_github_release_asset tests. https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2695678101 Signed-off-by: Mike Ball --- github/data_source_github_release_asset_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 9415e7b942..ae379a0058 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -27,7 +27,6 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, - Providers: testAccProviders, Steps: []resource.TestStep{ { Config: config, From dd22fcaea60db679e486b521bbafa475422500fe Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 16 Jan 2026 06:19:52 -0500 Subject: [PATCH 09/20] fix(data_source_github_release_asset): use composite ID To protect against the possibility that release asset IDs are not globally unique across GitHub, this configures the use of a 3-part composite ID when tracking `data_source_github_release_asset` instances in TF state. This addresses code review feedback from @deiga: https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2695674172 Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index fa904c30fe..54af953199 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -109,7 +109,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - d.SetId(strconv.FormatInt(asset.GetID(), 10)) + d.SetId(buildThreePartID(owner, repository, strconv.FormatInt(assetID, 10))) if err := d.Set("body", buf.String()); err != nil { return diag.FromErr(err) From e4542269bf0194af9abdbb71d0fc6e32bdf3dad2 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 16 Jan 2026 15:27:57 -0500 Subject: [PATCH 10/20] fix(data_source_github_release_asset): avoid client mutation Per code review feedback from @stevehipwell (https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2698364320), this avoids the use of `client.Repositories.DownloadReleaseAsset` out of concern the function modifies the GitHub client state which could cause unexpected behaviour in Terraform. Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 54af953199..1f2c4408c7 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -3,6 +3,7 @@ package github import ( "context" "io" + "net/http" "strconv" "strings" @@ -97,15 +98,27 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } + // Use a client copy to avoid possible mutation of shared GitHub client state + // by client.Repositories.DownloadReleaseAsset. clientCopy := client.Client() - respBody, _, err := client.Repositories.DownloadReleaseAsset(ctx, owner, repository, assetID, clientCopy) + req, err := http.NewRequestWithContext(ctx, "GET", asset.GetBrowserDownloadURL(), nil) if err != nil { return diag.FromErr(err) } - defer respBody.Close() + + req.Header.Set("Accept", "application/octet-stream") + resp, err := clientCopy.Do(req) + if err != nil { + return diag.FromErr(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return diag.Errorf("failed to get release asset (%s/%s %d): %s", owner, repository, assetID, resp.Status) + } buf := new(strings.Builder) - if _, err := io.Copy(buf, respBody); err != nil { + if _, err := io.Copy(buf, resp.Body); err != nil { return diag.FromErr(err) } From 28896b159695b22d32561a868801ef98b58683cf Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 16 Jan 2026 15:30:20 -0500 Subject: [PATCH 11/20] fix(data_source_github_release_asset): properly set updated_at This fixes a copy/paste bug; updated_at is now correctly set on the data source. Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 1f2c4408c7..80b32b838a 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -148,7 +148,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat if err := d.Set("created_at", asset.CreatedAt.String()); err != nil { return diag.FromErr(err) } - if err := d.Set("created_at", asset.UpdatedAt.String()); err != nil { + if err := d.Set("updated_at", asset.UpdatedAt.String()); err != nil { return diag.FromErr(err) } if err := d.Set("browser_download_url", asset.BrowserDownloadURL); err != nil { From 058a3a23b309b60220f6c3df8d1afc349b5f9346 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 16 Jan 2026 16:07:26 -0500 Subject: [PATCH 12/20] feat(data_source_github_release_asset): make download configurable Per code review feedback from @stevehipwell (https://github.com/integrations/terraform-provider-github/pull/2514#pullrequestreview-3670688896), this makes the release asset download optional, controlled by a `download_body` attribute, and off by default. Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 67 +++++++++++-------- .../data_source_github_release_asset_test.go | 34 +++++++++- website/docs/d/release_asset.html.markdown | 22 +++--- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 80b32b838a..3ecced5356 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -32,10 +32,16 @@ func dataSourceGithubReleaseAsset() *schema.Resource { Required: true, Description: "Name of the repository to retrieve the release asset from", }, + "download_body": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to download the asset content into the body attribute", + }, "body": { Type: schema.TypeString, Computed: true, - Description: "The release asset body", + Description: "The release asset body (requires download_body to be 'true'", }, "url": { Type: schema.TypeString, @@ -98,35 +104,8 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - // Use a client copy to avoid possible mutation of shared GitHub client state - // by client.Repositories.DownloadReleaseAsset. - clientCopy := client.Client() - req, err := http.NewRequestWithContext(ctx, "GET", asset.GetBrowserDownloadURL(), nil) - if err != nil { - return diag.FromErr(err) - } - - req.Header.Set("Accept", "application/octet-stream") - resp, err := clientCopy.Do(req) - if err != nil { - return diag.FromErr(err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return diag.Errorf("failed to get release asset (%s/%s %d): %s", owner, repository, assetID, resp.Status) - } - - buf := new(strings.Builder) - if _, err := io.Copy(buf, resp.Body); err != nil { - return diag.FromErr(err) - } - d.SetId(buildThreePartID(owner, repository, strconv.FormatInt(assetID, 10))) - if err := d.Set("body", buf.String()); err != nil { - return diag.FromErr(err) - } if err := d.Set("url", asset.URL); err != nil { return diag.FromErr(err) } @@ -155,5 +134,37 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } + if !d.Get("download_body").(bool) { + return nil + } + + // Use a client copy to avoid possible mutation of shared GitHub client state + // by client.Repositories.DownloadReleaseAsset. + clientCopy := client.Client() + req, err := http.NewRequestWithContext(ctx, "GET", asset.GetBrowserDownloadURL(), nil) + if err != nil { + return diag.FromErr(err) + } + + req.Header.Set("Accept", "application/octet-stream") + resp, err := clientCopy.Do(req) + if err != nil { + return diag.FromErr(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return diag.Errorf("failed to get release asset (%s/%s %d): %s", owner, repository, assetID, resp.Status) + } + + buf := new(strings.Builder) + if _, err := io.Copy(buf, resp.Body); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("body", buf.String()); err != nil { + return diag.FromErr(err) + } + return nil } diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index ae379a0058..53093f8605 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -15,13 +15,13 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { testReleaseAssetName := testAccConf.testPublicRelaseAssetName testReleaseAssetContent := testAccConf.testPublicReleaseAssetContent - t.Run("queries specified asset ID", func(t *testing.T) { - + t.Run("queries and downloads specified asset ID", func(t *testing.T) { config := fmt.Sprintf(` data "github_release_asset" "test" { repository = "%s" owner = "%s" asset_id = "%s" + download_body = true } `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) @@ -45,4 +45,34 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { }, }) }) + + t.Run("queries without downloading the specified asset ID", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_release_asset" "test" { + repository = "%s" + owner = "%s" + asset_id = "%s" + } + `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "asset_id", testReleaseAssetID, + ), + resource.TestCheckResourceAttr( + "data.github_release_asset.test", "name", testReleaseAssetName, + ), + resource.TestCheckNoResourceAttr( + "data.github_release_asset.test", "body", + ), + ), + }, + }, + }) + }) } diff --git a/website/docs/d/release_asset.html.markdown b/website/docs/d/release_asset.html.markdown index 7b26202241..f7b830151d 100644 --- a/website/docs/d/release_asset.html.markdown +++ b/website/docs/d/release_asset.html.markdown @@ -10,27 +10,30 @@ description: |- Use this data source to retrieve information about a GitHub release asset. ## Example Usage -To retrieve the latest release that is present in a repository: +To retrieve a specific release asset from a repository based on its ID: ```hcl -data "github_release" "example" { +data "github_release_asset" "example" { repository = "example-repository" owner = "example-owner" - retrieve_by = "latest" + asset_id = 12345 } ``` -To retrieve a specific release asset from a repository based on its ID: +To retrieve a specific release asset from a repository, and download its body +into a `body` attribute on the data source: ```hcl data "github_release_asset" "example" { - repository = "example-repository" - owner = "example-owner" - asset_id = 12345 + repository = "example-repository" + owner = "example-owner" + asset_id = 12345 + download_body = true } ``` -To retrieve the first release asset associated with the the latest release in a repository: + +To retrieve the first release asset associated with the latest release in a repository: ```hcl data "github_release" "example" { @@ -68,6 +71,7 @@ data "github_release_asset" "example" { * `repository` - (Required) Name of the repository to retrieve the release from * `owner` - (Required) Owner of the repository * `asset_id` - (Required) ID of the release asset to retrieve +* `download_body` - (Optional) If `true`, download the release asset to the `body` attribute. Defaults to `false`. ## Attributes Reference @@ -81,4 +85,4 @@ data "github_release_asset" "example" { * `created_at` - Date the asset was created * `updated_at` - Date the asset was last updated * `browser_download_url` - Browser URL from which the release asset can be downloaded -* `body` - The release asset body +* `body` - The release asset body (requires `download_body` to be `true`) From b038c0b52e44c7dc88275ee10efcb9dce2011517 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 21 Jan 2026 06:05:22 -0500 Subject: [PATCH 13/20] chore(data_source_github_release_asset): use `download_file`/`file` attributes Replace `download_body` argument and `body` attributes w/ `download_file`/`file`, respectively, towards the goal of improving the data source interface. This addresses code review feedback from @stevehipwell: https://github.com/integrations/terraform-provider-github/pull/2514#issuecomment-3777392100 Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 12 ++++++------ github/data_source_github_release_asset_test.go | 6 +++--- website/docs/d/release_asset.html.markdown | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 3ecced5356..5499605769 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -32,16 +32,16 @@ func dataSourceGithubReleaseAsset() *schema.Resource { Required: true, Description: "Name of the repository to retrieve the release asset from", }, - "download_body": { + "download_file": { Type: schema.TypeBool, Optional: true, Default: false, - Description: "Whether to download the asset content into the body attribute", + Description: "Whether to download the asset content into the file attribute", }, - "body": { + "file": { Type: schema.TypeString, Computed: true, - Description: "The release asset body (requires download_body to be 'true'", + Description: "The release asset file contents (requires `download_file` to be `true`", }, "url": { Type: schema.TypeString, @@ -134,7 +134,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - if !d.Get("download_body").(bool) { + if !d.Get("download_file").(bool) { return nil } @@ -162,7 +162,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - if err := d.Set("body", buf.String()); err != nil { + if err := d.Set("file", buf.String()); err != nil { return diag.FromErr(err) } diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 53093f8605..8eef3d66c8 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -21,7 +21,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { repository = "%s" owner = "%s" asset_id = "%s" - download_body = true + download_file = true } `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) @@ -38,7 +38,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { "data.github_release_asset.test", "name", testReleaseAssetName, ), resource.TestCheckResourceAttr( - "data.github_release_asset.test", "body", testReleaseAssetContent, + "data.github_release_asset.test", "file", testReleaseAssetContent, ), ), }, @@ -68,7 +68,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { "data.github_release_asset.test", "name", testReleaseAssetName, ), resource.TestCheckNoResourceAttr( - "data.github_release_asset.test", "body", + "data.github_release_asset.test", "file", ), ), }, diff --git a/website/docs/d/release_asset.html.markdown b/website/docs/d/release_asset.html.markdown index f7b830151d..6ecbb59c60 100644 --- a/website/docs/d/release_asset.html.markdown +++ b/website/docs/d/release_asset.html.markdown @@ -20,15 +20,15 @@ data "github_release_asset" "example" { } ``` -To retrieve a specific release asset from a repository, and download its body -into a `body` attribute on the data source: +To retrieve a specific release asset from a repository, and download the file +into a `file` attribute on the data source: ```hcl data "github_release_asset" "example" { repository = "example-repository" owner = "example-owner" asset_id = 12345 - download_body = true + download_file = true } ``` @@ -71,7 +71,7 @@ data "github_release_asset" "example" { * `repository` - (Required) Name of the repository to retrieve the release from * `owner` - (Required) Owner of the repository * `asset_id` - (Required) ID of the release asset to retrieve -* `download_body` - (Optional) If `true`, download the release asset to the `body` attribute. Defaults to `false`. +* `download_file` - (Optional) Whether to download the asset content into the file attribute. Defaults to `false`. ## Attributes Reference @@ -85,4 +85,4 @@ data "github_release_asset" "example" { * `created_at` - Date the asset was created * `updated_at` - Date the asset was last updated * `browser_download_url` - Browser URL from which the release asset can be downloaded -* `body` - The release asset body (requires `download_body` to be `true`) +* `file` - The release asset file contents (requires `download_file` to be `true`) From aeabad4cb5ec156473079ac7d07db37ed3b4ddf4 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 21 Jan 2026 09:52:43 -0500 Subject: [PATCH 14/20] fix(data_source_github_release_asset): base64 encode `file` Per code review feedback from @stevehipwell (https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2712584341), this ensures `file` is base64-encoded. Signed-off-by: Mike Ball --- github/acc_test.go | 2 +- github/data_source_github_release_asset.go | 21 ++++++++++++++++--- .../data_source_github_release_asset_test.go | 4 +++- website/docs/d/release_asset.html.markdown | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/github/acc_test.go b/github/acc_test.go index 8be239d9c7..7f0e3dce94 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -117,7 +117,7 @@ func TestMain(m *testing.M) { // https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0 testPublicRelaseAssetId: "207956097", testPublicRelaseAssetName: "terraform-provider-github_6.4.0_manifest.json", - testPublicReleaseAssetContent: "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}\n", + testPublicReleaseAssetContent: "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}", testPublicTemplateRepository: "template-repository", testPublicTemplateRepositoryOwner: "template-repository", testGHActionsAppInstallationId: 15368, diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index 5499605769..a4dce101b2 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -2,6 +2,7 @@ package github import ( "context" + "encoding/base64" "io" "net/http" "strconv" @@ -41,7 +42,7 @@ func dataSourceGithubReleaseAsset() *schema.Resource { "file": { Type: schema.TypeString, Computed: true, - Description: "The release asset file contents (requires `download_file` to be `true`", + Description: "The base64-encoded release asset file contents (requires `download_file` to be `true`", }, "url": { Type: schema.TypeString, @@ -158,8 +159,22 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat } buf := new(strings.Builder) - if _, err := io.Copy(buf, resp.Body); err != nil { - return diag.FromErr(err) + encoder := base64.NewEncoder(base64.StdEncoding, buf) + defer encoder.Close() + buffer := make([]byte, 4096) + for { + n, err := resp.Body.Read(buffer) + if err != nil && err != io.EOF { + return diag.FromErr(err) + } + if n > 0 { + if _, err := encoder.Write(buffer[:n]); err != nil { + return diag.FromErr(err) + } + } + if err == io.EOF { + break + } } if err := d.Set("file", buf.String()); err != nil { diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 8eef3d66c8..129d6b919a 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -1,6 +1,7 @@ package github import ( + "encoding/base64" "fmt" "testing" @@ -14,6 +15,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { testReleaseAssetID := testAccConf.testPublicRelaseAssetId testReleaseAssetName := testAccConf.testPublicRelaseAssetName testReleaseAssetContent := testAccConf.testPublicReleaseAssetContent + base64EncodedAssetContent := base64.StdEncoding.EncodeToString([]byte(testReleaseAssetContent)) t.Run("queries and downloads specified asset ID", func(t *testing.T) { config := fmt.Sprintf(` @@ -38,7 +40,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { "data.github_release_asset.test", "name", testReleaseAssetName, ), resource.TestCheckResourceAttr( - "data.github_release_asset.test", "file", testReleaseAssetContent, + "data.github_release_asset.test", "file", base64EncodedAssetContent, ), ), }, diff --git a/website/docs/d/release_asset.html.markdown b/website/docs/d/release_asset.html.markdown index 6ecbb59c60..e5354f331f 100644 --- a/website/docs/d/release_asset.html.markdown +++ b/website/docs/d/release_asset.html.markdown @@ -85,4 +85,4 @@ data "github_release_asset" "example" { * `created_at` - Date the asset was created * `updated_at` - Date the asset was last updated * `browser_download_url` - Browser URL from which the release asset can be downloaded -* `file` - The release asset file contents (requires `download_file` to be `true`) +* `file` - The base64-encoded release asset file contents (requires `download_file` to be `true`) From 9e5b63510a8a23fb063f283dc245a70faf423471 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 21 Jan 2026 20:07:58 -0500 Subject: [PATCH 15/20] chore(data_source_github_release_asset): adjust file contents attributes Per code review feedback from @stevehipwell, this changes `download_file`/`file` to `download_file_contents`/`file_contents`, respectively: https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2713331673 Signed-off-by: Mike Ball --- github/data_source_github_release_asset.go | 12 ++++++------ github/data_source_github_release_asset_test.go | 4 ++-- website/docs/d/release_asset.html.markdown | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/github/data_source_github_release_asset.go b/github/data_source_github_release_asset.go index a4dce101b2..29a5825258 100644 --- a/github/data_source_github_release_asset.go +++ b/github/data_source_github_release_asset.go @@ -33,16 +33,16 @@ func dataSourceGithubReleaseAsset() *schema.Resource { Required: true, Description: "Name of the repository to retrieve the release asset from", }, - "download_file": { + "download_file_contents": { Type: schema.TypeBool, Optional: true, Default: false, - Description: "Whether to download the asset content into the file attribute", + Description: "Whether to download the asset file content into the `file_contents` attribute", }, - "file": { + "file_contents": { Type: schema.TypeString, Computed: true, - Description: "The base64-encoded release asset file contents (requires `download_file` to be `true`", + Description: "The base64-encoded release asset file contents (requires `download_file_contents` to be `true`)", }, "url": { Type: schema.TypeString, @@ -135,7 +135,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - if !d.Get("download_file").(bool) { + if !d.Get("download_file_contents").(bool) { return nil } @@ -177,7 +177,7 @@ func dataSourceGithubReleaseAssetRead(ctx context.Context, d *schema.ResourceDat } } - if err := d.Set("file", buf.String()); err != nil { + if err := d.Set("file_contents", buf.String()); err != nil { return diag.FromErr(err) } diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 129d6b919a..966898cc29 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -23,7 +23,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { repository = "%s" owner = "%s" asset_id = "%s" - download_file = true + download_file_contents = true } `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) @@ -40,7 +40,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { "data.github_release_asset.test", "name", testReleaseAssetName, ), resource.TestCheckResourceAttr( - "data.github_release_asset.test", "file", base64EncodedAssetContent, + "data.github_release_asset.test", "file_contents", base64EncodedAssetContent, ), ), }, diff --git a/website/docs/d/release_asset.html.markdown b/website/docs/d/release_asset.html.markdown index e5354f331f..a5b9faa1ec 100644 --- a/website/docs/d/release_asset.html.markdown +++ b/website/docs/d/release_asset.html.markdown @@ -71,7 +71,7 @@ data "github_release_asset" "example" { * `repository` - (Required) Name of the repository to retrieve the release from * `owner` - (Required) Owner of the repository * `asset_id` - (Required) ID of the release asset to retrieve -* `download_file` - (Optional) Whether to download the asset content into the file attribute. Defaults to `false`. +* `download_file_contents` - (Optional) Whether to download the asset file content into the `file_contents` attribute (defaults to `false`) ## Attributes Reference @@ -85,4 +85,4 @@ data "github_release_asset" "example" { * `created_at` - Date the asset was created * `updated_at` - Date the asset was last updated * `browser_download_url` - Browser URL from which the release asset can be downloaded -* `file` - The base64-encoded release asset file contents (requires `download_file` to be `true`) +* `file_contents` - The base64-encoded release asset file contents (requires `download_file_contents` to be `true`) From 6b6c797b11f29dfce3145331b4048f700b3d080c Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 21 Jan 2026 20:14:35 -0500 Subject: [PATCH 16/20] chore(data_source_github_release_asset): improve acc test This improves the `data_source_github_release_asset` acceptance tests to be more TF-focused via use of the `base64decode` function, per code review request of @stevehipwell: https://github.com/integrations/terraform-provider-github/pull/2514#discussion_r2713332624 Signed-off-by: Mike Ball --- github/data_source_github_release_asset_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index 966898cc29..a798f3700e 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -1,7 +1,6 @@ package github import ( - "encoding/base64" "fmt" "testing" @@ -15,7 +14,6 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { testReleaseAssetID := testAccConf.testPublicRelaseAssetId testReleaseAssetName := testAccConf.testPublicRelaseAssetName testReleaseAssetContent := testAccConf.testPublicReleaseAssetContent - base64EncodedAssetContent := base64.StdEncoding.EncodeToString([]byte(testReleaseAssetContent)) t.Run("queries and downloads specified asset ID", func(t *testing.T) { config := fmt.Sprintf(` @@ -25,6 +23,10 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { asset_id = "%s" download_file_contents = true } + + output "github_release_asset_contents" { + value = base64decode(data.github_release_asset.test.file_contents) + } `, testReleaseRepository, testRepositoryOwner, testReleaseAssetID) resource.Test(t, resource.TestCase{ @@ -39,9 +41,7 @@ func TestAccGithubReleaseAssetDataSource(t *testing.T) { resource.TestCheckResourceAttr( "data.github_release_asset.test", "name", testReleaseAssetName, ), - resource.TestCheckResourceAttr( - "data.github_release_asset.test", "file_contents", base64EncodedAssetContent, - ), + resource.TestCheckOutput("github_release_asset_contents", testReleaseAssetContent), ), }, }, From 57edee52b8b09f48a09a57263b8147f926b58894 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Tue, 27 Jan 2026 12:50:14 -0500 Subject: [PATCH 17/20] chore(data_source_github_release_asset_test): appease linting This commits the result of `gofumpt`, as per lint failures in: https://github.com/integrations/terraform-provider-github/actions/runs/21232219897/job/61621068985?pr=2514 Signed-off-by: Mike Ball --- github/data_source_github_release_asset_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/github/data_source_github_release_asset_test.go b/github/data_source_github_release_asset_test.go index a798f3700e..134ee87668 100644 --- a/github/data_source_github_release_asset_test.go +++ b/github/data_source_github_release_asset_test.go @@ -8,7 +8,6 @@ import ( ) func TestAccGithubReleaseAssetDataSource(t *testing.T) { - testRepositoryOwner := testAccConf.testPublicRepositoryOwner testReleaseRepository := testAccConf.testPublicRepository testReleaseAssetID := testAccConf.testPublicRelaseAssetId From e0eb14bdaeaa792ddc339e8ea0858e2eabfe5306 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Tue, 27 Jan 2026 12:55:29 -0500 Subject: [PATCH 18/20] chore(config_test): appease lint errors This appeases lint warnings in `github/config_test.go`: https://github.com/integrations/terraform-provider-github/actions/runs/21232219897/job/61621068985?pr=2514 Signed-off-by: Mike Ball --- github/config_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/github/config_test.go b/github/config_test.go index 06eccb34ca..56c10c2e00 100644 --- a/github/config_test.go +++ b/github/config_test.go @@ -465,7 +465,6 @@ func TestPreviewHeaderInjectorTransport_RoundTrip(t *testing.T) { // Execute RoundTrip resp, err := injector.RoundTrip(req) - // Verify no error if err != nil { t.Fatalf("unexpected error: %v", err) @@ -504,7 +503,7 @@ func TestPreviewHeaderInjectorTransport_RoundTrip(t *testing.T) { } } -// mockRoundTripper is a mock implementation of http.RoundTripper for testing +// mockRoundTripper is a mock implementation of http.RoundTripper for testing. type mockRoundTripper struct { roundTripFunc func(*http.Request) (*http.Response, error) } From 502932e835fb6cd9db160dc774575597b8c75c2b Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Tue, 27 Jan 2026 13:02:43 -0500 Subject: [PATCH 19/20] chore(config): address staticcheck errors This addresses the following staticcheck error (seen in https://github.com/integrations/terraform-provider-github/actions/runs/21232219897/job/61621068985?pr=2514): ``` Error: github/config.go:196:13: QF1001: could apply De Morgan's law (staticcheck) ``` Signed-off-by: Mike Ball --- github/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/config.go b/github/config.go index 857e35e98c..a61dd541b7 100644 --- a/github/config.go +++ b/github/config.go @@ -193,7 +193,7 @@ func (injector *previewHeaderInjectorTransport) RoundTrip(req *http.Request) (*h // If one has been set, it's necessary to preserve it as-is, without // appending previewHeaders value. // See https://github.com/google/go-github/pull/3392 - } else if !(strings.ToLower(name) == "accept" && header == "application/octet-stream") { + } else if strings.ToLower(name) != "accept" || header != "application/octet-stream" { header = strings.Join([]string{header, value}, ",") } req.Header.Set(name, header) From 19bf183e217def5659dd977fe75261faccdd1c19 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 28 Jan 2026 14:22:50 -0500 Subject: [PATCH 20/20] fix(config_test): use canonical HTTP header names in expectedHeaders Go's http.Header canonicalizes header names (e.g., X-GitHub-Api-Version becomes X-Github-Api-Version). The test's unexpected header check was failing because it compared canonical names against non-canonical keys in the expectedHeaders map. Signed-off-by: Mike Ball --- github/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/config_test.go b/github/config_test.go index 56c10c2e00..1d88e231bc 100644 --- a/github/config_test.go +++ b/github/config_test.go @@ -390,7 +390,7 @@ func TestPreviewHeaderInjectorTransport_RoundTrip(t *testing.T) { existingHeaders: map[string]string{}, expectedHeaders: map[string]string{ "Accept": "application/vnd.github.v3+json", - "X-GitHub-Api-Version": "2022-11-28", + "X-Github-Api-Version": "2022-11-28", }, expectRoundTripCall: true, }, @@ -406,7 +406,7 @@ func TestPreviewHeaderInjectorTransport_RoundTrip(t *testing.T) { }, expectedHeaders: map[string]string{ "Accept": "application/json,application/vnd.github.v3+json", - "X-GitHub-Api-Version": "2021-01-01,2022-11-28", + "X-Github-Api-Version": "2021-01-01,2022-11-28", }, expectRoundTripCall: true, },