Merge branch 'trunk' into 8426-add-pr-update-cmd-no-local-update
This commit is contained in:
commit
a994edda93
11 changed files with 406 additions and 118 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -24,3 +24,5 @@ A clear and concise description of what you expected to happen and what actually
|
|||
### Logs
|
||||
|
||||
Paste the activity from your command line. Redact if needed.
|
||||
|
||||
<!-- Note: Set `GH_DEBUG=true` for verbose logs or `GH_DEBUG=api` for verbose logs with HTTP traffic details. -->
|
||||
|
|
|
|||
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@49df96e17e918a15956db358890b08e61c704919 # v1.2.0
|
||||
uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
|
||||
with:
|
||||
subject-path: "dist/gh_*"
|
||||
- name: Run createrepo
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -20,9 +20,9 @@ require (
|
|||
github.com/gabriel-vasile/mimetype v1.4.4
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.19.1
|
||||
github.com/google/go-containerregistry v0.19.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.5.2
|
||||
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
|
||||
|
|
@ -39,7 +39,7 @@ require (
|
|||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.3.2
|
||||
github.com/sigstore/sigstore-go v0.3.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
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
|
||||
|
|
@ -74,7 +74,7 @@ require (
|
|||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v24.0.9+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
|
|
@ -98,7 +98,7 @@ require (
|
|||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/itchyny/gojq v0.12.15 // indirect
|
||||
|
|
|
|||
27
go.sum
27
go.sum
|
|
@ -109,7 +109,6 @@ 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.3/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/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
|
|
@ -141,8 +140,8 @@ github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bc
|
|||
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
|
|
@ -198,8 +197,8 @@ github.com/google/certificate-transparency-go v1.1.8 h1:LGYKkgZF7satzgTak9R4yzfJ
|
|||
github.com/google/certificate-transparency-go v1.1.8/go.mod h1:bV/o8r0TBKRf1X//iiiSgWrvII4d7/8OiA+3vG26gI8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
|
||||
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||
github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
|
||||
github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
|
|
@ -218,8 +217,8 @@ github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/
|
|||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw=
|
||||
github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
|
@ -227,13 +226,12 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
|||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
|
||||
|
|
@ -411,8 +409,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
|
|
@ -422,7 +420,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command {
|
|||
Short: "Work with artifact attestations",
|
||||
Aliases: []string{"at"},
|
||||
Long: heredoc.Doc(`
|
||||
### NOTE: This feature is currently in beta, and subject to change.
|
||||
|
||||
Download and verify artifact attestations.
|
||||
`),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,18 +25,22 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
Args: cmdutil.ExactArgs(1, "must specify file path or container image URI, as well as one of --owner or --repo"),
|
||||
Short: "Verify an artifact's integrity using attestations",
|
||||
Long: heredoc.Docf(`
|
||||
### NOTE: This feature is currently in beta, and subject to change.
|
||||
|
||||
Verify the integrity and provenance of an artifact using its associated
|
||||
cryptographically signed attestations.
|
||||
|
||||
The command requires either:
|
||||
In order to verify an attestation, you must validate the identity of the Actions
|
||||
workflow that produced the attestation (a.k.a. the signer workflow). Given this
|
||||
identity, the verification process checks the signatures in the attestations,
|
||||
and confirms that the attestation refers to provided artifact.
|
||||
|
||||
To specify the artifact, the command requires:
|
||||
* a file path to an artifact, or
|
||||
* a container image URI (e.g. %[1]soci://<image-uri>%[1]s)
|
||||
* (note that if you provide an OCI URL, you must already be authenticated with
|
||||
its container registry)
|
||||
|
||||
In addition, the command requires either:
|
||||
To fetch the attestation, and validate the identity of the signer, the command
|
||||
requires either:
|
||||
* the %[1]s--repo%[1]s flag (e.g. --repo github/example).
|
||||
* the %[1]s--owner%[1]s flag (e.g. --owner github), or
|
||||
|
||||
|
|
@ -54,27 +58,38 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
To see the full results that are generated upon successful verification, i.e.
|
||||
for use with a policy engine, provide the %[1]s--format=json%[1]s flag.
|
||||
|
||||
The attestation's certificate's Subject Alternative Name (SAN) identifies the entity
|
||||
responsible for creating the attestation, which most of the time will be a GitHub
|
||||
Actions workflow file located inside your repository. By default, this command uses
|
||||
The signer workflow's identity is validated against the Subject Alternative Name (SAN)
|
||||
within the attestation certificate. Often, the signer workflow is the
|
||||
same workflow that started the run and generated the attestation, and will be
|
||||
located inside your repository. For this reason, by default this command uses
|
||||
either the %[1]s--repo%[1]s or the %[1]s--owner%[1]s flag value to validate the SAN.
|
||||
|
||||
However, if you generate attestations with a reusable workflow then the SAN will
|
||||
identify the reusable workflow – which may or may not be located inside your %[1]s--repo%[1]s
|
||||
or %[1]s--owner%[1]s. In these situations, you can use the %[1]s--cert-identity%[1]s or
|
||||
%[1]s--cert-identity-regex%[1]s flags to specify the reusable workflow's URI.
|
||||
However, sometimes the caller workflow is not the same workflow that
|
||||
performed the signing. If your attestation was generated via a reusable
|
||||
workflow, then that reusable workflow is the signer whose identity needs to be
|
||||
validated. In this situation, the signer workflow may or may not be located
|
||||
inside your %[1]s--repo%[1]s or %[1]s--owner%[1]s.
|
||||
|
||||
When using reusable workflows, use the %[1]s--signer-repo%[1]s, %[1]s--signer-workflow%[1]s,
|
||||
or %[1]s--cert-identity%[1]s flags to validate the signer workflow's identity.
|
||||
|
||||
For more policy verification options, see the other available flags.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Verify a local artifact linked with a repository
|
||||
# Verify an artifact linked with a repository
|
||||
$ gh attestation verify example.bin --repo github/example
|
||||
|
||||
# Verify a local artifact linked with an organization
|
||||
# Verify an artifact linked with an organization
|
||||
$ gh attestation verify example.bin --owner github
|
||||
|
||||
# Verify an OCI image using locally stored attestations
|
||||
# Verify an artifact and output the full verification result
|
||||
$ gh attestation verify example.bin --owner github --format json
|
||||
|
||||
# Verify an OCI image using attestations stored on disk
|
||||
$ gh attestation verify oci://<image-uri> --owner github --bundle sha256:foo.jsonl
|
||||
|
||||
# Verify an artifact signed with a reusable workflow
|
||||
$ gh attestation verify example.bin --owner github --signer-repo actions/example
|
||||
`),
|
||||
// PreRunE is used to validate flags before the command is run
|
||||
// If an error is returned, its message will be printed to the terminal
|
||||
|
|
|
|||
|
|
@ -21,21 +21,13 @@ type GetOptions struct {
|
|||
Config func() (gh.Config, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
VariableName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
}
|
||||
|
||||
type getVariableResponse struct {
|
||||
Value string `json:"value"`
|
||||
// Other available but unused fields
|
||||
// Name string `json:"name"`
|
||||
// UpdatedAt time.Time `json:"updated_at"`
|
||||
// Visibility shared.Visibility `json:"visibility"`
|
||||
// SelectedReposURL string `json:"selected_repositories_url"`
|
||||
// NumSelectedRepos int `json:"num_selected_repos"`
|
||||
}
|
||||
|
||||
func NewCmdGet(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command {
|
||||
opts := &GetOptions{
|
||||
IO: f.IOStreams,
|
||||
|
|
@ -72,6 +64,7 @@ func NewCmdGet(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command
|
|||
}
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Get a variable for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Get a variable for an environment")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.VariableJSONFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -116,8 +109,8 @@ func getRun(opts *GetOptions) error {
|
|||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
||||
var response getVariableResponse
|
||||
if err = client.REST(host, "GET", path, nil, &response); err != nil {
|
||||
var variable shared.Variable
|
||||
if err = client.REST(host, "GET", path, nil, &variable); err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("variable %s was not found", opts.VariableName)
|
||||
|
|
@ -126,7 +119,13 @@ func getRun(opts *GetOptions) error {
|
|||
return fmt.Errorf("failed to get variable %s: %w", opts.VariableName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", response.Value)
|
||||
if opts.Exporter != nil {
|
||||
if err := shared.PopulateSelectedRepositoryInformation(client, host, &variable); err != nil {
|
||||
return err
|
||||
}
|
||||
return opts.Exporter.Write(opts.IO, &variable)
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", variable.Value)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/variable/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -53,6 +55,18 @@ func TestNewCmdGet(t *testing.T) {
|
|||
cli: "-o TestOrg -e Development QUX",
|
||||
wantErr: cmdutil.FlagErrorf("%s", "specify only one of `--org` or `--env`"),
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--json name,value FOO",
|
||||
wants: GetOptions{
|
||||
VariableName: "FOO",
|
||||
Exporter: func() cmdutil.Exporter {
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields([]string{"name", "value"})
|
||||
return exporter
|
||||
}(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -85,17 +99,21 @@ func TestNewCmdGet(t *testing.T) {
|
|||
require.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
|
||||
require.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
|
||||
require.Equal(t, tt.wants.VariableName, gotOpts.VariableName)
|
||||
require.Equal(t, tt.wants.Exporter, gotOpts.Exporter)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getRun(t *testing.T) {
|
||||
tf, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *GetOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantOut string
|
||||
wantErr error
|
||||
name string
|
||||
opts *GetOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
jsonFields []string
|
||||
wantOut string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "getting repo variable",
|
||||
|
|
@ -104,7 +122,7 @@ func Test_getRun(t *testing.T) {
|
|||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/variables/VARIABLE_ONE"),
|
||||
httpmock.JSONResponse(getVariableResponse{
|
||||
httpmock.JSONResponse(shared.Variable{
|
||||
Value: "repo_var",
|
||||
}))
|
||||
},
|
||||
|
|
@ -118,7 +136,7 @@ func Test_getRun(t *testing.T) {
|
|||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "orgs/TestOrg/actions/variables/VARIABLE_ONE"),
|
||||
httpmock.JSONResponse(getVariableResponse{
|
||||
httpmock.JSONResponse(shared.Variable{
|
||||
Value: "org_var",
|
||||
}))
|
||||
},
|
||||
|
|
@ -132,12 +150,44 @@ func Test_getRun(t *testing.T) {
|
|||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/owner/repo/environments/Development/variables/VARIABLE_ONE"),
|
||||
httpmock.JSONResponse(getVariableResponse{
|
||||
httpmock.JSONResponse(shared.Variable{
|
||||
Value: "env_var",
|
||||
}))
|
||||
},
|
||||
wantOut: "env_var\n",
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
opts: &GetOptions{
|
||||
VariableName: "VARIABLE_ONE",
|
||||
},
|
||||
jsonFields: []string{"name", "value", "visibility", "updatedAt", "createdAt", "numSelectedRepos", "selectedReposURL"},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/variables/VARIABLE_ONE"),
|
||||
httpmock.JSONResponse(shared.Variable{
|
||||
Name: "VARIABLE_ONE",
|
||||
Value: "repo_var",
|
||||
UpdatedAt: tf,
|
||||
CreatedAt: tf,
|
||||
Visibility: shared.Organization,
|
||||
SelectedReposURL: "path/to/fetch/selected/repos",
|
||||
NumSelectedRepos: 0, // This should be populated in a second API call.
|
||||
}))
|
||||
reg.Register(httpmock.REST("GET", "path/to/fetch/selected/repos"),
|
||||
httpmock.JSONResponse(map[string]interface{}{
|
||||
"total_count": 99,
|
||||
}))
|
||||
},
|
||||
wantOut: `{
|
||||
"name": "VARIABLE_ONE",
|
||||
"value": "repo_var",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"visibility": "organization",
|
||||
"selectedReposURL": "path/to/fetch/selected/repos",
|
||||
"numSelectedRepos": 99
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "when the variable is not found, an error is returned",
|
||||
opts: &GetOptions{
|
||||
|
|
@ -185,6 +235,12 @@ func Test_getRun(t *testing.T) {
|
|||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
|
||||
if tt.jsonFields != nil {
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields(tt.jsonFields)
|
||||
tt.opts.Exporter = exporter
|
||||
}
|
||||
|
||||
err := getRun(tt.opts)
|
||||
if err != nil {
|
||||
require.EqualError(t, tt.wantErr, err.Error())
|
||||
|
|
@ -192,7 +248,11 @@ func Test_getRun(t *testing.T) {
|
|||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantOut, stdout.String())
|
||||
if tt.jsonFields != nil {
|
||||
require.JSONEq(t, tt.wantOut, stdout.String())
|
||||
} else {
|
||||
require.Equal(t, tt.wantOut, stdout.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,3 +260,23 @@ func Test_getRun(t *testing.T) {
|
|||
t.Run(tt.name+" no-tty", runTest(false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportVariables(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
tf, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
|
||||
vs := &shared.Variable{
|
||||
Name: "v1",
|
||||
Value: "test1",
|
||||
UpdatedAt: tf,
|
||||
CreatedAt: tf,
|
||||
Visibility: shared.All,
|
||||
SelectedReposURL: "https://someurl.com",
|
||||
NumSelectedRepos: 1,
|
||||
}
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields(shared.VariableJSONFields)
|
||||
require.NoError(t, exporter.Write(ios, vs))
|
||||
require.JSONEq(t,
|
||||
`{"name":"v1","numSelectedRepos":1,"selectedReposURL":"https://someurl.com","updatedAt":"2024-01-01T00:00:00Z","createdAt":"2024-01-01T00:00:00Z","value":"test1","visibility":"all"}`,
|
||||
stdout.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package list
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -30,14 +31,7 @@ type ListOptions struct {
|
|||
EnvName string
|
||||
}
|
||||
|
||||
var variableFields = []string{
|
||||
"name",
|
||||
"value",
|
||||
"visibility",
|
||||
"updatedAt",
|
||||
"numSelectedRepos",
|
||||
"selectedReposURL",
|
||||
}
|
||||
const fieldNumSelectedRepos = "numSelectedRepos"
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := &ListOptions{
|
||||
|
|
@ -76,7 +70,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List variables for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List variables for an environment")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, variableFields)
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.VariableJSONFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -103,9 +97,21 @@ func listRun(opts *ListOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var variables []Variable
|
||||
// Since populating the `NumSelectedRepos` field costs further API requests
|
||||
// (one per secret), it's important to avoid extra calls when the output will
|
||||
// not present the field's value. So, we should only populate this field in
|
||||
// these cases:
|
||||
// 1. The command is run in the TTY mode without the `--json <fields>` option.
|
||||
// 2. The command is run with `--json <fields>` option, and `numSelectedRepos`
|
||||
// is among the selected fields. In this case, TTY mode is irrelevant.
|
||||
showSelectedRepoInfo := opts.IO.IsStdoutTTY()
|
||||
if opts.Exporter != nil {
|
||||
// Note that if there's an exporter set, then we don't mind the TTY mode
|
||||
// because we just have to populate the requested fields.
|
||||
showSelectedRepoInfo = slices.Contains(opts.Exporter.Fields(), fieldNumSelectedRepos)
|
||||
}
|
||||
|
||||
var variables []shared.Variable
|
||||
switch variableEntity {
|
||||
case shared.Repository:
|
||||
variables, err = getRepoVariables(client, baseRepo)
|
||||
|
|
@ -170,20 +176,7 @@ func listRun(opts *ListOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type Variable struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Visibility shared.Visibility `json:"visibility"`
|
||||
SelectedReposURL string `json:"selected_repositories_url"`
|
||||
NumSelectedRepos int `json:"num_selected_repos"`
|
||||
}
|
||||
|
||||
func (v *Variable) ExportData(fields []string) map[string]interface{} {
|
||||
return cmdutil.StructExportData(v, fields)
|
||||
}
|
||||
|
||||
func fmtVisibility(s Variable) string {
|
||||
func fmtVisibility(s shared.Variable) string {
|
||||
switch s.Visibility {
|
||||
case shared.All:
|
||||
return "Visible to all repositories"
|
||||
|
|
@ -199,22 +192,23 @@ func fmtVisibility(s Variable) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func getRepoVariables(client *http.Client, repo ghrepo.Interface) ([]Variable, error) {
|
||||
func getRepoVariables(client *http.Client, repo ghrepo.Interface) ([]shared.Variable, error) {
|
||||
return getVariables(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/variables", ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
func getEnvVariables(client *http.Client, repo ghrepo.Interface, envName string) ([]Variable, error) {
|
||||
func getEnvVariables(client *http.Client, repo ghrepo.Interface, envName string) ([]shared.Variable, error) {
|
||||
path := fmt.Sprintf("repos/%s/environments/%s/variables", ghrepo.FullName(repo), envName)
|
||||
return getVariables(client, repo.RepoHost(), path)
|
||||
}
|
||||
|
||||
func getOrgVariables(client *http.Client, host, orgName string, showSelectedRepoInfo bool) ([]Variable, error) {
|
||||
func getOrgVariables(client *http.Client, host, orgName string, showSelectedRepoInfo bool) ([]shared.Variable, error) {
|
||||
variables, err := getVariables(client, host, fmt.Sprintf("orgs/%s/actions/variables", orgName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
if showSelectedRepoInfo {
|
||||
err = populateSelectedRepositoryInformation(client, host, variables)
|
||||
err = shared.PopulateMultipleSelectedRepositoryInformation(apiClient, host, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -222,13 +216,13 @@ func getOrgVariables(client *http.Client, host, orgName string, showSelectedRepo
|
|||
return variables, nil
|
||||
}
|
||||
|
||||
func getVariables(client *http.Client, host, path string) ([]Variable, error) {
|
||||
var results []Variable
|
||||
func getVariables(client *http.Client, host, path string) ([]shared.Variable, error) {
|
||||
var results []shared.Variable
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
path = fmt.Sprintf("%s?per_page=100", path)
|
||||
for path != "" {
|
||||
response := struct {
|
||||
Variables []Variable
|
||||
Variables []shared.Variable
|
||||
}{}
|
||||
var err error
|
||||
path, err = apiClient.RESTWithNext(host, "GET", path, nil, &response)
|
||||
|
|
@ -239,20 +233,3 @@ func getVariables(client *http.Client, host, path string) ([]Variable, error) {
|
|||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func populateSelectedRepositoryInformation(client *http.Client, host string, variables []Variable) error {
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
for i, variable := range variables {
|
||||
if variable.SelectedReposURL == "" {
|
||||
continue
|
||||
}
|
||||
response := struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{}
|
||||
if err := apiClient.REST(host, "GET", variable.SelectedReposURL, nil, &response); err != nil {
|
||||
return fmt.Errorf("failed determining selected repositories for %s: %w", variable.Name, err)
|
||||
}
|
||||
variables[i].NumSelectedRepos = response.TotalCount
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,10 +81,11 @@ func TestNewCmdList(t *testing.T) {
|
|||
|
||||
func Test_listRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
opts *ListOptions
|
||||
wantOut []string
|
||||
name string
|
||||
tty bool
|
||||
opts *ListOptions
|
||||
jsonFields []string
|
||||
wantOut []string
|
||||
}{
|
||||
{
|
||||
name: "repo tty",
|
||||
|
|
@ -107,6 +108,17 @@ func Test_listRun(t *testing.T) {
|
|||
"VARIABLE_THREE\tthree\t1975-11-30T00:00:00Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repo not tty, json",
|
||||
tty: false,
|
||||
opts: &ListOptions{},
|
||||
jsonFields: []string{"name", "value"},
|
||||
wantOut: []string{`[
|
||||
{"name":"VARIABLE_ONE","value":"one"},
|
||||
{"name":"VARIABLE_TWO","value":"two"},
|
||||
{"name":"VARIABLE_THREE","value":"three"}
|
||||
]`},
|
||||
},
|
||||
{
|
||||
name: "org tty",
|
||||
tty: true,
|
||||
|
|
@ -132,6 +144,19 @@ func Test_listRun(t *testing.T) {
|
|||
"VARIABLE_THREE\torg_three\t1975-11-30T00:00:00Z\tSELECTED",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org not tty, json",
|
||||
tty: false,
|
||||
opts: &ListOptions{
|
||||
OrgName: "UmbrellaCorporation",
|
||||
},
|
||||
jsonFields: []string{"name", "value"},
|
||||
wantOut: []string{`[
|
||||
{"name":"VARIABLE_ONE","value":"org_one"},
|
||||
{"name":"VARIABLE_TWO","value":"org_two"},
|
||||
{"name":"VARIABLE_THREE","value":"org_three"}
|
||||
]`},
|
||||
},
|
||||
{
|
||||
name: "env tty",
|
||||
tty: true,
|
||||
|
|
@ -157,6 +182,19 @@ func Test_listRun(t *testing.T) {
|
|||
"VARIABLE_THREE\tthree\t1975-11-30T00:00:00Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env not tty, json",
|
||||
tty: false,
|
||||
opts: &ListOptions{
|
||||
EnvName: "Development",
|
||||
},
|
||||
jsonFields: []string{"name", "value"},
|
||||
wantOut: []string{`[
|
||||
{"name":"VARIABLE_ONE","value":"one"},
|
||||
{"name":"VARIABLE_TWO","value":"two"},
|
||||
{"name":"VARIABLE_THREE","value":"three"}
|
||||
]`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -175,9 +213,9 @@ func Test_listRun(t *testing.T) {
|
|||
t1, _ := time.Parse("2006-01-02", "2020-12-04")
|
||||
t2, _ := time.Parse("2006-01-02", "1975-11-30")
|
||||
payload := struct {
|
||||
Variables []Variable
|
||||
Variables []shared.Variable
|
||||
}{
|
||||
Variables: []Variable{
|
||||
Variables: []shared.Variable{
|
||||
{
|
||||
Name: "VARIABLE_ONE",
|
||||
Value: "one",
|
||||
|
|
@ -196,7 +234,7 @@ func Test_listRun(t *testing.T) {
|
|||
},
|
||||
}
|
||||
if tt.opts.OrgName != "" {
|
||||
payload.Variables = []Variable{
|
||||
payload.Variables = []shared.Variable{
|
||||
{
|
||||
Name: "VARIABLE_ONE",
|
||||
Value: "org_one",
|
||||
|
|
@ -247,11 +285,138 @@ func Test_listRun(t *testing.T) {
|
|||
return t
|
||||
}
|
||||
|
||||
if tt.jsonFields != nil {
|
||||
jsonExporter := cmdutil.NewJSONExporter()
|
||||
jsonExporter.SetFields(tt.jsonFields)
|
||||
tt.opts.Exporter = jsonExporter
|
||||
}
|
||||
|
||||
err := listRun(tt.opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := fmt.Sprintf("%s\n", strings.Join(tt.wantOut, "\n"))
|
||||
assert.Equal(t, expected, stdout.String())
|
||||
if tt.jsonFields != nil {
|
||||
assert.JSONEq(t, expected, stdout.String())
|
||||
} else {
|
||||
assert.Equal(t, expected, stdout.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_listRun_populatesNumSelectedReposIfRequired asserts that NumSelectedRepos
|
||||
// field is populated **only** when it's going to be presented in the output. Since
|
||||
// populating this field costs further API requests (one per variable), it's important
|
||||
// to avoid extra calls when the output will not present the field's value. Note
|
||||
// that NumSelectedRepos is only meant for organization variables.
|
||||
//
|
||||
// We should only populate the NumSelectedRepos field in these cases:
|
||||
// 1. The command is run in the TTY mode without the `--json <fields>` option.
|
||||
// 2. The command is run with `--json <fields>` option, and `numSelectedRepos`
|
||||
// is among the selected fields. In this case, TTY mode is irrelevant.
|
||||
func Test_listRun_populatesNumSelectedReposIfRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
jsonFields []string
|
||||
wantPopulated bool
|
||||
}{
|
||||
{
|
||||
name: "org tty",
|
||||
tty: true,
|
||||
wantPopulated: true,
|
||||
},
|
||||
{
|
||||
name: "org tty, json with numSelectedRepos",
|
||||
tty: true,
|
||||
jsonFields: []string{"numSelectedRepos"},
|
||||
wantPopulated: true,
|
||||
},
|
||||
{
|
||||
name: "org tty, json without numSelectedRepos",
|
||||
tty: true,
|
||||
jsonFields: []string{"name"},
|
||||
wantPopulated: false,
|
||||
},
|
||||
{
|
||||
name: "org not tty",
|
||||
tty: false,
|
||||
wantPopulated: false,
|
||||
},
|
||||
{
|
||||
name: "org not tty, json with numSelectedRepos",
|
||||
tty: false,
|
||||
jsonFields: []string{"numSelectedRepos"},
|
||||
wantPopulated: true,
|
||||
},
|
||||
{
|
||||
name: "org not tty, json without numSelectedRepos",
|
||||
tty: false,
|
||||
jsonFields: []string{"name"},
|
||||
wantPopulated: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Verify(t)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "orgs/umbrellaOrganization/actions/variables"),
|
||||
httpmock.JSONResponse(struct{ Variables []shared.Variable }{
|
||||
[]shared.Variable{
|
||||
{
|
||||
Name: "VARIABLE",
|
||||
Visibility: shared.Selected,
|
||||
SelectedReposURL: "https://api.github.com/orgs/umbrellaOrganization/actions/variables/VARIABLE/repositories",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "orgs/umbrellaOrganization/actions/variables/VARIABLE/repositories"),
|
||||
httpmock.JSONResponse(struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{999}))
|
||||
|
||||
opts := &ListOptions{
|
||||
OrgName: "umbrellaOrganization",
|
||||
}
|
||||
|
||||
if tt.jsonFields != nil {
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields(tt.jsonFields)
|
||||
opts.Exporter = exporter
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
opts.IO = ios
|
||||
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
}
|
||||
opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
opts.Config = func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
opts.Now = func() time.Time {
|
||||
t, _ := time.Parse(time.RFC822, "4 Apr 24 00:00 UTC")
|
||||
return t
|
||||
}
|
||||
|
||||
require.NoError(t, listRun(opts))
|
||||
|
||||
if tt.wantPopulated {
|
||||
// There should be 2 requests; one to get the variables list and
|
||||
// another to populate the numSelectedRepos field.
|
||||
assert.Len(t, reg.Requests, 2)
|
||||
} else {
|
||||
// Only one requests to get the variables list.
|
||||
assert.Len(t, reg.Requests, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -279,18 +444,19 @@ func Test_getVariables_pagination(t *testing.T) {
|
|||
func TestExportVariables(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
tf, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
|
||||
vs := []Variable{{
|
||||
vs := []shared.Variable{{
|
||||
Name: "v1",
|
||||
Value: "test1",
|
||||
UpdatedAt: tf,
|
||||
CreatedAt: tf,
|
||||
Visibility: shared.All,
|
||||
SelectedReposURL: "https://someurl.com",
|
||||
NumSelectedRepos: 1,
|
||||
}}
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields(variableFields)
|
||||
exporter.SetFields(shared.VariableJSONFields)
|
||||
require.NoError(t, exporter.Write(ios, vs))
|
||||
require.JSONEq(t,
|
||||
`[{"name":"v1","numSelectedRepos":1,"selectedReposURL":"https://someurl.com","updatedAt":"2024-01-01T00:00:00Z","value":"test1","visibility":"all"}]`,
|
||||
`[{"name":"v1","numSelectedRepos":1,"selectedReposURL":"https://someurl.com","updatedAt":"2024-01-01T00:00:00Z","createdAt":"2024-01-01T00:00:00Z","value":"test1","visibility":"all"}]`,
|
||||
stdout.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ package shared
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
)
|
||||
|
||||
type Visibility string
|
||||
|
|
@ -20,6 +25,30 @@ const (
|
|||
Environment = "environment"
|
||||
)
|
||||
|
||||
type Variable struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
SelectedReposURL string `json:"selected_repositories_url"`
|
||||
NumSelectedRepos int `json:"num_selected_repos"`
|
||||
}
|
||||
|
||||
var VariableJSONFields = []string{
|
||||
"name",
|
||||
"value",
|
||||
"visibility",
|
||||
"updatedAt",
|
||||
"createdAt",
|
||||
"numSelectedRepos",
|
||||
"selectedReposURL",
|
||||
}
|
||||
|
||||
func (v *Variable) ExportData(fields []string) map[string]interface{} {
|
||||
return cmdutil.StructExportData(v, fields)
|
||||
}
|
||||
|
||||
func GetVariableEntity(orgName, envName string) (VariableEntity, error) {
|
||||
orgSet := orgName != ""
|
||||
envSet := envName != ""
|
||||
|
|
@ -36,3 +65,28 @@ func GetVariableEntity(orgName, envName string) (VariableEntity, error) {
|
|||
}
|
||||
return Repository, nil
|
||||
}
|
||||
|
||||
func PopulateMultipleSelectedRepositoryInformation(apiClient *api.Client, host string, variables []Variable) error {
|
||||
for i, variable := range variables {
|
||||
if err := PopulateSelectedRepositoryInformation(apiClient, host, &variable); err != nil {
|
||||
return err
|
||||
}
|
||||
variables[i] = variable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PopulateSelectedRepositoryInformation(apiClient *api.Client, host string, variable *Variable) error {
|
||||
if variable.SelectedReposURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{}
|
||||
if err := apiClient.REST(host, "GET", variable.SelectedReposURL, nil, &response); err != nil {
|
||||
return fmt.Errorf("failed determining selected repositories for %s: %w", variable.Name, err)
|
||||
}
|
||||
variable.NumSelectedRepos = response.TotalCount
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue