You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gh project item-edit command fails with value for --number is floating point #10342
Wrapping up my notes from digging into this a little more before separate recommendations to move this forward.
Questions
Is there any reason not to change this other reference to 64-bit float?
As explained below, there is 1 more reference that feels like we should be consistent in making this change.
Should we validate that the --number provided is between 2147483647 and -2147483647?
As explained below, it appears this isn't really a 64-bit float, so preventing users with argument validation rather than failing API calls seems like the way to go.
The only other reference to 32-bit floats in gh project commandset involves exporting project items along with their field values in gh project item-list --format json.
Though the GraphQL documentation states that Floats are 64-bit double precision, it appears that Number fields for Projects don't exactly measure up. Essentially, Number field values pulled out of the API are roughly 32-bit floats with the following limitations:
largest integer value: 2147483647
maximum decimal digit precision: 8
I'm working with the Projects team to update documentation to help Projects users broadly.
Understanding double float conversions within gh and server side
While exploring this problem further, I was doing some manual boundary testing to understand how the API endpoints behaved.
It seems that spf13/pflag or Go is reading flag values and rounding it to 7 decimal places on the client side, which raises the question if we need to ensure gh keeps up to 8 decimal places. 🤔
UI testing setting a Number field does show users an error when it exceed range, but not when the maximum precision is exceeded.
--number 2147483646.987654321 converts to 2147483646.9876542
andyfeller@Andys-MBP:cli/cli ‹10342-gh-project-item-edit-number›$ GH_DEBUG=api ./bin/gh project item-edit --project-id PVT_kwDOBWmBn84Al2QA --id PVTI_lADOBWmBn84Al2QAzgXCZ4c --field-id PVTF_lADOBWmBn84Al2QAzgnpmgo --number 2147483646.987654321
* Request at 2025-02-05 09:14:43.651574 -0500 EST m=+0.077929709
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 3204
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: America/New_York
> User-Agent: GitHub CLI v2.66.1-72-g64b63f61
GraphQL query:
mutation UpdateItemValues($input:UpdateProjectV2ItemFieldValueInput!){updateProjectV2ItemFieldValue(input:$input){projectV2Item{content{__typename,... on DraftIssue{id,body,title},... on PullRequest{body,title,number,url,repository{nameWithOwner}},... on Issue{body,title,number,url,repository{nameWithOwner}}},id,fieldValues(first: 100){nodes{__typename,... on ProjectV2ItemFieldDateValue{date,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldIterationValue{title,startDate,duration,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}},iterationId},... on ProjectV2ItemFieldLabelValue{labels(first: 10){nodes{name}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldNumberValue{number,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldSingleSelectValue{name,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldTextValue{text,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldMilestoneValue{milestone{title,description,dueOn},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldPullRequestValue{pullRequests(first:10){nodes{url}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldRepositoryValue{repository{url},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldUserValue{users(first: 10){nodes{login}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldReviewerValue{reviewers(first: 10){nodes{__typename,... on Team{name},... on User{login}}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}}}}}}}
GraphQL variables: {"input":{"projectId":"PVT_kwDOBWmBn84Al2QA","itemId":"PVTI_lADOBWmBn84Al2QAzgXCZ4c","fieldId":"PVTF_lADOBWmBn84Al2QAzgnpmgo","value":{"number":2147483646.9876542}}}
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'< Content-Type: application/json; charset=utf-8
< Date: Wed, 05 Feb 2025 14:14:44 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes: repo
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: CFBD:2888E6:9F15BC:13EF257:67A37253
< X-Oauth-Client-Id: 178c6fc778ccc68e1d6a
< X-Oauth-Scopes: admin:org, admin:public_key, admin:ssh_signing_key, gist, project, repo, workflow
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 5000
< X-Ratelimit-Reset: 1738765759
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 630
< X-Xss-Protection: 0
{
"data": {
"updateProjectV2ItemFieldValue": {
"projectV2Item": {
"content": {
"__typename": "Issue",
"body": "This is the body",
"title": "This is a title",
"number": 37,
"url": "https://github.com/tinyfists/gh-nonsense-internal/issues/37",
"repository": {
"nameWithOwner": "tinyfists/gh-nonsense-internal"
}
},
"id": "PVTI_lADOBWmBn84Al2QAzgXCZ4c",
"fieldValues": {
"nodes": [
{
"__typename": "ProjectV2ItemFieldRepositoryValue",
"repository": {
"url": "https://github.com/tinyfists/gh-nonsense-internal"
},
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgd1mYU",
"name": "Repository",
"dataType": "REPOSITORY"
}
},
{
"__typename": "ProjectV2ItemFieldTextValue",
"text": "This is a title",
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgd1mX8",
"name": "Title",
"dataType": "TITLE"
}
},
{
"__typename": "ProjectV2ItemFieldNumberValue",
"number": 2147483646.9876542,
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgnpmgo",
"name": "Floater",
"dataType": "NUMBER"
}
}
]
}
}
}
}
}
* Request took 595.488792ms
Edited item "This is a title"
--number 2147483646.1234567890 converts to 2147483646.1234567
andyfeller@Andys-MBP:cli/cli ‹10342-gh-project-item-edit-number›$ GH_DEBUG=api ./bin/gh project item-edit --project-id PVT_kwDOBWmBn84Al2QA --id PVTI_lADOBWmBn84Al2QAzgXCZ4c --field-id PVTF_lADOBWmBn84Al2QAzgnpmgo --number 2147483646.1234567890
* Request at 2025-02-05 09:25:10.33057 -0500 EST m=+0.064031876
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 3204
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: America/New_York
> User-Agent: GitHub CLI v2.66.1-72-g64b63f61
GraphQL query:
mutation UpdateItemValues($input:UpdateProjectV2ItemFieldValueInput!){updateProjectV2ItemFieldValue(input:$input){projectV2Item{content{__typename,... on DraftIssue{id,body,title},... on PullRequest{body,title,number,url,repository{nameWithOwner}},... on Issue{body,title,number,url,repository{nameWithOwner}}},id,fieldValues(first: 100){nodes{__typename,... on ProjectV2ItemFieldDateValue{date,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldIterationValue{title,startDate,duration,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}},iterationId},... on ProjectV2ItemFieldLabelValue{labels(first: 10){nodes{name}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldNumberValue{number,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldSingleSelectValue{name,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldTextValue{text,field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldMilestoneValue{milestone{title,description,dueOn},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldPullRequestValue{pullRequests(first:10){nodes{url}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldRepositoryValue{repository{url},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldUserValue{users(first: 10){nodes{login}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}},... on ProjectV2ItemFieldReviewerValue{reviewers(first: 10){nodes{__typename,... on Team{name},... on User{login}}},field{__typename,... on ProjectV2Field{id,name,dataType},... on ProjectV2IterationField{id,name,dataType},... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}}}}}}}}}
GraphQL variables: {"input":{"projectId":"PVT_kwDOBWmBn84Al2QA","itemId":"PVTI_lADOBWmBn84Al2QAzgXCZ4c","fieldId":"PVTF_lADOBWmBn84Al2QAzgnpmgo","value":{"number":2147483646.1234567}}}
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'< Content-Type: application/json; charset=utf-8
< Date: Wed, 05 Feb 2025 14:25:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes: repo
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: D060:25126:A8AD1E:1527D9E:67A374C6
< X-Oauth-Client-Id: 178c6fc778ccc68e1d6a
< X-Oauth-Scopes: admin:org, admin:public_key, admin:ssh_signing_key, gist, project, repo, workflow
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 5000
< X-Ratelimit-Reset: 1738765759
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 714
< X-Xss-Protection: 0
{
"data": {
"updateProjectV2ItemFieldValue": {
"projectV2Item": {
"content": {
"__typename": "Issue",
"body": "This is the body",
"title": "This is a title",
"number": 37,
"url": "https://github.com/tinyfists/gh-nonsense-internal/issues/37",
"repository": {
"nameWithOwner": "tinyfists/gh-nonsense-internal"
}
},
"id": "PVTI_lADOBWmBn84Al2QAzgXCZ4c",
"fieldValues": {
"nodes": [
{
"__typename": "ProjectV2ItemFieldRepositoryValue",
"repository": {
"url": "https://github.com/tinyfists/gh-nonsense-internal"
},
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgd1mYU",
"name": "Repository",
"dataType": "REPOSITORY"
}
},
{
"__typename": "ProjectV2ItemFieldTextValue",
"text": "This is a title",
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgd1mX8",
"name": "Title",
"dataType": "TITLE"
}
},
{
"__typename": "ProjectV2ItemFieldNumberValue",
"number": 2147483646.1234567,
"field": {
"__typename": "ProjectV2Field",
"id": "PVTF_lADOBWmBn84Al2QAzgnpmgo",
"name": "Floater",
"dataType": "NUMBER"
}
}
]
}
}
}
}
}
* Request took 613.7205ms
Edited item "This is a title"
@iamazeem@mikekistler : okay, wrapped up digging into this. I'd like to propose the following AC for to wrap up this issue. I don't think a whole lot of work needs to be done for most of these, but just stating clearly what the expectations once the work is merged.
Given v2 project with a Number field Whengh project item-edit --number VALUE is called where VALUE <= 2147483647 and VALUE >= -2147483647 ThenVALUE is converted to a 64-bit float according to Go strconv.ParseFloat(..., 64) and the item field is updated.
Given v2 project with a Number field And Given project item where this field has been set Whengh project item-list --format json is called ThenNumber field is return to the user without any loss of precision
Update
After working with @BagToad, we're removing the client-side validation AC from this as AC as Projects team is going to improve the server-side validation messaging so we don't need to duplicate it.
if_, err:=fmt.Fprintln(config.io.ErrOut, "error: no changes to make"); err!=nil {
returnerr
}
I suspect the challenge we have here is that 0 is the zero value for 32-bit and 64-bit floats even if the user does not provide --number flag.
To be clear, this is a problem, however let's proceed with the issue AC we have so far and file a separate issue for this new discovery while I confirm this with original hubber who introduced the commandset and figure out potential workarounds. 🤔
Activity
andyfeller commentedon Jan 31, 2025
Thanks for opening up this issue, @mikekistler! 🙇 Instead of
GH_DEBUG=true, would you be able to postGH_DEBUG=apiresults instead?My knee jerk is that somehow this number is being manipulated before being sent to the GitHub API, but let's see what ☝ yields.
mikekistler commentedon Feb 1, 2025
@andyfeller That was a good guess!
iamazeem commentedon Feb 2, 2025
@andyfeller: Looked into it.
Apparently, it's related to conversion from
float32tofloat64.From:
cli/pkg/cmd/project/item-edit/item_edit.go
Line 26 in fab84be
To:
cli/pkg/cmd/project/item-edit/item_edit.go
Line 177 in fab84be
Where,
githubv4.Floatisgraphql.Float:https://github.com/shurcooL/githubv4/blob/48295856cce734663ddbd790ff54800f784f3193/scalar.go#L37C2-L37C7
and,
graphql.Floatisfloat64:https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/scalar.go#L18
Ran this simple test in go playground:
and, the output confirms it:
Given above, fixing the type of
numberfield fromfloat32tofloat64should fix this issue.andyfeller commentedon Feb 5, 2025
Thanks for the follow up, @mikekistler, and the confirmation, @iamazeem!
Confirmation
From GraphQL API docs:
UpdateProjectV2ItemFieldValueInputinput used forupdateProjectV2ItemFieldValuemutationvaluefield isFloatChecking out #10374, I can confirm that this appears to address the
gh project item-setissue.Reproducing this on private project
How big of a problem is this?
Looking for other places the GitHub CLI is working with floats seems like there is a little more to this than the proposed changes:
cli/pkg/cmd/project/shared/queries/queries.go
Lines 252 to 255 in 06c5934
I'm working to trace this to the various commands that pull
Numberfields like this out of the API to see how this 32-bit float comes into play, too.cli/pkg/cmd/project/item-list/item_list.go
Lines 95 to 97 in 06c5934
Once I do, then I'll write up acceptance criteria to unblock contributions.
andyfeller commentedon Feb 5, 2025
Wrapping up my notes from digging into this a little more before separate recommendations to move this forward.
Questions
Is there any reason not to change this other reference to 64-bit float?
As explained below, there is 1 more reference that feels like we should be consistent in making this change.
Should we validate that the
--numberprovided is between2147483647and-2147483647?As explained below, it appears this isn't really a 64-bit float, so preventing users with argument validation rather than failing API calls seems like the way to go.
gh project item-list --format jsonuses 32-bit floatThe only other reference to 32-bit floats in
gh projectcommandset involves exporting project items along with their field values ingh project item-list --format json.Though the GraphQL documentation states that Floats are 64-bit double precision, it appears that
Numberfields for Projects don't exactly measure up. Essentially,Numberfield values pulled out of the API are roughly 32-bit floats with the following limitations:21474836478I'm working with the Projects team to update documentation to help Projects users broadly.
Understanding double float conversions within
ghand server sideWhile exploring this problem further, I was doing some manual boundary testing to understand how the API endpoints behaved.
It seems that
spf13/pflagor Go is reading flag values and rounding it to 7 decimal places on the client side, which raises the question if we need to ensureghkeeps up to 8 decimal places. 🤔UI testing setting a
Numberfield does show users an error when it exceed range, but not when the maximum precision is exceeded.--number 2147483646.987654321converts to2147483646.9876542--number 2147483646.1234567890converts to2147483646.1234567andyfeller commentedon Feb 5, 2025
Acceptance Criteria
@iamazeem @mikekistler : okay, wrapped up digging into this. I'd like to propose the following AC for to wrap up this issue. I don't think a whole lot of work needs to be done for most of these, but just stating clearly what the expectations once the work is merged.
Given v2 project with a
NumberfieldWhen
gh project item-edit --number VALUEis called whereVALUE <= 2147483647andVALUE >= -2147483647Then
VALUEis converted to a 64-bit float according to Gostrconv.ParseFloat(..., 64)and the item field is updated.Given v2 project with a
NumberfieldAnd Given project item where this field has been set
When
gh project item-list --format jsonis calledThen
Numberfield is return to the user without any loss of precisionUpdate
After working with @BagToad, we're removing the client-side validation AC from this as AC as Projects team is going to improve the server-side validation messaging so we don't need to duplicate it.
andyfeller commentedon Feb 5, 2025
@iamazeem : I've assigned the issue to you given your interest; thank you again for your continued support! ❤
iamazeem commentedon Feb 6, 2025
@andyfeller: Thank you for the detailed investigation and explanation! 🏆
Here's another case that I found out for
--numberthat 0 is discarded as an invalid value:However, given above valid ranges, it is a valid value and can be set from UI:
Please update AC accordingly. Thanks!
andyfeller commentedon Feb 6, 2025
@iamazeem : have you looked in the history behind that behavior?
Let's dig into it
There are a few places in code that checks for
--number 0, which harken back to #7375 wheregh projectcommandset was introduced:cli/pkg/cmd/project/item-edit/item_edit.go
Lines 79 to 96 in 756ba75
cli/pkg/cmd/project/item-edit/item_edit.go
Lines 148 to 155 in 756ba75
I suspect the challenge we have here is that
0is the zero value for 32-bit and 64-bit floats even if the user does not provide--numberflag.To be clear, this is a problem, however let's proceed with the issue AC we have so far and file a separate issue for this new discovery while I confirm this with original hubber who introduced the commandset and figure out potential workarounds. 🤔
andyfeller commentedon Feb 6, 2025
created #10380 for
--number 0and will add notes on potentially how to address it