Repository: cli/cli
File: pkg/cmd/release/create/http.go (Go)

Code before:

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// This code checks if we received a 404 while attempting to create a release without
	// the workflow scope, and if so, returns an error message that explains a possible
	// solution to the user.
	//
	// If the same file (with both the same path and contents) exists
	// on another branch in the repo, releases with workflow file changes can be
	// created without the workflow scope. Otherwise, the workflow scope is
	// required to create the release.
	//
	// https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
	if resp.StatusCode == http.StatusNotFound && tokenMissingWorkflowScope(resp) {
		normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname())
		errMissingRequiredWorkflowScope := errors.New(heredoc.Docf(`
				HTTP 404: Failed to create release, "workflow" scope may be required
				To request it, run gh auth refresh -h %[1]s -s workflow
			`, normalizedHostname))

		return nil, errMissingRequiredWorkflowScope
	}

	success := resp.StatusCode >= 200 && resp.StatusCode < 300
	if !success {
		return nil, api.HandleHTTPError(resp)
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var newRelease shared.Release
	err = json.Unmarshal(b, &newRelease)
	return &newRelease, err
}

func publishRelease(httpClient *http.Client, releaseURL string, discussionCategory string, isLatest *bool) (*shared.Release, error) {
	params := map[string]interface{}{"draft": false}
	if discussionCategory != "" {
		params["discussion_category_name"] = discussionCategory
	}

	if isLatest != nil {
		params["make_latest"] = fmt.Sprintf("%v", *isLatest)
	}

Review comment (bug):
This technically works though I don't know how much this should align to other places we  have to do similar errors:

https://github.com/cli/cli/blob/1ec23f698f50f709a54099aba633d0031d21d6b4/pkg/cmd/gpg-key/add/add.go#L87-L91

https://github.com/cli/cli/blob/1ec23f698f50f709a54099aba633d0031d21d6b4/pkg/cmd/gpg-key/list/list.go#L60-L65

https://github.com/cli/cli/blob/1ec23f698f50f709a54099aba633d0031d21d6b4/pkg/cmd/project/shared/queries/queries.go#L1461-L1466

https://github.com/cli/cli/blob/1ec23f698f50f709a54099aba633d0031d21d6b4/pkg/cmd/auth/status/status.go#L47-L54

To summarize:

1. Should we print the error here and return a silent error?
2. Should we emphasize the command to run?
3. Should we avoid the `HTTP 404` in the error message?

🤔 



Code after:
	path := fmt.Sprintf("repos/%s/%s/releases", repo.RepoOwner(), repo.RepoName())
	url := ghinstance.RESTPrefix(repo.RepoHost()) + path
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/json; charset=utf-8")

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// Check if we received a 404 while attempting to create a release without
	// the workflow scope, and if so, return an error message that explains a possible
	// solution to the user.
	//
	// If the same file (with both the same path and contents) exists
	// on another branch in the repo, releases with workflow file changes can be
	// created without the workflow scope. Otherwise, the workflow scope is
	// required to create the release, but the API does not indicate this criteria
	// beyond returning a 404.
	//
	// https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
	if resp.StatusCode == http.StatusNotFound && !tokenHasWorkflowScope(resp) {
		normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname())
		return nil, &errMissingRequiredWorkflowScope{
			Hostname: normalizedHostname,
		}
	}

	success := resp.StatusCode >= 200 && resp.StatusCode < 300
	if !success {
		return nil, api.HandleHTTPError(resp)
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var newRelease shared.Release
	err = json.Unmarshal(b, &newRelease)
	return &newRelease, err
}

func publishRelease(httpClient *http.Client, releaseURL string, discussionCategory string, isLatest *bool) (*shared.Release, error) {
	params := map[string]interface{}{"draft": false}
	if discussionCategory != "" {