Merge branch 'cli:trunk' into trunk
This commit is contained in:
commit
bb1a6f262a
207 changed files with 8341 additions and 1262 deletions
20
.github/CONTRIBUTING.md
vendored
20
.github/CONTRIBUTING.md
vendored
|
|
@ -6,9 +6,10 @@ We accept pull requests for bug fixes and features where we've discussed the app
|
|||
|
||||
Please do:
|
||||
|
||||
* Check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted.
|
||||
* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature.
|
||||
* Open an issue if things aren't working as expected.
|
||||
* Open an issue to propose a significant change.
|
||||
* Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below.
|
||||
* Open a pull request to fix a bug.
|
||||
* Open a pull request to fix documentation about a command.
|
||||
* Open a pull request for any issue labelled [`help wanted`][hw] or [`good first issue`][gfi].
|
||||
|
|
@ -52,7 +53,21 @@ We generate manual pages from source on every release. You do not need to submit
|
|||
|
||||
## Design guidelines
|
||||
|
||||
You may reference the [CLI Design System][] when suggesting features, and are welcome to use our [Google Docs Template][] to suggest designs.
|
||||
### Proposing a design
|
||||
|
||||
You may propose a design to solve an open bug or feature request issue that has both [the `needs-design` and `help-wanted` labels][needs design and help wanted].
|
||||
|
||||
To propose a design:
|
||||
|
||||
- Open a new issue using the [design proposal issue template](./ISSUE_TEMPLATE/submit-a-design-proposal.md).
|
||||
- Include a link to the issue that the design is for.
|
||||
- Describe the design you are proposing to resolve the issue, leveraging the [CLI Design System][].
|
||||
- Mock up the design you are proposing using our [Google Docs Template][] or code blocks.
|
||||
- Mock ups should cleary illustrate the command(s) being run and the expected output(s).
|
||||
|
||||
### (core team only) Revewing a design
|
||||
|
||||
A member of the core team will [triage](../docs/triage.md) the design proposal. Once a member of the core team has reviewed the design, they may add the [`help wanted`][hw] label to the issue, so a PR can be opened to provide the implementation.
|
||||
|
||||
## Resources
|
||||
|
||||
|
|
@ -62,6 +77,7 @@ You may reference the [CLI Design System][] when suggesting features, and are we
|
|||
|
||||
|
||||
[bug issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Abug
|
||||
[needs design and help wanted]: https://github.com/cli/cli/issues?q=state%3Aclosed%20is%3Aissue%20label%3Aneeds-design%20label%3A%22help%20wanted%22
|
||||
[feature request issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement
|
||||
[hw]: https://github.com/cli/cli/labels/help%20wanted
|
||||
[gfi]: https://github.com/cli/cli/labels/good%20first%20issue
|
||||
|
|
|
|||
58
.github/ISSUE_TEMPLATE/submit-a-design-proposal.md
vendored
Normal file
58
.github/ISSUE_TEMPLATE/submit-a-design-proposal.md
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
name: "🎨 Submit a design proposal"
|
||||
about: Submit a design to resolve an open issue that has both `needs-design` and `help-wanted` labels
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- See [CONTRIBUTING.md](../CONTRIBUTING.md#proposing-a-design) for more information.-->
|
||||
|
||||
### Link to issue for design submission
|
||||
|
||||
<!--
|
||||
Provide a link to the issue this design is for.
|
||||
|
||||
All design submissions must be linked to an open issue that
|
||||
has both the `needs-design` and `help-wanted` labels.
|
||||
-->
|
||||
|
||||
### Proposed Design
|
||||
|
||||
<!--
|
||||
Describe the design you are proposing to resolve the issue.
|
||||
|
||||
All CLI designs must adhere to the [Primer CLI design reference](https://primer.style/cli/).
|
||||
-->
|
||||
|
||||
### Mockup
|
||||
|
||||
<!--
|
||||
Provide a mockup of the design you are proposing. All mockups should clearly illustrate the command(s) being run and the expected output(s).
|
||||
|
||||
When color and formatting are important, consider using our [CLI design Google Docs Template](https://docs.google.com/document/d/1JIRErIUuJ6fTgabiFYfCH3x91pyHuytbfa0QLnTfXKM/edit#heading=h.or54sa47ylpg).
|
||||
|
||||
Code blocks can also be used to submit a design mockup - remember to include the command(s) being run. Example:
|
||||
|
||||
```shell
|
||||
$ gh issue list --json title -L 5
|
||||
[
|
||||
{
|
||||
"title": "`gh pr checks <pr> --required` should not fail when there are no required checks"
|
||||
},
|
||||
{
|
||||
"title": "gh pr view commits should include commit description"
|
||||
},
|
||||
{
|
||||
"title": "Adapt the color of the device code to the color used by the terminal"
|
||||
},
|
||||
{
|
||||
"title": "`gh pr create` does not default to fork when user has write access to upstream"
|
||||
},
|
||||
{
|
||||
"title": "First party discussions support"
|
||||
}
|
||||
]
|
||||
```
|
||||
-->
|
||||
5
.github/SECURITY.md
vendored
5
.github/SECURITY.md
vendored
|
|
@ -2,7 +2,10 @@ GitHub takes the security of our software products and services seriously, inclu
|
|||
|
||||
If you believe you have found a security vulnerability in GitHub CLI, you can report it to us in one of two ways:
|
||||
|
||||
* Report it to this repository directly using [private vulnerability reporting][]. Such reports are not eligible for a bounty reward.
|
||||
* Report it to this repository directly using [private vulnerability reporting][].
|
||||
* Include a description of your investigation of the GitHub CLI's codebase and why you believe an exploit is possible.
|
||||
* POCs and links to code are greatly encouraged.
|
||||
* Such reports are not eligible for a bounty reward.
|
||||
|
||||
* Submit the report through [HackerOne][] to be eligible for a bounty reward.
|
||||
|
||||
|
|
|
|||
2
.github/workflows/deployment.yml
vendored
2
.github/workflows/deployment.yml
vendored
|
|
@ -299,7 +299,7 @@ jobs:
|
|||
rpmsign --addsign dist/*.rpm
|
||||
- name: Attest release artifacts
|
||||
if: inputs.environment == 'production'
|
||||
uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1
|
||||
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
|
||||
with:
|
||||
subject-path: "dist/gh_*"
|
||||
- name: Run createrepo
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -27,4 +27,7 @@
|
|||
# vim
|
||||
*.swp
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
|
||||
vendor/
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -38,11 +38,16 @@ completions: bin/gh$(EXE)
|
|||
bin/gh$(EXE) completion -s fish > ./share/fish/vendor_completions.d/gh.fish
|
||||
bin/gh$(EXE) completion -s zsh > ./share/zsh/site-functions/_gh
|
||||
|
||||
# just a convenience task around `go test`
|
||||
# just convenience tasks around `go test`
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
# For more information, see https://github.com/cli/cli/blob/trunk/acceptance/README.md
|
||||
.PHONY: acceptance
|
||||
acceptance:
|
||||
go test -tags acceptance ./acceptance
|
||||
|
||||
## Site-related tasks are exclusively intended for use by the GitHub CLI team and for our release automation.
|
||||
|
||||
site:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||

|
||||
|
||||
GitHub CLI is supported for users on GitHub.com and GitHub Enterprise Server 2.20+ with support for macOS, Windows, and Linux.
|
||||
GitHub CLI is supported for users on GitHub.com, GitHub Enterprise Cloud, and GitHub Enterprise Server 2.20+ with support for macOS, Windows, and Linux.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
|||
195
acceptance/README.md
Normal file
195
acceptance/README.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
## Acceptance Tests
|
||||
|
||||
The acceptance tests are blackbox* tests that are expected to interact with resources on a real GitHub instance. They are built on top of the [`go-internal/testscript`](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript) package, which provides a framework for building tests for command line tools.
|
||||
|
||||
*Note: they aren't strictly blackbox because `exec gh` commands delegate to a binary set up by `testscript` that calls into `ghcmd.Main`. However, since our real `func main` is an extremely thin adapter over `ghcmd.Main`, this is reasonable. This tradeoff avoids us building the binary ourselves for the tests, and allows us to get code coverage metrics.
|
||||
|
||||
### Running the Acceptance Tests
|
||||
|
||||
The acceptance tests have a build constraint of `//go:build acceptance`, this means that `go test ./...` will continue to work without any modifications. The `acceptance` tag must therefore be provided when running `go test`.
|
||||
|
||||
The following environment variables are required:
|
||||
|
||||
#### `GH_ACCEPTANCE_HOST`
|
||||
|
||||
The GitHub host to target e.g. `github.com`
|
||||
|
||||
#### `GH_ACCEPTANCE_ORG`
|
||||
|
||||
The organization in which the acceptance tests can manage resources in. Consider using `gh-acceptance-testing` on `github.com`.
|
||||
|
||||
#### `GH_ACCEPTANCE_TOKEN`
|
||||
|
||||
The token to use for authenticatin with the `GH_ACCEPTANCE_HOST`. This must already have the necessary scopes for each test, and must have permissions to act in the `GH_ACCEPTANCE_ORG`. See [Effective Test Authoring](#effective-test-authoring) for how tests must handle tokens without sufficient scopes.
|
||||
|
||||
It's recommended to create and use a Legacy PAT for this; Fine-Grained PATs do not offer all the necessary privileges required. You can use an OAuth token provided via `gh auth login --web` and can provide it to the acceptance tests via `GH_ACCEPTANCE_TOKEN=$(gh auth token --hostname <host>)` but this can be a bit confusing and annoying if you `gh auth login` again without `-s` and lose the required scopes.
|
||||
|
||||
---
|
||||
|
||||
A full example invocation can be found below:
|
||||
|
||||
```
|
||||
GH_ACCEPTANCE_HOST=<host> GH_ACCEPTANCE_ORG=<org> GH_ACCEPTANCE_TOKEN=<token> go test -tags=acceptance ./acceptance
|
||||
```
|
||||
|
||||
While writing a new test, it can be useful to target that specific script by providing the `GH_ACCEPTANCE_SCRIPT` env var in combination with the `-run` flag, for example:
|
||||
|
||||
```
|
||||
GH_ACCEPTANCE_SCRIPT=pr-view.txtar GH_ACCEPTANCE_HOST=<host> GH_ACCEPTANCE_ORG=<org> GH_ACCEPTANCE_TOKEN=<token> go test -tags=acceptance -run ^TestPullRequests$ ./acceptance
|
||||
```
|
||||
|
||||
#### Code Coverage
|
||||
|
||||
To get code coverage, `go test` can be invoked with `coverpkg` and `coverprofile` like so:
|
||||
|
||||
```
|
||||
GH_ACCEPTANCE_HOST=<host> GH_ACCEPTANCE_ORG=<org> GH_ACCEPTANCE_TOKEN=<token> go test -tags=acceptance -coverprofile=coverage.out -coverpkg=./... ./acceptance
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
This section is to be expanded over time as we write more tests and learn more.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
The following custom environment variables are made available to the scripts:
|
||||
* `GH_HOST`: Set to value of the `GH_ACCEPTANCE_ORG` env var provided to `go test`
|
||||
* `ORG`: Set to the value of the `GH_ACCEPTANCE_ORG` env var provided to `go test`
|
||||
* `GH_TOKEN`: Set to the value of the `GH_ACCEPTANCE_TOKEN` env var provided to `go test`
|
||||
* `RANDOM_STRING`: Set to a length 10 random string of letters to help isolate globally visible resources
|
||||
* `SCRIPT_NAME`: Set to the name of the `testscript` currently running, without extension and replacing hyphens with underscores e.g. `pr_view`
|
||||
* `HOME`: Set to the initial working directory. Required for `git` operations
|
||||
* `GH_CONFIG_DIR`: Set to the initial working directory. Required for `gh` operations
|
||||
|
||||
#### Custom Commands
|
||||
|
||||
The following custom commands are defined within [`acceptance_test.go`](./acceptance_test.go) to help with writing tests:
|
||||
|
||||
- `defer`: register a command to run after the testscript completes
|
||||
|
||||
```txtar
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
```
|
||||
|
||||
- `env2upper`: set environment variable to the uppercase version of another environment variable
|
||||
|
||||
```txtar
|
||||
# Prepare organization secret, GitHub Actions uppercases secret names
|
||||
env2upper ORG_SECRET_NAME=$RANDOM_STRING
|
||||
```
|
||||
|
||||
- `replace`: replace placeholders in file with interpolated content provided
|
||||
|
||||
```txtar
|
||||
env2upper SECRET_NAME=$SCRIPT_NAME_$RANDOM_STRING
|
||||
|
||||
# Modify workflow file to use generated organization secret name
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
replace .github/workflows/workflow.yml SECRET_NAME=$SECRET_NAME
|
||||
|
||||
-- workflow.yml --
|
||||
on:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
ORG_SECRET: ${{ secrets.$SECRET_NAME }}
|
||||
```
|
||||
|
||||
- `stdout2env`: set environment variable containing standard output from previous command
|
||||
|
||||
```txtar
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body' --assignee '@me' --label 'bug'
|
||||
stdout2env PR_URL
|
||||
```
|
||||
|
||||
### Acceptance Test VS Code Support
|
||||
|
||||
Due to the `//go:build acceptance` build constraint, some functionality is limited because `gopls` isn't being informed about the tag. To resolve this, set the following in your `settings.json`:
|
||||
|
||||
```json
|
||||
"gopls": {
|
||||
"buildFlags": [
|
||||
"-tags=acceptance"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
You can install the [`txtar`](https://marketplace.visualstudio.com/items?itemName=brody715.txtar) or [`vscode-testscript`](https://marketplace.visualstudio.com/items?itemName=twpayne.vscode-testscript) extensions to get syntax highlighting.
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
When tests fail they fail like this:
|
||||
|
||||
```
|
||||
➜ go test -tags=acceptance ./acceptance
|
||||
--- FAIL: TestPullRequests (0.00s)
|
||||
--- FAIL: TestPullRequests/pr-merge (11.07s)
|
||||
testscript.go:584: WORK=/private/var/folders/45/sdnm1hp10nj1s9q57dp3bc5h0000gn/T/go-test-script2778137936/script-pr-merge
|
||||
# Use gh as a credential helper (0.693s)
|
||||
# Create a repository with a file so it has a default branch (1.155s)
|
||||
# Defer repo cleanup (0.000s)
|
||||
# Clone the repo (1.551s)
|
||||
# Prepare a branch to PR with a single file (1.168s)
|
||||
# Create the PR (1.903s)
|
||||
# Check that the file doesn't exist on the main branch (0.059s)
|
||||
# Merge the PR (2.426s)
|
||||
# Check that the state of the PR is now merged (0.571s)
|
||||
# Pull and check the file exists on the main branch (1.074s)
|
||||
# And check we had a merge commit (0.462s)
|
||||
> exec git show HEAD
|
||||
[stdout]
|
||||
commit 85d32c1a83ace270f6754c61f3f7e14956be0a47
|
||||
Author: William Martin <williammartin@william-github-laptop.kpn>
|
||||
Date: Fri Oct 11 15:23:56 2024 +0200
|
||||
|
||||
Add file.txt
|
||||
|
||||
diff --git a/file.txt b/file.txt
|
||||
new file mode 100644
|
||||
index 0000000..7449899
|
||||
--- /dev/null
|
||||
+++ b/file.txt
|
||||
@@ -0,0 +1 @@
|
||||
+Unimportant contents
|
||||
> stdout 'Merge pull request #1'
|
||||
FAIL: testdata/pr/pr-merge.txtar:42: no match for `Merge pull request #1` found in stdout
|
||||
```
|
||||
|
||||
This is generally enough information to understand why a test has failed. However, we can get more information by providing the `-v` flag to `go test`, which turns on verbose mode and shows each command and any associated `stdio`.
|
||||
|
||||
> [!WARNING]
|
||||
> Verbose mode dumps the `testscript` environment variables, so make sure there is nothing sensitive in there.
|
||||
> We have taken steps to [redact tokens](https://github.com/cli/cli/pull/9804) in log output but there's no
|
||||
> guarantee it's comprehensive.
|
||||
|
||||
By default `testscript` removes the directory in which it was running the script, and if you've been a conscientious engineer, you should be cleaning up resources using the `defer` statement. However, this can be an impediment to debugging. As such you can set `GH_ACCEPTANCE_PRESERVE_WORK_DIR=true` and `GH_ACCEPTANCE_SKIP_DEFER=true` to skip these cleanup steps.
|
||||
|
||||
### Effective Test Authoring
|
||||
|
||||
This section is to be expanded over time as we write more tests and learn more.
|
||||
|
||||
#### Test Isolation
|
||||
|
||||
The `testscript` library creates a somewhat isolated environment for each script. Each script gets a directory with limited environment variables by default. As far as reasonable, we should look to write scripts that depend on nothing more than themselves, the GitHub resources they manage, and limited additional environmental injection from our own `testscript` setup.
|
||||
|
||||
Here are some guidelines around test isolation:
|
||||
* Favour duplication in test setup over abstracting a new `testscript` command
|
||||
* Favour a `testscript` owning an entire resource lifecycle over shared resource until we see a performance or rate limiting issue
|
||||
* Use the `RANDOM_STRING` env var for globally visible resources to avoid conflicts
|
||||
|
||||
### Debris
|
||||
|
||||
Since these scripts are creating resources on a GitHub instance, we should try our best to cleanup after them. Use the `defer` keyword to ensure a command runs at the end of a test even in the case of failure.
|
||||
|
||||
#### Scope Validation
|
||||
|
||||
TODO: I believe tests should early exit if the correct scopes aren't in place to execute the entire lifecycle. It's extremely annoying if a `defer` fails to clean up resources because there's no `delete_repo` scope for example. However, I'm not sure yet whether this scope checking should be in the Go tests or in the scripts themselves. It seems very cool to understand required scopes for a script just by looking at the script itself.
|
||||
|
||||
### Further Reading
|
||||
|
||||
https://bitfieldconsulting.com/posts/test-scripts
|
||||
|
||||
https://atlasgo.io/blog/2024/09/09/how-go-tests-go-test
|
||||
|
||||
https://encore.dev/blog/testscript-hidden-testing-gem
|
||||
421
acceptance/acceptance_test.go
Normal file
421
acceptance/acceptance_test.go
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
//go:build acceptance
|
||||
|
||||
package acceptance_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghcmd"
|
||||
"github.com/cli/go-internal/testscript"
|
||||
)
|
||||
|
||||
func ghMain() int {
|
||||
return int(ghcmd.Main())
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testscript.RunMain(m, map[string]func() int{
|
||||
"gh": ghMain,
|
||||
}))
|
||||
}
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "api"))
|
||||
}
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "auth"))
|
||||
}
|
||||
|
||||
func TestGPGKeys(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "gpg-key"))
|
||||
}
|
||||
|
||||
func TestExtensions(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "extension"))
|
||||
}
|
||||
|
||||
func TestIssues(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "pr"))
|
||||
}
|
||||
|
||||
func TestLabels(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "label"))
|
||||
}
|
||||
|
||||
func TestOrg(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "org"))
|
||||
}
|
||||
|
||||
func TestProject(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "project"))
|
||||
}
|
||||
|
||||
func TestPullRequests(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "pr"))
|
||||
}
|
||||
|
||||
func TestReleases(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "release"))
|
||||
}
|
||||
|
||||
func TestRepo(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "repo"))
|
||||
}
|
||||
|
||||
func TestRulesets(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "ruleset"))
|
||||
}
|
||||
|
||||
func TestSearches(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "search"))
|
||||
}
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "secret"))
|
||||
}
|
||||
|
||||
func TestSSHKeys(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "ssh-key"))
|
||||
}
|
||||
|
||||
func TestVariables(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "variable"))
|
||||
}
|
||||
|
||||
func TestWorkflows(t *testing.T) {
|
||||
var tsEnv testScriptEnv
|
||||
if err := tsEnv.fromEnv(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "workflow"))
|
||||
}
|
||||
|
||||
func testScriptParamsFor(tsEnv testScriptEnv, command string) testscript.Params {
|
||||
var files []string
|
||||
if tsEnv.script != "" {
|
||||
files = []string{path.Join("testdata", command, tsEnv.script)}
|
||||
}
|
||||
|
||||
var dir string
|
||||
if len(files) == 0 {
|
||||
dir = path.Join("testdata", command)
|
||||
}
|
||||
|
||||
return testscript.Params{
|
||||
Dir: dir,
|
||||
Files: files,
|
||||
Setup: sharedSetup(tsEnv),
|
||||
Cmds: sharedCmds(tsEnv),
|
||||
RequireExplicitExec: true,
|
||||
RequireUniqueNames: true,
|
||||
TestWork: tsEnv.preserveWorkDir,
|
||||
}
|
||||
}
|
||||
|
||||
var keyT struct{}
|
||||
|
||||
func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error {
|
||||
return func(ts *testscript.Env) error {
|
||||
scriptName, ok := extractScriptName(ts.Vars)
|
||||
if !ok {
|
||||
ts.T().Fatal("script name not found")
|
||||
}
|
||||
|
||||
// When using script name to uniquely identify where test data comes from,
|
||||
// some places like GitHub Actions secret names don't accept hyphens.
|
||||
// Replace them with underscores until such a time this becomes a problem.
|
||||
ts.Setenv("SCRIPT_NAME", strings.ReplaceAll(scriptName, "-", "_"))
|
||||
ts.Setenv("HOME", ts.Cd)
|
||||
ts.Setenv("GH_CONFIG_DIR", ts.Cd)
|
||||
|
||||
ts.Setenv("GH_HOST", tsEnv.host)
|
||||
ts.Setenv("ORG", tsEnv.org)
|
||||
ts.Setenv("GH_TOKEN", tsEnv.token)
|
||||
|
||||
ts.Setenv("RANDOM_STRING", randomString(10))
|
||||
|
||||
ts.Values[keyT] = ts.T()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// sharedCmds defines a collection of custom testscript commands for our use.
|
||||
func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
return map[string]func(ts *testscript.TestScript, neg bool, args []string){
|
||||
"defer": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if neg {
|
||||
ts.Fatalf("unsupported: ! defer")
|
||||
}
|
||||
|
||||
if tsEnv.skipDefer {
|
||||
return
|
||||
}
|
||||
|
||||
tt, ok := ts.Value(keyT).(testscript.T)
|
||||
if !ok {
|
||||
ts.Fatalf("%v is not a testscript.T", ts.Value(keyT))
|
||||
}
|
||||
|
||||
ts.Defer(func() {
|
||||
// If you're wondering why we're not using ts.Check here, it's because it raises a panic, and testscript
|
||||
// only catches the panics directly from commands, not from the deferred functions. So what we do
|
||||
// instead is grab the `t` in the setup function and store it as a value. It's important that we use
|
||||
// `t` from the setup function because it represents the subtest created for each individual script,
|
||||
// rather than each top-level test.
|
||||
// See: https://github.com/rogpeppe/go-internal/issues/276
|
||||
if err := ts.Exec(args[0], args[1:]...); err != nil {
|
||||
tt.FailNow()
|
||||
}
|
||||
})
|
||||
},
|
||||
"env2upper": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if neg {
|
||||
ts.Fatalf("unsupported: ! env2upper")
|
||||
}
|
||||
if len(args) == 0 {
|
||||
ts.Fatalf("usage: env2upper name=value ...")
|
||||
}
|
||||
for _, env := range args {
|
||||
i := strings.Index(env, "=")
|
||||
|
||||
if i < 0 {
|
||||
ts.Fatalf("env2upper: argument does not match name=value")
|
||||
}
|
||||
|
||||
ts.Setenv(env[:i], strings.ToUpper(env[i+1:]))
|
||||
}
|
||||
},
|
||||
"replace": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if neg {
|
||||
ts.Fatalf("unsupported: ! replace")
|
||||
}
|
||||
if len(args) < 2 {
|
||||
ts.Fatalf("usage: replace file env...")
|
||||
}
|
||||
|
||||
src := ts.MkAbs(args[0])
|
||||
ts.Logf("replace src: %s", src)
|
||||
|
||||
// Preserve the existing file mode while replacing the contents similar to native cp behavior
|
||||
info, err := os.Stat(src)
|
||||
ts.Check(err)
|
||||
mode := info.Mode() & 0o777
|
||||
data, err := os.ReadFile(src)
|
||||
ts.Check(err)
|
||||
|
||||
for _, arg := range args[1:] {
|
||||
i := strings.Index(arg, "=")
|
||||
if i < 0 {
|
||||
ts.Fatalf("replace: %s argument does not match name=value", arg)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("$%s", arg[:i])
|
||||
value := arg[i+1:]
|
||||
ts.Logf("replace %s: %s", name, value)
|
||||
|
||||
// `replace` was originally built similar to `cp` and `cmpenv`, expanding environment variables within a file.
|
||||
// However files with content that looks like environments variable such as GitHub Actions workflows
|
||||
// were being modified unexpectedly. Thus `replace` has been designed to using string replacement
|
||||
// looking for `$KEY` specifically.
|
||||
data = []byte(strings.ReplaceAll(string(data), name, value))
|
||||
}
|
||||
|
||||
ts.Check(os.WriteFile(src, data, mode))
|
||||
},
|
||||
"stdout2env": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if neg {
|
||||
ts.Fatalf("unsupported: ! stdout2env")
|
||||
}
|
||||
if len(args) != 1 {
|
||||
ts.Fatalf("usage: stdout2env name")
|
||||
}
|
||||
|
||||
ts.Setenv(args[0], strings.TrimRight(ts.ReadFile("stdout"), "\n"))
|
||||
},
|
||||
"sleep": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if neg {
|
||||
ts.Fatalf("unsupported: ! sleep")
|
||||
}
|
||||
if len(args) != 1 {
|
||||
ts.Fatalf("usage: sleep seconds")
|
||||
}
|
||||
|
||||
// sleep for the given number of seconds
|
||||
seconds, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
ts.Fatalf("invalid number of seconds: %v", err)
|
||||
}
|
||||
|
||||
d := time.Duration(seconds) * time.Second
|
||||
time.Sleep(d)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func extractScriptName(vars []string) (string, bool) {
|
||||
for _, kv := range vars {
|
||||
if strings.HasPrefix(kv, "WORK=") {
|
||||
v := strings.Split(kv, "=")[1]
|
||||
return strings.CutPrefix(path.Base(v), "script-")
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
type missingEnvError struct {
|
||||
missingEnvs []string
|
||||
}
|
||||
|
||||
func (e missingEnvError) Error() string {
|
||||
return fmt.Sprintf("environment variable(s) %s must be set and non-empty", strings.Join(e.missingEnvs, ", "))
|
||||
}
|
||||
|
||||
type testScriptEnv struct {
|
||||
host string
|
||||
org string
|
||||
token string
|
||||
|
||||
script string
|
||||
|
||||
skipDefer bool
|
||||
preserveWorkDir bool
|
||||
}
|
||||
|
||||
func (e *testScriptEnv) fromEnv() error {
|
||||
envMap := map[string]string{}
|
||||
|
||||
requiredEnvVars := []string{
|
||||
"GH_ACCEPTANCE_HOST",
|
||||
"GH_ACCEPTANCE_ORG",
|
||||
"GH_ACCEPTANCE_TOKEN",
|
||||
}
|
||||
|
||||
var missingEnvs []string
|
||||
for _, key := range requiredEnvVars {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if val == "" || !ok {
|
||||
missingEnvs = append(missingEnvs, key)
|
||||
continue
|
||||
}
|
||||
|
||||
envMap[key] = val
|
||||
}
|
||||
|
||||
if len(missingEnvs) > 0 {
|
||||
return missingEnvError{missingEnvs: missingEnvs}
|
||||
}
|
||||
|
||||
if envMap["GH_ACCEPTANCE_ORG"] == "github" || envMap["GH_ACCEPTANCE_ORG"] == "cli" {
|
||||
return fmt.Errorf("GH_ACCEPTANCE_ORG cannot be 'github' or 'cli'")
|
||||
}
|
||||
|
||||
e.host = envMap["GH_ACCEPTANCE_HOST"]
|
||||
e.org = envMap["GH_ACCEPTANCE_ORG"]
|
||||
e.token = envMap["GH_ACCEPTANCE_TOKEN"]
|
||||
|
||||
e.script = os.Getenv("GH_ACCEPTANCE_SCRIPT")
|
||||
e.preserveWorkDir = os.Getenv("GH_ACCEPTANCE_PRESERVE_WORK_DIR") == "true"
|
||||
e.skipDefer = os.Getenv("GH_ACCEPTANCE_SKIP_DEFER") == "true"
|
||||
|
||||
return nil
|
||||
}
|
||||
3
acceptance/testdata/api/basic-graphql.txtar
vendored
Normal file
3
acceptance/testdata/api/basic-graphql.txtar
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Basic graphql request
|
||||
exec gh api graphql -f query='query { viewer { login } }'
|
||||
stdout '"login":'
|
||||
3
acceptance/testdata/api/basic-rest.txtar
vendored
Normal file
3
acceptance/testdata/api/basic-rest.txtar
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Basic REST request
|
||||
exec gh api /user
|
||||
stdout '"login":'
|
||||
25
acceptance/testdata/auth/auth-login-logout.txtar
vendored
Normal file
25
acceptance/testdata/auth/auth-login-logout.txtar
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# We aren't logged in at the moment, but GH_TOKEN will override the
|
||||
# need to login. We are going to clear GH_TOKEN first to ensure no
|
||||
# overrides are happening
|
||||
|
||||
# Copy $GH_TOKEN to a new env var
|
||||
env LOGIN_TOKEN=$GH_TOKEN
|
||||
|
||||
# Remove GH_TOKEN env var so we don't fall back to it
|
||||
env GH_TOKEN=''
|
||||
|
||||
# Login to the host by feeding the token to stdin
|
||||
exec echo $LOGIN_TOKEN
|
||||
stdin stdout
|
||||
exec gh auth login --hostname=$GH_HOST --with-token --insecure-storage
|
||||
|
||||
# Check that we are logged in
|
||||
exec gh auth status --hostname $GH_HOST
|
||||
stdout $GH_HOST
|
||||
|
||||
# Logout of the host
|
||||
exec gh auth logout --hostname $GH_HOST
|
||||
stderr 'Logged out of'
|
||||
|
||||
# Check that we are logged out
|
||||
! exec gh auth status --hostname $GH_HOST
|
||||
10
acceptance/testdata/auth/auth-setup-git.txtar
vendored
Normal file
10
acceptance/testdata/auth/auth-setup-git.txtar
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Check that the credential helper is unset for the host. This command is
|
||||
# expected to fail before gh auth setup-git is run.
|
||||
! exec git config --get credential.https://${GH_HOST}.helper
|
||||
|
||||
# Run the setup-git command
|
||||
exec gh auth setup-git
|
||||
|
||||
# Check that the credential helper is set to gh
|
||||
exec git config --get credential.https://${GH_HOST}.helper
|
||||
stdout '^.*gh auth git-credential$'
|
||||
3
acceptance/testdata/auth/auth-status.txtar
vendored
Normal file
3
acceptance/testdata/auth/auth-status.txtar
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Check the authentication status
|
||||
exec gh auth status --hostname $GH_HOST
|
||||
stdout '✓ Logged in to '
|
||||
3
acceptance/testdata/auth/auth-token.txtar
vendored
Normal file
3
acceptance/testdata/auth/auth-token.txtar
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Check authentication token
|
||||
exec gh auth token --hostname $GH_HOST
|
||||
stdout $GH_TOKEN
|
||||
69
acceptance/testdata/extension/extension.txtar
vendored
Normal file
69
acceptance/testdata/extension/extension.txtar
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Skip if Bash is not available given script extension
|
||||
[!exec:bash] skip
|
||||
|
||||
# Setup environment variables used for testscript
|
||||
env EXT_NAME=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env EXT_SCRIPT=gh-${EXT_NAME}
|
||||
env REPO=gh-${EXT_NAME}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create local repository for extension
|
||||
exec gh extension create $EXT_NAME
|
||||
cd $REPO
|
||||
|
||||
# Setup v1 executable baseline for extension
|
||||
mv ../v1.sh $EXT_SCRIPT
|
||||
chmod 777 $EXT_SCRIPT
|
||||
exec git add $EXT_SCRIPT
|
||||
exec git commit -m 'Setup extension as v1'
|
||||
|
||||
# Upload local extension repository
|
||||
exec gh repo create $ORG/$REPO --private --source . --push
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Verify extension shows up in search, sleep for indexing
|
||||
exec gh repo edit --add-topic gh-extension
|
||||
sleep 10
|
||||
exec gh extension search --owner $ORG $EXT_NAME
|
||||
stdout ${ORG}/${REPO}
|
||||
|
||||
# Verify repository can be installed as extension
|
||||
exec gh extension install $ORG/$REPO
|
||||
exec gh extension list
|
||||
stdout ${ORG}/${REPO}
|
||||
|
||||
# Verify v1 extension behavior before upgrade
|
||||
exec gh extension exec $EXT_NAME
|
||||
stdout 'gh ext create v1'
|
||||
|
||||
# Setup v2 executable upgrade for extension
|
||||
mv ../v2.sh $EXT_SCRIPT
|
||||
chmod 777 $EXT_SCRIPT
|
||||
exec git add $EXT_SCRIPT
|
||||
exec git commit -m 'Upgrade extension to v2'
|
||||
exec git push -u origin
|
||||
|
||||
# Verify v2 extension upgrade
|
||||
exec gh extension upgrade $EXT_NAME
|
||||
exec gh extension exec $EXT_NAME
|
||||
stdout 'gh ext upgrade v2'
|
||||
|
||||
# Verify extension can be removed
|
||||
exec gh extension remove $EXT_NAME
|
||||
! stdout ${ORG}/${REPO}
|
||||
|
||||
-- v1.sh --
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "gh ext create v1"
|
||||
|
||||
-- v2.sh --
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "gh ext upgrade v2"
|
||||
36
acceptance/testdata/gpg-key/gpg-key.txtar
vendored
Normal file
36
acceptance/testdata/gpg-key/gpg-key.txtar
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
skip 'it modifies the user''s personal GitHub account GPG keys'
|
||||
|
||||
# This test requires the admin:gpg_key scope to add and delete GPG keys to and
|
||||
# from the user's personal GitHub account.
|
||||
# This test uses a GPG key that generated for this test only. The private key
|
||||
# has been deleted
|
||||
|
||||
# Add the gpg key to GH account
|
||||
exec gh gpg-key add gpg-key.pub
|
||||
|
||||
# Verify the gpg key was added to GH account
|
||||
exec gh gpg-key list
|
||||
stdout '24C30F9C9115E747'
|
||||
|
||||
# Delete the gpg key from GH account
|
||||
exec gh gpg-key delete --yes '24C30F9C9115E747'
|
||||
|
||||
# Check the key is deleted
|
||||
exec gh gpg-key list
|
||||
! stdout '24C30F9C9115E747'
|
||||
|
||||
-- gpg-key.pub --
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZxpWhhYJKwYBBAHaRw8BAQdAmYiobR2ai/lVWOBtlAPRG1ZEMG5Effavpt5w
|
||||
n+wQ//W0R0dIIENMSSBhY2NlcHRhbmNlIHRlc3QgKGZvciBHSCBDTEkgYWNjZXB0
|
||||
YW5jZSB0ZXN0aW5nKSA8Y2xpQGdpdGh1Yi5jb20+iJkEExYKAEEWIQTEAQLLUl1x
|
||||
MDSmbL0kww+ckRXnRwUCZxpWhgIbAwUJAAFRgAULCQgHAgIiAgYVCgkICwIEFgID
|
||||
AQIeBwIXgAAKCRAkww+ckRXnRxkuAP9GiFi/etWxRjnkomdTaOU8Ccd6oHspuEzB
|
||||
PFxOJdYslQD+MXgY5UhM/q2iEVj0tiVsfRzDqB+g2weaF5EpqIwWcQ+4OARnGlaG
|
||||
EgorBgEEAZdVAQUBAQdA3D1vnVTc9URDQw/oAd1mG/zRX7vF4QrjFqFIt7uMf2gD
|
||||
AQgHiH4EGBYKACYWIQTEAQLLUl1xMDSmbL0kww+ckRXnRwUCZxpWhgIbDAUJAAFR
|
||||
gAAKCRAkww+ckRXnRxVuAQCngnR11jh2mob0FN0rPWce2juoJsh5gPB2d7LS4r5P
|
||||
VwEA6F2FeetcP51EyKyQGTp3GpmZgk0uCGJa1G5uqT+9mgc=
|
||||
=RLWi
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
20
acceptance/testdata/issue/issue-comment.txtar
vendored
Normal file
20
acceptance/testdata/issue/issue-comment.txtar
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
exec gh issue comment $ISSUE_URL --body 'Looks like a great feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view $ISSUE_URL --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
17
acceptance/testdata/issue/issue-create-basic.txtar
vendored
Normal file
17
acceptance/testdata/issue/issue-create-basic.txtar
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Check the issue was created
|
||||
exec gh issue view $ISSUE_URL
|
||||
stdout 'title:\tFeature Request$'
|
||||
19
acceptance/testdata/issue/issue-create-with-metadata.txtar
vendored
Normal file
19
acceptance/testdata/issue/issue-create-with-metadata.txtar
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body' --assignee '@me' --label 'bug'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Check the issue was create
|
||||
exec gh issue view $ISSUE_URL
|
||||
stdout 'title:\tFeature Request$'
|
||||
stdout 'assignees:\t.+$'
|
||||
stdout 'labels:\tbug$'
|
||||
16
acceptance/testdata/issue/issue-list.txtar
vendored
Normal file
16
acceptance/testdata/issue/issue-list.txtar
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
|
||||
# Check the issue is included in the list output
|
||||
exec gh issue list
|
||||
stdout 'OPEN\tFeature Request'
|
||||
17
acceptance/testdata/issue/issue-view.txtar
vendored
Normal file
17
acceptance/testdata/issue/issue-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Check the issue was created
|
||||
exec gh issue view $ISSUE_URL
|
||||
stdout 'title:\tFeature Request$'
|
||||
25
acceptance/testdata/label/label.txtar
vendored
Normal file
25
acceptance/testdata/label/label.txtar
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Setup useful env vars
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Create a repository
|
||||
exec gh repo create ${ORG}/${REPO} --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Set the GH_REPO env var to reduce redunant flags
|
||||
env GH_REPO=${ORG}/${REPO}
|
||||
|
||||
# Create a custom label
|
||||
exec gh label create 'acceptance-test' --description 'First Description'
|
||||
|
||||
# List the labels and check our custom label is there
|
||||
exec gh label list
|
||||
stdout 'acceptance-test\tFirst Description'
|
||||
|
||||
# Edit the label
|
||||
exec gh label edit 'acceptance-test' --description 'Edited Description'
|
||||
|
||||
# List the labels and check our custom label has been updated
|
||||
exec gh label list
|
||||
stdout 'acceptance-test\tEdited Description'
|
||||
6
acceptance/testdata/org/org-list.txtar
vendored
Normal file
6
acceptance/testdata/org/org-list.txtar
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# This test could fail if the user is a member of more than 30 organizations because
|
||||
# the `gh org list` command only returns the first 30 organizations the user is a member of
|
||||
|
||||
# List organizations the user is a member of
|
||||
exec gh org list
|
||||
stdout ${GH_ACCEPTANCE_ORG}
|
||||
30
acceptance/testdata/pr/pr-checkout.txtar
vendored
Normal file
30
acceptance/testdata/pr/pr-checkout.txtar
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Remove the local branch
|
||||
exec git checkout main
|
||||
exec git branch -D feature-branch
|
||||
stdout 'Deleted branch feature-branch'
|
||||
|
||||
# Checkout the PR
|
||||
exec gh pr checkout $PR_URL
|
||||
stderr 'Switched to a new branch ''feature-branch'''
|
||||
28
acceptance/testdata/pr/pr-comment.txtar
vendored
Normal file
28
acceptance/testdata/pr/pr-comment.txtar
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Comment on the PR
|
||||
exec gh pr comment $PR_URL --body 'Looks like a great feature!'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view $PR_URL --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
24
acceptance/testdata/pr/pr-create-basic.txtar
vendored
Normal file
24
acceptance/testdata/pr/pr-create-basic.txtar
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
|
||||
# Check the PR is indeed created
|
||||
exec gh pr view
|
||||
stdout 'Feature Title'
|
||||
26
acceptance/testdata/pr/pr-create-with-metadata.txtar
vendored
Normal file
26
acceptance/testdata/pr/pr-create-with-metadata.txtar
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body' --assignee '@me' --label 'bug'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Check the PR is indeed created
|
||||
exec gh pr view $PR_URL
|
||||
stdout 'assignees:\t.+$'
|
||||
stdout 'labels:\tbug$'
|
||||
24
acceptance/testdata/pr/pr-list.txtar
vendored
Normal file
24
acceptance/testdata/pr/pr-list.txtar
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
|
||||
# List PRs and see the new PR is in the list
|
||||
exec gh pr list
|
||||
stdout 'Feature Title\tfeature-branch\tOPEN'
|
||||
45
acceptance/testdata/pr/pr-merge-merge-strategy.txtar
vendored
Normal file
45
acceptance/testdata/pr/pr-merge-merge-strategy.txtar
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR with a single file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
mv ../file.txt file.txt
|
||||
exec git add .
|
||||
exec git commit -m 'Add file.txt'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Check that the file doesn't exist on the main branch
|
||||
exec git checkout main
|
||||
! exists file.txt
|
||||
|
||||
# Merge the PR
|
||||
exec gh pr merge $PR_URL --merge
|
||||
|
||||
# Check that the state of the PR is now merged
|
||||
exec gh pr view $PR_URL
|
||||
stdout 'state:\tMERGED$'
|
||||
|
||||
# Pull and check the file exists on the main branch
|
||||
exec git pull -r
|
||||
exists file.txt
|
||||
|
||||
# And check we had a merge commit
|
||||
exec git show HEAD
|
||||
stdout 'Merge pull request #1'
|
||||
|
||||
-- file.txt --
|
||||
Unimportant contents
|
||||
45
acceptance/testdata/pr/pr-merge-rebase-strategy.txtar
vendored
Normal file
45
acceptance/testdata/pr/pr-merge-rebase-strategy.txtar
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR with a single file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
mv ../file.txt file.txt
|
||||
exec git add .
|
||||
exec git commit -m 'Add file.txt'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Check that the file doesn't exist on the main branch
|
||||
exec git checkout main
|
||||
! exists file.txt
|
||||
|
||||
# Merge the PR
|
||||
exec gh pr merge $PR_URL --rebase
|
||||
|
||||
# Check that the state of the PR is now merged
|
||||
exec gh pr view $PR_URL
|
||||
stdout 'state:\tMERGED$'
|
||||
|
||||
# Pull and check the file exists on the main branch
|
||||
exec git pull -r
|
||||
exists file.txt
|
||||
|
||||
# And check our commit was rebased
|
||||
exec git show HEAD
|
||||
stdout 'Add file.txt'
|
||||
|
||||
-- file.txt --
|
||||
Unimportant contents
|
||||
25
acceptance/testdata/pr/pr-view.txtar
vendored
Normal file
25
acceptance/testdata/pr/pr-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# View the PR
|
||||
exec gh pr view $PR_URL
|
||||
stdout 'Feature Title'
|
||||
13
acceptance/testdata/project/project-create-delete.txtar
vendored
Normal file
13
acceptance/testdata/project/project-create-delete.txtar
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Create a project and get the project number
|
||||
env PROJECT_TITLE=$SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh project create --owner=$ORG --title=$PROJECT_TITLE --format='json' --jq='.number'
|
||||
stdout2env PROJECT_NUMBER
|
||||
|
||||
# Confirm the project has been created
|
||||
exec gh project view --owner=$ORG $PROJECT_NUMBER
|
||||
|
||||
# Delete the project
|
||||
exec gh project delete --owner=$ORG $PROJECT_NUMBER
|
||||
|
||||
# Confirm the project has been deleted
|
||||
! exec gh project view --owner=$ORG $PROJECT_NUMBER
|
||||
12
acceptance/testdata/release/release-create.txtar
vendored
Normal file
12
acceptance/testdata/release/release-create.txtar
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create a release in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh release create v1.2.3 --notes 'awesome release' --latest
|
||||
16
acceptance/testdata/release/release-list.txtar
vendored
Normal file
16
acceptance/testdata/release/release-list.txtar
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create a release in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh release create v1.2.3 --notes 'awesome release' --latest
|
||||
|
||||
# List the releases
|
||||
exec gh release list
|
||||
stdout 'v1.2.3'
|
||||
26
acceptance/testdata/release/release-upload-download.txtar
vendored
Normal file
26
acceptance/testdata/release/release-upload-download.txtar
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create a release in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh release create v1.2.3 --notes 'awesome release' --latest
|
||||
|
||||
# Upload an asset to the release
|
||||
exec gh release upload v1.2.3 ../asset.txt
|
||||
|
||||
# Download the asset from the release
|
||||
exec gh release download v1.2.3
|
||||
exists asset.txt
|
||||
|
||||
# Download the asset in archive form
|
||||
exec gh release download v1.2.3 --archive=zip
|
||||
exists $SCRIPT_NAME-$RANDOM_STRING-1.2.3.zip
|
||||
|
||||
-- asset.txt --
|
||||
Hello, world!
|
||||
16
acceptance/testdata/release/release-view.txtar
vendored
Normal file
16
acceptance/testdata/release/release-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create a release in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh release create v1.2.3 --notes 'awesome release' --latest
|
||||
|
||||
# View the release
|
||||
exec gh release view v1.2.3
|
||||
stdout 'v1.2.3'
|
||||
23
acceptance/testdata/repo/repo-archive-unarchive.txtar
vendored
Normal file
23
acceptance/testdata/repo/repo-archive-unarchive.txtar
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Check that the repo exists and isn't archived
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived'
|
||||
stdout 'false'
|
||||
|
||||
# Archive the repo
|
||||
exec gh repo archive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
||||
# Check that the repo is archived
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived'
|
||||
stdout 'true'
|
||||
|
||||
# Unarchive the repo
|
||||
exec gh repo unarchive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
||||
# Check that the repo is unarchived
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived'
|
||||
stdout 'false'
|
||||
11
acceptance/testdata/repo/repo-clone.txtar
vendored
Normal file
11
acceptance/testdata/repo/repo-clone.txtar
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Ensure the repo was cloned
|
||||
exists $SCRIPT_NAME-$RANDOM_STRING/README.md
|
||||
9
acceptance/testdata/repo/repo-create-view.txtar
vendored
Normal file
9
acceptance/testdata/repo/repo-create-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Check that the repo exists
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=name --jq='.name'
|
||||
stdout $SCRIPT_NAME-$RANDOM_STRING
|
||||
13
acceptance/testdata/repo/repo-delete.txtar
vendored
Normal file
13
acceptance/testdata/repo/repo-delete.txtar
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Check that the repo exists
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json name --jq '.name'
|
||||
stdout $SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Delete the repo
|
||||
exec gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Ensure that the repo was deleted
|
||||
! exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
stderr 'Could not resolve to a Repository with the name'
|
||||
31
acceptance/testdata/repo/repo-deploy-key.txtar
vendored
Normal file
31
acceptance/testdata/repo/repo-deploy-key.txtar
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Create and clone a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# cd to the repo and list the deploy keys. There should be no keys
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh repo deploy-key list --json=title
|
||||
! stdout title
|
||||
|
||||
# Add a deploy key
|
||||
exec gh repo deploy-key add ../deployKey.pub
|
||||
|
||||
# Ensure the deploy key was added
|
||||
exec gh repo deploy-key list --json=title --jq='.[].title'
|
||||
stdout myTitle
|
||||
|
||||
# Get the deploy key id
|
||||
exec gh repo deploy-key list --json=title,id --jq='.[].title="myTitle" | .[].id'
|
||||
stdout2env DEPLOY_KEY_ID
|
||||
|
||||
# Delete the deploy key
|
||||
exec gh repo deploy-key delete $DEPLOY_KEY_ID
|
||||
|
||||
# Ensure the deploy key was deleted
|
||||
exec gh repo deploy-key list --json=id --jq='.[].id'
|
||||
! stdout $DEPLOY_KEY_ID
|
||||
|
||||
-- deployKey.pub --
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZmdeRNskfpvYL5YHB/YJaW8hTEXpnvPMkx5Ri+YwUr myTitle
|
||||
16
acceptance/testdata/repo/repo-edit.txtar
vendored
Normal file
16
acceptance/testdata/repo/repo-edit.txtar
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Check that the repo description is empty
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description'
|
||||
! stdout '.'
|
||||
|
||||
# Edit the repo description
|
||||
exec gh repo edit $ORG/$SCRIPT_NAME-$RANDOM_STRING --description 'newDescription'
|
||||
|
||||
# Check that the repo description is updated
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description'
|
||||
stdout 'newDescription'
|
||||
42
acceptance/testdata/repo/repo-fork-sync.txtar
vendored
Normal file
42
acceptance/testdata/repo/repo-fork-sync.txtar
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create and clone a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Fork and clone the repo
|
||||
exec gh repo fork $ORG/$SCRIPT_NAME-$RANDOM_STRING --org $ORG --fork-name $SCRIPT_NAME-$RANDOM_STRING-fork --clone
|
||||
|
||||
# Defer fork cleanup
|
||||
defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING-fork --yes
|
||||
|
||||
# Sleep so that the BE has time to sync
|
||||
sleep 5
|
||||
|
||||
# Check that the repo was forked
|
||||
exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING-fork --json='isFork' --jq='.isFork'
|
||||
stdout 'true'
|
||||
|
||||
# Modify original repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mv ../asset.txt asset.txt
|
||||
exec git add .
|
||||
exec git commit -m 'Add asset.txt'
|
||||
exec git push
|
||||
|
||||
# Checkout the forked repo and ensure asset.txt is not present
|
||||
cd ../$SCRIPT_NAME-$RANDOM_STRING-fork
|
||||
exec git checkout main
|
||||
! exists asset.txt
|
||||
|
||||
# Sync the forked repo with the original repo
|
||||
exec gh repo sync
|
||||
|
||||
# Check that asset.txt now exists in the fork
|
||||
exists asset.txt
|
||||
|
||||
-- asset.txt --
|
||||
Hello, world!
|
||||
16
acceptance/testdata/repo/repo-list-rename.txtar
vendored
Normal file
16
acceptance/testdata/repo/repo-list-rename.txtar
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# List the repos and check for the new repo
|
||||
exec gh repo list $ORG --json=name --jq='.[].name'
|
||||
stdout $SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Rename the repo
|
||||
exec gh repo rename $SCRIPT_NAME-$RANDOM_STRING-renamed --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
||||
# Defer repo deletion
|
||||
defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING-renamed --yes
|
||||
|
||||
# List the repos and check for the renamed repo
|
||||
exec gh repo list $ORG --json=name --jq='.[].name'
|
||||
stdout $SCRIPT_NAME-$RANDOM_STRING-renamed
|
||||
17
acceptance/testdata/repo/repo-set-default.txtar
vendored
Normal file
17
acceptance/testdata/repo/repo-set-default.txtar
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Create and clone a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Ensure that no default is set
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh repo set-default --view
|
||||
stderr 'no default repository has been set; use `gh repo set-default` to select one'
|
||||
|
||||
# Set the default
|
||||
exec gh repo set-default $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Check that the default is set
|
||||
exec gh repo set-default --view
|
||||
stdout $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
62
acceptance/testdata/ruleset/ruleset.txtar
vendored
Normal file
62
acceptance/testdata/ruleset/ruleset.txtar
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Verify repository ruleset does not exist
|
||||
env LIST_MATCH=testscript\s+$ORG/$REPO (repo)
|
||||
exec gh ruleset list
|
||||
! stdout $LIST_MATCH
|
||||
|
||||
# Verify no repository ruleset applies to default branch
|
||||
exec gh ruleset check
|
||||
stdout '0 rules apply'
|
||||
|
||||
# Create a repository ruleset
|
||||
exec gh api /repos/{owner}/{repo}/rulesets -X POST --input ../create-repo-ruleset.json
|
||||
|
||||
# Verify repository ruleset does exist
|
||||
exec gh ruleset list
|
||||
stdout $LIST_MATCH
|
||||
|
||||
# Verify repository ruleset associated with branch
|
||||
exec gh ruleset check
|
||||
stdout '- pull_request:.+dismiss_stale_reviews_on_push: false.+require_code_owner_review: true.+require_last_push_approval: false.+required_approving_review_count: 1.+required_review_thread_resolution: false'
|
||||
|
||||
-- create-repo-ruleset.json --
|
||||
{
|
||||
"name": "testscript",
|
||||
"target": "branch",
|
||||
"enforcement": "active",
|
||||
"conditions": {
|
||||
"ref_name": {
|
||||
"include": [
|
||||
"~DEFAULT_BRANCH"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "pull_request",
|
||||
"parameters": {
|
||||
"dismiss_stale_reviews_on_push": false,
|
||||
"require_code_owner_review": true,
|
||||
"require_last_push_approval": false,
|
||||
"required_approving_review_count": 1,
|
||||
"required_review_thread_resolution": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
acceptance/testdata/search/search-issues.txtar
vendored
Normal file
20
acceptance/testdata/search/search-issues.txtar
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
exec gh issue create --title 'Feature Request' --body $RANDOM_STRING
|
||||
|
||||
# It takes some time for the issue to be created and indexed
|
||||
sleep 5
|
||||
|
||||
# Search for the issue
|
||||
exec gh search issues $RANDOM_STRING -R $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
stdout $RANDOM_STRING
|
||||
85
acceptance/testdata/secret/secret-org.txtar
vendored
Normal file
85
acceptance/testdata/secret/secret-org.txtar
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Confirm organization secret does not exist, will fail admin:org scope missing
|
||||
exec gh secret list --org $ORG
|
||||
! stdout $SECRET_NAME
|
||||
|
||||
# Create an organization secret
|
||||
exec gh secret set $SECRET_NAME --org $ORG --body 'just an organization secret' --repos $REPO
|
||||
|
||||
# Defer organization secret cleanup
|
||||
defer gh secret delete $SECRET_NAME --org $ORG
|
||||
|
||||
# Verify new organization secret exists
|
||||
exec gh secret list --org $ORG
|
||||
stdout $SECRET_NAME
|
||||
|
||||
# Commit workflow file creating dispatchable workflow able to verify secret matches
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
replace .github/workflows/workflow.yml SECRET_NAME=$SECRET_NAME
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch & delete
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Verify secret matched what was set earlier
|
||||
exec gh run view $RUN_ID --log
|
||||
stdout 'GitHub Actions secret value matches$'
|
||||
|
||||
-- workflow.yml --
|
||||
# This workflow is intended to assert the value of the GitHub Actions secret was set appropriately
|
||||
name: Test Workflow Name
|
||||
on:
|
||||
# Allow workflow to be dispatched by gh workflow run
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches
|
||||
assert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assert secret value matches
|
||||
env:
|
||||
ORG_SECRET: ${{ secrets.$SECRET_NAME }}
|
||||
run: |
|
||||
if [[ "$ORG_SECRET" == "just an organization secret" ]]; then
|
||||
echo "GitHub Actions secret value matches"
|
||||
else
|
||||
echo "GitHub Actions secret value does not match"
|
||||
exit 1
|
||||
fi
|
||||
80
acceptance/testdata/secret/secret-repo-env.txtar
vendored
Normal file
80
acceptance/testdata/secret/secret-repo-env.txtar
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Create a repository environment, will fail if organization does not have environment support
|
||||
exec gh api /repos/$ORG/$REPO/environments/testscripts -X PUT
|
||||
|
||||
# Create a repository environment secret
|
||||
exec gh secret set TESTSCRIPTS_ENV --env testscripts --body 'just a repository environment secret'
|
||||
|
||||
# Verify new repository secret exists
|
||||
exec gh secret list --env testscripts
|
||||
stdout 'TESTSCRIPTS_ENV'
|
||||
|
||||
# Commit workflow file creating dispatchable workflow able to verify secret matches
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch & delete
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Verify secret matched what was set earlier
|
||||
exec gh run view $RUN_ID --log
|
||||
stdout 'GitHub Actions secret value matches$'
|
||||
|
||||
-- workflow.yml --
|
||||
# This workflow is intended to assert the value of the GitHub Actions secret was set appropriately
|
||||
name: Test Workflow Name
|
||||
on:
|
||||
# Allow workflow to be dispatched by gh workflow run
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches
|
||||
assert:
|
||||
runs-on: ubuntu-latest
|
||||
environment: testscripts
|
||||
steps:
|
||||
- name: Assert secret value matches
|
||||
env:
|
||||
TESTSCRIPTS_ENV: ${{ secrets.TESTSCRIPTS_ENV }}
|
||||
run: |
|
||||
if [[ "$TESTSCRIPTS_ENV" == "just a repository environment secret" ]]; then
|
||||
echo "GitHub Actions secret value matches"
|
||||
else
|
||||
echo "GitHub Actions secret value does not match"
|
||||
exit 1
|
||||
fi
|
||||
76
acceptance/testdata/secret/secret-repo.txtar
vendored
Normal file
76
acceptance/testdata/secret/secret-repo.txtar
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Create a repository secret
|
||||
exec gh secret set TESTSCRIPTS --body 'just a repository secret'
|
||||
|
||||
# Verify new repository secret exists
|
||||
exec gh secret list
|
||||
stdout 'TESTSCRIPTS'
|
||||
|
||||
# Commit workflow file creating dispatchable workflow able to verify secret matches
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch & delete
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Verify secret matched what was set earlier
|
||||
exec gh run view $RUN_ID --log
|
||||
stdout 'GitHub Actions secret value matches$'
|
||||
|
||||
-- workflow.yml --
|
||||
# This workflow is intended to assert the value of the GitHub Actions secret was set appropriately
|
||||
name: Test Workflow Name
|
||||
on:
|
||||
# Allow workflow to be dispatched by gh workflow run
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches
|
||||
assert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assert secret value matches
|
||||
env:
|
||||
TESTSCRIPTS: ${{ secrets.TESTSCRIPTS }}
|
||||
run: |
|
||||
if [[ "$TESTSCRIPTS" == "just a repository secret" ]]; then
|
||||
echo "GitHub Actions secret value matches"
|
||||
else
|
||||
echo "GitHub Actions secret value does not match"
|
||||
exit 1
|
||||
fi
|
||||
24
acceptance/testdata/ssh-key/ssh-key.txtar
vendored
Normal file
24
acceptance/testdata/ssh-key/ssh-key.txtar
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
skip 'it modifies the user''s personal GitHub account SSH keys'
|
||||
|
||||
# scopes admin:ssh_signing_key,admin:public_key
|
||||
|
||||
# Add an SSH key to the account
|
||||
exec gh ssh-key add sshKey.pub --title 'acceptance-test-key'
|
||||
|
||||
# List the SSH keys
|
||||
exec gh ssh-key list
|
||||
stdout 'acceptance-test-key'
|
||||
|
||||
# Get the ID of the key we created
|
||||
exec gh api /user/keys --jq '.[] | select(.title == "acceptance-test-key") | .id'
|
||||
stdout2env SSH_KEY_ID
|
||||
|
||||
# Delete the SSH key
|
||||
exec gh ssh-key delete --yes ${SSH_KEY_ID}
|
||||
|
||||
# Check the key is deleted
|
||||
exec gh ssh-key list
|
||||
! stdout 'acceptance-test-key'
|
||||
|
||||
-- sshKey.pub --
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZmdeRNskfpvYL5YHB/YJaW8hTEXpnvPMkx5Ri+YwUr acceptance
|
||||
20
acceptance/testdata/variable/variable-org.txtar
vendored
Normal file
20
acceptance/testdata/variable/variable-org.txtar
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Setup environment variables used for testscript
|
||||
env2upper VAR_NAME=${SCRIPT_NAME}_${RANDOM_STRING}
|
||||
|
||||
# Confirm organization variable does not exist, will fail admin:org scope missing
|
||||
exec gh variable list --org $ORG
|
||||
! stdout $VAR_NAME
|
||||
|
||||
# Create an organization variable
|
||||
exec gh variable set $VAR_NAME --org $ORG --body 'just an org variable'
|
||||
|
||||
# Defer organization variable cleanup
|
||||
defer gh variable delete $VAR_NAME --org $ORG
|
||||
|
||||
# Verify new organization variable exists
|
||||
exec gh variable list --org $ORG
|
||||
stdout $VAR_NAME
|
||||
|
||||
# Verify organization variable can be retrieved
|
||||
exec gh variable get $VAR_NAME --org $ORG
|
||||
stdout 'just an org variable'
|
||||
32
acceptance/testdata/variable/variable-repo-env.txtar
vendored
Normal file
32
acceptance/testdata/variable/variable-repo-env.txtar
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env ENV_NAME=testscripts
|
||||
env VAR_NAME=TESTSCRIPTS_ENV
|
||||
|
||||
# Create a repository where the variable will be registered
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Create a repository environment, will fail if organization does not have environment support
|
||||
exec gh api /repos/$ORG/$REPO/environments/$ENV_NAME -X PUT --jq '.name'
|
||||
|
||||
# Verify repository environment variable does not exist
|
||||
exec gh variable list --env $ENV_NAME
|
||||
! stdout $VAR_NAME
|
||||
|
||||
# Create a repository environment variable
|
||||
exec gh variable set $VAR_NAME --env $ENV_NAME --body 'just a repo env variable'
|
||||
|
||||
# Verify new repository environment variable exists
|
||||
exec gh variable list --env $ENV_NAME
|
||||
stdout $VAR_NAME
|
||||
|
||||
# Verify repository environment variable can be retrieved
|
||||
exec gh variable get $VAR_NAME --env $ENV_NAME
|
||||
stdout 'just a repo env variable'
|
||||
28
acceptance/testdata/variable/variable-repo.txtar
vendored
Normal file
28
acceptance/testdata/variable/variable-repo.txtar
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env VAR_NAME=TESTSCRIPTS
|
||||
|
||||
# Create a repository where the variable will be registered
|
||||
exec gh repo create $ORG/$REPO --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$REPO
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$REPO
|
||||
cd $REPO
|
||||
|
||||
# Verify repository variable does not exist
|
||||
exec gh variable list
|
||||
! stdout $VAR_NAME
|
||||
|
||||
# Create a repository variable
|
||||
exec gh variable set $VAR_NAME --body 'just a repo variable'
|
||||
|
||||
# Verify new repository variable exists
|
||||
exec gh variable list
|
||||
stdout $VAR_NAME
|
||||
|
||||
# Verify repository variable can be retrieved
|
||||
exec gh variable get $VAR_NAME
|
||||
stdout 'just a repo variable'
|
||||
69
acceptance/testdata/workflow/cache-list-delete.txtar
vendored
Normal file
69
acceptance/testdata/workflow/cache-list-delete.txtar
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# List the cache
|
||||
exec gh cache list
|
||||
stdout 'Linux-values'
|
||||
|
||||
# Delete the cache
|
||||
exec gh cache delete 'Linux-values'
|
||||
|
||||
-- workflow.yml --
|
||||
name: Test Workflow Name
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Cache values
|
||||
id: cache-values
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: values.txt
|
||||
key: ${{ runner.os }}-values
|
||||
|
||||
- name: Generate values file
|
||||
if: steps.cache-values.outputs.cache-hit != 'true'
|
||||
run: echo "values" > values.txt
|
||||
73
acceptance/testdata/workflow/run-cancel.txtar
vendored
Normal file
73
acceptance/testdata/workflow/run-cancel.txtar
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to cancel
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# cancel the workflow run
|
||||
exec gh run cancel $RUN_ID
|
||||
stdout '✓ Request to cancel workflow [0-9]+ submitted.'
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID
|
||||
|
||||
# Check the workflow run is cancelled
|
||||
exec gh run list --json conclusion --jq '.[0].conclusion'
|
||||
stdout 'cancelled'
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: sleep 30
|
||||
74
acceptance/testdata/workflow/run-delete.txtar
vendored
Normal file
74
acceptance/testdata/workflow/run-delete.txtar
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch & delete
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Delete the workflow run
|
||||
exec gh run delete $RUN_ID
|
||||
stdout '✓ Request to delete workflow submitted.'
|
||||
|
||||
# It takes some time for a workflow run to be deleted
|
||||
sleep 5
|
||||
|
||||
# Check the workflow run is cancelled, which is implied by an empty list
|
||||
exec gh run list
|
||||
stdout ''
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
70
acceptance/testdata/workflow/run-download.txtar
vendored
Normal file
70
acceptance/testdata/workflow/run-download.txtar
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to watch
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Download the artifact
|
||||
exec gh run download $RUN_ID
|
||||
|
||||
# Check if we downloaded the artifact
|
||||
exists ./my-artifact/world.txt
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- run: echo hello > world.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: my-artifact
|
||||
path: world.txt
|
||||
72
acceptance/testdata/workflow/run-rerun.txtar
vendored
Normal file
72
acceptance/testdata/workflow/run-rerun.txtar
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to rerun
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# Rerun the workflow run
|
||||
exec gh run rerun $RUN_ID
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
66
acceptance/testdata/workflow/run-view.txtar
vendored
Normal file
66
acceptance/testdata/workflow/run-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Get the run ID we want to view
|
||||
exec gh run list --json databaseId --jq '.[0].databaseId'
|
||||
stdout2env RUN_ID
|
||||
|
||||
# Wait for workflow to complete
|
||||
exec gh run watch $RUN_ID --exit-status
|
||||
|
||||
# View the workflow run
|
||||
exec gh run view $RUN_ID
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
66
acceptance/testdata/workflow/workflow-enable-disable.txtar
vendored
Normal file
66
acceptance/testdata/workflow/workflow-enable-disable.txtar
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# disable the workflow
|
||||
exec gh workflow disable 'Test Workflow Name'
|
||||
|
||||
# Check that the listing shows it is disabled
|
||||
exec gh workflow list --all
|
||||
stdout 'Test\s+Workflow\s+Name\s+disabled_manually\s+\d+'
|
||||
|
||||
# enable the workflow
|
||||
exec gh workflow enable 'Test Workflow Name'
|
||||
|
||||
# Check the workflow is indeed enabled
|
||||
exec gh workflow list
|
||||
stdout 'Test\s+Workflow\s+Name\s+active\s+\d+'
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
52
acceptance/testdata/workflow/workflow-list.txtar
vendored
Normal file
52
acceptance/testdata/workflow/workflow-list.txtar
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
62
acceptance/testdata/workflow/workflow-run.txtar
vendored
Normal file
62
acceptance/testdata/workflow/workflow-run.txtar
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
# Run the workflow
|
||||
exec gh workflow run 'Test Workflow Name'
|
||||
|
||||
# It takes some time for a workflow run to register
|
||||
sleep 10
|
||||
|
||||
# Check the workflow run exists
|
||||
exec gh run list
|
||||
stdout 'Test Workflow Name'
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
52
acceptance/testdata/workflow/workflow-view.txtar
vendored
Normal file
52
acceptance/testdata/workflow/workflow-view.txtar
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# commit the workflow file
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
mkdir .github/workflows
|
||||
mv ../workflow.yml .github/workflows/workflow.yml
|
||||
exec git add .github/workflows/workflow.yml
|
||||
exec git commit -m 'Create workflow file'
|
||||
exec git push -u origin main
|
||||
|
||||
# Sleep because it takes a second for the workflow to register
|
||||
sleep 1
|
||||
|
||||
# Check the workflow is indeed created
|
||||
exec gh workflow view 'Test Workflow Name'
|
||||
stdout 'Test Workflow Name - workflow.yml'
|
||||
|
||||
-- workflow.yml --
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Test Workflow Name
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -249,7 +249,7 @@ func generateScopesSuggestion(statusCode int, endpointNeedsScopes, tokenHasScope
|
|||
return fmt.Sprintf(
|
||||
"This API operation needs the %[1]q scope. To request it, run: gh auth refresh -h %[2]s -s %[1]s",
|
||||
s,
|
||||
ghinstance.NormalizeHostname(hostname),
|
||||
ghauth.NormalizeHostname(hostname),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
type tokenGetter interface {
|
||||
|
|
@ -98,7 +98,7 @@ func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper
|
|||
// Only set header if an initial request or redirect request to the same host as the initial request.
|
||||
// If the host has changed during a redirect do not add the authentication token header.
|
||||
if !redirectHostnameChange {
|
||||
hostname := ghinstance.NormalizeHostname(getHost(req))
|
||||
hostname := ghauth.NormalizeHostname(getHost(req))
|
||||
if token, _ := cfg.ActiveToken(hostname); token != "" {
|
||||
req.Header.Set(authorization, fmt.Sprintf("token %s", token))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@ type Repository struct {
|
|||
MirrorURL string
|
||||
SecurityPolicyURL string
|
||||
|
||||
CreatedAt time.Time
|
||||
PushedAt *time.Time
|
||||
UpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
PushedAt *time.Time
|
||||
UpdatedAt time.Time
|
||||
ArchivedAt *time.Time
|
||||
|
||||
IsBlankIssuesEnabled bool
|
||||
IsSecurityPolicyEnabled bool
|
||||
|
|
@ -207,8 +208,24 @@ type IssueLabel struct {
|
|||
}
|
||||
|
||||
type License struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
SPDXID string `json:"spdx_id"`
|
||||
URL string `json:"url"`
|
||||
NodeID string `json:"node_id"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Description string `json:"description"`
|
||||
Implementation string `json:"implementation"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Conditions []string `json:"conditions"`
|
||||
Limitations []string `json:"limitations"`
|
||||
Body string `json:"body"`
|
||||
Featured bool `json:"featured"`
|
||||
}
|
||||
|
||||
type GitIgnore struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// RepoOwner is the login name of the owner
|
||||
|
|
@ -1391,3 +1408,53 @@ func RepoExists(client *Client, repo ghrepo.Interface) (bool, error) {
|
|||
return false, ghAPI.HandleHTTPError(resp)
|
||||
}
|
||||
}
|
||||
|
||||
// RepoLicenses fetches available repository licenses.
|
||||
// It uses API v3 because licenses are not supported by GraphQL.
|
||||
func RepoLicenses(httpClient *http.Client, hostname string) ([]License, error) {
|
||||
var licenses []License
|
||||
client := NewClientFromHTTP(httpClient)
|
||||
err := client.REST(hostname, "GET", "licenses", nil, &licenses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// RepoLicense fetches an available repository license.
|
||||
// It uses API v3 because licenses are not supported by GraphQL.
|
||||
func RepoLicense(httpClient *http.Client, hostname string, licenseName string) (*License, error) {
|
||||
var license License
|
||||
client := NewClientFromHTTP(httpClient)
|
||||
path := fmt.Sprintf("licenses/%s", licenseName)
|
||||
err := client.REST(hostname, "GET", path, nil, &license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &license, nil
|
||||
}
|
||||
|
||||
// RepoGitIgnoreTemplates fetches available repository gitignore templates.
|
||||
// It uses API v3 here because gitignore template isn't supported by GraphQL.
|
||||
func RepoGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, error) {
|
||||
var gitIgnoreTemplates []string
|
||||
client := NewClientFromHTTP(httpClient)
|
||||
err := client.REST(hostname, "GET", "gitignore/templates", nil, &gitIgnoreTemplates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gitIgnoreTemplates, nil
|
||||
}
|
||||
|
||||
// RepoGitIgnoreTemplate fetches an available repository gitignore template.
|
||||
// It uses API v3 here because gitignore template isn't supported by GraphQL.
|
||||
func RepoGitIgnoreTemplate(httpClient *http.Client, hostname string, gitIgnoreTemplateName string) (*GitIgnore, error) {
|
||||
var gitIgnoreTemplate GitIgnore
|
||||
client := NewClientFromHTTP(httpClient)
|
||||
path := fmt.Sprintf("gitignore/templates/%s", gitIgnoreTemplateName)
|
||||
err := client.REST(hostname, "GET", path, nil, &gitIgnoreTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gitIgnoreTemplate, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -566,3 +567,416 @@ func TestForkRepoReturnsErrorWhenForkIsNotPossible(t *testing.T) {
|
|||
// Then it provides a useful error message
|
||||
require.Equal(t, fmt.Errorf("%s/%s cannot be forked. A single user account cannot own both a parent and fork.", ownerLogin, repoName), err)
|
||||
}
|
||||
|
||||
func TestListLicenseTemplatesReturnsLicenses(t *testing.T) {
|
||||
hostname := "api.github.com"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "licenses"),
|
||||
httpmock.StringResponse(`[
|
||||
{
|
||||
"key": "mit",
|
||||
"name": "MIT License",
|
||||
"spdx_id": "MIT",
|
||||
"url": "https://api.github.com/licenses/mit",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "lgpl-3.0",
|
||||
"name": "GNU Lesser General Public License v3.0",
|
||||
"spdx_id": "LGPL-3.0",
|
||||
"url": "https://api.github.com/licenses/lgpl-3.0",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "mpl-2.0",
|
||||
"name": "Mozilla Public License 2.0",
|
||||
"spdx_id": "MPL-2.0",
|
||||
"url": "https://api.github.com/licenses/mpl-2.0",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "agpl-3.0",
|
||||
"name": "GNU Affero General Public License v3.0",
|
||||
"spdx_id": "AGPL-3.0",
|
||||
"url": "https://api.github.com/licenses/agpl-3.0",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "unlicense",
|
||||
"name": "The Unlicense",
|
||||
"spdx_id": "Unlicense",
|
||||
"url": "https://api.github.com/licenses/unlicense",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
},
|
||||
{
|
||||
"key": "gpl-3.0",
|
||||
"name": "GNU General Public License v3.0",
|
||||
"spdx_id": "GPL-3.0",
|
||||
"url": "https://api.github.com/licenses/gpl-3.0",
|
||||
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||
}
|
||||
]`,
|
||||
))
|
||||
}
|
||||
wantLicenses := []License{
|
||||
{
|
||||
Key: "mit",
|
||||
Name: "MIT License",
|
||||
SPDXID: "MIT",
|
||||
URL: "https://api.github.com/licenses/mit",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "lgpl-3.0",
|
||||
Name: "GNU Lesser General Public License v3.0",
|
||||
SPDXID: "LGPL-3.0",
|
||||
URL: "https://api.github.com/licenses/lgpl-3.0",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "mpl-2.0",
|
||||
Name: "Mozilla Public License 2.0",
|
||||
SPDXID: "MPL-2.0",
|
||||
URL: "https://api.github.com/licenses/mpl-2.0",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "agpl-3.0",
|
||||
Name: "GNU Affero General Public License v3.0",
|
||||
SPDXID: "AGPL-3.0",
|
||||
URL: "https://api.github.com/licenses/agpl-3.0",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "unlicense",
|
||||
Name: "The Unlicense",
|
||||
SPDXID: "Unlicense",
|
||||
URL: "https://api.github.com/licenses/unlicense",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "apache-2.0",
|
||||
Name: "Apache License 2.0",
|
||||
SPDXID: "Apache-2.0",
|
||||
URL: "https://api.github.com/licenses/apache-2.0",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
{
|
||||
Key: "gpl-3.0",
|
||||
Name: "GNU General Public License v3.0",
|
||||
SPDXID: "GPL-3.0",
|
||||
URL: "https://api.github.com/licenses/gpl-3.0",
|
||||
NodeID: "MDc6TGljZW5zZW1pdA==",
|
||||
HTMLURL: "",
|
||||
Description: "",
|
||||
Implementation: "",
|
||||
Permissions: nil,
|
||||
Conditions: nil,
|
||||
Limitations: nil,
|
||||
Body: "",
|
||||
},
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
gotLicenses, err := RepoLicenses(client, hostname)
|
||||
|
||||
assert.NoError(t, err, "Expected no error while fetching /licenses")
|
||||
assert.Equal(t, wantLicenses, gotLicenses, "Licenses fetched is not as expected")
|
||||
}
|
||||
|
||||
func TestLicenseTemplateReturnsLicense(t *testing.T) {
|
||||
licenseTemplateName := "mit"
|
||||
hostname := "api.github.com"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("licenses/%v", licenseTemplateName)),
|
||||
httpmock.StringResponse(`{
|
||||
"key": "mit",
|
||||
"name": "MIT License",
|
||||
"spdx_id": "MIT",
|
||||
"url": "https://api.github.com/licenses/mit",
|
||||
"node_id": "MDc6TGljZW5zZTEz",
|
||||
"html_url": "http://choosealicense.com/licenses/mit/",
|
||||
"description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.",
|
||||
"implementation": "Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file. Replace [year] with the current year and [fullname] with the name (or names) of the copyright holders.",
|
||||
"permissions": [
|
||||
"commercial-use",
|
||||
"modifications",
|
||||
"distribution",
|
||||
"private-use"
|
||||
],
|
||||
"conditions": [
|
||||
"include-copyright"
|
||||
],
|
||||
"limitations": [
|
||||
"liability",
|
||||
"warranty"
|
||||
],
|
||||
"body": "MIT License\n\nCopyright (c) [year] [fullname]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"featured": true
|
||||
}`,
|
||||
))
|
||||
}
|
||||
wantLicense := &License{
|
||||
Key: "mit",
|
||||
Name: "MIT License",
|
||||
SPDXID: "MIT",
|
||||
URL: "https://api.github.com/licenses/mit",
|
||||
NodeID: "MDc6TGljZW5zZTEz",
|
||||
HTMLURL: "http://choosealicense.com/licenses/mit/",
|
||||
Description: "A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.",
|
||||
Implementation: "Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file. Replace [year] with the current year and [fullname] with the name (or names) of the copyright holders.",
|
||||
Permissions: []string{
|
||||
"commercial-use",
|
||||
"modifications",
|
||||
"distribution",
|
||||
"private-use",
|
||||
},
|
||||
Conditions: []string{
|
||||
"include-copyright",
|
||||
},
|
||||
Limitations: []string{
|
||||
"liability",
|
||||
"warranty",
|
||||
},
|
||||
Body: "MIT License\n\nCopyright (c) [year] [fullname]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
Featured: true,
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
gotLicenseTemplate, err := RepoLicense(client, hostname, licenseTemplateName)
|
||||
|
||||
assert.NoError(t, err, fmt.Sprintf("Expected no error while fetching /licenses/%v", licenseTemplateName))
|
||||
assert.Equal(t, wantLicense, gotLicenseTemplate, fmt.Sprintf("License \"%v\" fetched is not as expected", licenseTemplateName))
|
||||
}
|
||||
|
||||
func TestLicenseTemplateReturnsErrorWhenLicenseTemplateNotFound(t *testing.T) {
|
||||
licenseTemplateName := "invalid-license"
|
||||
hostname := "api.github.com"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("licenses/%v", licenseTemplateName)),
|
||||
httpmock.StatusStringResponse(404, heredoc.Doc(`
|
||||
{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/rest/licenses/licenses#get-a-license",
|
||||
"status": "404"
|
||||
}`)),
|
||||
)
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
_, err := RepoLicense(client, hostname, licenseTemplateName)
|
||||
|
||||
assert.Error(t, err, fmt.Sprintf("Expected error while fetching /licenses/%v", licenseTemplateName))
|
||||
}
|
||||
|
||||
func TestListGitIgnoreTemplatesReturnsGitIgnoreTemplates(t *testing.T) {
|
||||
hostname := "api.github.com"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "gitignore/templates"),
|
||||
httpmock.StringResponse(`[
|
||||
"AL",
|
||||
"Actionscript",
|
||||
"Ada",
|
||||
"Agda",
|
||||
"Android",
|
||||
"AppEngine",
|
||||
"AppceleratorTitanium",
|
||||
"ArchLinuxPackages",
|
||||
"Autotools",
|
||||
"Ballerina",
|
||||
"C",
|
||||
"C++",
|
||||
"CFWheels",
|
||||
"CMake",
|
||||
"CUDA",
|
||||
"CakePHP",
|
||||
"ChefCookbook",
|
||||
"Clojure",
|
||||
"CodeIgniter",
|
||||
"CommonLisp",
|
||||
"Composer",
|
||||
"Concrete5",
|
||||
"Coq",
|
||||
"CraftCMS",
|
||||
"D"
|
||||
]`,
|
||||
))
|
||||
}
|
||||
wantGitIgnoreTemplates := []string{
|
||||
"AL",
|
||||
"Actionscript",
|
||||
"Ada",
|
||||
"Agda",
|
||||
"Android",
|
||||
"AppEngine",
|
||||
"AppceleratorTitanium",
|
||||
"ArchLinuxPackages",
|
||||
"Autotools",
|
||||
"Ballerina",
|
||||
"C",
|
||||
"C++",
|
||||
"CFWheels",
|
||||
"CMake",
|
||||
"CUDA",
|
||||
"CakePHP",
|
||||
"ChefCookbook",
|
||||
"Clojure",
|
||||
"CodeIgniter",
|
||||
"CommonLisp",
|
||||
"Composer",
|
||||
"Concrete5",
|
||||
"Coq",
|
||||
"CraftCMS",
|
||||
"D",
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
gotGitIgnoreTemplates, err := RepoGitIgnoreTemplates(client, hostname)
|
||||
|
||||
assert.NoError(t, err, "Expected no error while fetching /gitignore/templates")
|
||||
assert.Equal(t, wantGitIgnoreTemplates, gotGitIgnoreTemplates, "GitIgnore templates fetched is not as expected")
|
||||
}
|
||||
|
||||
func TestGitIgnoreTemplateReturnsGitIgnoreTemplate(t *testing.T) {
|
||||
gitIgnoreTemplateName := "Go"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("gitignore/templates/%v", gitIgnoreTemplateName)),
|
||||
httpmock.StringResponse(`{
|
||||
"name": "Go",
|
||||
"source": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with go test -c\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\ngo.work.sum\n\n# env file\n.env\n"
|
||||
}`,
|
||||
))
|
||||
}
|
||||
wantGitIgnoreTemplate := &GitIgnore{
|
||||
Name: "Go",
|
||||
Source: "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with go test -c\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\ngo.work.sum\n\n# env file\n.env\n",
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
gotGitIgnoreTemplate, err := RepoGitIgnoreTemplate(client, "api.github.com", gitIgnoreTemplateName)
|
||||
|
||||
assert.NoError(t, err, fmt.Sprintf("Expected no error while fetching /gitignore/templates/%v", gitIgnoreTemplateName))
|
||||
assert.Equal(t, wantGitIgnoreTemplate, gotGitIgnoreTemplate, fmt.Sprintf("GitIgnore template \"%v\" fetched is not as expected", gitIgnoreTemplateName))
|
||||
}
|
||||
|
||||
func TestGitIgnoreTemplateReturnsErrorWhenGitIgnoreTemplateNotFound(t *testing.T) {
|
||||
gitIgnoreTemplateName := "invalid-gitignore"
|
||||
httpStubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("gitignore/templates/%v", gitIgnoreTemplateName)),
|
||||
httpmock.StatusStringResponse(404, heredoc.Doc(`
|
||||
{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/v3/gitignore",
|
||||
"status": "404"
|
||||
}`)),
|
||||
)
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
client, _ := httpClient()
|
||||
defer reg.Verify(t)
|
||||
|
||||
_, err := RepoGitIgnoreTemplate(client, "api.github.com", gitIgnoreTemplateName)
|
||||
|
||||
assert.Error(t, err, fmt.Sprintf("Expected error while fetching /gitignore/templates/%v", gitIgnoreTemplateName))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ var RepositoryFields = []string{
|
|||
"createdAt",
|
||||
"pushedAt",
|
||||
"updatedAt",
|
||||
"archivedAt",
|
||||
|
||||
"isBlankIssuesEnabled",
|
||||
"isSecurityPolicyEnabled",
|
||||
|
|
|
|||
268
cmd/gh/main.go
268
cmd/gh/main.go
|
|
@ -1,276 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/config/migration"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var updaterEnabled = ""
|
||||
|
||||
type exitCode int
|
||||
|
||||
const (
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitPending exitCode = 8
|
||||
"github.com/cli/cli/v2/internal/ghcmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
code := mainRun()
|
||||
code := ghcmd.Main()
|
||||
os.Exit(int(code))
|
||||
}
|
||||
|
||||
func mainRun() exitCode {
|
||||
buildDate := build.Date
|
||||
buildVersion := build.Version
|
||||
hasDebug, _ := utils.IsDebugEnabled()
|
||||
|
||||
cmdFactory := factory.New(buildVersion)
|
||||
stderr := cmdFactory.IOStreams.ErrOut
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cfg, err := cmdFactory.Config(); err == nil {
|
||||
var m migration.MultiAccount
|
||||
if err := cfg.Migrate(m); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return exitError
|
||||
}
|
||||
}
|
||||
|
||||
updateCtx, updateCancel := context.WithCancel(ctx)
|
||||
defer updateCancel()
|
||||
updateMessageChan := make(chan *update.ReleaseInfo)
|
||||
go func() {
|
||||
rel, err := checkForUpdate(updateCtx, cmdFactory, buildVersion)
|
||||
if err != nil && hasDebug {
|
||||
fmt.Fprintf(stderr, "warning: checking for update failed: %v", err)
|
||||
}
|
||||
updateMessageChan <- rel
|
||||
}()
|
||||
|
||||
if !cmdFactory.IOStreams.ColorEnabled() {
|
||||
surveyCore.DisableColor = true
|
||||
ansi.DisableColors(true)
|
||||
} else {
|
||||
// override survey's poor choice of color
|
||||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
return ansi.ColorCode("default")
|
||||
default:
|
||||
return ansi.ColorCode(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable running gh from Windows File Explorer's address bar. Without this, the user is told to stop and run from a
|
||||
// terminal. With this, a user can clone a repo (or take other actions) directly from explorer.
|
||||
if len(os.Args) > 1 && os.Args[1] != "" {
|
||||
cobra.MousetrapHelpText = ""
|
||||
}
|
||||
|
||||
rootCmd, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
expandedArgs := []string{}
|
||||
if len(os.Args) > 0 {
|
||||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
||||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var extError *root.ExternalCommandExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if err == cmdutil.PendingError {
|
||||
return exitPending
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
// ensure the next shell prompt will start on its own line
|
||||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
return exitOK
|
||||
} else if errors.As(err, &noResultsError) {
|
||||
if cmdFactory.IOStreams.IsStdoutTTY() {
|
||||
fmt.Fprintln(stderr, noResultsError.Error())
|
||||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &extError) {
|
||||
// pass on exit codes from extensions and shell aliases
|
||||
return exitCode(extError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
||||
if strings.Contains(err.Error(), "Incorrect function") {
|
||||
fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.")
|
||||
fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty")
|
||||
return exitError
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
|
||||
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
|
||||
} else if u := factory.SSOURL(); u != "" {
|
||||
// handles organization SAML enforcement error
|
||||
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
|
||||
} else if msg := httpErr.ScopesSuggestion(); msg != "" {
|
||||
fmt.Fprintln(stderr, msg)
|
||||
}
|
||||
|
||||
return exitError
|
||||
}
|
||||
if root.HasFailed() {
|
||||
return exitError
|
||||
}
|
||||
|
||||
updateCancel() // if the update checker hasn't completed by now, abort it
|
||||
newRelease := <-updateMessageChan
|
||||
if newRelease != nil {
|
||||
isHomebrew := isUnderHomebrew(cmdFactory.Executable())
|
||||
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
|
||||
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
|
||||
return exitOK
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
ansi.Color("A new release of gh is available:", "yellow"),
|
||||
ansi.Color(strings.TrimPrefix(buildVersion, "v"), "cyan"),
|
||||
ansi.Color(strings.TrimPrefix(newRelease.Version, "v"), "cyan"))
|
||||
if isHomebrew {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew upgrade gh")
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
ansi.Color(newRelease.URL, "yellow"))
|
||||
}
|
||||
|
||||
return exitOK
|
||||
}
|
||||
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) {
|
||||
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
|
||||
if debug {
|
||||
fmt.Fprintln(out, dnsError)
|
||||
}
|
||||
fmt.Fprintln(out, "check your internet connection or https://githubstatus.com")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, err)
|
||||
|
||||
var flagError *cmdutil.FlagError
|
||||
if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") {
|
||||
if !strings.HasSuffix(err.Error(), "\n") {
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
fmt.Fprintln(out, cmd.UsageString())
|
||||
}
|
||||
}
|
||||
|
||||
func shouldCheckForUpdate() bool {
|
||||
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
||||
return false
|
||||
}
|
||||
if os.Getenv("CODESPACES") != "" {
|
||||
return false
|
||||
}
|
||||
return updaterEnabled != "" && !isCI() && isTerminal(os.Stdout) && isTerminal(os.Stderr)
|
||||
}
|
||||
|
||||
func isTerminal(f *os.File) bool {
|
||||
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
|
||||
}
|
||||
|
||||
// based on https://github.com/watson/ci-info/blob/HEAD/index.js
|
||||
func isCI() bool {
|
||||
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
|
||||
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
|
||||
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
|
||||
}
|
||||
|
||||
func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
|
||||
if !shouldCheckForUpdate() {
|
||||
return nil, nil
|
||||
}
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := updaterEnabled
|
||||
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
|
||||
return update.CheckForUpdate(ctx, httpClient, stateFilePath, repo, currentVersion)
|
||||
}
|
||||
|
||||
func isRecentRelease(publishedAt time.Time) bool {
|
||||
return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24
|
||||
}
|
||||
|
||||
// Check whether the gh binary was found under the Homebrew prefix
|
||||
func isUnderHomebrew(ghBinary string) bool {
|
||||
brewExe, err := safeexec.LookPath("brew")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
|
||||
return strings.HasPrefix(ghBinary, brewBinPrefix)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,14 @@ Install:
|
|||
|
||||
```bash
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& sudo apt update \
|
||||
&& sudo apt install gh -y
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& sudo apt update \
|
||||
&& sudo apt install gh -y
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> We were recently forced to change our GPG signing key. If you've previously downloaded the `githubcli-archive-keyring.gpg` file, you should re-download it again per above instructions. If you are using a keyserver to download the key, the ID of the new key is `23F3D4EA75716059`.
|
||||
|
||||
Upgrade:
|
||||
|
||||
```bash
|
||||
|
|
@ -33,16 +30,34 @@ sudo apt update
|
|||
sudo apt install gh
|
||||
```
|
||||
|
||||
### Fedora, CentOS, Red Hat Enterprise Linux (dnf)
|
||||
> [!NOTE]
|
||||
> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this.
|
||||
|
||||
### Fedora, CentOS, Red Hat Enterprise Linux (dnf5)
|
||||
|
||||
Install from our package repository for immediate access to latest releases:
|
||||
|
||||
```bash
|
||||
sudo dnf install 'dnf-command(config-manager)'
|
||||
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
||||
sudo dnf install dnf5-plugins
|
||||
sudo dnf config-manager addrepo --from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo
|
||||
sudo dnf install gh --repo gh-cli
|
||||
```
|
||||
|
||||
These commands apply for `dnf5`. If you're using `dnf4`, commands will vary slightly.
|
||||
|
||||
<details>
|
||||
<summary>Show dnf4 commands</summary>
|
||||
|
||||
```bash
|
||||
sudo dnf4 install 'dnf-command(config-manager)'
|
||||
sudo dnf4 config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
||||
sudo dnf4 install gh --repo gh-cli
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!NOTE]
|
||||
> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this.
|
||||
|
||||
Alternatively, install from the [community repository](https://packages.fedoraproject.org/pkgs/gh/gh/):
|
||||
|
||||
```bash
|
||||
|
|
@ -65,15 +80,15 @@ sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.re
|
|||
sudo yum install gh
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> We were recently forced to change our GPG signing key. If you've added the repository previously and now you're getting a GPG signing key error, disable the repository first with `sudo yum-config-manager --disable gh-cli` and add it again with `sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo`.
|
||||
|
||||
Upgrade:
|
||||
|
||||
```bash
|
||||
sudo yum update gh
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this.
|
||||
|
||||
### openSUSE/SUSE Linux (zypper)
|
||||
|
||||
Install:
|
||||
|
|
@ -91,6 +106,9 @@ sudo zypper ref
|
|||
sudo zypper update gh
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this.
|
||||
|
||||
## Manual installation
|
||||
|
||||
* [Download release binaries][releases page] that match your platform; or
|
||||
|
|
|
|||
|
|
@ -33,27 +33,32 @@ the changeset is feasible and to allow the associated CI run for new contributor
|
|||
|
||||
- can this be closed outright?
|
||||
- e.g. spam/junk
|
||||
- add the `invalid` label
|
||||
- close without comment
|
||||
- do we not want to do it?
|
||||
- e.g. have already discussed not wanting to do or duplicate issue
|
||||
- e.g. we have already discussed not wanting to do or it's a duplicate issue
|
||||
- add the appropriate label (e.g. `duplicate`)
|
||||
- comment and close
|
||||
- are we ok with outside contribution for this?
|
||||
- e.g. the task is relatively straightforward, but no people on our team have the bandwidth to take it on at the moment
|
||||
- do we want external contribution for this?
|
||||
- e.g. the task is relatively straightforward, but the core team does not have the bandwidth to take it on
|
||||
- ensure that the thread contains all the context necessary for someone new to pick this up
|
||||
- add `help wanted` label
|
||||
- add the `help wanted` label
|
||||
- consider adding `good first issue` label
|
||||
- do we want external design contribution for this?
|
||||
- e.g. the task is worthwhile, but needs design work to flesh out the details before implementation and the core team does not have the bandwidth to take it on
|
||||
- ensure that the thread contains all the context necessary for someone new to pick this up
|
||||
- add both the `help wanted` and `needs-design` labels
|
||||
- do we want to do it?
|
||||
- add the `core` label
|
||||
- comment acknowledging that
|
||||
- add `core` label
|
||||
- add to the project “TODO” column if this is something that should ship soon
|
||||
- is it intriguing, but requires discussion?
|
||||
- label `discuss`
|
||||
- label `needs-investigation` if engineering research is required before action can be taken
|
||||
- Add the `discuss` label
|
||||
- Add the `needs-investigation` label if engineering research is required before action can be taken
|
||||
- does it need more info from the issue author?
|
||||
- ask the user for details
|
||||
- add `needs-user-input` label
|
||||
- add the `needs-user-input` label
|
||||
- is it a usage/support question?
|
||||
- consider converting the Issue to a Discussion
|
||||
- Convert the Issue to a Discussion
|
||||
|
||||
## Weekly PR audit
|
||||
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ Describe how your new command would be used. Include mock-up examples, including
|
|||
|
||||
We take this step seriously because we believe in keeping `gh`'s interface consistent and intuitive.
|
||||
|
||||
## Step 2: Beta
|
||||
## Step 2: Public Preview
|
||||
|
||||
Once we've signed off on the proposed UX on the issue opened in step 1, develop your extension to at least beta quality. It's up to you if you actually want to go through a beta release phase with real users or not.
|
||||
Once we've signed off on the proposed UX on the issue opened in step 1, develop your extension to at least public preview quality. It's up to you if you actually want to go through a public preview release phase with real users or not.
|
||||
|
||||
## Step 3: Merge or no merge
|
||||
|
||||
With a beta in hand it's time to decide whether or not to mainline your extension into the `trunk` of `gh`. Some questions to consider:
|
||||
With a public preview in hand it's time to decide whether or not to mainline your extension into the `trunk` of `gh`. Some questions to consider:
|
||||
|
||||
- How complex is the support burden for your feature?
|
||||
|
||||
|
|
|
|||
39
go.mod
39
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module github.com/cli/cli/v2
|
||||
|
||||
go 1.22.0
|
||||
go 1.22.5
|
||||
|
||||
toolchain go1.22.5
|
||||
toolchain go1.22.6
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
|
|
@ -11,13 +11,14 @@ require (
|
|||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c
|
||||
github.com/cli/go-gh/v2 v2.9.0
|
||||
github.com/cli/oauth v1.0.1
|
||||
github.com/cli/go-gh/v2 v2.11.0
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
|
||||
github.com/cli/oauth v1.1.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.5
|
||||
github.com/gabriel-vasile/mimetype v1.4.6
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.20.2
|
||||
|
|
@ -25,8 +26,8 @@ require (
|
|||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/henvic/httpretty v0.1.3
|
||||
github.com/in-toto/in-toto-golang v0.9.0
|
||||
github.com/henvic/httpretty v0.1.4
|
||||
github.com/in-toto/attestation v1.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
|
|
@ -38,15 +39,15 @@ require (
|
|||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.3.2
|
||||
github.com/sigstore/sigstore-go v0.5.1
|
||||
github.com/sigstore/sigstore-go v0.6.2
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/term v0.25.0
|
||||
golang.org/x/text v0.19.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
|
|
@ -99,6 +100,7 @@ require (
|
|||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/itchyny/gojq v0.12.15 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
|
|
@ -132,7 +134,7 @@ require (
|
|||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/sigstore/rekor v1.3.6 // indirect
|
||||
github.com/sigstore/sigstore v1.8.7 // indirect
|
||||
github.com/sigstore/sigstore v1.8.9 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
|
|
@ -142,7 +144,7 @@ require (
|
|||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.7.0 // indirect
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.0 // indirect
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
|
|
@ -156,9 +158,10 @@ require (
|
|||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
|
|||
71
go.sum
71
go.sum
|
|
@ -95,10 +95,12 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1
|
|||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI=
|
||||
github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE=
|
||||
github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA=
|
||||
github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
|
||||
github.com/cli/go-gh/v2 v2.11.0 h1:TERLYMMWderKBO3lBff/JIu2+eSly2oFRgN2WvO+3eA=
|
||||
github.com/cli/go-gh/v2 v2.11.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE=
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E=
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk=
|
||||
github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs=
|
||||
github.com/cli/oauth v1.1.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
|
||||
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
|
|
@ -109,8 +111,9 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AX
|
|||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
|
|
@ -146,8 +149,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
|
|
@ -251,14 +254,16 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE=
|
||||
github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE=
|
||||
github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8=
|
||||
github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg=
|
||||
github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=
|
||||
github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
|
||||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q=
|
||||
github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs=
|
||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
|
@ -391,10 +396,10 @@ github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWw
|
|||
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
|
||||
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||
github.com/sigstore/sigstore v1.8.7 h1:L7/zKauHTg0d0Hukx7qlR4nifh6T6O6UIt9JBwAmTIg=
|
||||
github.com/sigstore/sigstore v1.8.7/go.mod h1:MPiQ/NIV034Fc3Kk2IX9/XmBQdK60wfmpvgK9Z1UjRA=
|
||||
github.com/sigstore/sigstore-go v0.5.1 h1:5IhKvtjlQBeLnjKkzMELNG4tIBf+xXQkDzhLV77+/8Y=
|
||||
github.com/sigstore/sigstore-go v0.5.1/go.mod h1:TuOfV7THHqiDaUHuJ5+QN23RP/YoKmsbwJpY+aaYPN0=
|
||||
github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk=
|
||||
github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w=
|
||||
github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A=
|
||||
github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g=
|
||||
|
|
@ -438,8 +443,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
|
||||
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.0 h1:rD8d9RotYBprZVgC+9oyTZ5MmawepnTSTqoDuxjWgbs=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.0/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.1 h1:11p9tXpq10KQEujxjcIjDSivMKCMLguls7erXHZnxJQ=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.1/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||
|
|
@ -483,24 +488,24 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -512,26 +517,26 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
|
||||
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import (
|
|||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/oauth"
|
||||
"github.com/henvic/httpretty"
|
||||
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -41,18 +43,16 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
|
|||
minimumScopes := []string{"repo", "read:org", "gist"}
|
||||
scopes := append(minimumScopes, additionalScopes...)
|
||||
|
||||
callbackURI := "http://127.0.0.1/callback"
|
||||
if ghinstance.IsEnterprise(oauthHost) {
|
||||
// the OAuth app on Enterprise hosts is still registered with a legacy callback URL
|
||||
// see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650
|
||||
callbackURI = "http://localhost/"
|
||||
host, err := oauth.NewGitHubHost(ghinstance.HostPrefix(oauthHost))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
flow := &oauth.Flow{
|
||||
Host: oauth.GitHubHost(ghinstance.HostPrefix(oauthHost)),
|
||||
Host: host,
|
||||
ClientID: oauthClientID,
|
||||
ClientSecret: oauthClientSecret,
|
||||
CallbackURI: callbackURI,
|
||||
CallbackURI: getCallbackURI(oauthHost),
|
||||
Scopes: scopes,
|
||||
DisplayCode: func(code, verificationURL string) error {
|
||||
fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code))
|
||||
|
|
@ -72,7 +72,7 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
|
|||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost)
|
||||
fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), authURL)
|
||||
_ = waitForEnter(IO.In)
|
||||
|
||||
if err := b.Browse(authURL); err != nil {
|
||||
|
|
@ -105,6 +105,16 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
|
|||
return token.Token, userLogin, nil
|
||||
}
|
||||
|
||||
func getCallbackURI(oauthHost string) string {
|
||||
callbackURI := "http://127.0.0.1/callback"
|
||||
if ghauth.IsEnterprise(oauthHost) {
|
||||
// the OAuth app on Enterprise hosts is still registered with a legacy callback URL
|
||||
// see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650
|
||||
callbackURI = "http://localhost/"
|
||||
}
|
||||
return callbackURI
|
||||
}
|
||||
|
||||
type cfg struct {
|
||||
token string
|
||||
}
|
||||
|
|
|
|||
36
internal/authflow/flow_test.go
Normal file
36
internal/authflow/flow_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package authflow
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getCallbackURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
oauthHost string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "dotcom",
|
||||
oauthHost: "github.com",
|
||||
want: "http://127.0.0.1/callback",
|
||||
},
|
||||
{
|
||||
name: "ghes",
|
||||
oauthHost: "my.server.com",
|
||||
want: "http://localhost/",
|
||||
},
|
||||
{
|
||||
name: "ghec data residency (ghe.com)",
|
||||
oauthHost: "stampname.ghe.com",
|
||||
want: "http://127.0.0.1/callback",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, getCallbackURI(tt.oauthHost))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,32 @@ func TestTokenFromKeyringForUserErrorsIfUsernameIsBlank(t *testing.T) {
|
|||
require.ErrorContains(t, err, "username cannot be blank")
|
||||
}
|
||||
|
||||
func TestHasActiveToken(t *testing.T) {
|
||||
// Given the user has logged in for a host
|
||||
authCfg := newTestAuthConfig(t)
|
||||
_, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When we check if that host has an active token
|
||||
hasActiveToken := authCfg.HasActiveToken("github.com")
|
||||
|
||||
// Then there is an active token
|
||||
require.True(t, hasActiveToken, "expected there to be an active token")
|
||||
}
|
||||
|
||||
func TestHasNoActiveToken(t *testing.T) {
|
||||
// Given there are no users logged in for a host
|
||||
authCfg := newTestAuthConfig(t)
|
||||
|
||||
// When we check if any host has an active token
|
||||
hasActiveToken := authCfg.HasActiveToken("github.com")
|
||||
|
||||
// Then there is no active token
|
||||
require.False(t, hasActiveToken, "expected there to be no active token")
|
||||
}
|
||||
|
||||
func TestTokenStoredInConfig(t *testing.T) {
|
||||
// When the user has logged in insecurely
|
||||
// Given the user has logged in insecurely
|
||||
authCfg := newTestAuthConfig(t)
|
||||
_, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/keyring"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
ghConfig "github.com/cli/go-gh/v2/pkg/config"
|
||||
)
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
|
|||
if c.tokenOverride != nil {
|
||||
return c.tokenOverride(hostname)
|
||||
}
|
||||
token, source := ghAuth.TokenFromEnvOrConfig(hostname)
|
||||
token, source := ghauth.TokenFromEnvOrConfig(hostname)
|
||||
if token == "" {
|
||||
var err error
|
||||
token, err = c.TokenFromKeyring(hostname)
|
||||
|
|
@ -217,6 +217,12 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
|
|||
return token, source
|
||||
}
|
||||
|
||||
// HasActiveToken returns true when a token for the hostname is present.
|
||||
func (c *AuthConfig) HasActiveToken(hostname string) bool {
|
||||
token, _ := c.ActiveToken(hostname)
|
||||
return token != ""
|
||||
}
|
||||
|
||||
// HasEnvToken returns true when a token has been specified in an
|
||||
// environment variable, else returns false.
|
||||
func (c *AuthConfig) HasEnvToken() bool {
|
||||
|
|
@ -234,7 +240,7 @@ func (c *AuthConfig) HasEnvToken() bool {
|
|||
// It has to use a hostname that is not going to be found in the hosts so that it
|
||||
// can guarantee that tokens will only be returned from a set env var.
|
||||
// Discussed here, but maybe worth revisiting: https://github.com/cli/cli/pull/7169#discussion_r1136979033
|
||||
token, _ := ghAuth.TokenFromEnvOrConfig(hostname)
|
||||
token, _ := ghauth.TokenFromEnvOrConfig(hostname)
|
||||
return token != ""
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +282,7 @@ func (c *AuthConfig) Hosts() []string {
|
|||
if c.hostsOverride != nil {
|
||||
return c.hostsOverride()
|
||||
}
|
||||
return ghAuth.KnownHosts()
|
||||
return ghauth.KnownHosts()
|
||||
}
|
||||
|
||||
// SetHosts will override any hosts resolution and return the given
|
||||
|
|
@ -291,7 +297,7 @@ func (c *AuthConfig) DefaultHost() (string, string) {
|
|||
if c.defaultHostOverride != nil {
|
||||
return c.defaultHostOverride()
|
||||
}
|
||||
return ghAuth.DefaultHost()
|
||||
return ghauth.DefaultHost()
|
||||
}
|
||||
|
||||
// SetDefaultHost will override any host resolution and return the given
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ type flagView struct {
|
|||
var flagsTemplate = `
|
||||
<dl class="flags">{{ range . }}
|
||||
<dt>{{ if .Shorthand }}<code>-{{.Shorthand}}</code>, {{ end }}
|
||||
<code>--{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}{{.DefValue}} </code></dt>
|
||||
<code>--{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}{{.DefValue}}</code></dt>
|
||||
<dd>{{.Usage}}</dd>
|
||||
{{ end }}</dl>
|
||||
`
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
type Detector interface {
|
||||
|
|
@ -62,7 +63,7 @@ func NewDetector(httpClient *http.Client, host string) Detector {
|
|||
}
|
||||
|
||||
func (d *detector) IssueFeatures() (IssueFeatures, error) {
|
||||
if !ghinstance.IsEnterprise(d.host) {
|
||||
if !ghauth.IsEnterprise(d.host) {
|
||||
return allIssueFeatures, nil
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
|
|||
}
|
||||
|
||||
func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
|
||||
if !ghinstance.IsEnterprise(d.host) {
|
||||
if !ghauth.IsEnterprise(d.host) {
|
||||
return allRepositoryFeatures, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ func TestIssueFeatures(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE empty response",
|
||||
hostname: "git.my.org",
|
||||
|
|
@ -271,6 +279,16 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: RepositoryFeatures{
|
||||
PullRequestTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
AutoMerge: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE empty response",
|
||||
hostname: "git.my.org",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ type Migration interface {
|
|||
// with knowledge on how to access encrypted storage when neccesarry.
|
||||
// Behavior is scoped to authentication specific tasks.
|
||||
type AuthConfig interface {
|
||||
// HasActiveToken returns true when a token for the hostname is present.
|
||||
HasActiveToken(hostname string) bool
|
||||
|
||||
// ActiveToken will retrieve the active auth token for the given hostname, searching environment variables,
|
||||
// general configuration, and finally encrypted storage.
|
||||
ActiveToken(hostname string) (token string, source string)
|
||||
|
|
|
|||
271
internal/ghcmd/cmd.go
Normal file
271
internal/ghcmd/cmd.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package ghcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/config/migration"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var updaterEnabled = ""
|
||||
|
||||
type exitCode int
|
||||
|
||||
const (
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitPending exitCode = 8
|
||||
)
|
||||
|
||||
func Main() exitCode {
|
||||
buildDate := build.Date
|
||||
buildVersion := build.Version
|
||||
hasDebug, _ := utils.IsDebugEnabled()
|
||||
|
||||
cmdFactory := factory.New(buildVersion)
|
||||
stderr := cmdFactory.IOStreams.ErrOut
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cfg, err := cmdFactory.Config(); err == nil {
|
||||
var m migration.MultiAccount
|
||||
if err := cfg.Migrate(m); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return exitError
|
||||
}
|
||||
}
|
||||
|
||||
updateCtx, updateCancel := context.WithCancel(ctx)
|
||||
defer updateCancel()
|
||||
updateMessageChan := make(chan *update.ReleaseInfo)
|
||||
go func() {
|
||||
rel, err := checkForUpdate(updateCtx, cmdFactory, buildVersion)
|
||||
if err != nil && hasDebug {
|
||||
fmt.Fprintf(stderr, "warning: checking for update failed: %v", err)
|
||||
}
|
||||
updateMessageChan <- rel
|
||||
}()
|
||||
|
||||
if !cmdFactory.IOStreams.ColorEnabled() {
|
||||
surveyCore.DisableColor = true
|
||||
ansi.DisableColors(true)
|
||||
} else {
|
||||
// override survey's poor choice of color
|
||||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
return ansi.ColorCode("default")
|
||||
default:
|
||||
return ansi.ColorCode(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable running gh from Windows File Explorer's address bar. Without this, the user is told to stop and run from a
|
||||
// terminal. With this, a user can clone a repo (or take other actions) directly from explorer.
|
||||
if len(os.Args) > 1 && os.Args[1] != "" {
|
||||
cobra.MousetrapHelpText = ""
|
||||
}
|
||||
|
||||
rootCmd, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
expandedArgs := []string{}
|
||||
if len(os.Args) > 0 {
|
||||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
||||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var extError *root.ExternalCommandExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if err == cmdutil.PendingError {
|
||||
return exitPending
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
// ensure the next shell prompt will start on its own line
|
||||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
return exitOK
|
||||
} else if errors.As(err, &noResultsError) {
|
||||
if cmdFactory.IOStreams.IsStdoutTTY() {
|
||||
fmt.Fprintln(stderr, noResultsError.Error())
|
||||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &extError) {
|
||||
// pass on exit codes from extensions and shell aliases
|
||||
return exitCode(extError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
||||
if strings.Contains(err.Error(), "Incorrect function") {
|
||||
fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.")
|
||||
fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty")
|
||||
return exitError
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
|
||||
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
|
||||
} else if u := factory.SSOURL(); u != "" {
|
||||
// handles organization SAML enforcement error
|
||||
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
|
||||
} else if msg := httpErr.ScopesSuggestion(); msg != "" {
|
||||
fmt.Fprintln(stderr, msg)
|
||||
}
|
||||
|
||||
return exitError
|
||||
}
|
||||
if root.HasFailed() {
|
||||
return exitError
|
||||
}
|
||||
|
||||
updateCancel() // if the update checker hasn't completed by now, abort it
|
||||
newRelease := <-updateMessageChan
|
||||
if newRelease != nil {
|
||||
isHomebrew := isUnderHomebrew(cmdFactory.Executable())
|
||||
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
|
||||
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
|
||||
return exitOK
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
ansi.Color("A new release of gh is available:", "yellow"),
|
||||
ansi.Color(strings.TrimPrefix(buildVersion, "v"), "cyan"),
|
||||
ansi.Color(strings.TrimPrefix(newRelease.Version, "v"), "cyan"))
|
||||
if isHomebrew {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew upgrade gh")
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
ansi.Color(newRelease.URL, "yellow"))
|
||||
}
|
||||
|
||||
return exitOK
|
||||
}
|
||||
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) {
|
||||
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
|
||||
if debug {
|
||||
fmt.Fprintln(out, dnsError)
|
||||
}
|
||||
fmt.Fprintln(out, "check your internet connection or https://githubstatus.com")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, err)
|
||||
|
||||
var flagError *cmdutil.FlagError
|
||||
if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") {
|
||||
if !strings.HasSuffix(err.Error(), "\n") {
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
fmt.Fprintln(out, cmd.UsageString())
|
||||
}
|
||||
}
|
||||
|
||||
func shouldCheckForUpdate() bool {
|
||||
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
||||
return false
|
||||
}
|
||||
if os.Getenv("CODESPACES") != "" {
|
||||
return false
|
||||
}
|
||||
return updaterEnabled != "" && !isCI() && isTerminal(os.Stdout) && isTerminal(os.Stderr)
|
||||
}
|
||||
|
||||
func isTerminal(f *os.File) bool {
|
||||
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
|
||||
}
|
||||
|
||||
// based on https://github.com/watson/ci-info/blob/HEAD/index.js
|
||||
func isCI() bool {
|
||||
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
|
||||
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
|
||||
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
|
||||
}
|
||||
|
||||
func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
|
||||
if !shouldCheckForUpdate() {
|
||||
return nil, nil
|
||||
}
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := updaterEnabled
|
||||
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
|
||||
return update.CheckForUpdate(ctx, httpClient, stateFilePath, repo, currentVersion)
|
||||
}
|
||||
|
||||
func isRecentRelease(publishedAt time.Time) bool {
|
||||
return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24
|
||||
}
|
||||
|
||||
// Check whether the gh binary was found under the Homebrew prefix
|
||||
func isUnderHomebrew(ghBinary string) bool {
|
||||
brewExe, err := safeexec.LookPath("brew")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
|
||||
return strings.HasPrefix(ghBinary, brewBinPrefix)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package ghcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
// DefaultHostname is the domain name of the default GitHub instance.
|
||||
|
|
@ -20,22 +22,10 @@ func Default() string {
|
|||
return defaultHostname
|
||||
}
|
||||
|
||||
// IsEnterprise reports whether a non-normalized host name looks like a GHE instance.
|
||||
func IsEnterprise(h string) bool {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
return normalizedHostName != defaultHostname && normalizedHostName != localhost
|
||||
}
|
||||
|
||||
// IsTenancy reports whether a non-normalized host name looks like a tenancy instance.
|
||||
func IsTenancy(h string) bool {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
return strings.HasSuffix(normalizedHostName, "."+tenancyHost)
|
||||
}
|
||||
|
||||
// TenantName extracts the tenant name from tenancy host name and
|
||||
// reports whether it found the tenant name.
|
||||
func TenantName(h string) (string, bool) {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
normalizedHostName := ghauth.NormalizeHostname(h)
|
||||
return cutSuffix(normalizedHostName, "."+tenancyHost)
|
||||
}
|
||||
|
||||
|
|
@ -43,22 +33,6 @@ func isGarage(h string) bool {
|
|||
return strings.EqualFold(h, "garage.github.com")
|
||||
}
|
||||
|
||||
// NormalizeHostname returns the canonical host name of a GitHub instance.
|
||||
func NormalizeHostname(h string) string {
|
||||
hostname := strings.ToLower(h)
|
||||
if strings.HasSuffix(hostname, "."+defaultHostname) {
|
||||
return defaultHostname
|
||||
}
|
||||
if strings.HasSuffix(hostname, "."+localhost) {
|
||||
return localhost
|
||||
}
|
||||
if before, found := cutSuffix(hostname, "."+tenancyHost); found {
|
||||
idx := strings.LastIndex(before, ".")
|
||||
return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
func HostnameValidator(hostname string) error {
|
||||
if len(strings.TrimSpace(hostname)) < 1 {
|
||||
return errors.New("a value is required")
|
||||
|
|
@ -73,7 +47,7 @@ func GraphQLEndpoint(hostname string) string {
|
|||
if isGarage(hostname) {
|
||||
return fmt.Sprintf("https://%s/api/graphql", hostname)
|
||||
}
|
||||
if IsEnterprise(hostname) {
|
||||
if ghauth.IsEnterprise(hostname) {
|
||||
return fmt.Sprintf("https://%s/api/graphql", hostname)
|
||||
}
|
||||
if strings.EqualFold(hostname, localhost) {
|
||||
|
|
@ -86,7 +60,7 @@ func RESTPrefix(hostname string) string {
|
|||
if isGarage(hostname) {
|
||||
return fmt.Sprintf("https://%s/api/v3/", hostname)
|
||||
}
|
||||
if IsEnterprise(hostname) {
|
||||
if ghauth.IsEnterprise(hostname) {
|
||||
return fmt.Sprintf("https://%s/api/v3/", hostname)
|
||||
}
|
||||
if strings.EqualFold(hostname, localhost) {
|
||||
|
|
@ -107,7 +81,7 @@ func GistHost(hostname string) string {
|
|||
if isGarage(hostname) {
|
||||
return fmt.Sprintf("%s/gist/", hostname)
|
||||
}
|
||||
if IsEnterprise(hostname) {
|
||||
if ghauth.IsEnterprise(hostname) {
|
||||
return fmt.Sprintf("%s/gist/", hostname)
|
||||
}
|
||||
if strings.EqualFold(hostname, localhost) {
|
||||
|
|
|
|||
|
|
@ -6,88 +6,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsEnterprise(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
host: "github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "api.github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "github.localhost",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "api.github.localhost",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "garage.github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "ghe.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
host: "example.com",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
if got := IsEnterprise(tt.host); got != tt.want {
|
||||
t.Errorf("IsEnterprise() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTenancy(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
host: "github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "github.localhost",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "garage.github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "ghe.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
host: "api.tenant.ghe.com",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
if got := IsTenancy(tt.host); got != tt.want {
|
||||
t.Errorf("IsTenancy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantName(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
|
|
@ -130,69 +48,6 @@ func TestTenantName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeHostname(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
host: "GitHub.com",
|
||||
want: "github.com",
|
||||
},
|
||||
{
|
||||
host: "api.github.com",
|
||||
want: "github.com",
|
||||
},
|
||||
{
|
||||
host: "ssh.github.com",
|
||||
want: "github.com",
|
||||
},
|
||||
{
|
||||
host: "upload.github.com",
|
||||
want: "github.com",
|
||||
},
|
||||
{
|
||||
host: "GitHub.localhost",
|
||||
want: "github.localhost",
|
||||
},
|
||||
{
|
||||
host: "api.github.localhost",
|
||||
want: "github.localhost",
|
||||
},
|
||||
{
|
||||
host: "garage.github.com",
|
||||
want: "github.com",
|
||||
},
|
||||
{
|
||||
host: "GHE.IO",
|
||||
want: "ghe.io",
|
||||
},
|
||||
{
|
||||
host: "git.my.org",
|
||||
want: "git.my.org",
|
||||
},
|
||||
{
|
||||
host: "ghe.com",
|
||||
want: "ghe.com",
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: "tenant.ghe.com",
|
||||
},
|
||||
{
|
||||
host: "api.tenant.ghe.com",
|
||||
want: "tenant.ghe.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
if got := NormalizeHostname(tt.host); got != tt.want {
|
||||
t.Errorf("NormalizeHostname() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostnameValidator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -254,6 +109,10 @@ func TestGraphQLEndpoint(t *testing.T) {
|
|||
host: "ghe.io",
|
||||
want: "https://ghe.io/api/graphql",
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: "https://api.tenant.ghe.com/graphql",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
|
|
@ -285,6 +144,10 @@ func TestRESTPrefix(t *testing.T) {
|
|||
host: "ghe.io",
|
||||
want: "https://ghe.io/api/v3/",
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: "https://api.tenant.ghe.com/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/cli/go-gh/v2/pkg/repository"
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ func FullName(r Interface) string {
|
|||
}
|
||||
|
||||
func defaultHost() string {
|
||||
host, _ := ghAuth.DefaultHost()
|
||||
host, _ := ghauth.DefaultHost()
|
||||
return host
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func (p *surveyPrompter) InputHostname() (string, error) {
|
|||
var result string
|
||||
err := p.ask(
|
||||
&survey.Input{
|
||||
Message: "GHE hostname:",
|
||||
Message: "Hostname:",
|
||||
}, &result, survey.WithValidator(func(v interface{}) error {
|
||||
return ghinstance.HostnameValidator(v.(string))
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -65,14 +65,19 @@ func FuzzyAgoAbbr(a, b time.Time) string {
|
|||
return b.Format("Jan _2, 2006")
|
||||
}
|
||||
|
||||
// DisplayURL returns a copy of the string urlStr removing everything except the hostname and path.
|
||||
// DisplayURL returns a copy of the string urlStr removing everything except the scheme, hostname, and path.
|
||||
// If the scheme is not specified, "https" is assumed.
|
||||
// If there is an error parsing urlStr then urlStr is returned without modification.
|
||||
func DisplayURL(urlStr string) string {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return urlStr
|
||||
}
|
||||
return u.Hostname() + u.Path
|
||||
scheme := u.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme + "://" + u.Hostname() + u.Path
|
||||
}
|
||||
|
||||
// RemoveDiacritics returns the input value without "diacritics", or accent marks
|
||||
|
|
|
|||
|
|
@ -146,3 +146,38 @@ func TestFormatSlice(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
url: "https://github.com/cli/cli/issues/9470",
|
||||
want: "https://github.com/cli/cli/issues/9470",
|
||||
},
|
||||
{
|
||||
name: "without scheme",
|
||||
url: "github.com/cli/cli/issues/9470",
|
||||
want: "https://github.com/cli/cli/issues/9470",
|
||||
},
|
||||
{
|
||||
name: "with query param and anchor",
|
||||
url: "https://github.com/cli/cli/issues/9470?q=is:issue#issue-command",
|
||||
want: "https://github.com/cli/cli/issues/9470",
|
||||
},
|
||||
{
|
||||
name: "preserve http protocol use despite insecure",
|
||||
url: "http://github.com/cli/cli/issues/9470",
|
||||
want: "http://github.com/cli/cli/issues/9470",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, DisplayURL(tt.url))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func newErrNoAttestations(name, digest string) ErrNoAttestations {
|
|||
}
|
||||
|
||||
type Attestation struct {
|
||||
Bundle *bundle.ProtobufBundle `json:"bundle"`
|
||||
Bundle *bundle.Bundle `json:"bundle"`
|
||||
}
|
||||
|
||||
type AttestationsResponse struct {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cli/cli/v2/api"
|
||||
ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -18,12 +20,14 @@ const (
|
|||
)
|
||||
|
||||
type apiClient interface {
|
||||
REST(hostname, method, p string, body io.Reader, data interface{}) error
|
||||
RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error)
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error)
|
||||
GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error)
|
||||
GetTrustDomain() (string, error)
|
||||
}
|
||||
|
||||
type LiveClient struct {
|
||||
|
|
@ -32,9 +36,7 @@ type LiveClient struct {
|
|||
logger *ioconfig.Handler
|
||||
}
|
||||
|
||||
func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient {
|
||||
host, _ := auth.DefaultHost()
|
||||
|
||||
func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClient {
|
||||
return &LiveClient{
|
||||
api: api.NewClientFromHTTP(hc),
|
||||
host: strings.TrimSuffix(host, "/"),
|
||||
|
|
@ -64,6 +66,15 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At
|
|||
return c.getAttestations(url, owner, digest, limit)
|
||||
}
|
||||
|
||||
// GetTrustDomain returns the current trust domain. If the default is used
|
||||
// the empty string is returned
|
||||
func (c *LiveClient) GetTrustDomain() (string, error) {
|
||||
return c.getTrustDomain(MetaPath)
|
||||
}
|
||||
|
||||
// Allow injecting backoff interval in tests.
|
||||
var getAttestationRetryInterval = time.Millisecond * 200
|
||||
|
||||
func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
|
||||
|
||||
|
|
@ -81,15 +92,31 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At
|
|||
|
||||
var attestations []*Attestation
|
||||
var resp AttestationsResponse
|
||||
var err error
|
||||
bo := backoff.NewConstantBackOff(getAttestationRetryInterval)
|
||||
|
||||
// if no attestation or less than limit, then keep fetching
|
||||
for url != "" && len(attestations) < limit {
|
||||
url, err = c.api.RESTWithNext(c.host, http.MethodGet, url, nil, &resp)
|
||||
err := backoff.Retry(func() error {
|
||||
newURL, restErr := c.api.RESTWithNext(c.host, http.MethodGet, url, nil, &resp)
|
||||
|
||||
if restErr != nil {
|
||||
if shouldRetry(restErr) {
|
||||
return restErr
|
||||
} else {
|
||||
return backoff.Permanent(restErr)
|
||||
}
|
||||
}
|
||||
|
||||
url = newURL
|
||||
attestations = append(attestations, resp.Attestations...)
|
||||
|
||||
return nil
|
||||
}, backoff.WithMaxRetries(bo, 3))
|
||||
|
||||
// bail if RESTWithNext errored out
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attestations = append(attestations, resp.Attestations...)
|
||||
}
|
||||
|
||||
if len(attestations) == 0 {
|
||||
|
|
@ -102,3 +129,38 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At
|
|||
|
||||
return attestations, nil
|
||||
}
|
||||
|
||||
func shouldRetry(err error) bool {
|
||||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) {
|
||||
if httpError.StatusCode >= 500 && httpError.StatusCode <= 599 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *LiveClient) getTrustDomain(url string) (string, error) {
|
||||
var resp MetaResponse
|
||||
|
||||
bo := backoff.NewConstantBackOff(getAttestationRetryInterval)
|
||||
err := backoff.Retry(func() error {
|
||||
restErr := c.api.REST(c.host, http.MethodGet, url, nil, &resp)
|
||||
if restErr != nil {
|
||||
if shouldRetry(restErr) {
|
||||
return restErr
|
||||
} else {
|
||||
return backoff.Permanent(restErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, backoff.WithMaxRetries(bo, 3))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Domains.ArtifactAttestations.TrustDomain, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,3 +172,94 @@ func TestGetByDigest_Error(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
||||
func TestGetTrustDomain(t *testing.T) {
|
||||
fetcher := mockMetaGenerator{
|
||||
TrustDomain: "foo",
|
||||
}
|
||||
|
||||
t.Run("with returned trust domain", func(t *testing.T) {
|
||||
c := LiveClient{
|
||||
api: mockAPIClient{
|
||||
OnREST: fetcher.OnREST,
|
||||
},
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
td, err := c.GetTrustDomain()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "foo", td)
|
||||
|
||||
})
|
||||
|
||||
t.Run("with error", func(t *testing.T) {
|
||||
c := LiveClient{
|
||||
api: mockAPIClient{
|
||||
OnREST: fetcher.OnRESTError,
|
||||
},
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
td, err := c.GetTrustDomain()
|
||||
require.Equal(t, "", td)
|
||||
require.ErrorContains(t, err, "test error")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestGetAttestationsRetries(t *testing.T) {
|
||||
getAttestationRetryInterval = 0
|
||||
|
||||
fetcher := mockDataGenerator{
|
||||
NumAttestations: 5,
|
||||
}
|
||||
|
||||
c := &LiveClient{
|
||||
api: mockAPIClient{
|
||||
OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(),
|
||||
},
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
// assert the error path was executed; because this is a paged
|
||||
// request, it should have errored twice
|
||||
fetcher.AssertNumberOfCalls(t, "FlakyOnRESTSuccessWithNextPage:error", 2)
|
||||
|
||||
// but we still successfully got the right data
|
||||
require.Equal(t, len(attestations), 10)
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
|
||||
// same test as above, but for GetByOwnerAndDigest:
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
// because we haven't reset the mock, we have added 2 more failed requests
|
||||
fetcher.AssertNumberOfCalls(t, "FlakyOnRESTSuccessWithNextPage:error", 4)
|
||||
|
||||
require.Equal(t, len(attestations), 10)
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
}
|
||||
|
||||
// test total retries
|
||||
func TestGetAttestationsMaxRetries(t *testing.T) {
|
||||
getAttestationRetryInterval = 0
|
||||
|
||||
fetcher := mockDataGenerator{
|
||||
NumAttestations: 5,
|
||||
}
|
||||
|
||||
c := &LiveClient{
|
||||
api: mockAPIClient{
|
||||
OnRESTWithNext: fetcher.OnREST500ErrorHandler(),
|
||||
},
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
_, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.Error(t, err)
|
||||
|
||||
fetcher.AssertNumberOfCalls(t, "OnREST500Error", 4)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,25 +6,35 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
cliAPI "github.com/cli/cli/v2/api"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type mockAPIClient struct {
|
||||
OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error)
|
||||
OnREST func(hostname, method, p string, body io.Reader, data interface{}) error
|
||||
}
|
||||
|
||||
func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
return m.OnRESTWithNext(hostname, method, p, body, data)
|
||||
}
|
||||
|
||||
func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data interface{}) error {
|
||||
return m.OnREST(hostname, method, p, body, data)
|
||||
}
|
||||
|
||||
type mockDataGenerator struct {
|
||||
mock.Mock
|
||||
NumAttestations int
|
||||
}
|
||||
|
||||
func (m mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
func (m *mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false)
|
||||
}
|
||||
|
||||
func (m mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
func (m *mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
// if path doesn't contain after, it means first time hitting the mock server
|
||||
// so return the first page and return the link header in the response
|
||||
if !strings.Contains(p, "after") {
|
||||
|
|
@ -35,7 +45,37 @@ func (m mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string,
|
|||
return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false)
|
||||
}
|
||||
|
||||
func (m mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) {
|
||||
// Returns a func that just calls OnRESTSuccessWithNextPage but half the time
|
||||
// it returns a 500 error.
|
||||
func (m *mockDataGenerator) FlakyOnRESTSuccessWithNextPageHandler() func(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
// set up the flake counter
|
||||
m.On("FlakyOnRESTSuccessWithNextPage:error").Return()
|
||||
|
||||
count := 0
|
||||
return func(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
if count%2 == 0 {
|
||||
m.MethodCalled("FlakyOnRESTSuccessWithNextPage:error")
|
||||
|
||||
count = count + 1
|
||||
return "", cliAPI.HTTPError{HTTPError: &ghAPI.HTTPError{StatusCode: 500}}
|
||||
} else {
|
||||
count = count + 1
|
||||
return m.OnRESTSuccessWithNextPage(hostname, method, p, body, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always returns a 500
|
||||
func (m *mockDataGenerator) OnREST500ErrorHandler() func(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
m.On("OnREST500Error").Return()
|
||||
return func(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
m.MethodCalled("OnREST500Error")
|
||||
|
||||
return "", cliAPI.HTTPError{HTTPError: &ghAPI.HTTPError{StatusCode: 500}}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) {
|
||||
atts := make([]*Attestation, m.NumAttestations)
|
||||
for j := 0; j < m.NumAttestations; j++ {
|
||||
att := makeTestAttestation()
|
||||
|
|
@ -65,7 +105,7 @@ func (m mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p strin
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
func (m *mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
resp := AttestationsResponse{
|
||||
Attestations: make([]*Attestation, 0),
|
||||
}
|
||||
|
|
@ -84,6 +124,29 @@ func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p stri
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
func (m *mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
|
||||
return "", errors.New("failed to get attestations")
|
||||
}
|
||||
|
||||
type mockMetaGenerator struct {
|
||||
TrustDomain string
|
||||
}
|
||||
|
||||
func (m mockMetaGenerator) OnREST(hostname, method, p string, body io.Reader, data interface{}) error {
|
||||
var template = `
|
||||
{
|
||||
"domains": {
|
||||
"artifact_attestations": {
|
||||
"trust_domain": "%s"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
var jsonString = fmt.Sprintf(template, m.TrustDomain)
|
||||
return json.Unmarshal([]byte(jsonString), &data)
|
||||
|
||||
}
|
||||
|
||||
func (m mockMetaGenerator) OnRESTError(hostname, method, p string, body io.Reader, data interface{}) error {
|
||||
return errors.New("test error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
type MockClient struct {
|
||||
OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error)
|
||||
OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error)
|
||||
OnGetTrustDomain func() (string, error)
|
||||
}
|
||||
|
||||
func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
|
|
@ -19,6 +20,10 @@ func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Att
|
|||
return m.OnGetByOwnerAndDigest(owner, digest, limit)
|
||||
}
|
||||
|
||||
func (m MockClient) GetTrustDomain() (string, error) {
|
||||
return m.OnGetTrustDomain()
|
||||
}
|
||||
|
||||
func makeTestAttestation() Attestation {
|
||||
return Attestation{Bundle: data.SigstoreBundle(nil)}
|
||||
}
|
||||
|
|
|
|||
15
pkg/cmd/attestation/api/trust_domain.go
Normal file
15
pkg/cmd/attestation/api/trust_domain.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package api
|
||||
|
||||
const MetaPath = "meta"
|
||||
|
||||
type ArtifactAttestations struct {
|
||||
TrustDomain string `json:"trust_domain"`
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
ArtifactAttestations ArtifactAttestations `json:"artifact_attestations"`
|
||||
}
|
||||
|
||||
type MetaResponse struct {
|
||||
Domains Domain `json:"domains"`
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ type DigestedArtifact struct {
|
|||
URL string
|
||||
digest string
|
||||
digestAlg string
|
||||
nameRef name.Reference
|
||||
}
|
||||
|
||||
func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) {
|
||||
|
|
@ -77,3 +80,7 @@ func (a *DigestedArtifact) Algorithm() string {
|
|||
func (a *DigestedArtifact) DigestWithAlg() string {
|
||||
return fmt.Sprintf("%s:%s", a.digestAlg, a.digest)
|
||||
}
|
||||
|
||||
func (a *DigestedArtifact) NameRef() name.Reference {
|
||||
return a.nameRef
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif
|
|||
return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err)
|
||||
}
|
||||
|
||||
digest, err := client.GetImageDigest(named.String())
|
||||
digest, nameRef, err := client.GetImageDigest(named.String())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -24,5 +25,6 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif
|
|||
URL: fmt.Sprintf("oci://%s", named.String()),
|
||||
digest: digest.Hex,
|
||||
digestAlg: digest.Algorithm,
|
||||
nameRef: nameRef,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue