Repository: cli/cli

Issue:
gh run view (--log||--log-failed) no longer produces logs?
I started to use this feature quite recently and initially it seemed to
work but now I simply cannot get any log, but no error either.

Is there any trick I can use to debug? Shall I provide an example of a
repo and run id for which I encounter this problem?

Hints from issue comments:
@iamazeem: Would be helpful if you could update the description according to the bug report template:
https://github.com/cli/cli/issues/new?template=bug_report.md

The usage of `GH_DEBUG` is mentioned in the note at the end of the template to generate the debug info.@shindere: Thanks a lot for the feedback!

So the command-line I use is:

```
GH_DEBUG=true gh run view --log-failed -R shindere/stdcompat 13677151402
```

which spendssome time but finally shows nothing at all instead of the
expected logs for failed jobs.


The gh logs are:

```
* Request at 2025-03-06 19:10:00.159927738 +0100 CET m=+0.091609212
* Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402?exclude_pull_requests=true
* Request took 346.44351ms
* Request at 2025-03-06 19:10:00.512362245 +0100 CET m=+0.444043782
* Request to https://api.github.com/repos/shindere/stdcompat/actions/workflows/144784623
* Request took 170.73035ms
* Request at 2025-03-06 19:10:00.683908615 +0100 CET m=+0.615590164
* Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402/jobs?per_page=100
* Request took 3.476002413s
```

Is it worth a run with `GH_DEBUG=api`?
@iamazeem: See https://github.com/cli/cli/issues/10551#issuecomment-2708132266 for the update.

---

Disregard this part containing the preliminary investigation:

<details>

Just looked at your failing workflows:
https://github.com/shindere/stdcompat/actions/workflows/ci.yml

The workflow run [`13677151402`](https://github.com/shindere/stdcompat/actions/runs/13677151402) is for a pull request and **`gh run view` command doesn't support that at the moment**.
You can observe the same from the `GH_DEBUG=true` logs i.e. `exclude_pull_requests=true`:

```
* Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402?exclude_pull_requests=true
```

I checked the code and the handling of pull requests is not there even when `exclude_pull_requests=false` (or omitted which means `false` by default according to its API [Get a workflow run](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#get-a-workflow-run)).

You should be able to see the logs for other non-PR runs e.g.:

https://github.com/shindere/stdcompat/commit/123c070e5e7adb49e04bf2fa4e7751e8fa4bc686

which is a direct commit (`push`) to the default branch.

Workflow run: https://github.com/shindere/stdcompat/actions/runs/13393034352
Run ID: **13393034352**

```shell
$ gh run view --log-failed -R shindere/stdcompat 13393034352
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6578622Z Current runner version: '2.322.0'
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6609497Z ##[group]Operating System
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6610284Z Ubuntu
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6610877Z 24.04.1
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6611425Z LTS
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6611911Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6612521Z ##[group]Runner Image
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6613112Z Image: ubuntu-24.04
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-02-18T14:42:25.6613809Z Version: 20250209.1.0
...
```

So, it's a **feature request**, not a bug.
Let's see what the maintainers decide after triage. 🤞

</details>@shindere: Many thanks for the clue.

I indeed so the parameter saying to exclude PRs, but it actually did
work in the past, perhaps because, without noticing, I used runs that
were associated to a direct push.

Is there a way when I get a failure notice to determine whether it's
associated to a PR or not?

I am a bit confused about why ti even does make a difference whether a
run is triggered by a PR or not, when it comes to looking at its logs?

In any case, a bunch of thanks for having figured that out!
@BagToad: @shindere @iamazeem Thank you for the investigation everyone 🙏 

I haven't finished digging into this yet, but I think something else may be going on because the command still fails even when `exclude_pull_requests` is removed from the request parameters. 

```
❯ GH_DEBUG=true ./bin/gh run view --log-failed -R shindere/stdcompat 13677151402
⣾* Request at 2025-03-07 10:39:08.946284 -0700 MST m=+0.085645709
* Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402
⢿* Request took 360.202792ms
* Request at 2025-03-07 10:39:09.355176 -0700 MST m=+0.494534209
* Request to https://api.github.com/repos/shindere/stdcompat/actions/workflows/144784623
⡿* Request took 143.70425ms
⣾* Request at 2025-03-07 10:39:09.555462 -0700 MST m=+0.694818876
* Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402/jobs?per_page=100
⣽* Request took 3.008319708s
```

I also noticed the command completes successfully when the `--log-failed` option is removed, even with the `exclude_pull_requests` parameter is present.

@shindere Does the command complete successfully with other PRs? Do you happen to have an example PR which the command completes successfully?@iamazeem: @BagToad:
The issues seems to be related to `--log` and `--log-failed` flags only.
Without these, the command runs fine for PR workflows as well.
The logs are downloaded once and cached locally as ZIP i.e. `~/.cache/gh/run-log-<RUN_ID>-<TIMESTAMP>.zip`.
Still looking into it to pinpoint the root cause.

May be related to `attachRunLog` and/or `displayRunLog` here:

https://github.com/cli/cli/blob/fc19ff321a4b7f4198f788f08da20249c7950fbd/pkg/cmd/run/view/view.go#L304-L324@shindere: Azeem Sajid (2025/03/07 09:51 -0800):
> iamazeem left a comment (cli/cli#10551)
> 
> The issues seems related to `--log` and `--log-failed` flags only.
> Without these, the command runs fine for PR workflows as well.
> The logs are downloaded once and cached locally as ZIP i.e.
> `~/.cache/gh/run-log-<RUN_ID>-<TIMESTAMP>.zip`.

On this topic, and if I may: is the internal structure of the zip files
one obtains documented somewhere?

I am asking because I thought each job would be associated to a
directory and each step of this job would be stored in a file in the
job's directory, whihc would then mean no files should be at the root
level of the archive. But, this is not what I observe and so far I
couldn't figure out a rational regarding the way the archive is
structured.
@iamazeem: @shindere: It's documented here as a comment above `attachRunLog`:

https://github.com/cli/cli/blob/fc19ff321a4b7f4198f788f08da20249c7950fbd/pkg/cmd/run/view/view.go#L621-L649

But, as you've observed, it different from the actual ZIP.
The cached ZIP has all the files on the root (except one directory):

```shell
$ unzip -l run-log-13677151402-1741181422
Archive:  run-log-13677151402-1741181422.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     1870  1980-00-00 00:00   0_build-ubuntu-old-ocamls (make-no-magic, 3.12.1).txt
     1870  1980-00-00 00:00   1_build-ubuntu-old-ocamls (make, 3.08.4).txt
     1870  1980-00-00 00:00   2_build-ubuntu-old-ocamls (make, 4.00.1).txt
     1870  1980-00-00 00:00   3_build-ubuntu-old-ocamls (make, 3.09.3).txt
     1870  1980-00-00 00:00   4_build-ubuntu-old-ocamls (make-no-magic, 4.00.1).txt
     1870  1980-00-00 00:00   5_build-ubuntu-old-ocamls (make-no-magic, 3.08.4).txt
     1870  1980-00-00 00:00   6_build-ubuntu-old-ocamls (make-no-magic, 3.10.2).txt
     1870  1980-00-00 00:00   7_build-ubuntu-old-ocamls (make, 4.01.0).txt
     1870  1980-00-00 00:00   8_build-ubuntu-old-ocamls (make-no-magic, 3.07).txt
     1870  1980-00-00 00:00   9_build-ubuntu-old-ocamls (make-no-magic, 3.11.2).txt
    70596  1980-00-00 00:00   10_build-ubuntu (dune, 4.08.x).txt
    99872  1980-00-00 00:00   11_build-macos-and-windows (macos-latest, make, 5.0.0).txt
    50453  1980-00-00 00:00   12_build-macos-and-windows (windows-latest, dune, 4.02.x).txt
    76064  1980-00-00 00:00   13_build-ubuntu (dune, 4.02.x).txt
     1870  1980-00-00 00:00   14_build-ubuntu-old-ocamls (make, 3.12.1).txt
     1870  1980-00-00 00:00   15_build-ubuntu-old-ocamls (make, 3.07).txt
     1870  1980-00-00 00:00   16_build-ubuntu-old-ocamls (make-no-magic, 3.09.3).txt
    45798  1980-00-00 00:00   17_build-macos-and-windows (macos-latest, make-no-magic, 4.02.x).txt
   128295  1980-00-00 00:00   18_build-macos-and-windows (windows-latest, make-no-magic, 4.14.x).txt
    45789  1980-00-00 00:00   19_build-macos-and-windows (macos-latest, dune, 4.02.x).txt
     1870  1980-00-00 00:00   20_build-ubuntu-old-ocamls (make, 3.11.2).txt
    99887  1980-00-00 00:00   21_build-macos-and-windows (macos-latest, make-no-magic, 5.0.0).txt
   128654  1980-00-00 00:00   22_build-macos-and-windows (windows-latest, make-no-magic, 5.2.0).txt
   128341  1980-00-00 00:00   23_build-macos-and-windows (windows-latest, make-no-magic, 5.1.1).txt
   128198  1980-00-00 00:00   24_build-macos-and-windows (windows-latest, make, 4.14.x).txt
    69959  1980-00-00 00:00   25_build-macos-and-windows (windows-latest, dune, 5.2.0).txt
   124492  1980-00-00 00:00   26_build-macos-and-windows (macos-latest, make, 5.1.1).txt
   126631  1980-00-00 00:00   27_build-ubuntu (make, 5.1.1).txt
    50453  1980-00-00 00:00   28_build-macos-and-windows (windows-latest, make, 4.02.x).txt
   102424  1980-00-00 00:00   29_build-ubuntu (make, 4.09.x).txt
    70410  1980-00-00 00:00   30_build-macos-and-windows (windows-latest, dune, 4.14.x).txt
    96956  1980-00-00 00:00   31_build-ubuntu (make, 4.03.x).txt
   124802  1980-00-00 00:00   32_build-macos-and-windows (macos-latest, make, 4.14.x).txt
   103788  1980-00-00 00:00   33_build-macos-and-windows (windows-latest, make-no-magic, 5.0.0).txt
   128647  1980-00-00 00:00   34_build-macos-and-windows (windows-latest, make, 5.1.1).txt
    47960  1980-00-00 00:00   35_build-macos-and-windows (windows-latest, make-no-magic, ocaml-variants.4.14.0+mingw64c).txt
    97492  1980-00-00 00:00   36_build-ubuntu (make, 4.07.x).txt
     1870  1980-00-00 00:00   37_build-ubuntu-old-ocamls (make-no-magic, 4.01.0).txt
    76143  1980-00-00 00:00   38_build-ubuntu (dune, 4.03.x).txt
    97653  1980-00-00 00:00   39_build-ubuntu (make, 4.04.x).txt
    76376  1980-00-00 00:00   40_build-ubuntu (dune, 4.06.x).txt
    66450  1980-00-00 00:00   41_build-macos-and-windows (macos-latest, dune, 4.14.x).txt
   124492  1980-00-00 00:00   42_build-macos-and-windows (macos-latest, make, 5.2.0).txt
    76898  1980-00-00 00:00   43_build-ubuntu (dune, 4.04.x).txt
    71527  1980-00-00 00:00   44_build-macos-and-windows (windows-latest, dune, 5.0.0).txt
    96924  1980-00-00 00:00   45_build-ubuntu (make-no-magic, 4.06.x).txt
    47951  1980-00-00 00:00   46_build-macos-and-windows (windows-latest, dune, ocaml-variants.4.14.0+mingw64c).txt
    68081  1980-00-00 00:00   47_build-ubuntu (dune, 4.11.x).txt
    74162  1980-00-00 00:00   48_build-ubuntu (dune, 4.07.x).txt
    70104  1980-00-00 00:00   49_build-ubuntu (dune, 5.0.0).txt
   126511  1980-00-00 00:00   50_build-ubuntu (make, 4.12.x).txt
   102496  1980-00-00 00:00   51_build-ubuntu (make, 4.08.x).txt
    50462  1980-00-00 00:00   52_build-macos-and-windows (windows-latest, make-no-magic, 4.02.x).txt
    45793  1980-00-00 00:00   53_build-macos-and-windows (macos-latest, make, 4.02.x).txt
   124815  1980-00-00 00:00   54_build-macos-and-windows (macos-latest, make-no-magic, 4.14.x).txt
    67950  1980-00-00 00:00   55_build-macos-and-windows (macos-latest, dune, 5.0.0).txt
    48105  1980-00-00 00:00   56_build-macos-and-windows (windows-latest, make, ocaml-variants.4.14.0+mingw64c).txt
   127019  1980-00-00 00:00   57_build-ubuntu (make-no-magic, 4.12.x).txt
     1870  1980-00-00 00:00   58_build-ubuntu-old-ocamls (make, 3.10.2).txt
   126513  1980-00-00 00:00   59_build-ubuntu (make, 4.13.x).txt
    97041  1980-00-00 00:00   60_build-ubuntu (make-no-magic, 4.03.x).txt
   103449  1980-00-00 00:00   61_build-macos-and-windows (windows-latest, make, 5.0.0).txt
    68011  1980-00-00 00:00   62_build-ubuntu (dune, 4.13.x).txt
   126573  1980-00-00 00:00   63_build-ubuntu (make-no-magic, 5.2.0).txt
   128575  1980-00-00 00:00   64_build-macos-and-windows (windows-latest, make, 5.2.0).txt
    65982  1980-00-00 00:00   65_build-macos-and-windows (macos-latest, dune, 5.1.1).txt
   124506  1980-00-00 00:00   66_build-macos-and-windows (macos-latest, make-no-magic, 5.1.1).txt
   101881  1980-00-00 00:00   67_build-ubuntu (make, 5.0.0).txt
    68165  1980-00-00 00:00   68_build-ubuntu (dune, 4.12.x).txt
   126592  1980-00-00 00:00   69_build-ubuntu (make, 4.11.x).txt
    97913  1980-00-00 00:00   70_build-ubuntu (make-no-magic, 4.05.x).txt
   124506  1980-00-00 00:00   71_build-macos-and-windows (macos-latest, make-no-magic, 5.2.0).txt
    96193  1980-00-00 00:00   72_build-ubuntu (make, 4.02.x).txt
   102439  1980-00-00 00:00   73_build-ubuntu (make-no-magic, 4.10.x).txt
    67977  1980-00-00 00:00   74_build-ubuntu (dune, 5.1.1).txt
    97596  1980-00-00 00:00   75_build-ubuntu (make-no-magic, 4.04.x).txt
    96910  1980-00-00 00:00   76_build-ubuntu (make, 4.06.x).txt
    70679  1980-00-00 00:00   77_build-ubuntu (dune, 4.09.x).txt
   126527  1980-00-00 00:00   78_build-ubuntu (make-no-magic, 4.11.x).txt
    65981  1980-00-00 00:00   79_build-macos-and-windows (macos-latest, dune, 5.2.0).txt
   101899  1980-00-00 00:00   80_build-ubuntu (make-no-magic, 5.0.0).txt
   126513  1980-00-00 00:00   81_build-ubuntu (make, 4.14.x).txt
    70144  1980-00-00 00:00   82_build-macos-and-windows (windows-latest, dune, 5.1.1).txt
   102504  1980-00-00 00:00   83_build-ubuntu (make, 4.10.x).txt
   102508  1980-00-00 00:00   84_build-ubuntu (make-no-magic, 4.08.x).txt
   126475  1980-00-00 00:00   85_build-ubuntu (make, 5.2.0).txt
   126489  1980-00-00 00:00   86_build-ubuntu (make-no-magic, 5.1.1).txt
   126527  1980-00-00 00:00   87_build-ubuntu (make-no-magic, 4.14.x).txt
    96279  1980-00-00 00:00   88_build-ubuntu (make-no-magic, 4.02.x).txt
    67977  1980-00-00 00:00   89_build-ubuntu (dune, 5.2.0).txt
    77171  1980-00-00 00:00   90_build-ubuntu (dune, 4.05.x).txt
      240  1980-00-00 00:00   build-ubuntu (dune, 4.05.x)/system.txt
   102435  1980-00-00 00:00   91_build-ubuntu (make-no-magic, 4.09.x).txt
    97660  1980-00-00 00:00   92_build-ubuntu (make-no-magic, 4.07.x).txt
   126604  1980-00-00 00:00   93_build-ubuntu (make-no-magic, 4.13.x).txt
    68011  1980-00-00 00:00   94_build-ubuntu (dune, 4.14.x).txt
    97898  1980-00-00 00:00   95_build-ubuntu (make, 4.05.x).txt
    70679  1980-00-00 00:00   96_build-ubuntu (dune, 4.10.x).txt
---------                     -------
  7587815                     98 files
```

Currently, the files are not being attached correctly in `attachRunLog`.
The issue lies in `logFilenameRegexp`:

https://github.com/cli/cli/blob/fc19ff321a4b7f4198f788f08da20249c7950fbd/pkg/cmd/run/view/view.go#L536-L553

Locally, for me, this is working fine:

```diff
- re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number)
+ re := fmt.Sprintf(`^\d+_%s.*\.txt`, regexp.QuoteMeta(sanitizedJobName))
```

Test run:

```shell
$ bin/gh run view --repo shindere/stdcompat 13677151402 --log-failed | head -n 100
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6031507Z Current runner version: '2.322.0'
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6056655Z ##[group]Operating System
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6057486Z Ubuntu
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6057960Z 24.04.2
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6058453Z LTS
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6058875Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6059454Z ##[group]Runner Image
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6060050Z Image: ubuntu-24.04
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6060519Z Version: 20250302.1.0
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6061844Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20250302.1/images/ubuntu/Ubuntu2404-Readme.md
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6063187Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20250302.1
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6064058Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6064610Z ##[group]Runner Image Provisioner
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6065211Z 2.0.422.1
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6065630Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6067959Z ##[group]GITHUB_TOKEN Permissions
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6070233Z Actions: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6071198Z Attestations: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6071808Z Checks: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6072346Z Contents: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6072814Z Deployments: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6073310Z Discussions: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6073791Z Issues: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6074299Z Metadata: read
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6074741Z Packages: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6075251Z Pages: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6075758Z PullRequests: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6076236Z RepositoryProjects: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6076806Z SecurityEvents: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6077355Z Statuses: write
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6077838Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6080096Z Secret source: Actions
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6081104Z Prepare workflow directory
build-ubuntu-old-ocamls (make, 3.08.4)	Set up job	2025-03-05T13:30:32.6681575Z ##[error]The template is not valid. .github/workflows/ci.yml (Line: 95, Col: 19): Unexpected value ''
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8959630Z Current runner version: '2.322.0'
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8985159Z ##[group]Operating System
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8985916Z Ubuntu
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8986403Z 24.04.2
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8987036Z LTS
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8987479Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8988284Z ##[group]Runner Image
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8988923Z Image: ubuntu-24.04
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8989468Z Version: 20250223.1.0
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8990474Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20250223.1/images/ubuntu/Ubuntu2404-Readme.md
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8992019Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20250223.1
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8992993Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8993479Z ##[group]Runner Image Provisioner
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8994118Z 2.0.422.1
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8994557Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8997071Z ##[group]GITHUB_TOKEN Permissions
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.8999554Z Actions: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9000241Z Attestations: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9000925Z Checks: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9001408Z Contents: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9001932Z Deployments: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9002473Z Discussions: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9002955Z Issues: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9003492Z Metadata: read
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9004001Z Packages: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9004499Z Pages: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9004931Z PullRequests: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9005524Z RepositoryProjects: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9006036Z SecurityEvents: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9006543Z Statuses: write
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9007046Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9009211Z Secret source: Actions
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9009886Z Prepare workflow directory
build-ubuntu-old-ocamls (make, 3.07)	Set up job	2025-03-05T13:37:50.9592271Z ##[error]The template is not valid. .github/workflows/ci.yml (Line: 95, Col: 19): Unexpected value ''
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7092655Z Current runner version: '2.322.0'
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7119780Z ##[group]Operating System
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7120524Z Ubuntu
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7121026Z 24.04.2
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7121518Z LTS
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7122011Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7122479Z ##[group]Runner Image
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7123089Z Image: ubuntu-24.04
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7123629Z Version: 20250302.1.0
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7124617Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20250302.1/images/ubuntu/Ubuntu2404-Readme.md
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7125984Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20250302.1
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7126852Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7127391Z ##[group]Runner Image Provisioner
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7127924Z 2.0.422.1
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7128619Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7130957Z ##[group]GITHUB_TOKEN Permissions
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7132999Z Actions: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7133712Z Attestations: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7134381Z Checks: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7134840Z Contents: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7135310Z Deployments: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7135854Z Discussions: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7136385Z Issues: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7136829Z Metadata: read
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7137360Z Packages: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7137830Z Pages: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7138523Z PullRequests: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7139249Z RepositoryProjects: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7139797Z SecurityEvents: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7140349Z Statuses: write
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7140827Z ##[endgroup]
build-ubuntu-old-ocamls (make, 3.09.3)	Set up job	2025-03-05T13:31:35.7143188Z Secret source: Actions
```

I'll be testing and verifying by correlating this output with the failed jobs.
I'll keep updating here as I proceed.

---

This is working without removing that `exclude_pull_requests=true` query parameter.
So, it's not related to that as I previously mentioned in my [comment](https://github.com/cli/cli/issues/10551#issuecomment-2705642694).@shindere: Kynan Ware (2025/03/07 09:43 -0800):
> BagToad left a comment (cli/cli#10551)
> 
> @shindere @iamazeem Thank you for the investigation everyone 🙏 
> 
> I haven't finished digging into this yet, but I think something else may be going on because the command still fails even when `exclude_pull_requests` is removed from the request parameters. 
> 
> ```
> ❯ GH_DEBUG=true ./bin/gh run view --log-failed -R shindere/stdcompat 13677151402
> ⣾* Request at 2025-03-07 10:39:08.946284 -0700 MST m=+0.085645709
> * Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402
> ⢿* Request took 360.202792ms
> * Request at 2025-03-07 10:39:09.355176 -0700 MST m=+0.494534209
> * Request to https://api.github.com/repos/shindere/stdcompat/actions/workflows/144784623
> ⡿* Request took 143.70425ms
> ⣾* Request at 2025-03-07 10:39:09.555462 -0700 MST m=+0.694818876
> * Request to https://api.github.com/repos/shindere/stdcompat/actions/runs/13677151402/jobs?per_page=100
> ⣽* Request took 3.008319708s
> ```
> 
> I also noticed the command completes successfully when the `--log-failed` option is removed, even with the `exclude_pull_requests` parameter is present.
> 
> @shindere Does the command complete successfully with other PRs?
> Do you happen to have an example PR which the command completes successfully?

Well to tell the truth it's the first one on which I tried it. It's just
that I was surprised because I was under the impression that it did
initially work and then stopped to work, but perhaps it's because the
initial runs were triggered by pushes of branches rather than a PR.

Unfortunately, I do not have access to these runs at the moment and
won't before a week or so, sorry about that.
@shindere: Azeem Sajid (2025/03/08 00:12 -0800):
> iamazeem left a comment (cli/cli#10551)
> 
> @shindere: It's documented here as a comment above `attachRunLog`:
> 
> https://github.com/cli/cli/blob/fc19ff321a4b7f4198f788f08da20249c7950fbd/pkg/cmd/run/view/view.go#L621-L649
> 
> But, as you've observed, it different from the actual ZIP.

Thanks for the pointer! Do we know why GH stores files this way?

> I'll be testing and verifying by correlating this output with the failed jobs.
> I'll keep updating here as I proceed.

Many thanks for all your detective work, warmly appreciated!
@iamazeem: @shindere 

> Do we know why GH stores files this way?

From the code, `gh` doesn't seem to do any intermediate processing.
The response is being read and then dumped as-is in ZIP format.@shindere: Azeem Sajid (2025/03/08 01:26 -0800):
> iamazeem left a comment (cli/cli#10551)
> 
> @shindere 
> 
> > Do we know why GH stores files this way?
> 
> From the code, `gh` doesn't seem to do any intermediate processing.
> The response is being read and then dumped as-is in ZIP format.

I'm sorry, I didn't mean the gh tool, I really meant the GitHub website,
hoping I managed to make more sense this time.
@iamazeem: > I'm sorry, I didn't mean the gh tool, I really meant the GitHub website

No idea why.
The [Download workflow run logs](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#download-workflow-run-logs) API just mentions that it'll be an archive of logs.
This related article [Downloading logs](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/monitoring-workflows/using-workflow-run-logs#downloading-logs) also mentions the same.

---

I suspected that it was related to the downloaded archive itself.

I forked https://github.com/ocamllibs/stdcompat to observe the workflow runs:
Fork: https://github.com/iamazeem/stdcompat
Workflow run 1 (direct push): https://github.com/ocamllibs/stdcompat/actions/runs/13677085883
Workflow run 2 (pull request): https://github.com/iamazeem/stdcompat/actions/runs/13737922588
PR: https://github.com/iamazeem/stdcompat/pull/1

Both seem to work fine with `--log-failed`.
Their respective archives contain the proper directory structure.

Regarding the directory structure:

- the files on the root contain the complete logs per job
- the files in the directories are divided by steps (each directory represents one job).

For older workflow runs, this directory structure is missing but the consolidated job logs exist.
`gh` relies on this directory structure for parsing against the failed jobs.

I verified the same behavior with another repo as well.
The older archives don't include the proper directory structure, the newer ones do.

Sample:

- Workflow: https://github.com/iamazeem/test/actions/workflows/ci-gh-intro.yml
- Workflow run (**last week**): https://github.com/iamazeem/test/actions/runs/13602281839/job/38029622005
  - Logs: https://github.com/iamazeem/test/suites/35061592725/logs?attempt=1
- Workflow run (**today**): https://github.com/iamazeem/test/actions/runs/13738273749/job/38424550755
  - Logs: https://github.com/iamazeem/test/suites/35392307248/logs?attempt=1

Please retrigger your workflow and verify if the same works for you.
If the above observed behavior is consistent, you should be able to observe the failure logs for the latest workflow run(s).@iamazeem: @BagToad: As observed yesterday, the older logs archive does not contain the separate steps files:

- Workflow run (**yesterday**): https://github.com/iamazeem/test/actions/runs/13738273749 (no steps files)
- Workflow run (**today**): https://github.com/iamazeem/test/actions/runs/13745882041 (steps files exist)

```shell
## Both workflows completed successfully.

## Download

$ gh run view --repo iamazeem/test --log-failed 13738273749
$ gh run view --repo iamazeem/test --log-failed 13745882041

## Check cache
## Observe the difference in the ZIP sizes (both workflows do the same work)
## Older: 3.6K
## Newer: 11K

$ ls -Gghl ~/.cache/gh/run-log-{13738273749,13745882041}-*.zip
-rw-rw-r-- 1 3.6K Mar  9 12:00 /home/azeem/.cache/gh/run-log-13738273749-1741442184.zip
-rw-rw-r-- 1  11K Mar  9 12:00 /home/azeem/.cache/gh/run-log-13745882041-1741503293.zip

## List the contents of the older archive
## Observe that the directory only contains `system.txt` file only, no steps files.

$ unzip -l ~/.cache/gh/run-log-13738273749-*.zip
Archive:  /home/azeem/.cache/gh/run-log-13738273749-1741442184.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    11704  1980-00-00 00:00   0_Explore-GitHub-Actions.txt
      418  1980-00-00 00:00   Explore-GitHub-Actions/system.txt
---------                     -------
    12122                     2 files

## List the contents of the newer archive
## Observe that the directory contains all the steps files.

$ unzip -l ~/.cache/gh/run-log-13745882041-*.zip
Archive:  /home/azeem/.cache/gh/run-log-13745882041-1741503293.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      419  2025-03-09 07:00   Explore-GitHub-Actions/6_Run echo 💡 The iamazeemtest repository has been cloned to.txt
       58  2025-03-09 07:00   Explore-GitHub-Actions/19_Complete job.txt
      329  2025-03-09 07:00   Explore-GitHub-Actions/9_Run echo 🍏 This job's status is success..txt
     5166  2025-03-09 07:00   Explore-GitHub-Actions/5_Check out repository code.txt
      419  2025-03-09 07:00   Explore-GitHub-Actions/3_Run echo 🐧 This job is now running on a Linux server host.txt
      425  2025-03-09 07:00   Explore-GitHub-Actions/7_Run echo 🖥️ The workflow is now ready to test your code o.txt
      437  2025-03-09 07:00   Explore-GitHub-Actions/2_Run echo 🎉 The job was automatically triggered by a workf.txt
     1432  2025-03-09 07:00   Explore-GitHub-Actions/18_Post Check out repository code.txt
     1529  2025-03-09 07:00   Explore-GitHub-Actions/1_Set up job.txt
        0  2025-03-09 07:00   Explore-GitHub-Actions/
    12131  2025-03-09 07:00   0_Explore-GitHub-Actions.txt
     1005  2025-03-09 07:00   Explore-GitHub-Actions/8_List files in the repository.txt
      482  2025-03-09 07:00   -2147483648_Explore-GitHub-Actions.txt
      482  2025-03-09 07:00   Explore-GitHub-Actions/4_Run echo 🔎 The name of your branch is refsheadsmain and y.txt
---------                     -------
    24314                     14 files
```

Need to verify this from the server side.
If this is expected then `gh` needs to update its logic.
`gh` should not rely on the existence of these steps files.
The consolidated files on the root need to be used instead.@shindere: Hello,

Sorry for the delayed response due to me being off.

Azeem Sajid (2025/03/08 06:14 -0800):
> Both seem to work fine with `--log-failed`.
> Their respective archives contain the proper directory structure.
> 
> Regarding the directory structure:
> 
> - the files on the root contain the complete logs per job
> - the files in the directories are divided by steps (each directory represents one job).

Ah! I completely missed the fact that their is redundancy in the
provided arhcive!

> For older workflow runs, this directory structure is missing but the consolidated job logs exist.
> `gh` relies on this directory structure for parsing against the failed
> jobs.

I'lm sorry but I am not so clear about what the distinction is between
old and new jobs?

> I verified the same behavior with another repo as well.
> The older archives don't include the proper directory structure, the newer ones do.
> 
> Sample:
> 
> - Workflow: https://github.com/iamazeem/test/actions/workflows/ci-gh-intro.yml
> - Workflow run (**last week**): https://github.com/iamazeem/test/actions/runs/13602281839/job/38029622005
>   - Logs: https://github.com/iamazeem/test/suites/35061592725/logs?attempt=1
> - Workflow run (**today**): https://github.com/iamazeem/test/actions/runs/13738273749/job/38424550755
>   - Logs: https://github.com/iamazeem/test/suites/35392307248/logs?attempt=1
> 
> Please retrigger your workflow and verify if the same works for you.

Which workflow would you like me to re-trigger and how should I proceed
to verify what you are asking?

> If the above observed behavior is consistent, you should be able to
> observe the failure logs for the latest workflow run(s).

So is it to say that, for enhanced stability gh should not rely on the
directory structure as it may not always be present?
@iamazeem: Hey @shindere,

> I'lm sorry but I am not so clear about what the distinction is between
> old and new jobs?

I'm not sure about the exact time frame.
Roughly, compare two workflow runs with 24 hours difference.

> Which workflow would you like me to re-trigger and how should I proceed
> to verify what you are asking?

I was referring to your own repo.
You may trigger workflows and compare them.

> So is it to say that, for enhanced stability gh should not rely on the
> directory structure as it may not always be present?

Yes, apparently, it seems so.
However, its implementation is a bit involved.
If the server-side would be able to retain the directories then it won't be necessary.@shindere: Azeem Sajid (2025/03/17 10:11 -0700):
> iamazeem left a comment (cli/cli#10551)
> 
> Hey @shindere,
> 
> > I'lm sorry but I am not so clear about what the distinction is between
> > old and new jobs?
> 
> I'm not sure about the exact time frame.
> Roughly, compare two workflow runs with 24 hours difference.
> 
> > Which workflow would you like me to re-trigger and how should I proceed
> > to verify what you are asking?
> 
> I was referring to your own repo.
> You may trigger workflows and compare them.
> 
> > So is it to say that, for enhanced stability gh should not rely on the
> > directory structure as it may not always be present?
> 
> Yes, apparently, it seems so.
> However, its implementation is a bit involved.
> If the server-side would be able to retain the directories then it
> won't be necessary.

Is there any hope that we get support, explanations or clarifications
from GitHub themselves?

If not, honestly what feels the simplest to me would simply be not to
rely on the directories and the per-step files they contain and just
work with the files at the root.

I do realise that, this way, it would become much more involved to
implement the `--log-failed` option but in a way, at least for the time
being I would be more than happy if I had a way to just download the zip
file for manual post processing.

If that could be provided, then I would be able to experiment with this
Zip file and see which possprocessing tasks are the most common to me
and I could come back with suggestions on what to implement.

Or, if the downloaded archive is deemed too unstable, should we perhaps
use the API to `view` individual logs and store them into files?

Am I correct that there is kind of a discrepency in the API in the sense
that it is possible to get success or failure results on a per step
basis, whereas the logs can be downloaded only on a per-job basis, that
is with a more gross granularity?
@iamazeem: @shindere 

> Is there any hope that we get support, explanations or clarifications
> from GitHub themselves?

Hopefully, yes. 🤞
Let's wait for its triage.
The maintainers should be able to revert back with more details.

---

To unblock yourself to work on your PR, you can simply retrigger its workflow.
You may add [`workflow_dispatch`](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch) trigger also to manually trigger it if need be.
I did the same in my fork (https://github.com/iamazeem/stdcompat) to observe these discrepancies.@williammartin: Hey @shindere @iamazeem, thanks for the investigation here (and nudge on the other issue, because I didn't have visibility into this).

So, I agree that the API is producing zip archives in a different directory structure. I'm not clear on when this happens @iamazeem.

Above you linked two workflow runs:
 * https://github.com/iamazeem/test/actions/runs/13602281839/job/38029622005 March 1st
 * https://github.com/iamazeem/test/actions/runs/13738273749/job/38424550755 March 8th

For me, the first one displays logs correctly, and the later one doesn't. That seems counter to your statement:

> The older archives don't include the proper directory structure, the newer ones do.

What am I missing?

Some years ago I asked the API team to include a manifest in the archive to avoid this kind of thing, but sadly it didn't get prioritised.@iamazeem: Hey @williammartin,

> Above you linked two workflow runs:
> 
> * https://github.com/iamazeem/test/actions/runs/13602281839/job/38029622005 March 1st
> * https://github.com/iamazeem/test/actions/runs/13738273749/job/38424550755 March 8th
> 
> For me, the first one displays logs correctly, and the later one doesn't. That seems counter to your statement:
> 
> > The older archives don't include the proper directory structure, the newer ones do.
> 
> What am I missing?

It was the observed behavior at that time.
I just checked and it's reversed now.

`gh run view 13602281839 --log` is showing the logs.
But, `gh run view 13738273749 --log` is not.

I just checked the cache and it confirms that the step logs are there for `13602281839`:

```shell
$ ls -Gghl ~/.cache/gh/run-log-*.zip
-rw-rw-r-- 1  11K Mar 31 11:30 /home/azeem/.cache/gh/run-log-13602281839-1740812585.zip
-rw-rw-r-- 1 3.6K Mar 31 11:30 /home/azeem/.cache/gh/run-log-13738273749-1741442184.zip

$ unzip -l /home/azeem/.cache/gh/run-log-13602281839-1740812585.zip
Archive:  /home/azeem/.cache/gh/run-log-13602281839-1740812585.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      419  2025-03-31 06:30   Explore-GitHub-Actions/6_Run echo 💡 The iamazeemtest repository has been cloned to.txt
      329  2025-03-31 06:30   Explore-GitHub-Actions/9_Run echo 🍏 This job's status is success..txt
     1432  2025-03-31 06:30   Explore-GitHub-Actions/18_Post Check out repository code.txt
     5166  2025-03-31 06:30   Explore-GitHub-Actions/5_Check out repository code.txt
      419  2025-03-31 06:30   Explore-GitHub-Actions/3_Run echo 🐧 This job is now running on a Linux server host.txt
      425  2025-03-31 06:30   Explore-GitHub-Actions/7_Run echo 🖥️ The workflow is now ready to test your code o.txt
       58  2025-03-31 06:30   Explore-GitHub-Actions/19_Complete job.txt
     1529  2025-03-31 06:30   Explore-GitHub-Actions/1_Set up job.txt
      437  2025-03-31 06:30   Explore-GitHub-Actions/2_Run echo 🎉 The job was automatically triggered by a workf.txt
        0  2025-03-31 06:30   Explore-GitHub-Actions/
    12132  2025-03-31 06:30   0_Explore-GitHub-Actions.txt
     1005  2025-03-31 06:30   Explore-GitHub-Actions/8_List files in the repository.txt
      482  2025-03-31 06:30   -2147483648_Explore-GitHub-Actions.txt
      482  2025-03-31 06:30   Explore-GitHub-Actions/4_Run echo 🔎 The name of your branch is refsheadsmain and y.txt
---------                     -------
    24315                     14 files

$ unzip -l /home/azeem/.cache/gh/run-log-13738273749-1741442184.zip
Archive:  /home/azeem/.cache/gh/run-log-13738273749-1741442184.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    11704  1980-00-00 00:00   0_Explore-GitHub-Actions.txt
      418  1980-00-00 00:00   Explore-GitHub-Actions/system.txt
---------                     -------
    12122                     2 files
```

That is another thing to look at because the downloaded logs from the `Download log archive` button from each job shows different archive content which isn't consistent with what the `gh run view` downloads (most probably, different URLs are being hit here):

```shell
$ ls -Gghl logs_*.zip
-rw-rw-r-- 1 3.2K Mar 31 11:35 logs_35061592725.zip
-rw-rw-r-- 1 3.6K Mar 31 11:27 logs_35392307248.zip

$ unzip -l logs_35392307248.zip
Archive:  logs_35392307248.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    11704  1980-00-00 00:00   0_Explore-GitHub-Actions.txt
      418  1980-00-00 00:00   Explore-GitHub-Actions/system.txt
---------                     -------
    12122                     2 files

$ unzip -l logs_35061592725.zip
Archive:  logs_35061592725.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    11704  1980-00-00 00:00   0_Explore-GitHub-Actions.txt
---------                     -------
    11704                     1 file
```

Anyways, you might also want to observe these discrepancies by experimenting on your own test workflows.@williammartin: Alright thanks, important thing was that I was not misunderstanding your investigation.@kronn: Just as another datapoint: I get the later "unparseable" format whenever I check. The `gh run view` looks nice, tells me where the error is, but I need to look at the webpage instead of being able to get this via CLI. 

Example: `gh run view 14170868997 -R hitobito/hitobito_die_mitte [--log-failed]`

Is the solution here buried in the cracks between internal teams?@williammartin: I spoke to the folks who own the API about this and they confirmed that there is some kind of rollout happening, and it seems like the new service that produces the log zip creates a different structure. I'm not surprised this happened eventually, because these were never really intended to be programmatically consumed as far as I can tell.

But alas, here we are. I'm speaking with that team about options on their end, and in the meantime let's see what we can do client-side.

Sorry for the inconvenience.@shindere: A bunch of thanks for your commitment in all this, @williammartin.

Just to add a bit of context, as a visually impaired user of GitHub,
exploring the logs with the web interface is tremendously difficult, not
to say impossible. So to me, even with a non-programmatic use of the
archives, the fact that there is some form of stability in their content
does matter quite a lot.

I don't mind too much what the content really is, but I do care about
this content being stable and somehow documented.

Ideally this content would be in line with the checks API (I think) but
even if not, it would be nice if the archives could contain always the
same files for a given job, and if the file names could maybe be more
regular, i.e. the numbers at the beginning of file names are prefixed
with as many zeros as necessary for the alphabetic and numeric sorting
order to coincide, maybe the ordering is predictable.

Also, having an option to download either all the logs or just the logs
of failed jobs would be super helpful tome.Most of the time I would
request an archive with only the logs of failed jobs, because most of
the time that's the only thing that matters to me and it is already
difficult, with the current archive, to figure out which files are of
interest and which are not. Not even mentionning that localizing an
error in a log file with a 40 characters braille device is quite
involved.

For the moment I have written two little scripts, one to get rid of the
timestamps and one to get rid of the escape sequences (which are used to
colorize the output I guess).

Maybe I should approach these ANSI sequences in a more clever way than
just removing them as, if they encode color information supposed to be
useful to sighted persons, converting to something that conveys the
semantic information but does so in another way may be interesting.
@iamazeem: @shindere
Regarding the color codes, you can simply use non-TTY output.
For example, pipe the output of the `gh` commands to `cat`:

```shell
gh run view <RUN_ID> --log-failed | cat
```

That'll give you the non-colorized output i.e. no terminal escape codes.@shindere: Azeem Sajid (2025/03/31 10:12 -0700):
> iamazeem left a comment (cli/cli#10551)
> 
> @shindere
> Regarding the color codes, you can simply use non-TTY output.

There may be a misunderstanding here: I am talking about the color codes
that are in the .txt files you download, not of those produced by `gh`.

Also, for the woraroudn you suggest, I think it works indeed but IMO
it's not super nice in general that there is a strong coupling between
writing to a terminal and using color codes. I am talking in general
here, because for what I can tell `gh` does honour the `NO_COLOR=1`
variable, and I think it's super importnat that, even if terminal and
the use of color codes are correlated by default (which IMO is already
subject to discussion as far as accessibility is concerned) there should
still be a way to disable the color codes while writing to a terminal,
as for instance you may want to have progression bars but no colors.

See https://no-color.org/
@williammartin: > Also, having an option to download either all the logs or just the logs of failed jobs would be super helpful to me.

If we introduced the equivalent of `gh run view --log --download`, that downloaded the `zip` into your current working directory (or `gh run view --log --download <filepath>` to place it somewhere, would you find that valuable to integrate with the other tools you use?@shindere: William Martin (2025/03/31 10:55 -0700):
> williammartin left a comment (cli/cli#10551)
> 
> > Also, having an option to download either all the logs or just the logs of failed jobs would be super helpful to me.
> 
> If we introduced the equivalent of `gh run view --log --download`,
> that downloaded the `zip` into your current working directory (or `gh
> run view --log --download <filepath>` to place it somewhere, would you
> find that valuable to integrate with the other tools you use?

Very, yes, thanks a lot for offering this.

In terms of UI onemay want to think about the shape of the command twice
but I think this is an implementation detail. I mean, as long as the
feature is there I think Ican cope with whatever.

One thing that may be useful might be the possibility to configure a
download directory in Git's configuration if that makes sense to others?

Also, and sorry if wha  I am going to state is obvouis, I think the path
of the archive should either be precitable (ideal), or, if not, the tool
should print it somehow.
@williammartin: Moved the log download discussion to a new issue: https://github.com/cli/cli/issues/10711, thanks.@babakks: Thanks everyone in the thread for digging into this. 👏

As a summary, there is a new service producing ZIP archives that is being rolled out and it's not a stable interface. We’re going to fix the CLI to work with both formats for the time being.@shindere: Thanks. Even if not stable, can you still provide a bit information
about the ocntent of the Zip file, please? An insight on what trigggers
whether it's one of the formats or another which is produced would
definitely please / relieve me.

Thanks!
@babakks: Sure. Actually, it's not a two totally different formats case. Sometimes, the ZIP archive includes the logs for the individual steps, sometimes it's not. This is due to the internals of the API back-end. In either case there is a top-level log file in the archive. The CLI will fallback to the mentioned file if the step-wise logs were not present. So, the differentiation is based on the directory structure and missing step files.

I hope this answers your question.@babakks: **UPDATE:** Check the updated list in a few comments below ([here](#issuecomment-2783352293)).

Here's the C/A for the fix.

## Acceptance Criteria

**Given** I have a job run whose logs are produced by the new results service
**When** I run `gh run view --logs <JOB-ID>`
**Then** the job logs are displayed with proper step name on every line

**Given** I have a job run whose logs are produced by new results service without individual step logs
**When** I run `gh run view --logs <JOB-ID>`
**Then** the job logs are displayed with UNKNOWN placeholder for step name on every line@shindere: Many thanks for the explanations, @Babak.

> Sure. Actually, it's not a two totally different formats case.
> Sometimes, the ZIP archive includes the logs for the individual steps,
> sometimes it's not.

This indeed matches what had already been found out by others who have
investigated here.

Isthere any determinism in this `sometimes`? Any clue  so that we know
when we are going to receive what, or is it unpredictable?

> This is due to the internals of the API back-end. In either case there
> is a top-level log file in the archive. The CLI will fallback to the
> mentioned file if the step-wise logs were not present. So, the
> differentiation is based on the directory structure and missing step
> files.

This, too, had already been found out as I am sure you are aware of if
you read the thread.

> I hope this answers your question.

Not quite, see above.

Will you please also take into account the previoous comments where I
mentionned it would be awesome to have the possibility to download a zip
with only the log of failed builds?
@babakks: No worries.

> Isthere any determinism in this `sometimes`? Any clue  so that we know
when we are going to receive what, or is it unpredictable?

This is due to how the Actions API work. Unfortunately, we don't yet have a full understanding about the underlying causes and we're still following up on this internally. So, the best we can do here the CLI side is to make it aware and adapt to the possible cases.

> Will you please also take into account the previoous comments where I
mentionned it would be awesome to have the possibility to download a zip
with only the log of failed builds?

Sure. I noted this to bring it up in our internal comms with the Actions team. Not to mention the ZIP archive is created at the API side and this request involves introducing changes to it so that the CLI can use it.
@babakks: During investigation, we noticed another issue with legacy filename formats. So, the updated A/C is as below:

## Acceptance Criteria

### 1. Step logs present
**Given** I have a job run whose logs are produced by the new results service
**When** I run `gh run view --logs <JOB-ID>`
**Then** the job logs are displayed with proper step name on every line

### 2. Step logs missing
**Given** I have a job run whose logs are produced by new results service without individual step logs
**When** I run `gh run view --logs <JOB-ID>`
**Then** the job logs are displayed with UNKNOWN placeholder for step name on every line

### 3. Legacy filename format
**Given** I have a job run whose logs are named in the legacy format (e.g., `-2147483648_issue.txt`) and its individual step logs are missing
**When** I run `gh run view --logs <JOB-ID>`
**Then** the job logs are displayed with UNKNOWN placeholder for step name on every line
@babakks: Thanks everyone for helping spot the problem here. A fix has been landed and will be available in the next release, which is very soon. 🎉


Solution patch:
diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml
index 60354a953..a7b03f40d 100644
--- a/.github/workflows/deployment.yml
+++ b/.github/workflows/deployment.yml
@@ -50,7 +50,7 @@ jobs:
         with:
           go-version-file: 'go.mod'
       - name: Install GoReleaser
-        uses: goreleaser/goreleaser-action@v6
+        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
         with:
           version: "~1.17.1"
           install-only: true
@@ -103,7 +103,7 @@ jobs:
           security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain"
           rm "$RUNNER_TEMP/cert.p12"
       - name: Install GoReleaser
-        uses: goreleaser/goreleaser-action@v6
+        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
         with:
           version: "~1.17.1"
           install-only: true
@@ -157,7 +157,7 @@ jobs:
         with:
           go-version-file: 'go.mod'
       - name: Install GoReleaser
-        uses: goreleaser/goreleaser-action@v6
+        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
         with:
           version: "~1.17.1"
           install-only: true
@@ -196,7 +196,7 @@ jobs:
         run: script/release --local "$TAG_NAME" --platform windows
       - name: Set up MSBuild
         id: setupmsbuild
-        uses: microsoft/setup-msbuild@v2.0.0
+        uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce
       - name: Build MSI
         shell: bash
         env:
@@ -384,7 +384,7 @@ jobs:
             git diff --name-status @{upstream}..
           fi
       - name: Bump homebrew-core formula
-        uses: mislav/bump-homebrew-formula-action@v3
+        uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b
         if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
         with:
           formula-name: gh
diff --git a/.github/workflows/homebrew-bump.yml b/.github/workflows/homebrew-bump.yml
index 2bc395a1e..228f1a345 100644
--- a/.github/workflows/homebrew-bump.yml
+++ b/.github/workflows/homebrew-bump.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Bump homebrew-core formula
-        uses: mislav/bump-homebrew-formula-action@v3
+        uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b
         if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
         with:
           formula-name: gh
diff --git a/README.md b/README.md
index fbd99a5a8..cefe1abb0 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,7 @@ There are two common ways to verify a downloaded release, depending if `gh` is a
   $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \
         --new-bundle-format \
         --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
-        --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \
+        --certificate-identity="https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk" \
         gh_2.62.0_macOS_arm64.zip
   Verified OK
   ```
diff --git a/go.mod b/go.mod
index bea712a2d..e0f76dce8 100644
--- a/go.mod
+++ b/go.mod
@@ -7,9 +7,11 @@ toolchain go1.23.5
 require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/MakeNowJust/heredoc v1.0.0
+	github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
 	github.com/briandowns/spinner v1.18.1
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
+	github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5
 	github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
 	github.com/cli/go-gh/v2 v2.12.0
 	github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
@@ -22,13 +24,14 @@ require (
 	github.com/gabriel-vasile/mimetype v1.4.8
 	github.com/gdamore/tcell/v2 v2.5.4
 	github.com/golang/snappy v0.0.4
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/google/go-containerregistry v0.20.3
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	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.4
+	github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
 	github.com/in-toto/attestation v1.1.1
 	github.com/joho/godotenv v1.5.1
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
@@ -40,18 +43,18 @@ require (
 	github.com/opentracing/opentracing-go v1.2.0
 	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.3
-	github.com/sigstore/sigstore-go v0.7.0
-	github.com/spf13/cobra v1.8.1
+	github.com/sigstore/protobuf-specs v0.4.1
+	github.com/sigstore/sigstore-go v0.7.1
+	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
 	github.com/zalando/go-keyring v0.2.5
-	golang.org/x/crypto v0.35.0
-	golang.org/x/sync v0.12.0
-	golang.org/x/term v0.30.0
-	golang.org/x/text v0.23.0
-	google.golang.org/grpc v1.69.4
-	google.golang.org/protobuf v1.36.5
+	golang.org/x/crypto v0.37.0
+	golang.org/x/sync v0.13.0
+	golang.org/x/term v0.31.0
+	golang.org/x/text v0.24.0
+	google.golang.org/grpc v1.71.0
+	google.golang.org/protobuf v1.36.6
 	gopkg.in/h2non/gock.v1 v1.1.2
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -64,12 +67,17 @@ require (
 	github.com/alecthomas/chroma/v2 v2.14.0 // indirect
 	github.com/alessio/shellescape v1.4.2 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/blang/semver v3.5.1+incompatible // indirect
+	github.com/catppuccin/go v0.3.0 // indirect
+	github.com/charmbracelet/bubbles v0.21.0 // indirect
+	github.com/charmbracelet/bubbletea v1.3.4 // indirect
 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
 	github.com/charmbracelet/x/ansi v0.8.0 // indirect
 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+	github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
 	github.com/charmbracelet/x/term v0.2.1 // indirect
 	github.com/cli/browser v1.3.0 // indirect
 	github.com/cli/shurcooL-graphql v0.0.4 // indirect
@@ -82,30 +90,32 @@ require (
 	github.com/docker/cli v27.5.0+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.8.2 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/fatih/color v1.16.0 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/go-chi/chi v4.1.2+incompatible // indirect
 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/analysis v0.23.0 // indirect
-	github.com/go-openapi/errors v0.22.0 // indirect
+	github.com/go-openapi/errors v0.22.1 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/loads v0.22.0 // indirect
 	github.com/go-openapi/runtime v0.28.0 // indirect
 	github.com/go-openapi/spec v0.21.0 // indirect
 	github.com/go-openapi/strfmt v0.23.0 // indirect
-	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/go-openapi/validate v0.24.0 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
 	github.com/google/certificate-transparency-go v1.3.1 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/in-toto/in-toto-golang v0.9.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -116,40 +126,42 @@ require (
 	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mailru/easyjson v0.9.0 // indirect
+	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
 	github.com/muesli/termenv v0.16.0 // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rodaine/table v1.0.1 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/sagikazarmark/locafero v0.4.0 // indirect
-	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/sagikazarmark/locafero v0.7.0 // indirect
 	github.com/sassoftware/relic v7.2.1+incompatible // indirect
 	github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
 	github.com/shibumi/go-pathspec v1.3.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
-	github.com/sigstore/rekor v1.3.8 // indirect
-	github.com/sigstore/sigstore v1.8.12 // indirect
-	github.com/sigstore/timestamp-authority v1.2.4 // indirect
+	github.com/sigstore/rekor v1.3.9 // indirect
+	github.com/sigstore/sigstore v1.9.1 // indirect
+	github.com/sigstore/timestamp-authority v1.2.5 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
-	github.com/spf13/afero v1.11.0 // indirect
-	github.com/spf13/cast v1.7.0 // indirect
-	github.com/spf13/viper v1.19.0 // indirect
+	github.com/spf13/afero v1.12.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
+	github.com/spf13/viper v1.20.1 // indirect
 	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
@@ -163,18 +175,17 @@ require (
 	github.com/yuin/goldmark-emoji v1.0.5 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel v1.33.0 // indirect
-	go.opentelemetry.io/otel/metric v1.33.0 // indirect
-	go.opentelemetry.io/otel/trace v1.33.0 // indirect
+	go.opentelemetry.io/otel v1.34.0 // indirect
+	go.opentelemetry.io/otel/metric v1.34.0 // indirect
+	go.opentelemetry.io/otel/trace v1.34.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
-	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
 	golang.org/x/tools v0.29.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 2b5a31212..5441c96c5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,17 @@
-cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
-cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
-cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
-cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
-cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
-cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
-cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
+cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME=
+cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc=
+cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
+cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
+cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
+cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
-cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
-cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
-cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A=
-cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc=
-cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
-cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
+cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM=
+cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM=
+cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk=
+cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE=
+cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q=
+cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY=
 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -21,18 +20,18 @@ github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjq
 github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
 github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
 github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -53,36 +52,38 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u
 github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
-github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
-github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
-github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
-github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
-github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
-github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk=
-github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
-github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
-github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
+github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
+github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc=
+github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
@@ -95,22 +96,32 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
 github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
+github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
+github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
+github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
 github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g=
 github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s=
+github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 h1:uOnMxWghHfEYm2DPMeIHHAEirV/TduBVC9ZRXGcX9Q8=
+github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5/go.mod h1:xl27E/xNaX3WwdkqpvBwjJcGWhupkU52CWLC5hReBTw=
 github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys=
 github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
 github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
 github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
 github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
 github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
-github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
-github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
+github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
@@ -131,7 +142,6 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
 github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
 github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -160,6 +170,10 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
@@ -167,8 +181,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
 github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
@@ -177,8 +191,6 @@ github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/
 github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
 github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
 github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
-github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
-github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
 github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -188,8 +200,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
 github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
-github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
-github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
+github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
+github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -202,32 +214,34 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
 github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
 github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
 github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
 github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go=
 github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
-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-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
 github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
 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.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
-github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
@@ -236,8 +250,8 @@ github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek
 github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
-github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
 github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
 github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -265,13 +279,12 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S
 github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
 github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
 github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 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.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
-github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
+github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4=
+github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA=
 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=
@@ -330,16 +343,16 @@ github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2T
 github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -356,10 +369,16 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@@ -378,8 +397,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
 github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -387,12 +406,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
-github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
+github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
@@ -409,10 +428,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
-github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
-github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
 github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
 github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
 github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
@@ -429,51 +446,44 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
 github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
-github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g=
-github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU=
-github.com/sigstore/rekor v1.3.8 h1:B8kJI8mpSIXova4Jxa6vXdJyysRxFGsEsLKBDl0rRjA=
-github.com/sigstore/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI=
-github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc=
-github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo=
-github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48=
-github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA=
-github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE=
-github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk=
-github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY=
-github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12/go.mod h1:NkPiz4XA0JcBSXzJUrjMj7Xi7oSTew1Ip3Zmt56mHlw=
-github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12 h1:kweBChR6M9FEvmxN3BMEcl7SNnwxTwKF7THYFKLOE5U=
-github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12/go.mod h1:6+d+A6oYt1W5OgtzgEVb21V7tAZ/C2Ihtzc5MNJbayY=
-github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs=
-github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12/go.mod h1:2uEeOb8xE2RC6OvzxKux1wkS39Zv8gA27z92m49xUTc=
-github.com/sigstore/timestamp-authority v1.2.4 h1:RjXZxOWorEiem/uSr0pFHVtQpyzpcFxgugo5jVqm3mw=
-github.com/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik=
+github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc=
+github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
+github.com/sigstore/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU=
+github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM=
+github.com/sigstore/sigstore v1.9.1 h1:bNMsfFATsMPaagcf+uppLk4C9rQZ2dh5ysmCxQBYWaw=
+github.com/sigstore/sigstore v1.9.1/go.mod h1:zUoATYzR1J3rLNp3jmp4fzIJtWdhC3ZM6MnpcBtnsE4=
+github.com/sigstore/sigstore-go v0.7.1 h1:lyzi3AjO6+BHc5zCf9fniycqPYOt3RaC08M/FRmQhVY=
+github.com/sigstore/sigstore-go v0.7.1/go.mod h1:AIRj4I3LC82qd07VFm3T2zXYiddxeBV1k/eoS8nTz0E=
+github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1 h1:/YcNq687WnXpIRXl04nLfJX741G4iW+w+7Nem2Zy0f4=
+github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1/go.mod h1:ApL9RpKsi7gkSYN0bMNdm/3jZ9EefxMmfYHfUmq2ZYM=
+github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1 h1:FnusXyTIInnwfIOzzl5PFilRm1I97dxMSOcCkZBu9Kc=
+github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1/go.mod h1:d5m5LOa/69a+t2YC9pDPwS1n2i/PhqB4cUKbpVDlKKE=
+github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1 h1:LFiYK1DEWQ6Hf/nroFzBMM+s5rVSjVL45Alpb5Ctl5A=
+github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1/go.mod h1:GFyFmDsE2wDuIHZD+4+JErGpA0S4zJsKNz5l2JVJd8s=
+github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1 h1:sIW6xe4yU5eIMH8fve2C78d+r29KmHnIb+7po+80bsY=
+github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1/go.mod h1:3pNf99GnK9eu3XUa5ebHzgEQSVYf9hqAoPFwbwD6O6M=
+github.com/sigstore/timestamp-authority v1.2.5 h1:W22JmwRv1Salr/NFFuP7iJuhytcZszQjldoB8GiEdnw=
+github.com/sigstore/timestamp-authority v1.2.5/go.mod h1:gWPKWq4HMWgPCETre0AakgBzcr9DRqHrsgbrRqsigOs=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-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.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
-github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-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.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=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -484,6 +494,12 @@ github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if37
 github.com/theupdateframework/go-tuf/v2 v2.0.2/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/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
+github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
+github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
+github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw=
+github.com/tink-crypto/tink-go/v2 v2.3.0 h1:4/TA0lw0lA/iVKBL9f8R5eP7397bfc4antAMXF5JRhs=
+github.com/tink-crypto/tink-go/v2 v2.3.0/go.mod h1:kfPOtXIadHlekBTeBtJrHWqoGL+Fm3JQg0wtltPuxLU=
 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
 github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
@@ -504,22 +520,22 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd
 go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
-go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
-go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
-go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
-go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
-go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
-go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
-go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
-go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
-go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
-go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
-go.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU=
-go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.step.sm/crypto v0.60.0 h1:UgSw8DFG5xUOGB3GUID17UA32G4j1iNQ4qoMhBmsVFw=
+go.step.sm/crypto v0.60.0/go.mod h1:Ep83Lv818L4gV0vhFTdPWRKnL6/5fRMpi8SaoP5ArSw=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -528,73 +544,72 @@ 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.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
-golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
-golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
-golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 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=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
-golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 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.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
 golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY=
-google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI=
-google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
-google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
-google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
-google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc=
+google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
 gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/config/config.go b/internal/config/config.go
index 476154d66..e7534dfdb 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -17,6 +17,7 @@ import (
 const (
 	aliasesKey            = "aliases"
 	browserKey            = "browser"
+	colorLabelsKey        = "color_labels"
 	editorKey             = "editor"
 	gitProtocolKey        = "git_protocol"
 	hostsKey              = "hosts"
@@ -113,6 +114,11 @@ func (c *cfg) Browser(hostname string) gh.ConfigEntry {
 	return c.GetOrDefault(hostname, browserKey).Unwrap()
 }
 
+func (c *cfg) ColorLabels(hostname string) gh.ConfigEntry {
+	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
+	return c.GetOrDefault(hostname, colorLabelsKey).Unwrap()
+}
+
 func (c *cfg) Editor(hostname string) gh.ConfigEntry {
 	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
 	return c.GetOrDefault(hostname, editorKey).Unwrap()
@@ -532,6 +538,8 @@ aliases:
 http_unix_socket:
 # What web browser gh should use when opening URLs. If blank, will refer to environment.
 browser:
+# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
+color_labels: disabled
 `
 
 type ConfigOption struct {
@@ -602,6 +610,15 @@ var Options = []ConfigOption{
 			return c.Browser(hostname).Value
 		},
 	},
+	{
+		Key:           colorLabelsKey,
+		Description:   "whether to display labels using their RGB hex color codes in terminals that support truecolor",
+		DefaultValue:  "disabled",
+		AllowedValues: []string{"enabled", "disabled"},
+		CurrentValue: func(c gh.Config, hostname string) string {
+			return c.ColorLabels(hostname).Value
+		},
+	},
 }
 
 func HomeDirPath(subdir string) (string, error) {
diff --git a/internal/config/stub.go b/internal/config/stub.go
index 71d44556d..78073da4a 100644
--- a/internal/config/stub.go
+++ b/internal/config/stub.go
@@ -55,6 +55,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
 	mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.Browser(hostname)
 	}
+	mock.ColorLabelsFunc = func(hostname string) gh.ConfigEntry {
+		return cfg.ColorLabels(hostname)
+	}
 	mock.EditorFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.Editor(hostname)
 	}
diff --git a/internal/gh/gh.go b/internal/gh/gh.go
index 8e640c41a..b17c6bd67 100644
--- a/internal/gh/gh.go
+++ b/internal/gh/gh.go
@@ -37,6 +37,8 @@ type Config interface {
 
 	// Browser returns the configured browser, optionally scoped by host.
 	Browser(hostname string) ConfigEntry
+	// ColorLabels returns the configured color_label setting, optionally scoped by host.
+	ColorLabels(hostname string) ConfigEntry
 	// Editor returns the configured editor, optionally scoped by host.
 	Editor(hostname string) ConfigEntry
 	// GitProtocol returns the configured git protocol, optionally scoped by host.
diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go
index 569af1fac..b94cb084d 100644
--- a/internal/gh/mock/config.go
+++ b/internal/gh/mock/config.go
@@ -31,6 +31,9 @@ var _ gh.Config = &ConfigMock{}
 //			CacheDirFunc: func() string {
 //				panic("mock out the CacheDir method")
 //			},
+//			ColorLabelsFunc: func(hostname string) gh.ConfigEntry {
+//				panic("mock out the ColorLabels method")
+//			},
 //			EditorFunc: func(hostname string) gh.ConfigEntry {
 //				panic("mock out the Editor method")
 //			},
@@ -83,6 +86,9 @@ type ConfigMock struct {
 	// CacheDirFunc mocks the CacheDir method.
 	CacheDirFunc func() string
 
+	// ColorLabelsFunc mocks the ColorLabels method.
+	ColorLabelsFunc func(hostname string) gh.ConfigEntry
+
 	// EditorFunc mocks the Editor method.
 	EditorFunc func(hostname string) gh.ConfigEntry
 
@@ -132,6 +138,11 @@ type ConfigMock struct {
 		// CacheDir holds details about calls to the CacheDir method.
 		CacheDir []struct {
 		}
+		// ColorLabels holds details about calls to the ColorLabels method.
+		ColorLabels []struct {
+			// Hostname is the hostname argument value.
+			Hostname string
+		}
 		// Editor holds details about calls to the Editor method.
 		Editor []struct {
 			// Hostname is the hostname argument value.
@@ -194,6 +205,7 @@ type ConfigMock struct {
 	lockAuthentication     sync.RWMutex
 	lockBrowser            sync.RWMutex
 	lockCacheDir           sync.RWMutex
+	lockColorLabels        sync.RWMutex
 	lockEditor             sync.RWMutex
 	lockGetOrDefault       sync.RWMutex
 	lockGitProtocol        sync.RWMutex
@@ -320,6 +332,38 @@ func (mock *ConfigMock) CacheDirCalls() []struct {
 	return calls
 }
 
+// ColorLabels calls ColorLabelsFunc.
+func (mock *ConfigMock) ColorLabels(hostname string) gh.ConfigEntry {
+	if mock.ColorLabelsFunc == nil {
+		panic("ConfigMock.ColorLabelsFunc: method is nil but Config.ColorLabels was just called")
+	}
+	callInfo := struct {
+		Hostname string
+	}{
+		Hostname: hostname,
+	}
+	mock.lockColorLabels.Lock()
+	mock.calls.ColorLabels = append(mock.calls.ColorLabels, callInfo)
+	mock.lockColorLabels.Unlock()
+	return mock.ColorLabelsFunc(hostname)
+}
+
+// ColorLabelsCalls gets all the calls that were made to ColorLabels.
+// Check the length with:
+//
+//	len(mockedConfig.ColorLabelsCalls())
+func (mock *ConfigMock) ColorLabelsCalls() []struct {
+	Hostname string
+} {
+	var calls []struct {
+		Hostname string
+	}
+	mock.lockColorLabels.RLock()
+	calls = mock.calls.ColorLabels
+	mock.lockColorLabels.RUnlock()
+	return calls
+}
+
 // Editor calls EditorFunc.
 func (mock *ConfigMock) Editor(hostname string) gh.ConfigEntry {
 	if mock.EditorFunc == nil {
diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go
index 1d4b11cbc..6ef61cf15 100644
--- a/internal/prompter/prompter.go
+++ b/internal/prompter/prompter.go
@@ -2,9 +2,12 @@ package prompter
 
 import (
 	"fmt"
+	"os"
+	"slices"
 	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
+	"github.com/charmbracelet/huh"
 	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/pkg/surveyext"
 	ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
@@ -13,20 +16,46 @@ import (
 //go:generate moq -rm -out prompter_mock.go . Prompter
 type Prompter interface {
 	// generic prompts from go-gh
-	Select(string, string, []string) (int, error)
+
+	// Select prompts the user to select an option from a list of options.
+	Select(prompt string, defaultValue string, options []string) (int, error)
+	// MultiSelect prompts the user to select one or more options from a list of options.
 	MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
-	Input(string, string) (string, error)
-	Password(string) (string, error)
-	Confirm(string, bool) (bool, error)
+	// Input prompts the user to enter a string value.
+	Input(prompt string, defaultValue string) (string, error)
+	// Password prompts the user to enter a password.
+	Password(prompt string) (string, error)
+	// Confirm prompts the user to confirm an action.
+	Confirm(prompt string, defaultValue bool) (bool, error)
 
 	// gh specific prompts
+
+	// AuthToken prompts the user to enter an authentication token.
 	AuthToken() (string, error)
-	ConfirmDeletion(string) error
+	// ConfirmDeletion prompts the user to confirm deletion of a resource by
+	// typing the requiredValue.
+	ConfirmDeletion(requiredValue string) error
+	// InputHostname prompts the user to enter a hostname.
 	InputHostname() (string, error)
-	MarkdownEditor(string, string, bool) (string, error)
+	// MarkdownEditor prompts the user to edit a markdown document in an editor.
+	// If blankAllowed is true, the user can skip the editor and an empty string
+	// will be returned.
+	MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
 }
 
 func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter {
+	accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
+	falseyValues := []string{"false", "0", "no", ""}
+
+	if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) {
+		return &accessiblePrompter{
+			stdin:     stdin,
+			stdout:    stdout,
+			stderr:    stderr,
+			editorCmd: editorCmd,
+		}
+	}
+
 	return &surveyPrompter{
 		prompter:  ghPrompter.New(stdin, stdout, stderr),
 		stdin:     stdin,
@@ -36,6 +65,209 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr
 	}
 }
 
+type accessiblePrompter struct {
+	stdin     ghPrompter.FileReader
+	stdout    ghPrompter.FileWriter
+	stderr    ghPrompter.FileWriter
+	editorCmd string
+}
+
+func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form {
+	return huh.NewForm(groups...).
+		WithTheme(huh.ThemeBase16()).
+		WithAccessible(true).
+		WithInput(p.stdin).
+		WithOutput(p.stdout)
+}
+
+func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) {
+	var result int
+	formOptions := []huh.Option[int]{}
+	for i, o := range options {
+		formOptions = append(formOptions, huh.NewOption(o, i))
+	}
+
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewSelect[int]().
+				Title(prompt).
+				Value(&result).
+				Options(formOptions...),
+		),
+	)
+
+	err := form.Run()
+	return result, err
+}
+
+func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
+	var result []int
+	formOptions := make([]huh.Option[int], len(options))
+	for i, o := range options {
+		formOptions[i] = huh.NewOption(o, i)
+	}
+
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewMultiSelect[int]().
+				Title(prompt).
+				Value(&result).
+				Limit(len(options)).
+				Options(formOptions...),
+		),
+	)
+
+	if err := form.Run(); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) {
+	result := defaultValue
+	prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue)
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewInput().
+				Title(prompt).
+				Value(&result),
+		),
+	)
+
+	err := form.Run()
+	return result, err
+}
+
+func (p *accessiblePrompter) Password(prompt string) (string, error) {
+	var result string
+	// EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode.
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewInput().
+				Title(prompt).
+				Value(&result),
+		),
+	)
+
+	err := form.Run()
+	if err != nil {
+		return "", err
+	}
+
+	return result, nil
+}
+
+func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
+	result := defaultValue
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewConfirm().
+				Title(prompt).
+				Value(&result),
+		),
+	)
+	if err := form.Run(); err != nil {
+		return false, err
+	}
+	return result, nil
+}
+
+func (p *accessiblePrompter) AuthToken() (string, error) {
+	var result string
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewInput().
+				Title("Paste your authentication token:").
+				// Note: if this validation fails, the prompt loops.
+				Validate(func(input string) error {
+					if input == "" {
+						return fmt.Errorf("token is required")
+					}
+					return nil
+				}).
+				Value(&result),
+			// This doesn't have any effect in accessible mode.
+			// EchoMode(huh.EchoModePassword),
+		),
+	)
+
+	err := form.Run()
+	return result, err
+}
+
+func (p *accessiblePrompter) ConfirmDeletion(requiredValue string) error {
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewInput().
+				Title(fmt.Sprintf("Type %q to confirm deletion", requiredValue)).
+				Validate(func(input string) error {
+					if input != requiredValue {
+						return fmt.Errorf("You entered: %q", input)
+					}
+					return nil
+				}),
+		),
+	)
+
+	return form.Run()
+}
+
+func (p *accessiblePrompter) InputHostname() (string, error) {
+	var result string
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewInput().
+				Title("Hostname:").
+				Validate(ghinstance.HostnameValidator).
+				Value(&result),
+		),
+	)
+
+	err := form.Run()
+	if err != nil {
+		return "", err
+	}
+	return result, nil
+}
+
+func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
+	var result string
+	skipOption := "skip"
+	launchOption := "launch"
+	options := []huh.Option[string]{
+		huh.NewOption(fmt.Sprintf("Launch %s", surveyext.EditorName(p.editorCmd)), launchOption),
+	}
+	if blankAllowed {
+		options = append(options, huh.NewOption("Skip", skipOption))
+	}
+
+	form := p.newForm(
+		huh.NewGroup(
+			huh.NewSelect[string]().
+				Title(prompt).
+				Options(options...).
+				Value(&result),
+		),
+	)
+
+	if err := form.Run(); err != nil {
+		return "", err
+	}
+
+	if result == skipOption {
+		return "", nil
+	}
+
+	// launchOption was selected
+	text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr)
+	if err != nil {
+		return "", err
+	}
+
+	return text, nil
+}
+
 type surveyPrompter struct {
 	prompter  *ghPrompter.Prompter
 	stdin     ghPrompter.FileReader
diff --git a/internal/tableprinter/table_printer.go b/internal/tableprinter/table_printer.go
index 69b22be12..47128afb4 100644
--- a/internal/tableprinter/table_printer.go
+++ b/internal/tableprinter/table_printer.go
@@ -73,7 +73,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch
 		// was not padded. In tests cs.Enabled() is false which allows us to avoid having to fix up
 		// numerous tests that verify header padding.
 		var paddingFunc func(int, string) string
-		if cs.Enabled() {
+		if cs.Enabled {
 			paddingFunc = text.PadRight
 		}
 
diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go
index 598be0747..6286c999d 100644
--- a/pkg/cmd/factory/default.go
+++ b/pkg/cmd/factory/default.go
@@ -293,6 +293,17 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
 		io.SetPager(pager.Value)
 	}
 
+	if ghColorLabels, ghColorLabelsExists := os.LookupEnv("GH_COLOR_LABELS"); ghColorLabelsExists {
+		switch ghColorLabels {
+		case "", "0", "false", "no":
+			io.SetColorLabels(false)
+		default:
+			io.SetColorLabels(true)
+		}
+	} else if prompt := cfg.ColorLabels(""); prompt.Value == "enabled" {
+		io.SetColorLabels(true)
+	}
+
 	io.SetAccessibleColorsEnabled(xcolor.IsAccessibleColorsEnabled())
 
 	return io
diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go
index 5392b997e..4f51bed25 100644
--- a/pkg/cmd/gist/create/create.go
+++ b/pkg/cmd/gist/create/create.go
@@ -138,7 +138,7 @@ func createRun(opts *CreateOptions) error {
 			processMessage = fmt.Sprintf("Creating gist %s", gistName)
 		}
 	}
-	fmt.Fprintf(errOut, "%s %s\n", cs.Gray("-"), processMessage)
+	fmt.Fprintf(errOut, "%s %s\n", cs.Muted("-"), processMessage)
 
 	httpClient, err := opts.HttpClient()
 	if err != nil {
diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go
index 53b577e4c..fc63f56ce 100644
--- a/pkg/cmd/gist/shared/shared.go
+++ b/pkg/cmd/gist/shared/shared.go
@@ -230,7 +230,7 @@ func PromptGists(prompter prompter.Prompter, client *http.Client, host string, c
 	for i, gist := range gists {
 		gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
 		// TODO: support dynamic maxWidth
-		opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime))
+		opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Muted(gistTime))
 	}
 
 	result, err := prompter.Select("Select a gist", "", opts)
diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go
index 705f8f703..f789c5b04 100644
--- a/pkg/cmd/gist/view/view.go
+++ b/pkg/cmd/gist/view/view.go
@@ -140,7 +140,7 @@ func viewRun(opts *ViewOptions) error {
 			if len(gist.Files) == 1 || opts.Filename != "" {
 				return fmt.Errorf("error: file is binary")
 			}
-			_, err = fmt.Fprintln(opts.IO.Out, cs.Gray("(skipping rendering binary content)"))
+			_, err = fmt.Fprintln(opts.IO.Out, cs.Muted("(skipping rendering binary content)"))
 			return nil
 		}
 
@@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error {
 
 	for i, fn := range filenames {
 		if showFilenames {
-			fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Gray(fn))
+			fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Muted(fn))
 		}
 		if err := render(gist.Files[fn]); err != nil {
 			return err
diff --git a/pkg/cmd/issue/shared/display.go b/pkg/cmd/issue/shared/display.go
index 0c56ffd2c..24a5137e3 100644
--- a/pkg/cmd/issue/shared/display.go
+++ b/pkg/cmd/issue/shared/display.go
@@ -38,13 +38,13 @@ func PrintIssues(io *iostreams.IOStreams, now time.Time, prefix string, totalCou
 		}
 		table.AddField(text.RemoveExcessiveWhitespace(issue.Title))
 		table.AddField(issueLabelList(&issue, cs, isTTY))
-		table.AddTimeField(now, issue.UpdatedAt, cs.Gray)
+		table.AddTimeField(now, issue.UpdatedAt, cs.Muted)
 		table.EndRow()
 	}
 	_ = table.Render()
 	remaining := totalCount - len(issues)
 	if remaining > 0 {
-		fmt.Fprintf(io.Out, cs.Gray("%sAnd %d more\n"), prefix, remaining)
+		fmt.Fprintf(io.Out, cs.Muted("%sAnd %d more\n"), prefix, remaining)
 	}
 }
 
