diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4ff7fbca2..4cc5df46a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -24,7 +24,7 @@ We accept pull requests for issues labelled `help wanted`. We encourage issues a ## Building the project Prerequisites: -- Go 1.25+ +- Go 1.26+ Build with: * Unix-like systems: `make` diff --git a/.github/ISSUE_TEMPLATE/feedback.md b/.github/ISSUE_TEMPLATE/feedback.md deleted file mode 100644 index 837c36632..000000000 --- a/.github/ISSUE_TEMPLATE/feedback.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "\U0001F4E3 Feedback" -about: Give us general feedback about the GitHub CLI -title: '' -labels: feedback -assignees: '' - ---- - -# CLI Feedback - -You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you! - -## What have you loved? - -_eg "the nice colors"_ - -## What was confusing or gave you pause? - -_eg "it did something unexpected"_ - -## Are there features you'd like to see added? - -_eg "gh cli needs mini-games"_ - -## Anything else? - -_eg "have a nice day"_ diff --git a/.github/licenses.tmpl b/.github/licenses.tmpl index f9e800d3d..33298300c 100644 --- a/.github/licenses.tmpl +++ b/.github/licenses.tmpl @@ -1,13 +1,8 @@ -# GitHub CLI dependencies +GitHub CLI third-party dependencies +==================================== -The following open source dependencies are used to build the [cli/cli][] GitHub CLI. +The following open source dependencies are used to build the GitHub CLI. -## Go Packages - -Some packages may only be included on certain architectures or operating systems. - -{{ range . }} -- [{{.Name}}](https://pkg.go.dev/{{.Name}}) ([{{.LicenseName}}]({{.LicenseURL}})) -{{- end }} - -[cli/cli]: https://github.com/cli/cli +{{ range . -}} +{{.Name}} ({{.Version}}) - {{.LicenseName}} - {{.LicenseURL}} +{{ end }} diff --git a/.github/secret_scanning.yml b/.github/secret_scanning.yml deleted file mode 100644 index 83ee7b460..000000000 --- a/.github/secret_scanning.yml +++ /dev/null @@ -1,3 +0,0 @@ -paths-ignore: - - 'third-party/**' - - 'third-party-licenses.*.md' diff --git a/.github/workflows/bump-go.yml b/.github/workflows/bump-go.yml index 827bbc608..f9647b210 100644 --- a/.github/workflows/bump-go.yml +++ b/.github/workflows/bump-go.yml @@ -2,6 +2,7 @@ name: Bump Go on: schedule: - cron: "0 3 * * *" # 3 AM UTC + workflow_dispatch: permissions: contents: write pull-requests: write diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 0470a6644..67ea742f6 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -50,7 +50,7 @@ jobs: with: go-version-file: 'go.mod' - name: Install GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: # The version is pinned not only for security purposes, but also to avoid breaking # our scripts, which rely on the specific file names generated by GoReleaser. @@ -70,7 +70,7 @@ jobs: run: | go run ./cmd/gen-docs --website --doc-path dist/manual tar -czvf dist/manual.tar.gz -C dist -- manual - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: linux if-no-files-found: error @@ -111,7 +111,7 @@ jobs: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" rm "$RUNNER_TEMP/cert.p12" - name: Install GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: # The version is pinned not only for security purposes, but also to avoid breaking # our scripts, which rely on the specific file names generated by GoReleaser. @@ -150,7 +150,7 @@ jobs: run: | shopt -s failglob script/pkgmacos "$TAG_NAME" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: macos if-no-files-found: error @@ -173,7 +173,7 @@ jobs: with: go-version-file: 'go.mod' - name: Install GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: # The version is pinned not only for security purposes, but also to avoid breaking # our scripts, which rely on the specific file names generated by GoReleaser. @@ -263,7 +263,7 @@ jobs: Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object { .\script\sign.ps1 $_.FullName } - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: windows if-no-files-found: error @@ -281,7 +281,7 @@ jobs: - name: Checkout cli/cli uses: actions/checkout@v6 - name: Merge built artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 - name: Checkout documentation site uses: actions/checkout@v6 with: @@ -334,7 +334,7 @@ jobs: rpmsign --addsign dist/*.rpm - name: Attest release artifacts if: inputs.environment == 'production' - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: "dist/gh_*" create-storage-record: false # (default: true) diff --git a/.github/workflows/feature-request-comment.yml b/.github/workflows/feature-request-comment.yml deleted file mode 100644 index 8426d7af2..000000000 --- a/.github/workflows/feature-request-comment.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Add feature-request comment -on: - issues: - types: - - labeled - -permissions: - issues: write - -jobs: - add-comment-to-feature-request-issues: - if: github.event.label.name == 'enhancement' - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - BODY: > - Thank you for your issue! We have categorized it as a feature request, - and it has been added to our backlog. In doing so, **we are not - committing to implementing this feature at this time**, but, we will - consider it for future releases based on community feedback and our own - product roadmap. - - - Unless you see the - https://github.com/cli/cli/labels/help%20wanted label, we are - not currently looking for external contributions for this feature. - - - **If you come across this issue and would like to see it implemented, - please add a thumbs up!** This will help us prioritize the feature. - Please only comment if you have additional information or viewpoints to - contribute. - steps: - - run: gh issue comment "$NUMBER" --body "$BODY" diff --git a/.github/workflows/issueauto.yml b/.github/workflows/issueauto.yml deleted file mode 100644 index cfdcff764..000000000 --- a/.github/workflows/issueauto.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Issue Automation -on: - issues: - types: [opened] - -permissions: - contents: none - issues: write - -jobs: - issue-auto: - runs-on: ubuntu-latest - environment: cli-automation - steps: - - name: label incoming issue - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - ISSUENUM: ${{ github.event.issue.number }} - ISSUEAUTHOR: ${{ github.event.issue.user.login }} - run: | - if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null - then - gh issue edit $ISSUENUM --add-label "needs-triage" - fi \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 94846ce67..d55a944c8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,14 +8,14 @@ on: - go.mod - go.sum - ".github/licenses.tmpl" - - "script/licenses*" + - "script/licenses" pull_request: paths: - "**.go" - go.mod - go.sum - ".github/licenses.tmpl" - - "script/licenses*" + - "script/licenses" permissions: contents: read jobs: @@ -48,18 +48,18 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.6.0 + version: v2.11.0 + # Verify that license generation succeeds for all release platforms (GOOS/GOARCH). + # This catches issues like new dependencies with unrecognized licenses before release time. + # # actions/setup-go does not setup the installed toolchain to be preferred over the system install, # which causes go-licenses to raise "Package ... does not have module info" errors. # For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633 - # - # go-licenses has been pinned for automation use. - - name: Check licenses + - name: Verify license generation run: | export GOROOT=$(go env GOROOT) export PATH=${GOROOT}/bin:$PATH - go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 # v2.0.1 make licenses-check # Discover vulnerabilities within Go standard libraries used to build GitHub CLI using govulncheck. @@ -77,7 +77,7 @@ jobs: # `govulncheck` exits unsuccessfully if vulnerabilities are found, providing results in stdout. # See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck#hdr-Exit_codes for more information on exit codes. # - # On go1.25, To make `-mode binary` work we need to make sure the binary is built with `go build -buildvcs=false` + # On go1.25+, To make `-mode binary` work we need to make sure the binary is built with `go build -buildvcs=false` # Since our builds do not use `-buildvcs=false`, we run in source mode here instead. - name: Check Go vulnerabilities run: | diff --git a/.github/workflows/pr-help-wanted.yml b/.github/workflows/pr-help-wanted.yml deleted file mode 100644 index b63d025bc..000000000 --- a/.github/workflows/pr-help-wanted.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: PR Help Wanted Check -on: - pull_request_target: - types: [opened] - workflow_dispatch: - inputs: - pr_number: - description: "Pull Request number to check" - required: true - type: string - -permissions: - contents: none - issues: read - pull-requests: write - -jobs: - check-help-wanted: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set PR variables for workflow_dispatch event - id: pr-vars-dispatch - if: github.event_name == 'workflow_dispatch' - env: - PR_NUMBER: ${{ github.event.inputs.pr_number }} - run: | - # We only need to construct the PR URL from the dispatch event input. - echo "pr_url=https://github.com/cli/cli/pull/${PR_NUMBER}" >> $GITHUB_OUTPUT - - - name: Check for issues without help-wanted label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # These variables are optionally used in the check-help-wanted.sh - # script for additional checks; but they are not strictly necessary - # for the script to run. This is why we are okay with them being - # empty when the event is workflow_dispatch. - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} - PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} - PR_URL: ${{ github.event.pull_request.html_url || steps.pr-vars-dispatch.outputs.pr_url }} - run: | - # Run the script to check for issues without help-wanted label - bash .github/workflows/scripts/check-help-wanted.sh "${PR_URL}" diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml deleted file mode 100644 index 40dfee846..000000000 --- a/.github/workflows/prauto.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: PR Automation -on: - pull_request_target: - types: [ready_for_review, opened, reopened] - -permissions: - contents: none - issues: write - pull-requests: write - -jobs: - pr-auto: - runs-on: ubuntu-latest - environment: cli-automation - steps: - - name: lint pr - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - PRBODY: ${{ github.event.pull_request.body }} - PRNUM: ${{ github.event.pull_request.number }} - PRHEAD: ${{ github.event.pull_request.head.label }} - PRAUTHOR: ${{ github.event.pull_request.user.login }} - PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} - if: "!github.event.pull_request.draft" - run: | - commentPR () { - gh pr comment $PRNUM -b "${1}" - } - - closePR () { - gh pr close $PRNUM - } - - colID () { - gh api graphql -f query='query($owner:String!, $repo:String!) { - repository(owner:$owner, name:$repo) { - project(number:1) { - columns(first:10) { nodes {id,name} } - } - } - }' -f owner="${GH_REPO%/*}" -f repo="${GH_REPO#*/}" \ - -q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id" - } - - if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null - then - if [ "$PR_AUTHOR_TYPE" != "Bot" ] - then - gh pr edit $PRNUM --add-assignee $PRAUTHOR - fi - exit 0 - fi - - gh pr edit $PRNUM --add-label "external" - - if [ "$PRHEAD" = "cli:trunk" ] - then - closePR - exit 0 - fi - - if [ $(wc -c <<<"$PRBODY") -lt 10 ] - then - commentPR "Thanks for the pull request! We're a small team and it's helpful to have context around community submissions in order to review them appropriately. Our automation has closed this pull request since it does not have an adequate description. Please edit the body of this pull request to describe what this does, then reopen it." - closePR - exit 0 - fi - - if ! grep -Eq '(#|issues/)[0-9]+' <<<"$PRBODY" - then - commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message." - fi - - exit 0 diff --git a/.github/workflows/scripts/check-help-wanted.sh b/.github/workflows/scripts/check-help-wanted.sh deleted file mode 100755 index d713be144..000000000 --- a/.github/workflows/scripts/check-help-wanted.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash - -set -e - -PR_URL="$1" - -if [ -z "$PR_URL" ]; then - echo "Usage: $0 " - echo "" - echo "Check if the PR references any non-help-wanted issues and, if so, comment" - echo "on it explaining why the team might close/dismiss it." - exit 1 -fi - -# Skip if PR is from a bot or org member -if [ "$PR_AUTHOR_TYPE" = "Bot" ] || [ "$PR_AUTHOR_ASSOCIATION" = "MEMBER" ] || [ "$PR_AUTHOR_ASSOCIATION" = "OWNER" ]; then - echo "Skipping check for PR $PR_URL as it is from a bot ($PR_AUTHOR_TYPE) or an org member ($PR_AUTHOR_ASSOCIATION: MEMBER/OWNER)" - exit 0 -fi - -# Skip if PR is a draft -if [ "$(gh pr view "${PR_URL}" --json isDraft --jq '.isDraft')" != "false" ]; then - echo "Skipping check for PR $PR_URL as it is a draft" - exit 0 -fi - -# Extract PR number from URL for logging -PR_NUM="$(basename "$PR_URL")" - -# Extract cli/cli closing issues references from PR -CLOSING_ISSUES="$(gh pr view "$PR_URL" --json closingIssuesReferences --jq '.closingIssuesReferences[] | select(.repository.name == "cli" and .repository.owner.login == "cli") | .number')" - -if [ -z "$CLOSING_ISSUES" ]; then - echo "No closing issues found for PR #$PR_NUM" - exit 0 -fi - -# Check each closing issue for 'help-wanted' label -ISSUES_WITHOUT_HELP_WANTED=() - -for issue_num in $CLOSING_ISSUES; do - echo "Checking issue #$issue_num for 'help wanted' label..." - - # Get issue labels - LABELS=$(gh issue view "$issue_num" --json labels --jq '.labels[].name') - - # Skip if the issue has the gh-attestion or gh-codespace label - # This is because the codeowners for these commands may not be public - # cli org members, and so unless we authenticate with a PAT, we can't - # know who is an external contributor or not. - # So we skip these issues to avoid falsely writing a comment - # on each PR opened by these codeowners. - if echo "$LABELS" | grep -q -e "gh-attestation" -e "gh-codespace"; then - echo "Issue #$issue_num is skipped due to labels" - continue - fi - - # Check if 'help wanted' label exists - if ! echo "$LABELS" | grep -qE '^help wanted$'; then - ISSUES_WITHOUT_HELP_WANTED+=("$issue_num") - echo "Issue #$issue_num does not have 'help wanted' label" - else - echo "Issue #$issue_num has 'help wanted' label" - fi -done - -# If we found issues without 'help wanted' label, post a comment -if [ ${#ISSUES_WITHOUT_HELP_WANTED[@]} -gt 0 ]; then - echo "Found ${#ISSUES_WITHOUT_HELP_WANTED[@]} issues without 'help wanted' label" - - # Build issue list for comment - ISSUE_LIST="" - for issue_num in "${ISSUES_WITHOUT_HELP_WANTED[@]}"; do - ISSUE_LIST="$ISSUE_LIST- #$issue_num"$'\n' - done - - # Create comment message - gh pr comment "$PR_URL" --body-file - < diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml deleted file mode 100644 index 543a909c8..000000000 --- a/.github/workflows/stale-issues.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Marks/closes stale issues -on: - schedule: - - cron: "0 3 * * *" # 3 AM UTC - -permissions: - issues: write - -jobs: - mark-stale-issues: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v10 - with: - start-date: "2025-07-10T00:00:00Z" # Skip for issues created before this date - days-before-issue-stale: 30 - only-issue-labels: - "needs-triage,needs-user-input" # Only issues with all of these labels can be marked as stale - exempt-issue-labels: "keep" # Issues marked with this label should not be marked as stale - stale-issue-label: "stale" # Mark stale issues with this label - stale-issue-message: | - This issue has been automatically marked as stale because it has not had any activity in the last 30 days, - and it will be closed in 30 days if no further activity occurs. - - If you think this is a mistake, please comment on this issue to keep it open. - - days-before-issue-close: 30 - close-issue-reason: "not_planned" - close-issue-message: | - This issue has been automatically closed due to inactivity. - - If you think this is a mistake, please comment on this issue. - - # Exclude PRs from closing or being marked as stale - days-before-pr-stale: -1 - days-before-pr-close: -1 diff --git a/.github/workflows/triage-discussion-label.yml b/.github/workflows/triage-discussion-label.yml new file mode 100644 index 000000000..e2e4ea5e5 --- /dev/null +++ b/.github/workflows/triage-discussion-label.yml @@ -0,0 +1,23 @@ +name: Process Discuss Label +run-name: ${{ github.event_name == 'issues' && github.event.issue.title || github.event.pull_request.title }} +permissions: {} +on: + issues: + types: + - labeled + # pull_request_target (not pull_request) to access secrets for fork PRs. + # Safe: no PR code is checked out or executed. + pull_request_target: + types: + - labeled + +jobs: + discuss: + if: github.event.action == 'labeled' && github.event.label.name == 'discuss' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-discuss.yml@main + with: + target_repo: 'github/cli' + cc_team: '@github/cli' + environment: cli-discuss-automation + secrets: + discussion_token: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} diff --git a/.github/workflows/triage-issues.yml b/.github/workflows/triage-issues.yml new file mode 100644 index 000000000..199952ee2 --- /dev/null +++ b/.github/workflows/triage-issues.yml @@ -0,0 +1,61 @@ +name: Issue Triaging +on: + issues: + types: [opened, reopened, labeled, unlabeled, closed] + +jobs: + label-incoming: + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'unlabeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-label-incoming.yml@main + permissions: + issues: write + + close-invalid: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-invalid.yml@main + permissions: + contents: read + issues: write + pull-requests: write + + close-suspected-spam: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-suspected-spam.yml@main + permissions: + issues: write + + close-single-word: + if: github.event.action == 'opened' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-single-word-issues.yml@main + permissions: + issues: write + + close-off-topic: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-off-topic.yml@main + permissions: + issues: write + + enhancement-comment: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-enhancement-comment.yml@main + permissions: + issues: write + + unable-to-reproduce: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-unable-to-reproduce-comment.yml@main + permissions: + issues: write + + remove-needs-triage: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-remove-needs-triage.yml@main + permissions: + issues: write + + on-issue-close: + if: github.event.action == 'closed' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-on-issue-close.yml@main + permissions: + issues: write diff --git a/.github/workflows/triage-pull-requests.yml b/.github/workflows/triage-pull-requests.yml new file mode 100644 index 000000000..92ba43d4a --- /dev/null +++ b/.github/workflows/triage-pull-requests.yml @@ -0,0 +1,59 @@ +name: PR Triaging +on: + pull_request_target: + types: [opened, reopened, edited, labeled, ready_for_review] + schedule: + - cron: '0 4 * * *' # Daily at 4 AM UTC — close unmet-requirements PRs + +jobs: + label-external: + if: >- + github.event_name == 'pull_request_target' && + (github.event.action == 'opened' || github.event.action == 'reopened') + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-label-external-pr.yml@main + permissions: + issues: write + pull-requests: write + repository-projects: read + + close-from-default-branch: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'opened' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-from-default-branch.yml@main + with: + default_branch: trunk + permissions: + pull-requests: write + + check-requirements: + if: >- + github.event_name == 'pull_request_target' && + (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'edited') + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-pr-requirements.yml@main + permissions: + issues: read + pull-requests: write + + close-unmet-requirements: + if: github.event_name == 'schedule' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-pr-requirements.yml@main + permissions: + issues: read + pull-requests: write + + close-no-help-wanted: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-no-help-wanted.yml@main + permissions: + pull-requests: write + + ready-for-review: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-ready-for-review.yml@main + permissions: + pull-requests: write diff --git a/.github/workflows/triage-scheduled-tasks.yml b/.github/workflows/triage-scheduled-tasks.yml new file mode 100644 index 000000000..8dd0793b2 --- /dev/null +++ b/.github/workflows/triage-scheduled-tasks.yml @@ -0,0 +1,26 @@ +name: Triage Scheduled Tasks +on: + workflow_dispatch: + issue_comment: + types: [created] + schedule: + - cron: '5 * * * *' # Hourly — no-response close + - cron: '0 3 * * *' # Daily at 3 AM UTC — stale issues + +jobs: + no-response: + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-no-response-close.yml@main + permissions: + issues: write + + stale: + if: github.event.schedule == '0 3 * * *' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-stale-issues.yml@main + with: + days_before_stale: 30 + days_before_close: -1 + start_date: '2025-07-10T00:00:00Z' + stale_issue_label: 'stale' + exempt_issue_labels: 'keep' + permissions: + issues: write diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index a2ca17160..000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Discussion Triage -run-name: ${{ github.event_name == 'issues' && github.event.issue.title || github.event.pull_request.title }} -permissions: {} -on: - issues: - types: - - labeled - pull_request_target: - types: - - labeled -env: - TARGET_REPO: github/cli -jobs: - issue: - environment: cli-discuss-automation - runs-on: ubuntu-latest - if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'discuss' - steps: - - name: Create issue based on source issue - env: - BODY: ${{ github.event.issue.body }} - CREATED: ${{ github.event.issue.created_at }} - GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} - LINK: ${{ github.repository }}#${{ github.event.issue.number }} - TITLE: ${{ github.event.issue.title }} - TRIGGERED_BY: ${{ github.triggering_actor }} - run: | - # Markdown quote source body by replacing newlines for newlines and markdown quoting - BODY="${BODY//$'\n'/$'\n'> }" - - # Create issue using dynamically constructed body within heredoc - cat << EOF | gh issue create --title "Triage issue \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage - **Title:** $TITLE - **Issue:** $LINK - **Created:** $CREATED - **Triggered by:** @$TRIGGERED_BY - - --- - - cc: @github/cli - - > $BODY - EOF - - pull_request: - runs-on: ubuntu-latest - environment: cli-discuss-automation - if: github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'discuss' - steps: - - name: Create issue based on source pull request - env: - BODY: ${{ github.event.pull_request.body }} - CREATED: ${{ github.event.pull_request.created_at }} - GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} - LINK: ${{ github.repository }}#${{ github.event.pull_request.number }} - TITLE: ${{ github.event.pull_request.title }} - TRIGGERED_BY: ${{ github.triggering_actor }} - run: | - # Markdown quote source body by replacing newlines for newlines and markdown quoting - BODY="${BODY//$'\n'/$'\n'> }" - - # Create issue using dynamically constructed body within heredoc - cat << EOF | gh issue create --title "Triage PR \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage - **Title:** $TITLE - **Pull request:** $LINK - **Created:** $CREATED - **Triggered by:** @$TRIGGERED_BY - - --- - - cc: @github/cli - - > $BODY - EOF diff --git a/.gitignore b/.gitignore index a4b73ac7a..b82a00c72 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ # Windows resource files /cmd/gh/*.syso +# Third-party licenses +/internal/licenses/embed/*/* +!/internal/licenses/embed/*/PLACEHOLDER + # VS Code .vscode diff --git a/.goreleaser.yml b/.goreleaser.yml index 15f5c2f12..b264b58e8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,6 +20,9 @@ builds: goos: [darwin] goarch: [amd64, arm64] hooks: + pre: + - cmd: bash ./script/licenses {{ .Os }} {{ .Arch }} + output: true post: - cmd: ./script/sign '{{ .Path }}' output: true @@ -33,6 +36,10 @@ builds: goarch: ["386", arm, amd64, arm64] env: - CGO_ENABLED=0 + hooks: + pre: + - cmd: bash ./script/licenses {{ .Os }} {{ .Arch }} + output: true binary: bin/gh main: ./cmd/gh ldflags: @@ -42,6 +49,9 @@ builds: goos: [windows] goarch: ["386", amd64, arm64] hooks: + pre: + - cmd: bash ./script/licenses {{ .Os }} {{ .Arch }} + output: true post: - cmd: pwsh .\script\sign.ps1 '{{ .Path }}' output: true diff --git a/Makefile b/Makefile index f823f6e93..4efdbfbed 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ endif ## Install/uninstall tasks are here for use on *nix platform. On Windows, there is no equivalent. DESTDIR := -prefix := /usr/local +prefix ?= /usr/local bindir := ${prefix}/bin datadir := ${prefix}/share mandir := ${datadir}/man @@ -109,8 +109,8 @@ endif .PHONY: licenses licenses: - ./script/licenses + ./script/licenses $$(go env GOOS) $$(go env GOARCH) .PHONY: licenses-check licenses-check: - ./script/licenses-check + ./script/licenses --check diff --git a/api/client.go b/api/client.go index e6ff59c59..2eb3f3ff2 100644 --- a/api/client.go +++ b/api/client.go @@ -10,12 +10,15 @@ import ( "regexp" "strings" + "github.com/cli/cli/v2/pkg/set" ghAPI "github.com/cli/go-gh/v2/pkg/api" ghauth "github.com/cli/go-gh/v2/pkg/auth" ) const ( accept = "Accept" + apiVersion = "X-GitHub-Api-Version" + apiVersionValue = "2022-11-28" authorization = "Authorization" cacheTTL = "X-GH-CACHE-TTL" graphqlFeatures = "GraphQL-Features" @@ -178,6 +181,10 @@ func handleResponse(err error) error { var gqlErr *ghAPI.GraphQLError if errors.As(err, &gqlErr) { + scopeErr := GenerateScopeErrorForGQL(gqlErr) + if scopeErr != nil { + return scopeErr + } return GraphQLError{ GraphQLError: gqlErr, } @@ -186,6 +193,39 @@ func handleResponse(err error) error { return err } +func GenerateScopeErrorForGQL(gqlErr *ghAPI.GraphQLError) error { + missing := set.NewStringSet() + for _, e := range gqlErr.Errors { + if e.Type != "INSUFFICIENT_SCOPES" { + continue + } + missing.AddValues(requiredScopesFromServerMessage(e.Message)) + } + if missing.Len() > 0 { + s := missing.ToSlice() + return fmt.Errorf( + "error: your authentication token is missing required scopes %v\n"+ + "To request it, run: gh auth refresh -s %s", + s, + strings.Join(s, ",")) + } + return nil +} + +var scopesRE = regexp.MustCompile(`one of the following scopes: \[(.+?)]`) + +func requiredScopesFromServerMessage(msg string) []string { + m := scopesRE.FindStringSubmatch(msg) + if m == nil { + return nil + } + var scopes []string + for _, mm := range strings.Split(m[1], ",") { + scopes = append(scopes, strings.Trim(mm, "' ")) + } + return scopes +} + // ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth // scopes in case a server response indicates that there are missing scopes. func ScopesSuggestion(resp *http.Response) string { @@ -264,6 +304,7 @@ func clientOptions(hostname string, transport http.RoundTripper) ghAPI.ClientOpt AuthToken: "none", Headers: map[string]string{ authorization: "", + apiVersion: apiVersionValue, }, Host: hostname, SkipDefaultHeaders: true, diff --git a/api/client_test.go b/api/client_test.go index 1701a17a9..ad75c1889 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/go-gh/v2/pkg/api" "github.com/stretchr/testify/assert" ) @@ -245,13 +246,90 @@ func TestHTTPHeaders(t *testing.T) { assert.NoError(t, err) wantHeader := map[string]string{ - "Accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", - "Authorization": "token MYTOKEN", - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "GitHub CLI v1.2.3", + "Accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + "Authorization": "token MYTOKEN", + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "GitHub CLI v1.2.3", + "X-GitHub-Api-Version": "2022-11-28", } for name, value := range wantHeader { assert.Equal(t, value, gotReq.Header.Get(name), name) } assert.Equal(t, "", stderr.String()) } + +func TestGenerateScopeErrorForGQL(t *testing.T) { + tests := []struct { + name string + gqlError *api.GraphQLError + wantErr bool + expected string + }{ + { + name: "missing scope", + gqlError: &api.GraphQLError{ + Errors: []api.GraphQLErrorItem{ + { + Type: "INSUFFICIENT_SCOPES", + Message: "The 'addProjectV2ItemById' field requires one of the following scopes: ['project']", + }, + }, + }, + wantErr: true, + expected: "error: your authentication token is missing required scopes [project]\n" + + "To request it, run: gh auth refresh -s project", + }, + + { + name: "ignore non-scope errors", + gqlError: &api.GraphQLError{ + Errors: []api.GraphQLErrorItem{ + { + Type: "NOT_FOUND", + Message: "Could not resolve to a Repository", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := GenerateScopeErrorForGQL(tt.gqlError) + if tt.wantErr { + assert.NotNil(t, err) + assert.Equal(t, tt.expected, err.Error()) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestRequiredScopesFromServerMessage(t *testing.T) { + tests := []struct { + msg string + expected []string + }{ + { + msg: "requires one of the following scopes: ['project']", + expected: []string{"project"}, + }, + { + msg: "requires one of the following scopes: ['repo', 'read:org']", + expected: []string{"repo", "read:org"}, + }, + { + msg: "no match here", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.msg, func(t *testing.T) { + output := requiredScopesFromServerMessage(tt.msg) + assert.Equal(t, tt.expected, output) + }) + } +} diff --git a/api/export_pr_test.go b/api/export_pr_test.go index 1f310693e..ec7b00249 100644 --- a/api/export_pr_test.go +++ b/api/export_pr_test.go @@ -107,6 +107,32 @@ func TestIssue_ExportData(t *testing.T) { } `), }, + { + name: "assignees", + fields: []string{"assignees"}, + inputJSON: heredoc.Doc(` + { "assignees": { "nodes": [ + { + "id": "MDQ6VXNlcjE=", + "login": "monalisa", + "name": "Mona Lisa", + "databaseId": 1234 + } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "assignees": [ + { + "id": "MDQ6VXNlcjE=", + "login": "monalisa", + "name": "Mona Lisa", + "databaseId": 1234 + } + ] + } + `), + }, { name: "linked pull requests", fields: []string{"closedByPullRequestsReferences"}, @@ -316,6 +342,32 @@ func TestPullRequest_ExportData(t *testing.T) { } `), }, + { + name: "assignees", + fields: []string{"assignees"}, + inputJSON: heredoc.Doc(` + { "assignees": { "nodes": [ + { + "id": "MDQ6VXNlcjE=", + "login": "monalisa", + "name": "Mona Lisa", + "databaseId": 1234 + } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "assignees": [ + { + "id": "MDQ6VXNlcjE=", + "login": "monalisa", + "name": "Mona Lisa", + "databaseId": 1234 + } + ] + } + `), + }, { name: "linked issues", fields: []string{"closingIssuesReferences"}, diff --git a/api/http_client.go b/api/http_client.go index ab7d49063..9957f6bc5 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -49,7 +49,8 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) { } headers := map[string]string{ - userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), + userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), + apiVersion: apiVersionValue, } clientOpts.Headers = headers diff --git a/api/http_client_test.go b/api/http_client_test.go index 9a915837f..824bc0f1b 100644 --- a/api/http_client_test.go +++ b/api/http_client_test.go @@ -39,9 +39,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": {"token MYTOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -53,9 +54,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "example.com", wantHeader: map[string][]string{ - "authorization": {"token GHETOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token GHETOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -68,9 +70,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": nil, // should not be set - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -83,9 +86,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "example.com", wantHeader: map[string][]string{ - "authorization": nil, // should not be set - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -98,9 +102,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": {"token MYTOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: heredoc.Doc(` * Request at