@@ -56,7 +56,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme, colorize bool)
 	labelNames := make([]string, 0, len(issue.Labels.Nodes))
 	for _, label := range issue.Labels.Nodes {
 		if colorize {
-			labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
+			labelNames = append(labelNames, cs.Label(label.Color, label.Name))
 		} else {
 			labelNames = append(labelNames, label.Name)
 		}
diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go
index b188c6a4c..8e3aa6040 100644
--- a/pkg/cmd/issue/view/view.go
+++ b/pkg/cmd/issue/view/view.go
@@ -228,7 +228,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue
 	var md string
 	var err error
 	if issue.Body == "" {
-		md = fmt.Sprintf("\n  %s\n\n", cs.Gray("No description provided"))
+		md = fmt.Sprintf("\n  %s\n\n", cs.Muted("No description provided"))
 	} else {
 		md, err = markdown.Render(issue.Body,
 			markdown.WithTheme(opts.IO.TerminalTheme()),
@@ -250,7 +250,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue
 	}
 
 	// Footer
-	fmt.Fprintf(out, cs.Gray("View this issue on GitHub: %s\n"), issue.URL)
+	fmt.Fprintf(out, cs.Muted("View this issue on GitHub: %s\n"), issue.URL)
 
 	return nil
 }
@@ -317,7 +317,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme) string {
 		if cs == nil {
 			labelNames[i] = label.Name
 		} else {
-			labelNames[i] = cs.HexToRGB(label.Color, label.Name)
+			labelNames[i] = cs.Label(label.Color, label.Name)
 		}
 	}
 
diff --git a/pkg/cmd/label/list.go b/pkg/cmd/label/list.go
index fe1e9cdb7..4fe2f5ec9 100644
--- a/pkg/cmd/label/list.go
+++ b/pkg/cmd/label/list.go
@@ -137,7 +137,12 @@ func printLabels(io *iostreams.IOStreams, labels []label) error {
 	table := tableprinter.New(io, tableprinter.WithHeader("NAME", "DESCRIPTION", "COLOR"))
 
 	for _, label := range labels {
-		table.AddField(label.Name, tableprinter.WithColor(cs.ColorFromRGB(label.Color)))
+		// Colorize the label using tableprinter's WithColor function for it to handle non-TTY situations
+		labelColor := tableprinter.WithColor(func(s string) string {
+			return cs.Label(label.Color, s)
+		})
+
+		table.AddField(label.Name, labelColor)
 		table.AddField(label.Description)
 		table.AddField("#" + label.Color)
 
diff --git a/pkg/cmd/pr/checks/output.go b/pkg/cmd/pr/checks/output.go
index 5d2f080c6..24105c3e2 100644
--- a/pkg/cmd/pr/checks/output.go
+++ b/pkg/cmd/pr/checks/output.go
@@ -30,7 +30,7 @@ func addRow(tp *tableprinter.TablePrinter, io *iostreams.IOStreams, o check) {
 		markColor = cs.Yellow
 	case "skipping", "cancel":
 		mark = "-"
-		markColor = cs.Gray
+		markColor = cs.Muted
 	}
 
 	if io.IsStdoutTTY() {
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index 0066bbc6e..5f8979c11 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -879,37 +879,37 @@ func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *s
 }
 
 func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}, state *shared.IssueMetadataState) error {
-	iofmt := io.ColorScheme()
+	cs := io.ColorScheme()
 	out := io.Out
 
 	fmt.Fprint(out, "Would have created a Pull Request with:\n")
-	fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Title"), params["title"].(string))
-	fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("Draft"), params["draft"])
-	fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Base"), params["baseRefName"])
-	fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Head"), params["headRefName"])
+	fmt.Fprintf(out, "%s: %s\n", cs.Bold("Title"), params["title"].(string))
+	fmt.Fprintf(out, "%s: %t\n", cs.Bold("Draft"), params["draft"])
+	fmt.Fprintf(out, "%s: %s\n", cs.Bold("Base"), params["baseRefName"])
+	fmt.Fprintf(out, "%s: %s\n", cs.Bold("Head"), params["headRefName"])
 	if len(state.Labels) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Labels"), strings.Join(state.Labels, ", "))
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Labels"), strings.Join(state.Labels, ", "))
 	}
 	if len(state.Reviewers) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Reviewers"), strings.Join(state.Reviewers, ", "))
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Reviewers"), strings.Join(state.Reviewers, ", "))
 	}
 	if len(state.Assignees) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Assignees"), strings.Join(state.Assignees, ", "))
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Assignees"), strings.Join(state.Assignees, ", "))
 	}
 	if len(state.Milestones) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Milestones"), strings.Join(state.Milestones, ", "))
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), strings.Join(state.Milestones, ", "))
 	}
 	if len(state.Projects) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Projects"), strings.Join(state.Projects, ", "))
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.Projects, ", "))
 	}
-	fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("MaintainerCanModify"), params["maintainerCanModify"])
+	fmt.Fprintf(out, "%s: %t\n", cs.Bold("MaintainerCanModify"), params["maintainerCanModify"])
 
-	fmt.Fprintf(out, "%s\n", iofmt.Bold("Body:"))
+	fmt.Fprintf(out, "%s\n", cs.Bold("Body:"))
 	// Body
 	var md string
 	var err error
 	if len(params["body"].(string)) == 0 {
-		md = fmt.Sprintf("%s\n", iofmt.Gray("No description provided"))
+		md = fmt.Sprintf("%s\n", cs.Muted("No description provided"))
 	} else {
 		md, err = markdown.Render(params["body"].(string),
 			markdown.WithTheme(io.TerminalTheme()),
diff --git a/pkg/cmd/pr/review/review.go b/pkg/cmd/pr/review/review.go
index 25f81d973..cafa6ce8f 100644
--- a/pkg/cmd/pr/review/review.go
+++ b/pkg/cmd/pr/review/review.go
@@ -191,7 +191,7 @@ func reviewRun(opts *ReviewOptions) error {
 
 	switch reviewData.State {
 	case api.ReviewComment:
-		fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Gray("-"), ghrepo.FullName(baseRepo), pr.Number)
+		fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Muted("-"), ghrepo.FullName(baseRepo), pr.Number)
 	case api.ReviewApprove:
 		fmt.Fprintf(opts.IO.ErrOut, "%s Approved pull request %s#%d\n", cs.SuccessIcon(), ghrepo.FullName(baseRepo), pr.Number)
 	case api.ReviewRequestChanges:
diff --git a/pkg/cmd/pr/shared/comments.go b/pkg/cmd/pr/shared/comments.go
index a05108d7b..7c6e9154c 100644
--- a/pkg/cmd/pr/shared/comments.go
+++ b/pkg/cmd/pr/shared/comments.go
@@ -62,7 +62,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul
 	hiddenCount := totalCount - retrievedCount
 
 	if preview && hiddenCount > 0 {
-		fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
+		fmt.Fprint(&b, cs.Muted(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
 		fmt.Fprintf(&b, "\n\n\n")
 	}
 
@@ -79,7 +79,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul
 	}
 
 	if preview && hiddenCount > 0 {
-		fmt.Fprint(&b, cs.Gray("Use --comments to view the full conversation"))
+		fmt.Fprint(&b, cs.Muted("Use --comments to view the full conversation"))
 		fmt.Fprintln(&b)
 	}
 
@@ -122,7 +122,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin
 	var md string
 	var err error
 	if comment.Content() == "" {
-		md = fmt.Sprintf("\n  %s\n\n", cs.Gray("No body provided"))
+		md = fmt.Sprintf("\n  %s\n\n", cs.Muted("No body provided"))
 	} else {
 		md, err = markdown.Render(comment.Content(),
 			markdown.WithTheme(io.TerminalTheme()),
@@ -135,7 +135,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin
 
 	// Footer
 	if comment.Link() != "" {
-		fmt.Fprintf(&b, cs.Gray("View the full review: %s\n\n"), comment.Link())
+		fmt.Fprintf(&b, cs.Muted("View the full review: %s\n\n"), comment.Link())
 	}
 
 	return b.String(), nil
diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go
index 02482951c..b4d83c719 100644
--- a/pkg/cmd/pr/shared/display.go
+++ b/pkg/cmd/pr/shared/display.go
@@ -60,7 +60,7 @@ func PrintHeader(io *iostreams.IOStreams, s string) {
 }
 
 func PrintMessage(io *iostreams.IOStreams, s string) {
-	fmt.Fprintln(io.Out, io.ColorScheme().Gray(s))
+	fmt.Fprintln(io.Out, io.ColorScheme().Muted(s))
 }
 
 func ListNoResults(repoName string, itemName string, hasFilters bool) error {
@@ -83,7 +83,7 @@ func ListHeader(repoName string, itemName string, matchCount int, totalMatchCoun
 }
 
 func PrCheckStatusSummaryWithColor(cs *iostreams.ColorScheme, checks api.PullRequestChecksStatus) string {
-	var summary = cs.Gray("No checks")
+	var summary = cs.Muted("No checks")
 	if checks.Total > 0 {
 		if checks.Failing > 0 {
 			if checks.Failing == checks.Total {
diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go
index b7b390bf2..eb120e5a7 100644
--- a/pkg/cmd/pr/status/status.go
+++ b/pkg/cmd/pr/status/status.go
@@ -316,6 +316,6 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) {
 	}
 	remaining := totalCount - len(prs)
 	if remaining > 0 {
-		fmt.Fprintf(w, cs.Gray("  And %d more\n"), remaining)
+		fmt.Fprintln(w, cs.Mutedf("  And %d more", remaining))
 	}
 }
diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go
index ccad4fa77..997f74d87 100644
--- a/pkg/cmd/pr/view/view.go
+++ b/pkg/cmd/pr/view/view.go
@@ -260,7 +260,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P
 	var md string
 	var err error
 	if pr.Body == "" {
-		md = fmt.Sprintf("\n  %s\n\n", cs.Gray("No description provided"))
+		md = fmt.Sprintf("\n  %s\n\n", cs.Muted("No description provided"))
 	} else {
 		md, err = markdown.Render(pr.Body,
 			markdown.WithTheme(opts.IO.TerminalTheme()),
@@ -282,7 +282,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P
 	}
 
 	// Footer
-	fmt.Fprintf(out, cs.Gray("View this pull request on GitHub: %s\n"), pr.URL)
+	fmt.Fprintf(out, cs.Muted("View this pull request on GitHub: %s\n"), pr.URL)
 
 	return nil
 }
@@ -423,7 +423,7 @@ func prLabelList(pr api.PullRequest, cs *iostreams.ColorScheme) string {
 
 	labelNames := make([]string, 0, len(pr.Labels.Nodes))
 	for _, label := range pr.Labels.Nodes {
-		labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
+		labelNames = append(labelNames, cs.Label(label.Color, label.Name))
 	}
 
 	list := strings.Join(labelNames, ", ")
diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go
index a32482e65..c9030f299 100644
--- a/pkg/cmd/release/view/view.go
+++ b/pkg/cmd/release/view/view.go
@@ -129,19 +129,19 @@ func viewRun(opts *ViewOptions) error {
 }
 
 func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
-	iofmt := io.ColorScheme()
+	cs := io.ColorScheme()
 	w := io.Out
 
-	fmt.Fprintf(w, "%s\n", iofmt.Bold(release.TagName))
+	fmt.Fprintf(w, "%s\n", cs.Bold(release.TagName))
 	if release.IsDraft {
-		fmt.Fprintf(w, "%s • ", iofmt.Red("Draft"))
+		fmt.Fprintf(w, "%s • ", cs.Red("Draft"))
 	} else if release.IsPrerelease {
-		fmt.Fprintf(w, "%s • ", iofmt.Yellow("Pre-release"))
+		fmt.Fprintf(w, "%s • ", cs.Yellow("Pre-release"))
 	}
 	if release.IsDraft {
-		fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))))
+		fmt.Fprintln(w, cs.Mutedf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt)))
 	} else {
-		fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))))
+		fmt.Fprintln(w, cs.Mutedf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt)))
 	}
 
 	renderedDescription, err := markdown.Render(release.Body,
@@ -153,7 +153,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
 	fmt.Fprintln(w, renderedDescription)
 
 	if len(release.Assets) > 0 {
-		fmt.Fprintf(w, "%s\n", iofmt.Bold("Assets"))
+		fmt.Fprintln(w, cs.Bold("Assets"))
 		//nolint:staticcheck // SA1019: Showing NAME|SIZE headers adds nothing to table.
 		table := tableprinter.New(io, tableprinter.NoHeader)
 		for _, a := range release.Assets {
@@ -168,7 +168,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
 		fmt.Fprint(w, "\n")
 	}
 
-	fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL)))
+	fmt.Fprintln(w, cs.Mutedf("View on GitHub: %s", release.URL))
 	return nil
 }
 
diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go
index cd7c56ea8..be0be2ddd 100644
--- a/pkg/cmd/repo/create/create.go
+++ b/pkg/cmd/repo/create/create.go
@@ -262,10 +262,15 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
 
 func createRun(opts *CreateOptions) error {
 	if opts.Interactive {
+		cfg, err := opts.Config()
+		if err != nil {
+			return err
+		}
+		host, _ := cfg.Authentication().DefaultHost()
 		answer, err := opts.Prompter.Select("What would you like to do?", "", []string{
-			"Create a new repository on GitHub from scratch",
-			"Create a new repository on GitHub from a template repository",
-			"Push an existing local repository to GitHub",
+			fmt.Sprintf("Create a new repository on %s from scratch", host),
+			fmt.Sprintf("Create a new repository on %s from a template repository", host),
+			fmt.Sprintf("Push an existing local repository to %s", host),
 		})
 		if err != nil {
 			return err
@@ -323,7 +328,9 @@ func createFromScratch(opts *CreateOptions) error {
 		if idx := strings.IndexRune(opts.Name, '/'); idx > 0 {
 			targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:])
 		}
-		confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true)
+		confirmed, err := opts.Prompter.Confirm(
+			fmt.Sprintf(`This will create "%s" as a %s repository on %s. Continue?`, targetRepo, strings.ToLower(opts.Visibility), host),
+			true)
 		if err != nil {
 			return err
 		} else if !confirmed {
@@ -392,9 +399,10 @@ func createFromScratch(opts *CreateOptions) error {
 	isTTY := opts.IO.IsStdoutTTY()
 	if isTTY {
 		fmt.Fprintf(opts.IO.Out,
-			"%s Created repository %s on GitHub\n  %s\n",
+			"%s Created repository %s on %s\n  %s\n",
 			cs.SuccessIconWithColor(cs.Green),
 			ghrepo.FullName(repo),
+			host,
 			repo.URL)
 	} else {
 		fmt.Fprintln(opts.IO.Out, repo.URL)
@@ -482,7 +490,9 @@ func createFromTemplate(opts *CreateOptions) error {
 	if idx := strings.IndexRune(opts.Name, '/'); idx > 0 {
 		targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:])
 	}
-	confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true)
+	confirmed, err := opts.Prompter.Confirm(
+		fmt.Sprintf(`This will create "%s" as a %s repository on %s. Continue?`, targetRepo, strings.ToLower(opts.Visibility), host),
+		true)
 	if err != nil {
 		return err
 	} else if !confirmed {
@@ -496,9 +506,10 @@ func createFromTemplate(opts *CreateOptions) error {
 
 	cs := opts.IO.ColorScheme()
 	fmt.Fprintf(opts.IO.Out,
-		"%s Created repository %s on GitHub\n  %s\n",
+		"%s Created repository %s on %s\n  %s\n",
 		cs.SuccessIconWithColor(cs.Green),
 		ghrepo.FullName(repo),
+		host,
 		repo.URL)
 
 	opts.Clone, err = opts.Prompter.Confirm("Clone the new repository locally?", true)
@@ -622,9 +633,10 @@ func createFromLocal(opts *CreateOptions) error {
 
 	if isTTY {
 		fmt.Fprintf(stdout,
-			"%s Created repository %s on GitHub\n  %s\n",
+			"%s Created repository %s on %s\n  %s\n",
 			cs.SuccessIconWithColor(cs.Green),
 			ghrepo.FullName(repo),
+			host,
 			repo.URL)
 	} else {
 		fmt.Fprintln(stdout, repo.URL)
diff --git a/pkg/cmd/repo/license/view/view.go b/pkg/cmd/repo/license/view/view.go
index d3e63c241..14228ba36 100644
--- a/pkg/cmd/repo/license/view/view.go
+++ b/pkg/cmd/repo/license/view/view.go
@@ -119,9 +119,9 @@ func renderLicense(license *api.License, opts *ViewOptions) error {
 	cs := opts.IO.ColorScheme()
 	var out strings.Builder
 	if opts.IO.IsStdoutTTY() {
-		out.WriteString(fmt.Sprintf("\n%s\n", cs.Gray(license.Description)))
-		out.WriteString(fmt.Sprintf("\n%s\n", cs.Grayf("To implement: %s", license.Implementation)))
-		out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Grayf("For more information, see: %s", license.HTMLURL)))
+		out.WriteString(fmt.Sprintf("\n%s\n", cs.Muted(license.Description)))
+		out.WriteString(fmt.Sprintf("\n%s\n", cs.Mutedf("To implement: %s", license.Implementation)))
+		out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Mutedf("For more information, see: %s", license.HTMLURL)))
 	}
 	out.WriteString(license.Body)
 	_, err := opts.IO.Out.Write([]byte(out.String()))
diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go
index 06f85d048..b13276c80 100644
--- a/pkg/cmd/repo/view/view.go
+++ b/pkg/cmd/repo/view/view.go
@@ -181,7 +181,7 @@ func viewRun(opts *ViewOptions) error {
 
 	var readmeContent string
 	if readme == nil {
-		readmeContent = cs.Gray("This repository does not have a README")
+		readmeContent = cs.Muted("This repository does not have a README")
 	} else if isMarkdownFile(readme.Filename) {
 		var err error
 		readmeContent, err = markdown.Render(readme.Content,
@@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error {
 
 	description := repo.Description
 	if description == "" {
-		description = cs.Gray("No description provided")
+		description = cs.Muted("No description provided")
 	}
 
 	repoData := struct {
@@ -209,7 +209,7 @@ func viewRun(opts *ViewOptions) error {
 		FullName:    cs.Bold(fullName),
 		Description: description,
 		Readme:      readmeContent,
-		View:        cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
+		View:        cs.Mutedf("View this repository on GitHub: %s", openURL),
 	}
 
 	return tmpl.Execute(stdout, repoData)
diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go
index a7daa7b84..7f8fb1c2e 100644
--- a/pkg/cmd/root/help.go
+++ b/pkg/cmd/root/help.go
@@ -87,7 +87,7 @@ func isRootCmd(command *cobra.Command) bool {
 	return command != nil && !command.HasParent()
 }
 
-func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
+func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
 	flags := command.Flags()
 
 	if isRootCmd(command) {
diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go
index db0ef098d..b85d64ca3 100644
--- a/pkg/cmd/root/help_topic.go
+++ b/pkg/cmd/root/help_topic.go
@@ -81,6 +81,9 @@ var HelpTopics = []helpTopic{
 			%[1]sCLICOLOR_FORCE%[1]s: set to a value other than %[1]s0%[1]s to keep ANSI colors in output
 			even when the output is piped.
 
+			%[1]sGH_COLOR_LABELS%[1]s: set to any value to display labels using their RGB hex color codes in terminals that
+			support truecolor.
+
 			%[1]sGH_FORCE_TTY%[1]s: set to any value to force terminal-style output even when the output is
 			redirected. When the value is a number, it is interpreted as the number of columns
 			available in the viewport. When the value is a percentage, it will be applied against
@@ -108,6 +111,9 @@ var HelpTopics = []helpTopic{
 			%[1]sGH_MDWIDTH%[1]s: default maximum width for markdown render wrapping.  The max width of lines
 			wrapped on the terminal will be taken as the lesser of the terminal width, this value, or 120 if
 			not specified.  This value is used, for example, with %[1]spr view%[1]s subcommand.
+
+			%[1]sGH_ACCESSIBLE_PROMPTER%[1]s (preview): set to a truthy value to enable prompts that are
+			more compatible with speech synthesis and braille screen readers.
 		`, "`"),
 	},
 	{
diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go
index 2ec149729..a3556d743 100644
--- a/pkg/cmd/run/shared/presentation.go
+++ b/pkg/cmd/run/shared/presentation.go
@@ -52,7 +52,8 @@ func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) stri
 
 	for _, a := range annotations {
 		lines = append(lines, fmt.Sprintf("%s %s", AnnotationSymbol(cs, a), a.Message))
-		lines = append(lines, cs.Grayf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine))
+		// Following newline is essential for spacing between annotations
+		lines = append(lines, cs.Mutedf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine))
 	}
 
 	return strings.Join(lines, "\n")
diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go
index ce909fd77..b58f6b0f7 100644
--- a/pkg/cmd/run/shared/shared.go
+++ b/pkg/cmd/run/shared/shared.go
@@ -230,6 +230,8 @@ type Job struct {
 	CompletedAt time.Time `json:"completed_at"`
 	URL         string    `json:"html_url"`
 	RunID       int64     `json:"run_id"`
+
+	Log *zip.File
 }
 
 type Step struct {
@@ -239,7 +241,8 @@ type Step struct {
 	Number      int
 	StartedAt   time.Time `json:"started_at"`
 	CompletedAt time.Time `json:"completed_at"`
-	Log         *zip.File
+
+	Log *zip.File
 }
 
 type Steps []Step
@@ -575,7 +578,7 @@ func Symbol(cs *iostreams.ColorScheme, status Status, conclusion Conclusion) (st
 		case Success:
 			return cs.SuccessIconWithColor(noColor), cs.Green
 		case Skipped, Neutral:
-			return "-", cs.Gray
+			return "-", cs.Muted
 		default:
 			return cs.FailureIconWithColor(noColor), cs.Red
 		}
diff --git a/pkg/cmd/run/view/fixtures/run_log.zip b/pkg/cmd/run/view/fixtures/run_log.zip
index 757a63983..60701d925 100644
Binary files a/pkg/cmd/run/view/fixtures/run_log.zip and b/pkg/cmd/run/view/fixtures/run_log.zip differ
diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go
index c794cff9a..d46c2c9a9 100644
--- a/pkg/cmd/run/view/view.go
+++ b/pkg/cmd/run/view/view.go
@@ -397,7 +397,7 @@ func runView(opts *ViewOptions) error {
 			for _, a := range artifacts {
 				expiredBadge := ""
 				if a.Expired {
-					expiredBadge = cs.Gray(" (expired)")
+					expiredBadge = cs.Muted(" (expired)")
 				}
 				fmt.Fprintf(out, "%s%s\n", a.Name, expiredBadge)
 			}
@@ -411,7 +411,7 @@ func runView(opts *ViewOptions) error {
 		} else {
 			fmt.Fprintf(out, "For more information about a job, try: gh run view --job=<job-id>\n")
 		}
-		fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
+		fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL))
 
 		if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
 			return cmdutil.SilentError
@@ -423,7 +423,7 @@ func runView(opts *ViewOptions) error {
 		} else {
 			fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
 		}
-		fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
+		fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL))
 
 		if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
 			return cmdutil.SilentError
@@ -533,7 +533,7 @@ func promptForJob(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs []sh
 
 const JOB_NAME_MAX_LENGTH = 90
 
-func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
+func getJobNameForLogFilename(name string) string {
 	// As described in https://github.com/cli/cli/issues/5011#issuecomment-1570713070, there are a number of steps
 	// the server can take when producing the downloaded zip file that can result in a mismatch between the job name
 	// and the filename in the zip including:
@@ -545,9 +545,20 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
 	// * Strip `/` which occur when composite action job names are constructed of the form `<JOB_NAME`> / <ACTION_NAME>`
 	// * Truncate long job names
 	//
-	sanitizedJobName := strings.ReplaceAll(job.Name, "/", "")
+	sanitizedJobName := strings.ReplaceAll(name, "/", "")
 	sanitizedJobName = strings.ReplaceAll(sanitizedJobName, ":", "")
 	sanitizedJobName = truncateAsUTF16(sanitizedJobName, JOB_NAME_MAX_LENGTH)
+	return sanitizedJobName
+}
+
+func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp {
+	sanitizedJobName := getJobNameForLogFilename(job.Name)
+	re := fmt.Sprintf(`^-?\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName))
+	return regexp.MustCompile(re)
+}
+
+func stepLogFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
+	sanitizedJobName := getJobNameForLogFilename(job.Name)
 	re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number)
 	return regexp.MustCompile(re)
 }
@@ -627,17 +638,36 @@ func truncateAsUTF16(str string, max int) string {
 //	│   ├── 2_anotherstepname.txt
 //	│   ├── 3_stepstepname.txt
 //	│   └── 4_laststepname.txt
-//	└── jobname2/
-//	    ├── 1_stepname.txt
-//	    └── 2_somestepname.txt
+//	├── jobname2/
+//	|   ├── 1_stepname.txt
+//	|   └── 2_somestepname.txt
+//	├── 0_jobname1.txt
+//	├── 1_jobname2.txt
+//	└── -9999999999_jobname3.txt
 //
 // It iterates through the list of jobs and tries to find the matching
 // log in the zip file. If the matching log is found it is attached
 // to the job.
+//
+// The top-level .txt files include the logs for an entire job run. Note that
+// the prefixed number is either:
+//   - An ordinal and cannot be mapped to the corresponding job's ID.
+//   - A negative integer which is the ID of the job in the old Actions service.
+//     The service right now tries to get logs and use an ordinal in a loop.
+//     However, if it doesn't get the logs, it falls back to an old service
+//     where the ID can apparently be negative.
 func attachRunLog(rlz *zip.Reader, jobs []shared.Job) {
 	for i, job := range jobs {
+		re := jobLogFilenameRegexp(job)
+		for _, file := range rlz.File {
+			if re.MatchString(file.Name) {
+				jobs[i].Log = file
+				break
+			}
+		}
+
 		for j, step := range job.Steps {
-			re := logFilenameRegexp(job, step)
+			re := stepLogFilenameRegexp(job, step)
 			for _, file := range rlz.File {
 				if re.MatchString(file.Name) {
 					jobs[i].Steps[j].Log = file
@@ -650,6 +680,13 @@ func attachRunLog(rlz *zip.Reader, jobs []shared.Job) {
 
 func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error {
 	for _, job := range jobs {
+		// To display a run log, we first try to compile it from individual step
+		// logs, because this way we can prepend lines with the corresponding
+		// step name. However, at the time of writing, logs are sometimes being
+		// served by a service that doesn’t include the step logs (none of them),
+		// in which case we fall back to print the entire job run log.
+		var hasStepLogs bool
+
 		steps := job.Steps
 		sort.Sort(steps)
 		for _, step := range steps {
@@ -659,18 +696,49 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error {
 			if step.Log == nil {
 				continue
 			}
+			hasStepLogs = true
 			prefix := fmt.Sprintf("%s\t%s\t", job.Name, step.Name)
-			f, err := step.Log.Open()
-			if err != nil {
+			if err := printZIPFile(w, step.Log, prefix); err != nil {
 				return err
 			}
-			scanner := bufio.NewScanner(f)
-			for scanner.Scan() {
-				fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text())
-			}
-			f.Close()
+		}
+
+		if hasStepLogs {
+			continue
+		}
+
+		if failed && !shared.IsFailureState(job.Conclusion) {
+			continue
+		}
+
+		if job.Log == nil {
+			continue
+		}
+
+		// Here, we fall back to the job run log, which means we do not know
+		// the step name of lines. However, we want to keep the same line
+		// formatting to avoid breaking any code or script that rely on the
+		// tab-delimited formatting. So, an unknown-step placeholder is used
+		// instead of the actual step name.
+		prefix := fmt.Sprintf("%s\tUNKNOWN STEP\t", job.Name)
+		if err := printZIPFile(w, job.Log, prefix); err != nil {
+			return err
 		}
 	}
 
 	return nil
 }
+
+func printZIPFile(w io.Writer, file *zip.File, prefix string) error {
+	f, err := file.Open()
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text())
+	}
+	return nil
+}
diff --git a/pkg/cmd/search/commits/commits.go b/pkg/cmd/search/commits/commits.go
index bc37684a2..fb1742dc9 100644
--- a/pkg/cmd/search/commits/commits.go
+++ b/pkg/cmd/search/commits/commits.go
@@ -161,7 +161,7 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Commi
 		tp.AddField(commit.Sha)
 		tp.AddField(text.RemoveExcessiveWhitespace(commit.Info.Message))
 		tp.AddField(commit.Author.Login)
-		tp.AddTimeField(now, commit.Info.Author.Date, cs.Gray)
+		tp.AddTimeField(now, commit.Info.Author.Date, cs.Muted)
 		tp.EndRow()
 	}
 	if io.IsStdoutTTY() {
diff --git a/pkg/cmd/search/repos/repos.go b/pkg/cmd/search/repos/repos.go
index 0bad650d3..2815ee6dc 100644
--- a/pkg/cmd/search/repos/repos.go
+++ b/pkg/cmd/search/repos/repos.go
@@ -171,14 +171,14 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Repos
 			tags = append(tags, "archived")
 		}
 		info := strings.Join(tags, ", ")
-		infoColor := cs.Gray
+		infoColor := cs.Muted
 		if repo.IsPrivate {
 			infoColor = cs.Yellow
 		}
 		tp.AddField(repo.FullName, tableprinter.WithColor(cs.Bold))
 		tp.AddField(text.RemoveExcessiveWhitespace(repo.Description))
 		tp.AddField(info, tableprinter.WithColor(infoColor))
-		tp.AddTimeField(now, repo.UpdatedAt, cs.Gray)
+		tp.AddTimeField(now, repo.UpdatedAt, cs.Muted)
 		tp.EndRow()
 	}
 	if io.IsStdoutTTY() {
diff --git a/pkg/cmd/search/shared/shared.go b/pkg/cmd/search/shared/shared.go
index f0a346fc8..3282599cf 100644
--- a/pkg/cmd/search/shared/shared.go
+++ b/pkg/cmd/search/shared/shared.go
@@ -132,7 +132,7 @@ func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType,
 		}
 		tp.AddField(text.RemoveExcessiveWhitespace(issue.Title))
 		tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()))
-		tp.AddTimeField(now, issue.UpdatedAt, cs.Gray)
+		tp.AddTimeField(now, issue.UpdatedAt, cs.Muted)
 		tp.EndRow()
 	}
 
@@ -158,7 +158,7 @@ func listIssueLabels(issue *search.Issue, cs *iostreams.ColorScheme, colorize bo
 	labelNames := make([]string, 0, len(issue.Labels))
 	for _, label := range issue.Labels {
 		if colorize {
-			labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
+			labelNames = append(labelNames, cs.Label(label.Color, label.Name))
 		} else {
 			labelNames = append(labelNames, label.Name)
 		}
diff --git a/pkg/cmd/status/status.go b/pkg/cmd/status/status.go
index 0ed7dd3a7..c9acce8bd 100644
--- a/pkg/cmd/status/status.go
+++ b/pkg/cmd/status/status.go
@@ -740,7 +740,7 @@ func statusRun(opts *StatusOptions) error {
 		errs := sg.authErrors.ToSlice()
 		sort.Strings(errs)
 		for _, msg := range errs {
-			fmt.Fprintln(out, cs.Gray(fmt.Sprintf("warning: %s", msg)))
+			fmt.Fprintln(out, cs.Mutedf("warning: %s", msg))
 		}
 	}
 
diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go
index 188d79e22..2e550f496 100644
--- a/pkg/cmd/workflow/view/view.go
+++ b/pkg/cmd/workflow/view/view.go
@@ -175,7 +175,7 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Inte
 		out := opts.IO.Out
 
 		fileName := workflow.Base()
-		fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Gray(fileName))
+		fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Muted(fileName))
 		fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID))
 
 		codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml)
diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go
index 58d13e6ef..f786e19cd 100644
--- a/pkg/iostreams/color.go
+++ b/pkg/iostreams/color.go
@@ -41,33 +41,24 @@ var (
 	}
 )
 
-// NewColorScheme initializes color logic based on provided terminal capabilities.
-// Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported,
-// and terminal theme detected.
-func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors bool, theme string) *ColorScheme {
-	return &ColorScheme{
-		enabled:          enabled,
-		is256enabled:     is256enabled,
-		hasTrueColor:     trueColor,
-		accessibleColors: accessibleColors,
-		theme:            theme,
-	}
-}
-
+// ColorScheme controls how text is colored based upon terminal capabilities and user preferences.
 type ColorScheme struct {
-	enabled          bool
-	is256enabled     bool
-	hasTrueColor     bool
-	accessibleColors bool
-	theme            string
-}
-
-func (c *ColorScheme) Enabled() bool {
-	return c.enabled
+	// Enabled is whether color is used at all.
+	Enabled bool
+	// EightBitColor is whether the terminal supports 8-bit, 256 colors.
+	EightBitColor bool
+	// TrueColor is whether the terminal supports 24-bit, 16 million colors.
+	TrueColor bool
+	// Accessible is whether colors must be base 16 colors that users can customize in terminal preferences.
+	Accessible bool
+	// ColorLabels is whether labels are colored based on their truecolor RGB hex color.
+	ColorLabels bool
+	// Theme is the terminal background color theme used to contextually color text for light, dark, or none at all.
+	Theme string
 }
 
 func (c *ColorScheme) Bold(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return bold(t)
@@ -79,16 +70,16 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string {
 
 func (c *ColorScheme) Muted(t string) string {
 	// Fallback to previous logic if accessible colors preview is disabled.
-	if !c.accessibleColors {
+	if !c.Accessible {
 		return c.Gray(t)
 	}
 
 	// Muted text is only stylized if color is enabled.
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 
-	switch c.theme {
+	switch c.Theme {
 	case LightTheme:
 		return lightThemeMuted(t)
 	case DarkTheme:
@@ -103,7 +94,7 @@ func (c *ColorScheme) Mutedf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) Red(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return red(t)
@@ -114,7 +105,7 @@ func (c *ColorScheme) Redf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) Yellow(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return yellow(t)
@@ -125,7 +116,7 @@ func (c *ColorScheme) Yellowf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) Green(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return green(t)
@@ -136,30 +127,30 @@ func (c *ColorScheme) Greenf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) GreenBold(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return greenBold(t)
 }
 
-// Use Muted instead for thematically contrasting color.
+// Deprecated: Use Muted instead for thematically contrasting color.
 func (c *ColorScheme) Gray(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
-	if c.is256enabled {
+	if c.EightBitColor {
 		return gray256(t)
 	}
 	return gray(t)
 }
 
-// Use Mutedf instead for thematically contrasting color.
+// Deprecated: Use Mutedf instead for thematically contrasting color.
 func (c *ColorScheme) Grayf(t string, args ...interface{}) string {
 	return c.Gray(fmt.Sprintf(t, args...))
 }
 
 func (c *ColorScheme) Magenta(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return magenta(t)
@@ -170,7 +161,7 @@ func (c *ColorScheme) Magentaf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) Cyan(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return cyan(t)
@@ -181,14 +172,14 @@ func (c *ColorScheme) Cyanf(t string, args ...interface{}) string {
 }
 
 func (c *ColorScheme) CyanBold(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return cyanBold(t)
 }
 
 func (c *ColorScheme) Blue(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 	return blue(t)
@@ -219,7 +210,7 @@ func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string {
 }
 
 func (c *ColorScheme) HighlightStart() string {
-	if !c.enabled {
+	if !c.Enabled {
 		return ""
 	}
 
@@ -227,7 +218,7 @@ func (c *ColorScheme) HighlightStart() string {
 }
 
 func (c *ColorScheme) Highlight(t string) string {
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 
@@ -235,7 +226,7 @@ func (c *ColorScheme) Highlight(t string) string {
 }
 
 func (c *ColorScheme) Reset() string {
-	if !c.enabled {
+	if !c.Enabled {
 		return ""
 	}
 
@@ -255,7 +246,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string {
 	case "green":
 		fn = c.Green
 	case "gray":
-		fn = c.Gray
+		fn = c.Muted
 	case "magenta":
 		fn = c.Magenta
 	case "cyan":
@@ -271,17 +262,9 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string {
 	return fn
 }
 
-// ColorFromRGB returns a function suitable for TablePrinter.AddField
-// that calls HexToRGB, coloring text if supported by the terminal.
-func (c *ColorScheme) ColorFromRGB(hex string) func(string) string {
-	return func(s string) string {
-		return c.HexToRGB(hex, s)
-	}
-}
-
-// HexToRGB uses the given hex to color x if supported by the terminal.
-func (c *ColorScheme) HexToRGB(hex string, x string) string {
-	if !c.enabled || !c.hasTrueColor || len(hex) != 6 {
+// Label stylizes text based on label's RGB hex color.
+func (c *ColorScheme) Label(hex string, x string) string {
+	if !c.Enabled || !c.TrueColor || !c.ColorLabels || len(hex) != 6 {
 		return x
 	}
 
@@ -293,11 +276,11 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string {
 
 func (c *ColorScheme) TableHeader(t string) string {
 	// Table headers are only stylized if color is enabled including underline modifier.
-	if !c.enabled {
+	if !c.Enabled {
 		return t
 	}
 
-	switch c.theme {
+	switch c.Theme {
 	case DarkTheme:
 		return darkThemeTableHeader(t)
 	case LightTheme:
diff --git a/pkg/iostreams/console.go b/pkg/iostreams/console.go
index 89bdd1daa..72d070396 100644
--- a/pkg/iostreams/console.go
+++ b/pkg/iostreams/console.go
@@ -5,7 +5,7 @@ package iostreams
 
 import "os"
 
-func hasAlternateScreenBuffer(hasTrueColor bool) bool {
+func hasAlternateScreenBuffer(_ bool) bool {
 	// on non-Windows, we just assume that alternate screen buffer is supported in most cases
 	return os.Getenv("TERM") != "dumb"
 }
diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go
index 30981386b..f5e3c2aee 100644
--- a/pkg/iostreams/iostreams.go
+++ b/pkg/iostreams/iostreams.go
@@ -72,6 +72,7 @@ type IOStreams struct {
 
 	colorOverride           bool
 	colorEnabled            bool
+	colorLabels             bool
 	accessibleColorsEnabled bool
 
 	pagerCommand string
@@ -103,6 +104,10 @@ func (s *IOStreams) HasTrueColor() bool {
 	return s.term.IsTrueColorSupported()
 }
 
+func (s *IOStreams) ColorLabels() bool {
+	return s.colorLabels
+}
+
 // DetectTerminalTheme is a utility to call before starting the output pager so that the terminal background
 // can be reliably detected.
 func (s *IOStreams) DetectTerminalTheme() {
@@ -135,6 +140,10 @@ func (s *IOStreams) SetColorEnabled(colorEnabled bool) {
 	s.colorEnabled = colorEnabled
 }
 
+func (s *IOStreams) SetColorLabels(colorLabels bool) {
+	s.colorLabels = colorLabels
+}
+
 func (s *IOStreams) SetStdinTTY(isTTY bool) {
 	s.stdinTTYOverride = true
 	s.stdinIsTTY = isTTY
@@ -367,7 +376,14 @@ func (s *IOStreams) TerminalWidth() int {
 }
 
 func (s *IOStreams) ColorScheme() *ColorScheme {
-	return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.TerminalTheme())
+	return &ColorScheme{
+		Enabled:       s.ColorEnabled(),
+		EightBitColor: s.ColorSupport256(),
+		TrueColor:     s.HasTrueColor(),
+		Accessible:    s.AccessibleColorsEnabled(),
+		ColorLabels:   s.ColorLabels(),
+		Theme:         s.TerminalTheme(),
+	}
 }
 
 func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {



Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index fef87ddc6..67a9a98d1 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -32,6 +32,7 @@ func TestNewConfigProvidesFallback(t *testing.T) {
 	requireKeyWithValue(t, spiedCfg, []string{aliasesKey, "co"}, "pr checkout")
 	requireKeyWithValue(t, spiedCfg, []string{httpUnixSocketKey}, "")
 	requireKeyWithValue(t, spiedCfg, []string{browserKey}, "")
+	requireKeyWithValue(t, spiedCfg, []string{colorLabelsKey}, "disabled")
 }
 
 func TestGetOrDefaultApplicationDefaults(t *testing.T) {
@@ -137,6 +138,7 @@ func TestFallbackConfig(t *testing.T) {
 	requireKeyWithValue(t, cfg, []string{aliasesKey, "co"}, "pr checkout")
 	requireKeyWithValue(t, cfg, []string{httpUnixSocketKey}, "")
 	requireKeyWithValue(t, cfg, []string{browserKey}, "")
+	requireKeyWithValue(t, cfg, []string{colorLabelsKey}, "disabled")
 	requireNoKey(t, cfg, []string{"unknown"})
 }
 
diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go
new file mode 100644
index 000000000..56096972d
--- /dev/null
+++ b/internal/prompter/accessible_prompter_test.go
@@ -0,0 +1,493 @@
+//go:build !windows
+
+package prompter_test
+
+import (
+	"fmt"
+	"io"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/Netflix/go-expect"
+	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/creack/pty"
+	"github.com/hinshun/vt10x"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// The following tests are broadly testing the accessible prompter, and NOT asserting
+// on the prompter's complete and exact output strings.
+//
+// These tests generally operate with this logic:
+// - Wait for a particular substring (a portion of the prompt) to appear
+// - Send input
+// - Wait for another substring to appear or for control to return to the test
+// - Assert that the input value was returned from the prompter function
+
+// In the future, expanding these tests to assert on the exact prompt strings
+// would help build confidence in `huh` upgrades, but for now these tests
+// are sufficient to ensure that the accessible prompter behaves roughly as expected
+// but doesn't mandate that prompts always look exactly the same.
+func TestAccessiblePrompter(t *testing.T) {
+	t.Run("Select", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Choose:")
+			require.NoError(t, err)
+
+			// Select option 1
+			_, err = console.SendLine("1")
+			require.NoError(t, err)
+		}()
+
+		selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
+		require.NoError(t, err)
+		assert.Equal(t, 0, selectValue)
+	})
+
+	t.Run("MultiSelect", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Select a number")
+			require.NoError(t, err)
+
+			// Select options 1 and 2
+			_, err = console.SendLine("1")
+			require.NoError(t, err)
+			_, err = console.SendLine("2")
+			require.NoError(t, err)
+
+			// This confirms selections
+			_, err = console.SendLine("0")
+			require.NoError(t, err)
+		}()
+
+		multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"})
+		require.NoError(t, err)
+		assert.Equal(t, []int{0, 1}, multiSelectValue)
+	})
+
+	t.Run("Input", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		dummyText := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Enter some characters")
+			require.NoError(t, err)
+
+			// Enter a number
+			_, err = console.SendLine(dummyText)
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.Input("Enter some characters", "")
+		require.NoError(t, err)
+		assert.Equal(t, dummyText, inputValue)
+	})
+
+	t.Run("Input - blank input returns default value", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		dummyDefaultValue := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Enter some characters")
+			require.NoError(t, err)
+
+			// Enter nothing
+			_, err = console.SendLine("")
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
+		require.NoError(t, err)
+		assert.Equal(t, dummyDefaultValue, inputValue)
+	})
+
+	t.Run("Password", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		dummyPassword := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Enter password")
+			require.NoError(t, err)
+
+			// Enter a number
+			_, err = console.SendLine(dummyPassword)
+			require.NoError(t, err)
+		}()
+
+		passwordValue, err := p.Password("Enter password")
+		require.NoError(t, err)
+		require.Equal(t, dummyPassword, passwordValue)
+	})
+
+	t.Run("Confirm", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Are you sure")
+			require.NoError(t, err)
+
+			// Confirm
+			_, err = console.SendLine("y")
+			require.NoError(t, err)
+		}()
+
+		confirmValue, err := p.Confirm("Are you sure", false)
+		require.NoError(t, err)
+		require.Equal(t, true, confirmValue)
+	})
+
+	t.Run("Confirm - blank input returns default", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Are you sure")
+			require.NoError(t, err)
+
+			// Enter nothing
+			_, err = console.SendLine("")
+			require.NoError(t, err)
+		}()
+
+		confirmValue, err := p.Confirm("Are you sure", false)
+		require.NoError(t, err)
+		require.Equal(t, false, confirmValue)
+	})
+
+	t.Run("AuthToken", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		dummyAuthToken := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Paste your authentication token:")
+			require.NoError(t, err)
+
+			// Enter some dummy auth token
+			_, err = console.SendLine(dummyAuthToken)
+			require.NoError(t, err)
+		}()
+
+		authValue, err := p.AuthToken()
+		require.NoError(t, err)
+		require.Equal(t, dummyAuthToken, authValue)
+	})
+
+	t.Run("AuthToken - blank input returns error", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		dummyAuthTokenForAfterFailure := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Paste your authentication token:")
+			require.NoError(t, err)
+
+			// Enter nothing
+			_, err = console.SendLine("")
+			require.NoError(t, err)
+
+			// Expect an error message
+			_, err = console.ExpectString("token is required")
+			require.NoError(t, err)
+
+			// Now enter some dummy auth token to return control back to the test
+			_, err = console.SendLine(dummyAuthTokenForAfterFailure)
+			require.NoError(t, err)
+		}()
+
+		authValue, err := p.AuthToken()
+		require.NoError(t, err)
+		require.Equal(t, dummyAuthTokenForAfterFailure, authValue)
+	})
+
+	t.Run("ConfirmDeletion", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		requiredValue := "test"
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
+			require.NoError(t, err)
+
+			// Confirm
+			_, err = console.SendLine(requiredValue)
+			require.NoError(t, err)
+		}()
+
+		// An err indicates that the confirmation text sent did not match
+		err := p.ConfirmDeletion(requiredValue)
+		require.NoError(t, err)
+	})
+
+	t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		requiredValue := "test"
+		badInputValue := "garbage"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
+			require.NoError(t, err)
+
+			// Confirm with bad input
+			_, err = console.SendLine(badInputValue)
+			require.NoError(t, err)
+
+			// Expect an error message and loop back to the prompt
+			_, err = console.ExpectString(fmt.Sprintf("You entered: %q", badInputValue))
+			require.NoError(t, err)
+
+			// Confirm with the correct input to return control back to the test
+			_, err = console.SendLine(requiredValue)
+			require.NoError(t, err)
+		}()
+
+		// An err indicates that the confirmation text sent did not match
+		err := p.ConfirmDeletion(requiredValue)
+		require.NoError(t, err)
+	})
+
+	t.Run("InputHostname", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		hostname := "example.com"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Hostname:")
+			require.NoError(t, err)
+
+			// Enter the hostname
+			_, err = console.SendLine(hostname)
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.InputHostname()
+		require.NoError(t, err)
+		require.Equal(t, hostname, inputValue)
+	})
+
+	t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("How to edit?")
+			require.NoError(t, err)
+
+			// Enter 2, to select "skip"
+			_, err = console.SendLine("2")
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.MarkdownEditor("How to edit?", "", true)
+		require.NoError(t, err)
+		require.Equal(t, "", inputValue)
+	})
+
+	t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+		defaultValue := "12345abcdefg"
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("How to edit?")
+			require.NoError(t, err)
+
+			// Enter number 2 to select "skip". This shouldn't be allowed.
+			_, err = console.SendLine("2")
+			require.NoError(t, err)
+
+			// Expect a notice to enter something valid since blank is disallowed.
+			_, err = console.ExpectString("invalid input. please try again")
+			require.NoError(t, err)
+
+			// Send a 1 to select to open the editor. This will immediately exit
+			_, err = console.SendLine("1")
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false)
+		require.NoError(t, err)
+		require.Equal(t, defaultValue, inputValue)
+	})
+
+	t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestAcessiblePrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("How to edit?")
+			require.NoError(t, err)
+
+			// Enter number 2 to select "skip". This shouldn't be allowed.
+			_, err = console.SendLine("2")
+			require.NoError(t, err)
+
+			// Expect a notice to enter something valid since blank is disallowed.
+			_, err = console.ExpectString("invalid input. please try again")
+			require.NoError(t, err)
+
+			// Send a 1 to select to open the editor since skip is invalid and
+			// we need to return control back to the test.
+			_, err = console.SendLine("1")
+			require.NoError(t, err)
+		}()
+
+		inputValue, err := p.MarkdownEditor("How to edit?", "", false)
+		require.NoError(t, err)
+		require.Equal(t, "", inputValue)
+	})
+}
+
+func TestSurveyPrompter(t *testing.T) {
+	// This not a comprehensive test of the survey prompter, but it does
+	// demonstrate that the survey prompter is used when the
+	// accessible prompter is disabled.
+	t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) {
+		console := newTestVirtualTerminal(t)
+		p := newTestSurveyPrompter(t, console)
+
+		go func() {
+			// Wait for prompt to appear
+			_, err := console.ExpectString("Select a number")
+			require.NoError(t, err)
+
+			// Send a newline to select the first option
+			// Note: This would not work with the accessible prompter
+			// because it would requires sending a 1 to select the first option.
+			// So it proves we are seeing a survey prompter.
+			_, err = console.SendLine("")
+			require.NoError(t, err)
+		}()
+
+		selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
+		require.NoError(t, err)
+		assert.Equal(t, 0, selectValue)
+	})
+}
+
+func newTestVirtualTerminal(t *testing.T) *expect.Console {
+	t.Helper()
+
+	// Create a PTY and hook up a virtual terminal emulator
+	ptm, pts, err := pty.Open()
+	require.NoError(t, err)
+
+	term := vt10x.New(vt10x.WithWriter(pts))
+
+	// Create a console via Expect that allows scripting against the terminal
+	consoleOpts := []expect.ConsoleOpt{
+		expect.WithStdin(ptm),
+		expect.WithStdout(term),
+		expect.WithCloser(ptm, pts),
+		failOnExpectError(t),
+		failOnSendError(t),
+		expect.WithDefaultTimeout(time.Second),
+	}
+
+	console, err := expect.NewConsole(consoleOpts...)
+	require.NoError(t, err)
+	t.Cleanup(func() { testCloser(t, console) })
+
+	return console
+}
+
+func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
+	t.Helper()
+
+	t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
+	// `echo`` is chose as the editor command because it immediately returns
+	// a success exit code, returns an empty string, doesn't require any user input,
+	// and since this file is only built on Linux, it is near guaranteed to be available.
+	return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
+}
+
+func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter {
+	t.Helper()
+
+	t.Setenv("GH_ACCESSIBLE_PROMPTER", "false")
+	return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
+}
+
+// failOnExpectError adds an observer that will fail the test in a standardised way
+// if any expectation on the command output fails, without requiring an explicit
+// assertion.
+//
+// Use WithRelaxedIO to disable this behaviour.
+func failOnExpectError(t *testing.T) expect.ConsoleOpt {
+	t.Helper()
+	return expect.WithExpectObserver(
+		func(matchers []expect.Matcher, buf string, err error) {
+			t.Helper()
+
+			if err == nil {
+				return
+			}
+
+			if len(matchers) == 0 {
+				t.Fatalf("Error occurred while matching %q: %s\n", buf, err)
+			}
+
+			var criteria []string
+			for _, matcher := range matchers {
+				criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria()))
+			}
+			t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err)
+		},
+	)
+}
+
+// failOnSendError adds an observer that will fail the test in a standardised way
+// if any sending of input fails, without requiring an explicit assertion.
+//
+// Use WithRelaxedIO to disable this behaviour.
+func failOnSendError(t *testing.T) expect.ConsoleOpt {
+	t.Helper()
+	return expect.WithSendObserver(
+		func(msg string, n int, err error) {
+			t.Helper()
+
+			if err != nil {
+				t.Fatalf("Failed to send %q: %s\n", msg, err)
+			}
+			if len(msg) != n {
+				t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg)
+			}
+		},
+	)
+}
+
+// testCloser is a helper to fail the test if a Closer fails to close.
+func testCloser(t *testing.T, closer io.Closer) {
+	t.Helper()
+	if err := closer.Close(); err != nil {
+		t.Errorf("Close failed: %s", err)
+	}
+}
diff --git a/pkg/cmd/attestation/test/data/data.go b/pkg/cmd/attestation/test/data/data.go
index b33efaa28..ef3c35c20 100644
--- a/pkg/cmd/attestation/test/data/data.go
+++ b/pkg/cmd/attestation/test/data/data.go
@@ -5,13 +5,17 @@ import (
 	"testing"
 
 	"github.com/sigstore/sigstore-go/pkg/bundle"
-	sgData "github.com/sigstore/sigstore-go/pkg/testing/data"
 )
 
 //go:embed sigstore-js-2.1.0-bundle.json
 var SigstoreBundleRaw []byte
 
-// SigstoreBundle returns a test *sigstore.Bundle
+// SigstoreBundle returns a test sigstore-go bundle.Bundle
 func SigstoreBundle(t *testing.T) *bundle.Bundle {
-	return sgData.TestBundle(t, SigstoreBundleRaw)
+	b := &bundle.Bundle{}
+	err := b.UnmarshalJSON(SigstoreBundleRaw)
+	if err != nil {
+		t.Fatalf("failed to unmarshal sigstore bundle: %v", err)
+	}
+	return b
 }
diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go
index 65f83d659..2184d0f16 100644
--- a/pkg/cmd/config/list/list_test.go
+++ b/pkg/cmd/config/list/list_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/pkg/cmdutil"
@@ -91,14 +92,16 @@ func Test_listRun(t *testing.T) {
 				return cfg
 			}(),
 			input: &ListOptions{Hostname: "HOST"},
-			stdout: `git_protocol=ssh
-editor=/usr/bin/vim
-prompt=disabled
-prefer_editor_prompt=enabled
-pager=less
-http_unix_socket=
-browser=brave
-`,
+			stdout: heredoc.Doc(`
+				git_protocol=ssh
+				editor=/usr/bin/vim
+				prompt=disabled
+				prefer_editor_prompt=enabled
+				pager=less
+				http_unix_socket=
+				browser=brave
+				color_labels=disabled
+			`),
 		},
 	}
 
diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go
index 94955bb30..c0275d1de 100644
--- a/pkg/cmd/factory/default_test.go
+++ b/pkg/cmd/factory/default_test.go
@@ -432,6 +432,84 @@ func Test_ioStreams_prompt(t *testing.T) {
 	}
 }
 
+func Test_ioStreams_colorLabels(t *testing.T) {
+	tests := []struct {
+		name               string
+		config             gh.Config
+		colorLabelsEnabled bool
+		env                map[string]string
+	}{
+		{
+			name:               "default config",
+			colorLabelsEnabled: false,
+		},
+		{
+			name:               "config with colorLabels enabled",
+			config:             enableColorLabelsConfig(),
+			colorLabelsEnabled: true,
+		},
+		{
+			name:               "config with colorLabels disabled",
+			config:             disableColorLabelsConfig(),
+			colorLabelsEnabled: false,
+		},
+		{
+			name:               "colorLabels enabled via `1` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "1"},
+			colorLabelsEnabled: true,
+		},
+		{
+			name:               "colorLabels enabled via `true` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "true"},
+			colorLabelsEnabled: true,
+		},
+		{
+			name:               "colorLabels enabled via `yes` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "yes"},
+			colorLabelsEnabled: true,
+		},
+		{
+			name:               "colorLabels disable via empty string in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": ""},
+			colorLabelsEnabled: false,
+		},
+		{
+			name:               "colorLabels disabled via `0` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "0"},
+			colorLabelsEnabled: false,
+		},
+		{
+			name:               "colorLabels disabled via `false` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "false"},
+			colorLabelsEnabled: false,
+		},
+		{
+			name:               "colorLabels disabled via `no` in GH_COLOR_LABELS env var",
+			env:                map[string]string{"GH_COLOR_LABELS": "no"},
+			colorLabelsEnabled: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.env != nil {
+				for k, v := range tt.env {
+					t.Setenv(k, v)
+				}
+			}
+			f := New("1")
+			f.Config = func() (gh.Config, error) {
+				if tt.config == nil {
+					return config.NewBlankConfig(), nil
+				} else {
+					return tt.config, nil
+				}
+			}
+			io := ioStreams(f)
+			assert.Equal(t, tt.colorLabelsEnabled, io.ColorLabels())
+		})
+	}
+}
+
 func TestSSOURL(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -537,3 +615,11 @@ func pagerConfig() gh.Config {
 func disablePromptConfig() gh.Config {
 	return config.NewFromString("prompt: disabled")
 }
+
+func disableColorLabelsConfig() gh.Config {
+	return config.NewFromString("color_labels: disabled")
+}
+
+func enableColorLabelsConfig() gh.Config {
+	return config.NewFromString("color_labels: enabled")
+}
diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go
index ad6d8cdb5..14351418f 100644
--- a/pkg/cmd/gist/list/list_test.go
+++ b/pkg/cmd/gist/list/list_test.go
@@ -654,50 +654,57 @@ func Test_highlightMatch(t *testing.T) {
 	tests := []struct {
 		name  string
 		input string
-		color bool
+		cs    *iostreams.ColorScheme
 		want  string
 	}{
 		{
 			name:  "single match",
 			input: "Octo",
+			cs:    &iostreams.ColorScheme{},
 			want:  "Octo",
 		},
 		{
 			name:  "single match (color)",
 			input: "Octo",
-			color: true,
-			want:  "\x1b[0;30;43mOcto\x1b[0m",
+			cs: &iostreams.ColorScheme{
+				Enabled: true,
+			},
+			want: "\x1b[0;30;43mOcto\x1b[0m",
 		},
 		{
 			name:  "single match with extra",
 			input: "Hello, Octocat!",
+			cs:    &iostreams.ColorScheme{},
 			want:  "Hello, Octocat!",
 		},
 		{
 			name:  "single match with extra (color)",
 			input: "Hello, Octocat!",
-			color: true,
-			want:  "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m",
+			cs: &iostreams.ColorScheme{
+				Enabled: true,
+			},
+			want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m",
 		},
 		{
 			name:  "multiple matches",
 			input: "Octocat/octo",
+			cs:    &iostreams.ColorScheme{},
 			want:  "Octocat/octo",
 		},
 		{
 			name:  "multiple matches (color)",
 			input: "Octocat/octo",
-			color: true,
-			want:  "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m",
+			cs: &iostreams.ColorScheme{
+				Enabled: true,
+			},
+			want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			cs := iostreams.NewColorScheme(tt.color, false, false, false, iostreams.NoTheme)
-
 			matched := false
-			got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight)
+			got, err := highlightMatch(tt.input, regex, &matched, tt.cs.Blue, tt.cs.Highlight)
 			assert.NoError(t, err)
 			assert.True(t, matched)
 			assert.Equal(t, tt.want, got)
diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go
index c33cfdad6..5f1f17e60 100644
--- a/pkg/cmd/repo/create/create_test.go
+++ b/pkg/cmd/repo/create/create_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/gh"
+	ghmock "github.com/cli/cli/v2/internal/gh/mock"
 	"github.com/cli/cli/v2/internal/prompter"
 	"github.com/cli/cli/v2/internal/run"
 	"github.com/cli/cli/v2/pkg/cmdutil"
@@ -201,7 +202,7 @@ func Test_createRun(t *testing.T) {
 			name:       "interactive create from scratch with gitignore and license",
 			opts:       &CreateOptions{Interactive: true},
 			tty:        true,
-			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n  https://github.com/OWNER/REPO\n",
+			wantStdout: "✓ Created repository OWNER/REPO on github.com\n  https://github.com/OWNER/REPO\n",
 			promptStubs: func(p *prompter.PrompterMock) {
 				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
 					switch message {
@@ -211,7 +212,7 @@ func Test_createRun(t *testing.T) {
 						return true, nil
 					case "Would you like to add a license?":
 						return true, nil
-					case `This will create "REPO" as a private repository on GitHub. Continue?`:
+					case `This will create "REPO" as a private repository on github.com. Continue?`:
 						return defaultValue, nil
 					case "Clone the new repository locally?":
 						return defaultValue, nil
@@ -232,7 +233,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
+						return prompter.IndexFor(options, "Create a new repository on github.com from scratch")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					case "Choose a license":
@@ -267,7 +268,7 @@ func Test_createRun(t *testing.T) {
 			name:       "interactive create from scratch but with prompted owner",
 			opts:       &CreateOptions{Interactive: true},
 			tty:        true,
-			wantStdout: "✓ Created repository org1/REPO on GitHub\n  https://github.com/org1/REPO\n",
+			wantStdout: "✓ Created repository org1/REPO on github.com\n  https://github.com/org1/REPO\n",
 			promptStubs: func(p *prompter.PrompterMock) {
 				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
 					switch message {
@@ -277,7 +278,7 @@ func Test_createRun(t *testing.T) {
 						return false, nil
 					case "Would you like to add a license?":
 						return false, nil
-					case `This will create "org1/REPO" as a private repository on GitHub. Continue?`:
+					case `This will create "org1/REPO" as a private repository on github.com. Continue?`:
 						return true, nil
 					case "Clone the new repository locally?":
 						return false, nil
@@ -300,7 +301,7 @@ func Test_createRun(t *testing.T) {
 					case "Repository owner":
 						return prompter.IndexFor(options, "org1")
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
+						return prompter.IndexFor(options, "Create a new repository on github.com from scratch")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -345,7 +346,7 @@ func Test_createRun(t *testing.T) {
 						return false, nil
 					case "Would you like to add a license?":
 						return false, nil
-					case `This will create "REPO" as a private repository on GitHub. Continue?`:
+					case `This will create "REPO" as a private repository on github.com. Continue?`:
 						return false, nil
 					default:
 						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
@@ -364,7 +365,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
+						return prompter.IndexFor(options, "Create a new repository on github.com from scratch")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -409,7 +410,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Push an existing local repository to GitHub")
+						return prompter.IndexFor(options, "Push an existing local repository to github.com")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -441,7 +442,7 @@ func Test_createRun(t *testing.T) {
 				cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
 				cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
 			},
-			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n  https://github.com/OWNER/REPO\n",
+			wantStdout: "✓ Created repository OWNER/REPO on github.com\n  https://github.com/OWNER/REPO\n",
 		},
 		{
 			name: "interactive with existing bare repository public and push",
@@ -475,7 +476,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Push an existing local repository to GitHub")
+						return prompter.IndexFor(options, "Push an existing local repository to github.com")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -509,7 +510,7 @@ func Test_createRun(t *testing.T) {
 				cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
 				cs.Register(`git -C . push origin --mirror`, 0, "")
 			},
-			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n  https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n",
+			wantStdout: "✓ Created repository OWNER/REPO on github.com\n  https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n",
 		},
 		{
 			name: "interactive with existing repository public add remote and push",
@@ -543,7 +544,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Push an existing local repository to GitHub")
+						return prompter.IndexFor(options, "Push an existing local repository to github.com")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -577,7 +578,7 @@ func Test_createRun(t *testing.T) {
 				cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
 				cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "")
 			},
-			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n  https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
+			wantStdout: "✓ Created repository OWNER/REPO on github.com\n  https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
 		},
 		{
 			name: "interactive create from a template repository",
@@ -586,7 +587,7 @@ func Test_createRun(t *testing.T) {
 			promptStubs: func(p *prompter.PrompterMock) {
 				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
 					switch message {
-					case `This will create "OWNER/REPO" as a private repository on GitHub. Continue?`:
+					case `This will create "OWNER/REPO" as a private repository on github.com. Continue?`:
 						return defaultValue, nil
 					case "Clone the new repository locally?":
 						return defaultValue, nil
@@ -611,7 +612,7 @@ func Test_createRun(t *testing.T) {
 					case "Choose a template repository":
 						return prompter.IndexFor(options, "REPO")
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository")
+						return prompter.IndexFor(options, "Create a new repository on github.com from a template repository")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -654,7 +655,7 @@ func Test_createRun(t *testing.T) {
 			execStubs: func(cs *run.CommandStubber) {
 				cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 0, "")
 			},
-			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n  https://github.com/OWNER/REPO\n",
+			wantStdout: "✓ Created repository OWNER/REPO on github.com\n  https://github.com/OWNER/REPO\n",
 		},
 		{
 			name: "interactive create from template repo but there are no template repos",
@@ -680,7 +681,7 @@ func Test_createRun(t *testing.T) {
 				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
 					switch message {
 					case "What would you like to do?":
-						return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository")
+						return prompter.IndexFor(options, "Create a new repository on github.com from a template repository")
 					case "Visibility":
 						return prompter.IndexFor(options, "Private")
 					default:
@@ -950,6 +951,88 @@ func Test_createRun(t *testing.T) {
 			},
 			wantStdout: "https://github.com/OWNER/REPO\n",
 		},
+		{
+			name: "interactive create from scratch with host override",
+			opts: &CreateOptions{
+				Interactive: true,
+				Config: func() (gh.Config, error) {
+					cfg := &ghmock.ConfigMock{
+						AuthenticationFunc: func() gh.AuthConfig {
+							authCfg := &config.AuthConfig{}
+							authCfg.SetHosts([]string{"example.com"})
+							authCfg.SetDefaultHost("example.com", "GH_HOST")
+							return authCfg
+						},
+					}
+					return cfg, nil
+				},
+			},
+			tty: true,
+			promptStubs: func(p *prompter.PrompterMock) {
+				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
+					switch message {
+					case "Would you like to add a README file?":
+						return false, nil
+					case "Would you like to add a .gitignore?":
+						return false, nil
+					case "Would you like to add a license?":
+						return false, nil
+					case `This will create "REPO" as a private repository on example.com. Continue?`:
+						return defaultValue, nil
+					case "Clone the new repository locally?":
+						return false, nil
+					default:
+						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
+					}
+				}
+				p.InputFunc = func(message, defaultValue string) (string, error) {
+					switch message {
+					case "Repository name":
+						return "REPO", nil
+					case "Description":
+						return "my new repo", nil
+					default:
+						return "", fmt.Errorf("unexpected input prompt: %s", message)
+					}
+				}
+				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
+					switch message {
+					case "What would you like to do?":
+						return prompter.IndexFor(options, "Create a new repository on example.com from scratch")
+					case "Visibility":
+						return prompter.IndexFor(options, "Private")
+					case "Choose a license":
+						return prompter.IndexFor(options, "GNU Lesser General Public License v3.0")
+					case "Choose a .gitignore template":
+						return prompter.IndexFor(options, "Go")
+					default:
+						return 0, fmt.Errorf("unexpected select prompt: %s", message)
+					}
+				}
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query UserCurrent\b`),
+					httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
+				reg.Register(
+					httpmock.GraphQL(`mutation RepositoryCreate\b`),
+					httpmock.StringResponse(`
+						{
+							"data": {
+								"createRepository": {
+									"repository": {
+										"id": "REPOID",
+										"name": "REPO",
+										"owner": {"login":"OWNER"},
+										"url": "https://example.com/OWNER/REPO"
+									}
+								}
+							}
+						}`),
+				)
+			},
+			wantStdout: "✓ Created repository OWNER/REPO on example.com\n  https://example.com/OWNER/REPO\n",
+		},
 	}
 	for _, tt := range tests {
 		prompterMock := &prompter.PrompterMock{}
@@ -965,8 +1048,11 @@ func Test_createRun(t *testing.T) {
 		tt.opts.HttpClient = func() (*http.Client, error) {
 			return &http.Client{Transport: reg}, nil
 		}
-		tt.opts.Config = func() (gh.Config, error) {
-			return config.NewBlankConfig(), nil
+
+		if tt.opts.Config == nil {
+			tt.opts.Config = func() (gh.Config, error) {
+				return config.NewBlankConfig(), nil
+			}
 		}
 
 		tt.opts.GitClient = &git.Client{
diff --git a/pkg/cmd/run/shared/test.go b/pkg/cmd/run/shared/test.go
index 0619541a4..5a8a4584e 100644
--- a/pkg/cmd/run/shared/test.go
+++ b/pkg/cmd/run/shared/test.go
@@ -104,6 +104,60 @@ var SuccessfulJob Job = Job{
 	},
 }
 
+// Note that this run *has* steps, but in the ZIP archive the step logs are not
+// included.
+var SuccessfulJobWithoutStepLogs Job = Job{
+	ID:          11,
+	Status:      Completed,
+	Conclusion:  Success,
+	Name:        "cool job with no step logs",
+	StartedAt:   TestRunStartTime,
+	CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
+	URL:         "https://github.com/jobs/11",
+	RunID:       3,
+	Steps: []Step{
+		{
+			Name:       "fob the barz",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     1,
+		},
+		{
+			Name:       "barz the fob",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     2,
+		},
+	},
+}
+
+// Note that this run *has* steps, but in the ZIP archive the step logs are not
+// included.
+var LegacySuccessfulJobWithoutStepLogs Job = Job{
+	ID:          12,
+	Status:      Completed,
+	Conclusion:  Success,
+	Name:        "legacy cool job with no step logs",
+	StartedAt:   TestRunStartTime,
+	CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
+	URL:         "https://github.com/jobs/12",
+	RunID:       3,
+	Steps: []Step{
+		{
+			Name:       "fob the barz",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     1,
+		},
+		{
+			Name:       "barz the fob",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     2,
+		},
+	},
+}
+
 var FailedJob Job = Job{
 	ID:          20,
 	Status:      Completed,
@@ -129,6 +183,60 @@ var FailedJob Job = Job{
 	},
 }
 
+// Note that this run *has* steps, but in the ZIP archive the step logs are not
+// included.
+var FailedJobWithoutStepLogs Job = Job{
+	ID:          21,
+	Status:      Completed,
+	Conclusion:  Failure,
+	Name:        "sad job with no step logs",
+	StartedAt:   TestRunStartTime,
+	CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
+	URL:         "https://github.com/jobs/21",
+	RunID:       1234,
+	Steps: []Step{
+		{
+			Name:       "barf the quux",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     1,
+		},
+		{
+			Name:       "quux the barf",
+			Status:     Completed,
+			Conclusion: Failure,
+			Number:     2,
+		},
+	},
+}
+
+// Note that this run *has* steps, but in the ZIP archive the step logs are not
+// included.
+var LegacyFailedJobWithoutStepLogs Job = Job{
+	ID:          22,
+	Status:      Completed,
+	Conclusion:  Failure,
+	Name:        "legacy sad job with no step logs",
+	StartedAt:   TestRunStartTime,
+	CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
+	URL:         "https://github.com/jobs/22",
+	RunID:       1234,
+	Steps: []Step{
+		{
+			Name:       "barf the quux",
+			Status:     Completed,
+			Conclusion: Success,
+			Number:     1,
+		},
+		{
+			Name:       "quux the barf",
+			Status:     Completed,
+			Conclusion: Failure,
+			Number:     2,
+		},
+	},
+}
+
 var SuccessfulJobAnnotations []Annotation = []Annotation{
 	{
 		JobName:   "cool job",
diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go
index 3a04fb186..2205a9031 100644
--- a/pkg/cmd/run/view/view_test.go
+++ b/pkg/cmd/run/view/view_test.go
@@ -990,6 +990,619 @@ func TestViewRun(t *testing.T) {
 			},
 			wantOut: quuxTheBarfLogOutput,
 		},
+		{
+			name: "interactive with log, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt: true,
+				Log:    true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021")
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ cool job with no step logs")
+					})
+			},
+			wantOut: coolJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with log, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				JobID: "11",
+				Log:   true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/11"),
+					httpmock.JSONResponse(shared.SuccessfulJobWithoutStepLogs))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: coolJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "interactive with log-failed, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt:    true,
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return 4, nil
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "X sad job with no step logs")
+					})
+			},
+			wantOut: sadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with log-failed, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				JobID:     "21",
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/21"),
+					httpmock.JSONResponse(shared.FailedJobWithoutStepLogs))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: sadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "interactive with run log, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt: true,
+				Log:    true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021")
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "View all jobs in this run")
+					})
+			},
+			wantOut: expectedRunLogOutputWithNoSteps,
+		},
+		{
+			name: "noninteractive with run log, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				RunID: "3",
+				Log:   true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: expectedRunLogOutputWithNoSteps,
+		},
+		{
+			name: "interactive with run log-failed, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt:    true,
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return 4, nil
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "View all jobs in this run")
+					})
+			},
+			wantOut: sadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with run log-failed, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				RunID:     "1234",
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.SuccessfulJobWithoutStepLogs,
+							shared.FailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: sadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "interactive with log, legacy service data, with no step logs available",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt: true,
+				Log:    true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021")
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ legacy cool job with no step logs")
+					})
+			},
+			wantOut: legacyCoolJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with log, legacy service data, with no step logs available",
+			opts: &ViewOptions{
+				JobID: "12",
+				Log:   true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/12"),
+					httpmock.JSONResponse(shared.LegacySuccessfulJobWithoutStepLogs))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: legacyCoolJobRunWithNoStepLogsLogOutput,
+		},
+
+		{
+			name: "interactive with log-failed, legacy service data, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt:    true,
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return 4, nil
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "X legacy sad job with no step logs")
+					})
+			},
+			wantOut: legacySadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with log-failed, legacy service data, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				JobID:     "22",
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/22"),
+					httpmock.JSONResponse(shared.LegacyFailedJobWithoutStepLogs))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: legacySadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "interactive with run log, legacy service data, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt: true,
+				Log:    true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021")
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "View all jobs in this run")
+					})
+			},
+			wantOut: expectedLegacyRunLogOutputWithNoSteps,
+		},
+		{
+			name: "noninteractive with run log, legacy service data, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				RunID: "3",
+				Log:   true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
+					httpmock.JSONResponse(shared.SuccessfulRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/3/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: expectedLegacyRunLogOutputWithNoSteps,
+		},
+		{
+			name: "interactive with run log-failed, legacy service data, with no step logs available (#10551)",
+			tty:  true,
+			opts: &ViewOptions{
+				Prompt:    true,
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRuns,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterSelect("Select a workflow run",
+					[]string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"},
+					func(_, _ string, opts []string) (int, error) {
+						return 4, nil
+					})
+				pm.RegisterSelect("View a specific job in this run?",
+					[]string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "View all jobs in this run")
+					})
+			},
+			wantOut: legacySadJobRunWithNoStepLogsLogOutput,
+		},
+		{
+			name: "noninteractive with run log-failed, legacy service data, with no step logs available (#10551)",
+			opts: &ViewOptions{
+				RunID:     "1234",
+				LogFailed: true,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
+					httpmock.JSONResponse(shared.FailedRun))
+				reg.Register(
+					httpmock.REST("GET", "runs/1234/jobs"),
+					httpmock.JSONResponse(shared.JobsPayload{
+						Jobs: []shared.Job{
+							shared.LegacySuccessfulJobWithoutStepLogs,
+							shared.LegacyFailedJobWithoutStepLogs,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
+					httpmock.FileResponse("./fixtures/run_log.zip"))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
+					httpmock.JSONResponse(shared.TestWorkflow))
+			},
+			wantOut: legacySadJobRunWithNoStepLogsLogOutput,
+		},
 		{
 			name: "run log but run is not done",
 			tty:  true,
@@ -1419,14 +2032,23 @@ func TestViewRun(t *testing.T) {
 //	├── sad job/
 //	│   ├── 1_barf the quux.txt
 //	│   └── 2_quux the barf.txt
-//	└── ad job/
-//	    └── 1_barf the quux.txt
+//	├── ad job/
+//	|   └── 1_barf the quux.txt
+//	├── 0_cool job.txt
+//	├── 1_sad job.txt
+//	├── 2_cool job with no step logs.txt
+//	├── 3_sad job with no step logs.txt
+//	├── -9999999999_legacy cool job with no step logs.txt
+//	└── -9999999999_legacy sad job with no step logs.txt
+
 func Test_attachRunLog(t *testing.T) {
 	tests := []struct {
-		name         string
-		job          shared.Job
-		wantMatch    bool
-		wantFilename string
+		name             string
+		job              shared.Job
+		wantJobMatch     bool
+		wantJobFilename  string
+		wantStepMatch    bool
+		wantStepFilename string
 	}{
 		{
 			name: "matching job name and step number 1",
@@ -1437,8 +2059,10 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "cool job/1_fob the barz.txt",
+			wantJobMatch:     true,
+			wantJobFilename:  "0_cool job.txt",
+			wantStepMatch:    true,
+			wantStepFilename: "cool job/1_fob the barz.txt",
 		},
 		{
 			name: "matching job name and step number 2",
@@ -1449,8 +2073,10 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 2,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "cool job/2_barz the fob.txt",
+			wantJobMatch:     true,
+			wantJobFilename:  "0_cool job.txt",
+			wantStepMatch:    true,
+			wantStepFilename: "cool job/2_barz the fob.txt",
 		},
 		{
 			name: "matching job name and step number and mismatch step name",
@@ -1461,8 +2087,10 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "cool job/1_fob the barz.txt",
+			wantJobMatch:     true,
+			wantJobFilename:  "0_cool job.txt",
+			wantStepMatch:    true,
+			wantStepFilename: "cool job/1_fob the barz.txt",
 		},
 		{
 			name: "matching job name and mismatch step number",
@@ -1473,7 +2101,53 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 3,
 				}},
 			},
-			wantMatch: false,
+			wantJobMatch:    true,
+			wantJobFilename: "0_cool job.txt",
+			wantStepMatch:   false,
+		},
+		{
+			name: "matching job name with no step logs",
+			job: shared.Job{
+				Name: "cool job with no step logs",
+				Steps: []shared.Step{{
+					Name:   "fob the barz",
+					Number: 1,
+				}},
+			},
+			wantJobMatch:    true,
+			wantJobFilename: "2_cool job with no step logs.txt",
+			wantStepMatch:   false,
+		},
+		{
+			name: "matching job name with no step data",
+			job: shared.Job{
+				Name: "cool job with no step logs",
+			},
+			wantJobMatch:    true,
+			wantJobFilename: "2_cool job with no step logs.txt",
+			wantStepMatch:   false,
+		},
+		{
+			name: "matching job name with legacy filename and no step logs",
+			job: shared.Job{
+				Name: "legacy cool job with no step logs",
+				Steps: []shared.Step{{
+					Name:   "fob the barz",
+					Number: 1,
+				}},
+			},
+			wantJobMatch:    true,
+			wantJobFilename: "-9999999999_legacy cool job with no step logs.txt",
+			wantStepMatch:   false,
+		},
+		{
+			name: "matching job name with legacy filename and no step data",
+			job: shared.Job{
+				Name: "legacy cool job with no step logs",
+			},
+			wantJobMatch:    true,
+			wantJobFilename: "-9999999999_legacy cool job with no step logs.txt",
+			wantStepMatch:   false,
 		},
 		{
 			name: "one job name is a suffix of another",
@@ -1484,8 +2158,8 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "ad job/1_barf the quux.txt",
+			wantStepMatch:    true,
+			wantStepFilename: "ad job/1_barf the quux.txt",
 		},
 		{
 			name: "escape metacharacters in job name",
@@ -1496,7 +2170,8 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 0,
 				}},
 			},
-			wantMatch: false,
+			wantJobMatch:  false,
+			wantStepMatch: false,
 		},
 		{
 			name: "mismatching job name",
@@ -1507,7 +2182,8 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch: false,
+			wantJobMatch:  false,
+			wantStepMatch: false,
 		},
 		{
 			name: "job name with forward slash matches dir with slash removed",
@@ -1518,9 +2194,10 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch: true,
+			wantJobMatch:  false,
+			wantStepMatch: true,
 			// not the double space in the dir name, as the slash has been removed
-			wantFilename: "cool job  with slash/1_fob the barz.txt",
+			wantStepFilename: "cool job  with slash/1_fob the barz.txt",
 		},
 		{
 			name: "job name with colon matches dir with colon removed",
@@ -1531,8 +2208,9 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "cool job  with colon/1_fob the barz.txt",
+			wantJobMatch:     false,
+			wantStepMatch:    true,
+			wantStepFilename: "cool job  with colon/1_fob the barz.txt",
 		},
 		{
 			name: "Job name with really long name (over the ZIP limit)",
@@ -1543,8 +2221,9 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnine/1_Long Name Job.txt",
+			wantJobMatch:     false,
+			wantStepMatch:    true,
+			wantStepFilename: "thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnine/1_Long Name Job.txt",
 		},
 		{
 			name: "Job name that would be truncated by the C# server to split a grapheme",
@@ -1555,8 +2234,9 @@ func Test_attachRunLog(t *testing.T) {
 					Number: 1,
 				}},
 			},
-			wantMatch:    true,
-			wantFilename: "Emoji Test 😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅�/1_Emoji Job.txt",
+			wantJobMatch:     false,
+			wantStepMatch:    true,
+			wantStepFilename: "Emoji Test 😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅�/1_Emoji Job.txt",
 		},
 	}
 
@@ -1566,17 +2246,27 @@ func Test_attachRunLog(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			jobs := []shared.Job{tt.job}
 
-			attachRunLog(&run_log_zip_reader.Reader, []shared.Job{tt.job})
+			attachRunLog(&run_log_zip_reader.Reader, jobs)
 
 			t.Logf("Job details: ")
 
-			for _, step := range tt.job.Steps {
-				log := step.Log
-				logPresent := log != nil
-				require.Equal(t, tt.wantMatch, logPresent, "log not present")
-				if logPresent {
-					require.Equal(t, tt.wantFilename, log.Name, "Filename mismatch")
+			job := jobs[0]
+
+			jobLog := job.Log
+			jobLogPresent := jobLog != nil
+			require.Equal(t, tt.wantJobMatch, jobLogPresent, "job log not present")
+			if jobLogPresent {
+				require.Equal(t, tt.wantJobFilename, jobLog.Name, "job log filename mismatch")
+			}
+
+			for _, step := range job.Steps {
+				stepLog := step.Log
+				stepLogPresent := stepLog != nil
+				require.Equal(t, tt.wantStepMatch, stepLogPresent, "step log not present")
+				if stepLogPresent {
+					require.Equal(t, tt.wantStepFilename, stepLog.Name, "step log filename mismatch")
 				}
 			}
 		})
@@ -1607,9 +2297,35 @@ sad job	quux the barf	log line 2
 sad job	quux the barf	log line 3
 `)
 
+var coolJobRunWithNoStepLogsLogOutput = heredoc.Doc(`
+cool job with no step logs	UNKNOWN STEP	log line 1
+cool job with no step logs	UNKNOWN STEP	log line 2
+cool job with no step logs	UNKNOWN STEP	log line 3
+`)
+
+var legacyCoolJobRunWithNoStepLogsLogOutput = heredoc.Doc(`
+legacy cool job with no step logs	UNKNOWN STEP	log line 1
+legacy cool job with no step logs	UNKNOWN STEP	log line 2
+legacy cool job with no step logs	UNKNOWN STEP	log line 3
+`)
+
+var sadJobRunWithNoStepLogsLogOutput = heredoc.Doc(`
+sad job with no step logs	UNKNOWN STEP	log line 1
+sad job with no step logs	UNKNOWN STEP	log line 2
+sad job with no step logs	UNKNOWN STEP	log line 3
+`)
+
+var legacySadJobRunWithNoStepLogsLogOutput = heredoc.Doc(`
+legacy sad job with no step logs	UNKNOWN STEP	log line 1
+legacy sad job with no step logs	UNKNOWN STEP	log line 2
+legacy sad job with no step logs	UNKNOWN STEP	log line 3
+`)
+
 var coolJobRunLogOutput = fmt.Sprintf("%s%s", fobTheBarzLogOutput, barfTheFobLogOutput)
 var sadJobRunLogOutput = fmt.Sprintf("%s%s", barfTheQuuxLogOutput, quuxTheBarfLogOutput)
 var expectedRunLogOutput = fmt.Sprintf("%s%s", coolJobRunLogOutput, sadJobRunLogOutput)
+var expectedRunLogOutputWithNoSteps = fmt.Sprintf("%s%s", coolJobRunWithNoStepLogsLogOutput, sadJobRunWithNoStepLogsLogOutput)
+var expectedLegacyRunLogOutputWithNoSteps = fmt.Sprintf("%s%s", legacyCoolJobRunWithNoStepLogsLogOutput, legacySadJobRunWithNoStepLogsLogOutput)
 
 func TestRunLog(t *testing.T) {
 	t.Run("when the cache dir doesn't exist, exists return false", func(t *testing.T) {
diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go
index b0cf994e3..f6a72e2a7 100644
--- a/pkg/iostreams/color_test.go
+++ b/pkg/iostreams/color_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestColorFromRGB(t *testing.T) {
+func TestLabel(t *testing.T) {
 	tests := []struct {
 		name  string
 		hex   string
@@ -20,77 +20,57 @@ func TestColorFromRGB(t *testing.T) {
 			hex:   "fc0303",
 			text:  "red",
 			wants: "\033[38;2;252;3;3mred\033[0m",
-			cs:    NewColorScheme(true, true, true, false, NoTheme),
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				ColorLabels:   true,
+			},
 		},
 		{
 			name:  "no truecolor",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(true, true, false, false, NoTheme),
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				ColorLabels:   true,
+			},
 		},
 		{
 			name:  "no color",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, false, NoTheme),
+			cs: &ColorScheme{
+				ColorLabels: true,
+			},
 		},
 		{
 			name:  "invalid hex",
 			hex:   "fc0",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, false, NoTheme),
-		},
-	}
-
-	for _, tt := range tests {
-		fn := tt.cs.ColorFromRGB(tt.hex)
-		assert.Equal(t, tt.wants, fn(tt.text))
-	}
-}
-
-func TestHexToRGB(t *testing.T) {
-	tests := []struct {
-		name  string
-		hex   string
-		text  string
-		wants string
-		cs    *ColorScheme
-	}{
-		{
-			name:  "truecolor",
-			hex:   "fc0303",
-			text:  "red",
-			wants: "\033[38;2;252;3;3mred\033[0m",
-			cs:    NewColorScheme(true, true, true, false, NoTheme),
-		},
-		{
-			name:  "no truecolor",
-			hex:   "fc0303",
-			text:  "red",
-			wants: "red",
-			cs:    NewColorScheme(true, true, false, false, NoTheme),
+			cs: &ColorScheme{
+				ColorLabels: true,
+			},
 		},
 		{
-			name:  "no color",
+			name:  "no color labels",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, false, NoTheme),
-		},
-		{
-			name:  "invalid hex",
-			hex:   "fc0",
-			text:  "red",
-			wants: "red",
-			cs:    NewColorScheme(false, false, false, false, NoTheme),
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				ColorLabels:   true,
+			},
 		},
 	}
 
 	for _, tt := range tests {
-		output := tt.cs.HexToRGB(tt.hex, tt.text)
+		output := tt.cs.Label(tt.hex, tt.text)
 		assert.Equal(t, tt.wants, output)
 	}
 }
@@ -108,62 +88,110 @@ func TestTableHeader(t *testing.T) {
 		expected string
 	}{
 		{
-			name:     "when color is disabled, text is not stylized",
-			cs:       NewColorScheme(false, false, false, true, NoTheme),
+			name: "when color is disabled, text is not stylized",
+			cs: &ColorScheme{
+				Accessible: true,
+				Theme:      NoTheme,
+			},
 			input:    "this should not be stylized",
 			expected: "this should not be stylized",
 		},
 		{
-			name:     "when 4-bit color is enabled but no theme, 4-bit default color and underline are used",
-			cs:       NewColorScheme(true, false, false, true, NoTheme),
+			name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used",
+			cs: &ColorScheme{
+				Enabled:    true,
+				Accessible: true,
+				Theme:      NoTheme,
+			},
 			input:    "this should have no explicit color but underlined",
 			expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
 		},
 		{
-			name:     "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used",
-			cs:       NewColorScheme(true, false, false, true, LightTheme),
+			name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used",
+			cs: &ColorScheme{
+				Enabled:    true,
+				Accessible: true,
+				Theme:      LightTheme,
+			},
 			input:    "this should have dark foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
 		},
 		{
-			name:     "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used",
-			cs:       NewColorScheme(true, false, false, true, DarkTheme),
+			name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used",
+			cs: &ColorScheme{
+				Enabled:    true,
+				Accessible: true,
+				Theme:      DarkTheme,
+			},
 			input:    "this should have light foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
 		},
 		{
-			name:     "when 8-bit color is enabled but no theme, 4-bit default color and underline are used",
-			cs:       NewColorScheme(true, true, false, true, NoTheme),
+			name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				Accessible:    true,
+				Theme:         NoTheme,
+			},
 			input:    "this should have no explicit color but underlined",
 			expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
 		},
 		{
-			name:     "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used",
-			cs:       NewColorScheme(true, true, false, true, LightTheme),
+			name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				Accessible:    true,
+				Theme:         LightTheme,
+			},
 			input:    "this should have dark foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
 		},
 		{
-			name:     "when 8-bit color is true and theme is dark, 4-bit light color and underline are used",
-			cs:       NewColorScheme(true, true, false, true, DarkTheme),
+			name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				Accessible:    true,
+				Theme:         DarkTheme,
+			},
 			input:    "this should have light foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
 		},
 		{
-			name:     "when 24-bit color is enabled but no theme, 4-bit default color and underline are used",
-			cs:       NewColorScheme(true, true, true, true, NoTheme),
+			name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         NoTheme,
+			},
 			input:    "this should have no explicit color but underlined",
 			expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
 		},
 		{
-			name:     "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used",
-			cs:       NewColorScheme(true, true, true, true, LightTheme),
+			name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         LightTheme,
+			},
 			input:    "this should have dark foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
 		},
 		{
-			name:     "when 24-bit color is true and theme is dark, 4-bit light color and underline are used",
-			cs:       NewColorScheme(true, true, true, true, DarkTheme),
+			name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         DarkTheme,
+			},
 			input:    "this should have light foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
 		},
@@ -191,43 +219,70 @@ func TestMuted(t *testing.T) {
 	}{
 		{
 			name:     "when color is disabled but accessible colors are disabled, text is not stylized",
-			cs:       NewColorScheme(false, false, false, false, NoTheme),
+			cs:       &ColorScheme{},
 			input:    "this should not be stylized",
 			expected: "this should not be stylized",
 		},
 		{
-			name:     "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used",
-			cs:       NewColorScheme(true, false, false, false, NoTheme),
+			name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used",
+			cs: &ColorScheme{
+				Enabled: true,
+			},
 			input:    "this should be 4-bit gray",
 			expected: fmt.Sprintf("%sthis should be 4-bit gray%s", gray4bit, reset),
 		},
 		{
-			name:     "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
-			cs:       NewColorScheme(true, true, false, false, NoTheme),
+			name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+			},
 			input:    "this should be 8-bit gray",
 			expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset),
 		},
 		{
-			name:     "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
-			cs:       NewColorScheme(true, true, true, false, NoTheme),
+			name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+			},
 			input:    "this should be 8-bit gray",
 			expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset),
 		},
 		{
-			name:     "when 4-bit color is enabled and theme is dark, 4-bit light color is used",
-			cs:       NewColorScheme(true, true, true, true, DarkTheme),
+			name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         DarkTheme,
+			},
 			input:    "this should be 4-bit dim black",
 			expected: fmt.Sprintf("%sthis should be 4-bit dim black%s", dimBlack4bit, reset),
 		},
 		{
-			name:     "when 4-bit color is enabled and theme is light, 4-bit dark color is used",
-			cs:       NewColorScheme(true, true, true, true, LightTheme),
+			name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         LightTheme,
+			},
 			input:    "this should be 4-bit bright black",
 			expected: fmt.Sprintf("%sthis should be 4-bit bright black%s", brightBlack4bit, reset),
 		},
 		{
-			name:     "when 4-bit color is enabled but no theme, 4-bit default color is used",
-			cs:       NewColorScheme(true, true, true, true, NoTheme),
+			name: "when 4-bit color is enabled but no theme, 4-bit default color is used",
+			cs: &ColorScheme{
+				Enabled:       true,
+				EightBitColor: true,
+				TrueColor:     true,
+				Accessible:    true,
+				Theme:         NoTheme,
+			},
 			input:    "this should have no explicit color",
 			expected: "this should have no explicit color",
 		},